Skip to content

Commit de87d03

Browse files
committed
feat(auth): iteration-4
1 parent 451a4cf commit de87d03

47 files changed

Lines changed: 3775 additions & 486 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
{
22
"permissions": {
3-
"allow": [
4-
"Bash(python:*)",
5-
"Bash(.specify/scripts/bash/create-new-feature.sh:*)",
6-
"Bash(cat:*)",
7-
"Bash(.specify/scripts/bash/setup-plan.sh:*)"
8-
],
3+
"allow": [],
94
"deny": [],
105
"ask": []
116
},

backend/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from vector_store import vector_store
2222
from rate_limiting import rate_limiter
2323
from session_service import session_service
24+
from src.auth.routes import router as auth_router
2425

2526
# Configure logging
2627
logging.basicConfig(level=logging.INFO)
@@ -273,6 +274,9 @@ async def health_check():
273274
"""
274275
return {"status": "healthy", "timestamp": datetime.utcnow()}
275276

277+
# Include auth routes
278+
app.include_router(auth_router, prefix="/api", tags=["authentication"])
279+
276280
@app.get("/")
277281
async def root():
278282
"""
@@ -285,6 +289,7 @@ async def root():
285289
"/api/chat - Chat endpoint",
286290
"/api/retrieve - Content retrieval",
287291
"/api/sessions/{session_id} - Get session details",
292+
"/api/auth - Authentication endpoints",
288293
"/health - Health check"
289294
]
290295
}

backend/requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ pydantic-settings==2.1.0
1010
alembic==1.13.1
1111
uuid==1.30
1212
tiktoken==0.5.2
13-
requests==2.31.0
13+
requests==2.31.0
14+
bcrypt==4.0.1
15+
pyjwt==2.8.0

