114 lines
3.9 KiB
Python
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
|