Database stores times in UTC but serialized without timezone info, causing frontend to misinterpret as local time. Now all datetime fields include 'Z' suffix to indicate UTC, enabling proper timezone conversion in the browser. - Add UTCDatetimeBaseModel base class for Pydantic schemas - Update model to_dict() methods to append 'Z' suffix - Affects: tasks, users, sessions, audit logs, translations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
86 lines
3.4 KiB
Python
86 lines
3.4 KiB
Python
"""
|
|
Tool_OCR - Session Model
|
|
Secure token storage and session management for external authentication
|
|
"""
|
|
|
|
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey
|
|
from sqlalchemy.orm import relationship
|
|
from datetime import datetime
|
|
|
|
from app.core.database import Base
|
|
|
|
|
|
class Session(Base):
|
|
"""
|
|
User session model for external API token management
|
|
|
|
Stores encrypted tokens from external authentication API
|
|
and tracks session metadata for security auditing.
|
|
"""
|
|
|
|
__tablename__ = "tool_ocr_sessions"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
user_id = Column(Integer, ForeignKey("tool_ocr_users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
comment="Foreign key to users table")
|
|
access_token = Column(Text, nullable=True,
|
|
comment="Encrypted JWT access token from external API")
|
|
id_token = Column(Text, nullable=True,
|
|
comment="Encrypted JWT ID token from external API")
|
|
refresh_token = Column(Text, nullable=True,
|
|
comment="Encrypted refresh token (if provided by API)")
|
|
token_type = Column(String(50), default="Bearer", nullable=False,
|
|
comment="Token type (typically 'Bearer')")
|
|
expires_at = Column(DateTime, nullable=False, index=True,
|
|
comment="Token expiration timestamp from API")
|
|
issued_at = Column(DateTime, nullable=False,
|
|
comment="Token issue timestamp from API")
|
|
|
|
# Session metadata for security
|
|
ip_address = Column(String(45), nullable=True,
|
|
comment="Client IP address (IPv4/IPv6)")
|
|
user_agent = Column(String(500), nullable=True,
|
|
comment="Client user agent string")
|
|
|
|
# Timestamps
|
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
|
last_accessed_at = Column(DateTime, default=datetime.utcnow,
|
|
onupdate=datetime.utcnow, nullable=False,
|
|
comment="Last time this session was used")
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="sessions")
|
|
|
|
def __repr__(self):
|
|
return f"<Session(id={self.id}, user_id={self.user_id}, expires_at='{self.expires_at}')>"
|
|
|
|
def to_dict(self):
|
|
"""Convert session to dictionary (excluding sensitive tokens).
|
|
|
|
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
|
|
"""
|
|
return {
|
|
"id": self.id,
|
|
"user_id": self.user_id,
|
|
"token_type": self.token_type,
|
|
"expires_at": self.expires_at.isoformat() + 'Z' if self.expires_at else None,
|
|
"issued_at": self.issued_at.isoformat() + 'Z' if self.issued_at else None,
|
|
"ip_address": self.ip_address,
|
|
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None,
|
|
"last_accessed_at": self.last_accessed_at.isoformat() + 'Z' if self.last_accessed_at else None
|
|
}
|
|
|
|
@property
|
|
def is_expired(self) -> bool:
|
|
"""Check if session token is expired"""
|
|
return datetime.utcnow() >= self.expires_at if self.expires_at else True
|
|
|
|
@property
|
|
def time_until_expiry(self) -> int:
|
|
"""Get seconds until token expiration"""
|
|
if not self.expires_at:
|
|
return 0
|
|
delta = self.expires_at - datetime.utcnow()
|
|
return max(0, int(delta.total_seconds()))
|