Critical: Lock state was purely React useState — refreshing the page reset it. Now persisted server-side via is_locked/locked_at columns on user_sessions. POST /auth/lock sets the flag, /auth/verify-password clears it, and GET /auth/status returns is_locked so the frontend initializes correctly. UI: Cache accent color in localStorage and apply via inline script in index.html before React hydrates to eliminate the cyan flash on load. UI: Increase TanStack Query gcTime from 5min to 30min so page data survives component unmount/remount across tab switches without skeleton. UI: Move Projects nav onClick from the icon element to the full-width container div so the entire row is clickable when the sidebar is collapsed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
28 lines
1.1 KiB
Python
28 lines
1.1 KiB
Python
from sqlalchemy import String, Boolean, ForeignKey, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
from datetime import datetime
|
|
from app.database import Base
|
|
|
|
|
|
class UserSession(Base):
|
|
__tablename__ = "user_sessions"
|
|
|
|
# UUID4 hex — avoids integer primary key enumeration
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
|
expires_at: Mapped[datetime] = mapped_column(nullable=False)
|
|
revoked: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# Session lock — persists across page refresh
|
|
is_locked: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
|
locked_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
|
|
|
# Audit fields for security logging
|
|
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
|
|
user_agent: Mapped[str | None] = mapped_column(String(255), nullable=True)
|