Backend Implementation (FastAPI + MongoDB): - JWT authentication with access/refresh tokens - User registration and login endpoints - Password hashing with bcrypt (fixed 72-byte limit) - Protected endpoints with JWT middleware - Token refresh mechanism - Role-Based Access Control (RBAC) structure - Pydantic v2 models and async MongoDB with Motor - API endpoints: /api/auth/register, /api/auth/login, /api/auth/me, /api/auth/refresh Frontend Implementation (React + TypeScript + Material-UI): - Login and Register pages with validation - AuthContext for global authentication state - API client with Axios interceptors for token refresh - Protected routes with automatic redirect - User profile display in navigation - Logout functionality Technical Achievements: - Resolved bcrypt 72-byte limit (replaced passlib with native bcrypt) - Fixed Pydantic v2 compatibility (PyObjectId, ConfigDict) - Implemented automatic token refresh on 401 errors - Created comprehensive test suite for all auth endpoints Docker & Kubernetes: - Backend image: yakenator/site11-console-backend:latest - Frontend image: yakenator/site11-console-frontend:latest - Deployed to site11-pipeline namespace - Nginx reverse proxy configuration Documentation: - CONSOLE_ARCHITECTURE.md - Complete system architecture - PHASE1_COMPLETION.md - Detailed completion report - PROGRESS.md - Updated with Phase 1 status All authentication endpoints tested and verified working. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
90 lines
3.0 KiB
Python
90 lines
3.0 KiB
Python
from datetime import datetime
|
|
from typing import Optional, List, Annotated
|
|
from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
|
|
from pydantic_core import core_schema
|
|
from bson import ObjectId
|
|
|
|
|
|
class PyObjectId(str):
|
|
"""Custom ObjectId type for Pydantic v2"""
|
|
|
|
@classmethod
|
|
def __get_pydantic_core_schema__(cls, source_type, handler):
|
|
return core_schema.union_schema([
|
|
core_schema.is_instance_schema(ObjectId),
|
|
core_schema.chain_schema([
|
|
core_schema.str_schema(),
|
|
core_schema.no_info_plain_validator_function(cls.validate),
|
|
])
|
|
],
|
|
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
lambda x: str(x)
|
|
))
|
|
|
|
@classmethod
|
|
def validate(cls, v):
|
|
if isinstance(v, ObjectId):
|
|
return v
|
|
if isinstance(v, str) and ObjectId.is_valid(v):
|
|
return ObjectId(v)
|
|
raise ValueError("Invalid ObjectId")
|
|
|
|
|
|
class UserRole(str):
|
|
"""User roles"""
|
|
ADMIN = "admin"
|
|
EDITOR = "editor"
|
|
VIEWER = "viewer"
|
|
|
|
|
|
class OAuthProvider(BaseModel):
|
|
"""OAuth provider information"""
|
|
provider: str = Field(..., description="OAuth provider name (google, github, azure)")
|
|
provider_user_id: str = Field(..., description="User ID from the provider")
|
|
access_token: Optional[str] = Field(None, description="Access token (encrypted)")
|
|
refresh_token: Optional[str] = Field(None, description="Refresh token (encrypted)")
|
|
|
|
|
|
class UserProfile(BaseModel):
|
|
"""User profile information"""
|
|
avatar_url: Optional[str] = None
|
|
department: Optional[str] = None
|
|
timezone: str = "Asia/Seoul"
|
|
|
|
|
|
class User(BaseModel):
|
|
"""User model"""
|
|
id: Optional[PyObjectId] = Field(alias="_id", default=None)
|
|
email: EmailStr = Field(..., description="User email")
|
|
username: str = Field(..., min_length=3, max_length=50, description="Username")
|
|
hashed_password: str = Field(..., description="Hashed password")
|
|
full_name: Optional[str] = Field(None, description="Full name")
|
|
role: str = Field(default=UserRole.VIEWER, description="User role")
|
|
permissions: List[str] = Field(default_factory=list, description="User permissions")
|
|
oauth_providers: List[OAuthProvider] = Field(default_factory=list)
|
|
profile: UserProfile = Field(default_factory=UserProfile)
|
|
status: str = Field(default="active", description="User status")
|
|
is_active: bool = Field(default=True)
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
last_login_at: Optional[datetime] = None
|
|
|
|
model_config = ConfigDict(
|
|
populate_by_name=True,
|
|
arbitrary_types_allowed=True,
|
|
json_encoders={ObjectId: str},
|
|
json_schema_extra={
|
|
"example": {
|
|
"email": "user@example.com",
|
|
"username": "johndoe",
|
|
"full_name": "John Doe",
|
|
"role": "viewer"
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
class UserInDB(User):
|
|
"""User model with password hash"""
|
|
pass
|