diff --git a/auth.py b/auth.py index feaef5c..174ca31 100644 --- a/auth.py +++ b/auth.py @@ -1,8 +1,12 @@ import httpx -from fastapi import APIRouter, Request, HTTPException +from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.responses import RedirectResponse import os import secrets +from auth.deps import get_current_user +from db import SessionLocal +from models import User +from auth.jwt import create_access_token, create_refresh_token, decode_token router = APIRouter() @@ -28,7 +32,13 @@ def login_google(): ) response = RedirectResponse(url) - response.set_cookie("oauth_state", state, httponly=True) + response.set_cookie( + "oauth_state", + state, + httponly=True, + secure=True, + samesite="lax" + ) return response @router.get("/auth/callback") @@ -57,10 +67,68 @@ async def auth_callback(request: Request, code: str, state: str): GOOGLE_USERINFO_URL, headers={"Authorization": f"Bearer {access_token}"}, ) + user_info = user_res.json() + db = SessionLocal() + + user = db.query(User).filter(User.email == user_info["email"]).first() + + if not user: + user = User( + google_id=user_info["id"], + email=user_info["email"], + name=user_info["name"], + avatar=user_info["picture"], + ) + db.add(user) + db.commit() + db.refresh(user) + + db.close() + + jwt_token = create_access_token({ + "sub": user.id, + "email": user.email, + }) + + refresh_token = create_refresh_token({ + "sub": user.id, + "email": user.email, + }) + return { - "email": user_info["email"], - "name": user_info["name"], - "picture": user_info["picture"], - } \ No newline at end of file + "access_token": jwt_token, + "refresh_token": refresh_token, + "token_type": "bearer", + "user": { + "email": user.email, + "name": user.name, + "avatar": user.avatar, + } + } + +@router.get("/me") +def me(user = Depends(get_current_user)): + return { + "email": user.email, + "name": user.name, + "avatar": user.avatar, + } + +@router.post("/refresh") +def refresh_token(refresh_token: str): + payload = decode_token(refresh_token) + + if payload.get("type") != "refresh": + raise HTTPException(status_code=401) + + user_id = payload.get("sub") + + new_access = create_access_token({"sub": user_id}) + + return {"access_token": new_access} + +@router.post("/logout") +def logout(): + return {"message": "ok"} \ No newline at end of file diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auth/deps.py b/auth/deps.py new file mode 100644 index 0000000..633a850 --- /dev/null +++ b/auth/deps.py @@ -0,0 +1,19 @@ +from fastapi import Request, HTTPException +import jwt +import os + +JWT_SECRET = os.getenv("JWT_SECRET") +JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") + + +def get_current_user(request: Request): + token = request.cookies.get("access_token") + + if not token: + raise HTTPException(status_code=401, detail="Not authenticated") + + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + return payload["user_id"] + except: + raise HTTPException(status_code=401, detail="Invalid token") \ No newline at end of file diff --git a/auth/jwt.py b/auth/jwt.py new file mode 100644 index 0000000..63c3ed3 --- /dev/null +++ b/auth/jwt.py @@ -0,0 +1,53 @@ +import jwt +from datetime import datetime, timedelta, timezone +import os +from fastapi import HTTPException, status + +JWT_SECRET = os.getenv("JWT_SECRET") +JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") + +def now(): + return datetime.now(timezone.utc) + +def create_access_token(data: dict): + payload = { + **data, + "type": "access", + "exp": now() + timedelta(minutes=60), + "iat": now(), + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + +def create_refresh_token(data: dict): + payload = { + **data, + "type": "refresh", + "exp": now() + timedelta(days=7), + "iat": now(), + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + +def decode_token(token: str): + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token expired") + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Invalid token") + +def verify_access_token(token: str): + payload = decode_token(token) + + if payload.get("type") != "access": + raise HTTPException(status_code=401, detail="Invalid access token") + + return payload + +def verify_refresh_token(token: str): + payload = decode_token(token) + + if payload.get("type") != "refresh": + raise HTTPException(status_code=401, detail="Invalid refresh token") + + return payload \ No newline at end of file diff --git a/main.py b/main.py index 0972331..a0aa7c6 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,9 @@ from fastapi import FastAPI from auth import router as auth_router +from models import Base +from db import engine + +Base.metadata.create_all(bind=engine) app = FastAPI()