playreel/backend/routers/auth.py
2026-09-16 13:04:49 +09:00

114 lines
3.9 KiB
Python

"""/api/auth — 구글 로그인과 세션
브라우저가 구글에서 받아온 credential을 한 번 검증하고, 그 뒤로는 자체 쿠키를 쓴다.
"""
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from tables.user import User
from utils import session_token
from utils.database import get_session
from utils.google_identity import (GoogleLoginDisabled, GoogleTokenInvalid, is_enabled,
verify_id_token)
router = APIRouter(prefix="/api/auth", tags=["auth"])
class GoogleLoginBody(BaseModel):
credential: str
def user_view(user: User) -> dict:
"""구글 sub는 내보내지 않는다 — 화면이 쓸 일이 없다"""
return {
"email": user.email,
"name": user.name,
"picture": user.picture_url,
"jobs_created": user.jobs_created,
"job_limit": user.job_limit,
"jobs_left": user.jobs_left,
}
async def find_or_create(session: AsyncSession, account) -> User:
user = await session.get(User, account.sub)
if user is None:
user = User(id=account.sub, email=account.email, name=account.name,
picture_url=account.picture)
session.add(user)
else:
# 이메일·이름·사진은 구글 쪽에서 바뀔 수 있어 로그인할 때마다 맞춘다
user.email, user.name, user.picture_url = (account.email, account.name,
account.picture)
await session.commit()
return user
@router.get("/config")
async def config():
"""프론트가 버튼을 그릴지 정하는 데 쓴다. client_id는 비밀이 아니다"""
from settings import settings
return {"enabled": is_enabled(), "client_id": settings.google_client_id}
@router.post("/google")
async def google_login(body: GoogleLoginBody, response: Response,
session: AsyncSession = Depends(get_session)):
try:
account = await verify_id_token(body.credential)
except GoogleLoginDisabled:
raise HTTPException(503, "구글 로그인이 설정되지 않았습니다")
except GoogleTokenInvalid as failure:
raise HTTPException(401, str(failure))
user = await find_or_create(session, account)
response.set_cookie(
session_token.COOKIE_NAME, session_token.issue(user.id),
max_age=session_token.cookie_max_age(),
httponly=True, samesite="lax", path="/",
)
return user_view(user)
@router.get("/me")
async def me(request: Request, session: AsyncSession = Depends(get_session)):
user = await optional_user(request, session)
if user is None:
raise HTTPException(401, "로그인이 필요합니다")
return user_view(user)
@router.post("/logout")
async def logout(response: Response):
response.delete_cookie(session_token.COOKIE_NAME, path="/")
return {"ok": True}
async def optional_user(request: Request, session: AsyncSession) -> User | None:
token = request.cookies.get(session_token.COOKIE_NAME)
if not token:
return None
try:
user_id = session_token.read(token)
except session_token.SessionInvalid:
return None
return await session.get(User, user_id)
async def current_user(request: Request,
session: AsyncSession = Depends(get_session)) -> User:
"""로그인이 필요한 엔드포인트가 의존성으로 받는다"""
user = await optional_user(request, session)
if user is None:
raise HTTPException(401, "로그인이 필요합니다")
return user
async def creating_user(user: User = Depends(current_user)) -> User:
"""잡을 만드는 엔드포인트용 — 한도를 여기서 막는다"""
if not user.can_create_job:
raise HTTPException(
403, f"만들 수 있는 개수를 다 썼습니다 ({user.jobs_created}/{user.job_limit})")
return user