zephyrdark 1dae2945c3
All checks were successful
Deploy to Production / deploy (push) Successful in 1m31s
feat: client-side password hashing and admin user auto-seeding
- Hash passwords with SHA-256 on frontend before transmission to prevent
  raw password exposure in network traffic and server logs
- Switch login endpoint from OAuth2 form-data to JSON body
- Auto-create admin user on startup from ADMIN_USERNAME/ADMIN_PASSWORD
  env vars, solving login failure after registration was disabled
- Update auth tests to match new SHA-256 + JSON login flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:21:36 +09:00

70 lines
2.0 KiB
Python

"""
Authentication API endpoints.
"""
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import (
verify_password,
get_password_hash,
create_access_token,
)
from app.core.config import get_settings
from app.models.user import User
from app.schemas import Token, UserCreate, UserResponse, LoginRequest
from app.api.deps import CurrentUser
router = APIRouter(prefix="/api/auth", tags=["auth"])
settings = get_settings()
@router.post("/login", response_model=Token)
async def login(
login_data: LoginRequest,
db: Annotated[Session, Depends(get_db)],
):
"""Login and get access token. Expects SHA-256 hashed password."""
user = db.query(User).filter(User.username == login_data.username).first()
if not user or not verify_password(login_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=timedelta(minutes=settings.access_token_expire_minutes),
)
return Token(access_token=access_token)
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserCreate,
db: Annotated[Session, Depends(get_db)],
):
"""Register a new user. Disabled for production."""
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Registration is disabled",
)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(current_user: CurrentUser):
"""Get current user information."""
return current_user
@router.post("/logout")
async def logout():
"""Logout (client should discard token)."""
return {"message": "Successfully logged out"}