"""Gemeinsame Modelle: User, LoginCode. Modul-spezifische Modelle (Project, Task) liegen in app/projects/models.py. Alle Zeitstempel werden in UTC gespeichert. """ from datetime import datetime, timezone from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from .extensions import db def utcnow() -> datetime: """Zeitzonenbewusster UTC-Zeitstempel (für DB-Defaults).""" return datetime.now(timezone.utc) def _as_aware(dt: datetime) -> datetime: """SQLite gibt Datetimes naiv zurück — als UTC interpretieren.""" return dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt class User(UserMixin, db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True, nullable=False, index=True) created_at = db.Column(db.DateTime, default=utcnow, nullable=False) login_codes = db.relationship( "LoginCode", back_populates="user", cascade="all, delete-orphan", ) projects = db.relationship( "Project", back_populates="user", cascade="all, delete-orphan", ) tasks = db.relationship( "Task", back_populates="user", cascade="all, delete-orphan", ) def __repr__(self) -> str: return f"" class LoginCode(db.Model): """Einmaliger, zeitlich begrenzter Login-Code (gehasht gespeichert).""" __tablename__ = "login_codes" id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey("users.id"), nullable=False, index=True ) code_hash = db.Column(db.String(255), nullable=False) created_at = db.Column(db.DateTime, default=utcnow, nullable=False) expires_at = db.Column(db.DateTime, nullable=False) used = db.Column(db.Boolean, default=False, nullable=False) user = db.relationship("User", back_populates="login_codes") def set_code(self, code: str) -> None: """Klartext-Code hashen und ablegen (nie im Klartext speichern).""" self.code_hash = generate_password_hash(code) def check_code(self, code: str) -> bool: return check_password_hash(self.code_hash, code) @property def is_expired(self) -> bool: return utcnow() > _as_aware(self.expires_at) def is_valid(self, code: str) -> bool: """Code ist gültig: nicht benutzt, nicht abgelaufen, korrekt.""" return (not self.used) and (not self.is_expired) and self.check_code(code) def __repr__(self) -> str: return f""