refactor: rename canto-backend → backend, canto-frontend → frontend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 18:11:00 +08:00
parent 2fa9c1fcb6
commit 60489eab59
327 changed files with 0 additions and 0 deletions

216
backend/api/auth.py Normal file
View File

@@ -0,0 +1,216 @@
from datetime import timedelta
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from slowapi import Limiter
from slowapi.util import get_remote_address
from config import settings
from core.security import (
get_password_hash,
verify_password,
create_access_token,
decode_access_token
)
from db.database import get_db
from db.crud import get_user_by_username, get_user_by_email, create_user, change_user_password, get_user_preferences, update_user_preferences, can_user_use_nsfw, get_system_setting
from schemas.user import User, UserCreate, Token, PasswordChange, UserPreferences, UserPreferencesResponse
from schemas.audiobook import LLMConfigResponse
router = APIRouter(prefix="/auth", tags=["authentication"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token", auto_error=not settings.DEV_MODE)
limiter = Limiter(key_func=get_remote_address)
async def get_current_user(
token: Annotated[Optional[str], Depends(oauth2_scheme)],
db: Session = Depends(get_db)
) -> User:
if settings.DEV_MODE and not token:
user = get_user_by_username(db, username="admin")
if user:
return user
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
if token is None:
raise credentials_exception
username = decode_access_token(token)
if username is None:
raise credentials_exception
user = get_user_by_username(db, username=username)
if user is None:
raise credentials_exception
return user
@router.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
@limiter.limit("5/minute")
async def register(request: Request, user_data: UserCreate, db: Session = Depends(get_db)):
existing_user = get_user_by_username(db, username=user_data.username)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered"
)
existing_email = get_user_by_email(db, email=user_data.email)
if existing_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
hashed_password = get_password_hash(user_data.password)
user = create_user(
db,
username=user_data.username,
email=user_data.email,
hashed_password=hashed_password
)
return user
@router.post("/token", response_model=Token)
@limiter.limit("5/minute")
async def login(
request: Request,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Session = Depends(get_db)
):
user = get_user_by_username(db, username=form_data.username)
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/dev-token", response_model=Token)
async def dev_token(db: Session = Depends(get_db)):
if not settings.DEV_MODE:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not available outside DEV_MODE")
user = get_user_by_username(db, username="admin")
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Admin user not found")
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/me", response_model=User)
@limiter.limit("30/minute")
async def get_current_user_info(
request: Request,
current_user: Annotated[User, Depends(get_current_user)]
):
return current_user
@router.post("/change-password", response_model=User)
@limiter.limit("5/minute")
async def change_password(
request: Request,
password_data: PasswordChange,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db)
):
if not verify_password(password_data.current_password, current_user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect"
)
new_hashed_password = get_password_hash(password_data.new_password)
user = change_user_password(
db,
user_id=current_user.id,
new_hashed_password=new_hashed_password
)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
@router.get("/preferences", response_model=UserPreferencesResponse)
@limiter.limit("30/minute")
async def get_preferences(
request: Request,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db)
):
prefs = get_user_preferences(db, current_user.id)
return {
"default_backend": "local",
"onboarding_completed": prefs.get("onboarding_completed", False),
"available_backends": ["local"]
}
@router.put("/preferences")
@limiter.limit("10/minute")
async def update_preferences(
request: Request,
preferences: UserPreferences,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db)
):
updated_user = update_user_preferences(
db,
current_user.id,
preferences.model_dump()
)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {"message": "Preferences updated successfully"}
@router.get("/llm-config", response_model=LLMConfigResponse)
@limiter.limit("30/minute")
async def get_llm_config(
request: Request,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db)
):
return LLMConfigResponse(
base_url=get_system_setting(db, "llm_base_url"),
model=get_system_setting(db, "llm_model"),
has_key=bool(get_system_setting(db, "llm_api_key")),
)
@router.get("/nsfw-access")
@limiter.limit("30/minute")
async def get_nsfw_access(
request: Request,
current_user: Annotated[User, Depends(get_current_user)],
):
return {"has_access": can_user_use_nsfw(current_user)}