feat: Phase 1 - Complete authentication system with JWT

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>
This commit is contained in:
jungwoo choi
2025-10-28 16:23:07 +09:00
parent 161f206ae2
commit f4b75b96a5
51 changed files with 2480 additions and 100 deletions

View File

@ -0,0 +1,89 @@
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