Unify database schema: Resolve all User model conflicts and auth table incompatibilities
Major changes: - Consolidate 3 different User models into single unified model (models/user.py) - Use UUID primary keys throughout (matches existing database schema) - Add comprehensive authentication fields while preserving existing data - Remove duplicate User model from auth.py, keep APIKey/RefreshToken/TokenBlacklist - Update all imports to use unified User model consistently - Create database migration (002_add_auth_fields.sql) for safe schema upgrade - Fix frontend User interface to handle UUID string IDs - Add backward compatibility fields (name property, role field) - Maintain relationships for authentication features (api_keys, refresh_tokens) Schema conflicts resolved: ✅ Migration schema (UUID, 7 fields) + Basic model (Integer, 6 fields) + Auth model (Integer, 10 fields) → Unified model (UUID, 12 fields with full backward compatibility) ✅ Field inconsistencies (name vs full_name) resolved with compatibility property ✅ Database foreign key constraints updated for UUID relationships ✅ JWT token handling fixed for UUID user IDs This completes the holistic database schema unification requested after quick patching caused conflicts. All existing data preserved, full auth system functional. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,8 @@ from app.core.auth_deps import (
|
||||
get_current_superuser,
|
||||
require_admin
|
||||
)
|
||||
from app.models.auth import User, APIKey, RefreshToken, TokenBlacklist, API_SCOPES, DEFAULT_API_SCOPES
|
||||
from app.models.user import User
|
||||
from app.models.auth import APIKey, RefreshToken, TokenBlacklist, API_SCOPES, DEFAULT_API_SCOPES
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def get_current_active_user(
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Get current authenticated and active user."""
|
||||
from app.models.auth import User
|
||||
from app.models.user import User
|
||||
|
||||
user = db.query(User).filter(User.id == user_context["user_id"]).first()
|
||||
if not user:
|
||||
|
||||
@@ -5,19 +5,18 @@ Creates all tables and sets up initial data.
|
||||
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import engine, SessionLocal
|
||||
from app.models.auth import Base as AuthBase, User, API_SCOPES
|
||||
from app.models.auth import APIKey
|
||||
from app.core.database import engine, SessionLocal, Base
|
||||
from app.models.user import User
|
||||
from app.models.auth import API_SCOPES, APIKey
|
||||
|
||||
# Import other model bases here as they're created
|
||||
# from app.models.workflows import Base as WorkflowsBase
|
||||
# from app.models.agents import Base as AgentsBase
|
||||
# Import all models to ensure they're registered with Base
|
||||
from app.models import user, auth, agent, project
|
||||
|
||||
def create_tables():
|
||||
"""Create all database tables."""
|
||||
try:
|
||||
# Create auth tables
|
||||
AuthBase.metadata.create_all(bind=engine)
|
||||
# Create all tables using the unified Base
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Add other model bases here
|
||||
# WorkflowsBase.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -224,8 +224,8 @@ class AuthManager:
|
||||
raise AuthenticationError("Token has been revoked")
|
||||
|
||||
# Get user information
|
||||
user_id = int(payload.get("sub"))
|
||||
from app.models.auth import User
|
||||
user_id = payload.get("sub") # UUID as string
|
||||
from app.models.user import User
|
||||
user = session.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user or not user.is_active:
|
||||
@@ -279,7 +279,7 @@ def create_token_response(user_id: int, user_data: Dict[str, Any]) -> Dict[str,
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Hash a password for storage."""
|
||||
from app.models.auth import User
|
||||
from app.models.user import User
|
||||
return User.hash_password(password)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Authentication and authorization models for Hive platform.
|
||||
Includes users, API keys, and JWT token management.
|
||||
Includes API keys and JWT token management.
|
||||
User model is now in models/user.py for consistency.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
@@ -8,80 +9,22 @@ from typing import Optional, List
|
||||
import secrets
|
||||
import string
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from passlib.context import CryptContext
|
||||
|
||||
Base = declarative_base()
|
||||
from ..core.database import Base
|
||||
|
||||
# Password hashing context
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model for authentication and authorization."""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
full_name = Column(String(255), nullable=True)
|
||||
|
||||
# User status and permissions
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superuser = Column(Boolean, default=False)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
api_keys = relationship("APIKey", back_populates="user", cascade="all, delete-orphan")
|
||||
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
def verify_password(self, password: str) -> bool:
|
||||
"""Verify a password against the hashed password."""
|
||||
return pwd_context.verify(password, self.hashed_password)
|
||||
|
||||
@classmethod
|
||||
def hash_password(cls, password: str) -> str:
|
||||
"""Hash a password for storage."""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def set_password(self, password: str) -> None:
|
||||
"""Set a new password for the user."""
|
||||
self.hashed_password = self.hash_password(password)
|
||||
|
||||
def update_last_login(self) -> None:
|
||||
"""Update the last login timestamp."""
|
||||
self.last_login = datetime.utcnow()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert user to dictionary (excluding sensitive data)."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"email": self.email,
|
||||
"full_name": self.full_name,
|
||||
"is_active": self.is_active,
|
||||
"is_superuser": self.is_superuser,
|
||||
"is_verified": self.is_verified,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"last_login": self.last_login.isoformat() if self.last_login else None,
|
||||
}
|
||||
|
||||
|
||||
class APIKey(Base):
|
||||
"""API Key model for programmatic access to Hive API."""
|
||||
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# API Key details
|
||||
name = Column(String(255), nullable=False) # Human-readable name
|
||||
@@ -178,7 +121,7 @@ class RefreshToken(Base):
|
||||
__tablename__ = "refresh_tokens"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Token details
|
||||
token_hash = Column(String(255), unique=True, index=True, nullable=False)
|
||||
|
||||
@@ -1,14 +1,85 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean
|
||||
"""
|
||||
Unified User model for Hive platform.
|
||||
Combines authentication and basic user functionality with UUID support.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
import uuid
|
||||
from sqlalchemy import Column, String, DateTime, Boolean, Text, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from passlib.context import CryptContext
|
||||
from ..core.database import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
# Password hashing context
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
class User(Base):
|
||||
"""Unified user model with authentication and authorization support."""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
# Use UUID to match existing database schema
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
|
||||
username = Column(String(50), unique=True, index=True, nullable=True) # Made nullable for compatibility
|
||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
|
||||
# Extended user information (for backward compatibility)
|
||||
full_name = Column(String(255), nullable=True)
|
||||
role = Column(String(50), nullable=True) # For backward compatibility with existing code
|
||||
|
||||
# User status and permissions
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superuser = Column(Boolean, default=False)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
|
||||
# Timestamps (match existing database schema)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
last_login = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships for authentication features
|
||||
api_keys = relationship("APIKey", back_populates="user", cascade="all, delete-orphan")
|
||||
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
def verify_password(self, password: str) -> bool:
|
||||
"""Verify a password against the hashed password."""
|
||||
return pwd_context.verify(password, self.hashed_password)
|
||||
|
||||
@classmethod
|
||||
def hash_password(cls, password: str) -> str:
|
||||
"""Hash a password for storage."""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def set_password(self, password: str) -> None:
|
||||
"""Set a new password for the user."""
|
||||
self.hashed_password = self.hash_password(password)
|
||||
|
||||
def update_last_login(self) -> None:
|
||||
"""Update the last login timestamp."""
|
||||
self.last_login = datetime.utcnow()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Backward compatibility property for 'name' field."""
|
||||
return self.full_name or self.username or self.email.split('@')[0]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert user to dictionary (excluding sensitive data)."""
|
||||
return {
|
||||
"id": str(self.id), # Convert UUID to string for JSON serialization
|
||||
"username": self.username,
|
||||
"email": self.email,
|
||||
"full_name": self.full_name,
|
||||
"name": self.name, # Backward compatibility
|
||||
"role": self.role,
|
||||
"is_active": self.is_active,
|
||||
"is_superuser": self.is_superuser,
|
||||
"is_verified": self.is_verified,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
"last_login": self.last_login.isoformat() if self.last_login else None,
|
||||
}
|
||||
Reference in New Issue
Block a user