backend/src/auth/models.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Authentication and User Profile Models for the Physical AI Book Platform
3+
4+
This module defines the data models for user authentication and profile management,
5+
including user accounts and background information for personalization.
6+
"""
7+
8+
from sqlalchemy import Column, Integer, String, Boolean, DateTime, UUID, ForeignKey
9+
from sqlalchemy.dialects.postgresql import UUID as PostgresUUID
10+
from sqlalchemy.sql import func
11+
from database import Base
12+
import uuid
13+
14+
15+
class User(Base):
16+
"""
17+
User model representing a registered user account with authentication credentials.
18+
This model works with better-auth for authentication while maintaining
19+
compatibility with the existing system.
20+
"""
21+
__tablename__ = "users"
22+
23+
id = Column(PostgresUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
24+
email = Column(String(255), unique=True, nullable=False)
25+
password_hash = Column(String(255), nullable=False)
26+
created_at = Column(DateTime(timezone=True), server_default=func.now())
27+
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
28+
is_active = Column(Boolean, default=True)
29+
email_verified = Column(Boolean, default=False)
30+
31+
def __repr__(self):
32+
return f"<User(id={self.id}, email='{self.email}')>"
33+
34+
35+
class UserProfile(Base):
36+
"""
37+
UserProfile model containing user background information for content personalization.
38+
This model stores software and hardware experience levels that are required at
39+
registration and can be updated later.
40+
"""
41+
__tablename__ = "user_profiles"
42+
43+
id = Column(PostgresUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
44+
user_id = Column(PostgresUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
45+
software_experience = Column(String(20), nullable=False) # beginner, intermediate, advanced
46+
hardware_experience = Column(String(20), nullable=False) # beginner, intermediate, advanced
47+
background_preference = Column(String(20), nullable=False) # software, hardware, mixed
48+
created_at = Column(DateTime(timezone=True), server_default=func.now())
49+
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
50+
51+
def __repr__(self):
52+
return f"<UserProfile(id={self.id}, user_id={self.user_id})>"
53+
54+
def to_dict(self):
55+
"""
56+
Convert the profile to a dictionary representation for API responses.
57+
"""
58+
return {
59+
"id": str(self.id),
60+
"user_id": str(self.user_id),
61+
"software_experience": self.software_experience,
62+
"hardware_experience": self.hardware_experience,
63+
"background_preference": self.background_preference,
64+
"created_at": self.created_at.isoformat() if self.created_at else None,
65+
"updated_at": self.updated_at.isoformat() if self.updated_at else None
66+
}

backend/src/auth/routes.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""
2+
Authentication Routes for the Physical AI Book Platform
3+
4+
This module defines the API endpoints for user authentication and profile management,
5+
including registration, login, logout, and profile operations.
6+
"""
7+
8+
from fastapi import APIRouter, Depends, HTTPException, status, Request
9+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
10+
from sqlalchemy.orm import Session
11+
from typing import Optional
12+
from database import get_db
13+
from .models import User
14+
from .services import auth_service
15+
from pydantic import BaseModel
16+
import json
17+
18+
19+
router = APIRouter(prefix="/auth", tags=["authentication"])
20+
21+
# Define request/response models
22+
class RegisterRequest(BaseModel):
23+
email: str
24+
password: str
25+
software_experience: str
26+
hardware_experience: str
27+
background_preference: str
28+
29+
class LoginRequest(BaseModel):
30+
email: str
31+
password: str
32+
33+
class UpdateProfileRequest(BaseModel):
34+
software_experience: Optional[str] = None
35+
hardware_experience: Optional[str] = None
36+
background_preference: Optional[str] = None
37+
38+
39+
# HTTP Bearer token for authentication
40+
security = HTTPBearer()
41+
42+
43+
def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)):
44+
"""
45+
Get the current user ID from the authorization token.
46+
"""
47+
token = credentials.credentials
48+
user_id = auth_service.verify_session_token(token)
49+
50+
if not user_id:
51+
raise HTTPException(
52+
status_code=status.HTTP_401_UNAUTHORIZED,
53+
detail="Invalid or expired token",
54+
headers={"WWW-Authenticate": "Bearer"},
55+
)
56+
57+
# Verify user still exists in database
58+
user = db.query(User).filter(User.id == user_id, User.is_active == True).first()
59+
if not user:
60+
raise HTTPException(
61+
status_code=status.HTTP_401_UNAUTHORIZED,
62+
detail="User no longer exists",
63+
headers={"WWW-Authenticate": "Bearer"},
64+
)
65+
66+
return user_id
67+
68+
69+
@router.post("/register", status_code=status.HTTP_201_CREATED)
70+
async def register(request: RegisterRequest, db: Session = Depends(get_db)):
71+
"""
72+
Register a new user with profile information.
73+
74+
Creates a new user account and associated profile with background information.
75+
"""
76+
result = auth_service.register_user(
77+
db=db,
78+
email=request.email,
79+
password=request.password,
80+
software_experience=request.software_experience,
81+
hardware_experience=request.hardware_experience,
82+
background_preference=request.background_preference
83+
)
84+
85+
if not result["success"]:
86+
raise HTTPException(
87+
status_code=status.HTTP_400_BAD_REQUEST,
88+
detail=result["error"]
89+
)
90+
91+
# Create session token for auto-login
92+
token = auth_service.create_session_token(result["user"]["id"])
93+
94+
return {
95+
"success": True,
96+
"message": "User registered successfully",
97+
"user": result["user"],
98+
"profile": result["profile"],
99+
"token": token
100+
}
101+
102+
103+
@router.post("/login")
104+
async def login(request: LoginRequest, db: Session = Depends(get_db)):
105+
"""
106+
Authenticate user and create session.
107+
108+
Authenticates user credentials and returns a session token.
109+
"""
110+
result = auth_service.authenticate_user(
111+
db=db,
112+
email=request.email,
113+
password=request.password
114+
)
115+
116+
if not result["success"]:
117+
raise HTTPException(
118+
status_code=status.HTTP_401_UNAUTHORIZED,
119+
detail=result["error"]
120+
)
121+
122+
# Create session token
123+
token = auth_service.create_session_token(result["user"]["id"])
124+
125+
return {
126+
"success": True,
127+
"message": "Login successful",
128+
"user": result["user"],
129+
"profile": result["profile"],
130+
"token": token
131+
}
132+
133+
134+
@router.post("/logout")
135+
async def logout(credentials: HTTPAuthorizationCredentials = Depends(security)):
136+
"""
137+
Logout user and destroy session.
138+
139+
In a stateless JWT system, this is typically handled on the client side.
140+
This endpoint can be used to notify the server of logout.
141+
"""
142+
# In a real system, you might add the token to a blacklist
143+
return {
144+
"success": True,
145+
"message": "Logout successful"
146+
}
147+
148+
149+
@router.get("/profile")
150+
async def get_profile(current_user_id: str = Depends(get_current_user_id), db: Session = Depends(get_db)):
151+
"""
152+
Get current user's profile.
153+
154+
Retrieves the profile information for the authenticated user.
155+
"""
156+
result = auth_service.get_user_profile(db=db, user_id=current_user_id)
157+
158+
if not result["success"]:
159+
raise HTTPException(
160+
status_code=status.HTTP_400_BAD_REQUEST,
161+
detail=result["error"]
162+
)
163+
164+
return {
165+
"success": True,
166+
"profile": result["profile"]
167+
}
168+
169+
170+
@router.put("/profile")
171+
async def update_profile(
172+
request: UpdateProfileRequest,
173+
current_user_id: str = Depends(get_current_user_id),
174+
db: Session = Depends(get_db)
175+
):
176+
"""
177+
Update current user's profile.
178+
179+
Updates the profile information for the authenticated user.
180+
"""
181+
result = auth_service.update_user_profile(
182+
db=db,
183+
user_id=current_user_id,
184+
software_experience=request.software_experience,
185+
hardware_experience=request.hardware_experience,
186+
background_preference=request.background_preference
187+
)
188+
189+
if not result["success"]:
190+
raise HTTPException(
191+
status_code=status.HTTP_400_BAD_REQUEST,
192+
detail=result["error"]
193+
)
194+
195+
return {
196+
"success": True,
197+
"profile": result["profile"]
198+
}

0 commit comments

Comments
 (0)