Initial commit – TimeMaster Zeiterfassung & HR-Tool

Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-23 20:03:27 +02:00
commit 1fedd683e0
178 changed files with 29896 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import uuid
import enum
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
if TYPE_CHECKING:
from app.models.company import Company
from app.models.department import Department
from app.models.session import Session
class UserRole(str, enum.Enum):
SUPER_ADMIN = "SUPER_ADMIN"
COMPANY_ADMIN = "COMPANY_ADMIN"
HR = "HR"
MANAGER = "MANAGER"
EMPLOYEE = "EMPLOYEE"
class AuthProvider(str, enum.Enum):
LOCAL = "local"
LDAP = "ldap"
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
company_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"))
department_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("departments.id", ondelete="SET NULL"))
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
password_hash: Mapped[str | None] = mapped_column(Text, nullable=True)
first_name: Mapped[str] = mapped_column(String(100), nullable=False)
last_name: Mapped[str] = mapped_column(String(100), nullable=False)
role: Mapped[UserRole] = mapped_column(Enum(UserRole), nullable=False, default=UserRole.EMPLOYEE)
auth_provider: Mapped[AuthProvider] = mapped_column(
Enum(AuthProvider, name="authprovider", values_callable=lambda x: [e.value for e in x]),
nullable=False, default=AuthProvider.LOCAL,
)
ldap_dn: Mapped[str | None] = mapped_column(Text)
work_schedule_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("work_schedules.id", ondelete="SET NULL")
)
# Kiosk auth
kiosk_pin_hash: Mapped[str | None] = mapped_column(Text)
kiosk_qr_token: Mapped[str | None] = mapped_column(Text, unique=True)
# Kalender-Kürzel (vom Manager setzbar, für CalDAV-Template $kuerzel)
kuerzel: Mapped[str | None] = mapped_column(String(20))
# Personalnummer (numerisch, eindeutig pro Firma; bleibt nach Deaktivierung reserviert)
personnel_number: Mapped[str | None] = mapped_column(String(50))
# TOTP / 2FA
totp_secret: Mapped[str | None] = mapped_column(String(64))
totp_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Permissions
can_manual_time_entry: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Account state
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
invite_token_hash: Mapped[str | None] = mapped_column(Text)
invite_expires: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Relationships
company: Mapped["Company"] = relationship("Company", back_populates="users")
department: Mapped["Department | None"] = relationship(
"Department",
primaryjoin="User.department_id == Department.id",
foreign_keys="[User.department_id]",
back_populates="members",
)
sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user", cascade="all, delete-orphan", lazy="noload")
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
def is_admin_or_above(self) -> bool:
return self.role in (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
def __repr__(self) -> str:
return f"<User {self.email} [{self.role}]>"