Spaces:
Running
Running
Merge pull request #214 from Exodus2004/feat/db-uuid-postgres
Browse files- backend/app/auth.py +4 -4
- backend/app/models.py +48 -8
backend/app/auth.py
CHANGED
|
@@ -30,10 +30,10 @@ def verify_password(plain: str, hashed: str) -> bool:
|
|
| 30 |
|
| 31 |
# ── JWT Token ────────────────────────────────────────
|
| 32 |
|
| 33 |
-
def create_access_token(user_id
|
| 34 |
"""Create a JWT access token with user_id as the subject."""
|
| 35 |
payload = {
|
| 36 |
-
"sub": user_id,
|
| 37 |
"type": "access",
|
| 38 |
"exp": datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_ACCESS_EXPIRY_MINUTES),
|
| 39 |
"iat": datetime.now(timezone.utc),
|
|
@@ -41,10 +41,10 @@ def create_access_token(user_id: str) -> str:
|
|
| 41 |
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
|
| 42 |
|
| 43 |
|
| 44 |
-
def create_refresh_token(user_id
|
| 45 |
"""Create a JWT refresh token with user_id as the subject."""
|
| 46 |
payload = {
|
| 47 |
-
"sub": user_id,
|
| 48 |
"type": "refresh",
|
| 49 |
"exp": datetime.now(timezone.utc) + timedelta(days=settings.JWT_REFRESH_EXPIRY_DAYS),
|
| 50 |
"iat": datetime.now(timezone.utc),
|
|
|
|
| 30 |
|
| 31 |
# ── JWT Token ────────────────────────────────────────
|
| 32 |
|
| 33 |
+
def create_access_token(user_id) -> str:
|
| 34 |
"""Create a JWT access token with user_id as the subject."""
|
| 35 |
payload = {
|
| 36 |
+
"sub": str(user_id),
|
| 37 |
"type": "access",
|
| 38 |
"exp": datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_ACCESS_EXPIRY_MINUTES),
|
| 39 |
"iat": datetime.now(timezone.utc),
|
|
|
|
| 41 |
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
|
| 42 |
|
| 43 |
|
| 44 |
+
def create_refresh_token(user_id) -> str:
|
| 45 |
"""Create a JWT refresh token with user_id as the subject."""
|
| 46 |
payload = {
|
| 47 |
+
"sub": str(user_id),
|
| 48 |
"type": "refresh",
|
| 49 |
"exp": datetime.now(timezone.utc) + timedelta(days=settings.JWT_REFRESH_EXPIRY_DAYS),
|
| 50 |
"iat": datetime.now(timezone.utc),
|
backend/app/models.py
CHANGED
|
@@ -8,6 +8,9 @@ import hashlib
|
|
| 8 |
from datetime import datetime, timezone
|
| 9 |
|
| 10 |
from cryptography.fernet import Fernet
|
|
|
|
|
|
|
|
|
|
| 11 |
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Boolean, Enum as SQLAlchemyEnum
|
| 12 |
from sqlalchemy.types import TypeDecorator
|
| 13 |
from sqlalchemy.orm import relationship
|
|
@@ -15,6 +18,38 @@ from sqlalchemy.orm import relationship
|
|
| 15 |
from app.database import Base
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
class EncryptedString(TypeDecorator):
|
| 19 |
"""
|
| 20 |
A custom SQLAlchemy type that transparently encrypts strings in the database
|
|
@@ -72,7 +107,7 @@ class User(Base):
|
|
| 72 |
"""
|
| 73 |
__tablename__ = "users"
|
| 74 |
|
| 75 |
-
id = Column(
|
| 76 |
username = Column(String(80), unique=True, nullable=False, index=True)
|
| 77 |
email = Column(String(120), unique=True, nullable=False, index=True)
|
| 78 |
hashed_password = Column(String(255), nullable=False)
|
|
@@ -102,8 +137,8 @@ class ApiKey(Base):
|
|
| 102 |
"""
|
| 103 |
__tablename__ = "api_keys"
|
| 104 |
|
| 105 |
-
id = Column(
|
| 106 |
-
user_id = Column(
|
| 107 |
key_prefix = Column(String(10), nullable=False)
|
| 108 |
hashed_key = Column(String(255), nullable=False, unique=True, index=True)
|
| 109 |
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
@@ -119,6 +154,11 @@ class Document(Base):
|
|
| 119 |
"""
|
| 120 |
__tablename__ = "documents"
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
id = Column(String, primary_key=True, default=generate_uuid)
|
| 123 |
user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True)
|
| 124 |
filename = Column(String(255), nullable=False) # Internal UUID-based filename
|
|
@@ -142,9 +182,9 @@ class ChatMessage(Base):
|
|
| 142 |
"""
|
| 143 |
__tablename__ = "chat_messages"
|
| 144 |
|
| 145 |
-
id = Column(
|
| 146 |
-
user_id = Column(
|
| 147 |
-
document_id = Column(
|
| 148 |
role = Column(String(20), nullable=False) # "user" | "assistant"
|
| 149 |
content = Column(Text, nullable=False)
|
| 150 |
sources_json = Column(Text, nullable=True) # JSON representation of retrieved sources
|
|
@@ -162,8 +202,8 @@ class SharedMessage(Base):
|
|
| 162 |
"""
|
| 163 |
__tablename__ = "shared_messages"
|
| 164 |
|
| 165 |
-
id = Column(
|
| 166 |
-
message_id = Column(
|
| 167 |
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
| 168 |
|
| 169 |
# Relationships
|
|
|
|
| 8 |
from datetime import datetime, timezone
|
| 9 |
|
| 10 |
from cryptography.fernet import Fernet
|
| 11 |
+
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Boolean
|
| 12 |
+
from sqlalchemy.types import TypeDecorator, CHAR
|
| 13 |
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
| 14 |
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Boolean, Enum as SQLAlchemyEnum
|
| 15 |
from sqlalchemy.types import TypeDecorator
|
| 16 |
from sqlalchemy.orm import relationship
|
|
|
|
| 18 |
from app.database import Base
|
| 19 |
|
| 20 |
|
| 21 |
+
class GUID(TypeDecorator):
|
| 22 |
+
"""Platform-independent GUID type.
|
| 23 |
+
Uses PostgreSQL's UUID type, otherwise uses CHAR(36).
|
| 24 |
+
"""
|
| 25 |
+
impl = CHAR
|
| 26 |
+
cache_ok = True
|
| 27 |
+
|
| 28 |
+
def load_dialect_impl(self, dialect):
|
| 29 |
+
if dialect.name == 'postgresql':
|
| 30 |
+
return dialect.type_descriptor(PG_UUID(as_uuid=True))
|
| 31 |
+
else:
|
| 32 |
+
return dialect.type_descriptor(CHAR(36))
|
| 33 |
+
|
| 34 |
+
def process_bind_param(self, value, dialect):
|
| 35 |
+
if value is None:
|
| 36 |
+
return value
|
| 37 |
+
if isinstance(value, uuid.UUID):
|
| 38 |
+
return value if dialect.name == 'postgresql' else str(value)
|
| 39 |
+
try:
|
| 40 |
+
val_uuid = uuid.UUID(value)
|
| 41 |
+
return val_uuid if dialect.name == 'postgresql' else str(val_uuid)
|
| 42 |
+
except ValueError:
|
| 43 |
+
if dialect.name == 'postgresql':
|
| 44 |
+
return uuid.UUID(int=0)
|
| 45 |
+
return value
|
| 46 |
+
|
| 47 |
+
def process_result_value(self, value, dialect):
|
| 48 |
+
if value is None:
|
| 49 |
+
return value
|
| 50 |
+
return str(value)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
class EncryptedString(TypeDecorator):
|
| 54 |
"""
|
| 55 |
A custom SQLAlchemy type that transparently encrypts strings in the database
|
|
|
|
| 107 |
"""
|
| 108 |
__tablename__ = "users"
|
| 109 |
|
| 110 |
+
id = Column(GUID, primary_key=True, default=uuid.uuid4)
|
| 111 |
username = Column(String(80), unique=True, nullable=False, index=True)
|
| 112 |
email = Column(String(120), unique=True, nullable=False, index=True)
|
| 113 |
hashed_password = Column(String(255), nullable=False)
|
|
|
|
| 137 |
"""
|
| 138 |
__tablename__ = "api_keys"
|
| 139 |
|
| 140 |
+
id = Column(GUID, primary_key=True, default=uuid.uuid4)
|
| 141 |
+
user_id = Column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
| 142 |
key_prefix = Column(String(10), nullable=False)
|
| 143 |
hashed_key = Column(String(255), nullable=False, unique=True, index=True)
|
| 144 |
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
| 154 |
"""
|
| 155 |
__tablename__ = "documents"
|
| 156 |
|
| 157 |
+
id = Column(GUID, primary_key=True, default=uuid.uuid4)
|
| 158 |
+
user_id = Column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
| 159 |
+
filename = Column(String(255), nullable=False) # Stored filename (UUID-based)
|
| 160 |
+
original_name = Column(String(255), nullable=False) # User's original filename
|
| 161 |
+
file_size = Column(Integer, default=0) # Size in bytes
|
| 162 |
id = Column(String, primary_key=True, default=generate_uuid)
|
| 163 |
user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True)
|
| 164 |
filename = Column(String(255), nullable=False) # Internal UUID-based filename
|
|
|
|
| 182 |
"""
|
| 183 |
__tablename__ = "chat_messages"
|
| 184 |
|
| 185 |
+
id = Column(GUID, primary_key=True, default=uuid.uuid4)
|
| 186 |
+
user_id = Column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
| 187 |
+
document_id = Column(GUID, ForeignKey("documents.id"), nullable=True, index=True)
|
| 188 |
role = Column(String(20), nullable=False) # "user" | "assistant"
|
| 189 |
content = Column(Text, nullable=False)
|
| 190 |
sources_json = Column(Text, nullable=True) # JSON representation of retrieved sources
|
|
|
|
| 202 |
"""
|
| 203 |
__tablename__ = "shared_messages"
|
| 204 |
|
| 205 |
+
id = Column(GUID, primary_key=True, default=uuid.uuid4)
|
| 206 |
+
message_id = Column(GUID, ForeignKey("chat_messages.id"), nullable=False, unique=True, index=True)
|
| 207 |
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
| 208 |
|
| 209 |
# Relationships
|