Compare commits

...

4 Commits

Author SHA1 Message Date
Zayden-Jung bc67f5a579 *.log 2026-05-15 22:59:32 +08:00
Zayden-Jung 1c513a2f3e login with JWT 2026-05-15 22:58:53 +08:00
Zayden-Jung d14fe10ea7 Google OAuthbase version 2026-05-15 21:46:18 +08:00
Zayden-Jung 6f534aafce fix Dockerfile by adapting uv package management 2026-05-15 21:31:03 +08:00
8 changed files with 188 additions and 20 deletions
+4 -1
View File
@@ -9,4 +9,7 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
.env .env
*.json
*.log
+15 -2
View File
@@ -2,8 +2,21 @@ FROM python:3.14-slim
WORKDIR /app WORKDIR /app
# 安装系统依赖(curl + 构建依赖)
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 安装 uv
RUN pip install --no-cache-dir uv
# 先复制依赖文件
COPY pyproject.toml uv.lock ./
# 用 uv 安装依赖
RUN uv sync --system
# 再复制代码
COPY . . COPY . .
RUN pip install --no-cache-dir fastapi uvicorn sqlalchemy psycopg2-binary httpx python-jose
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+90 -15
View File
@@ -1,59 +1,134 @@
import httpx import httpx
from fastapi import APIRouter, Request from fastapi import APIRouter, Depends, Request, HTTPException
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
import os 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() router = APIRouter()
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
REDIRECT_URI = "http://localhost:8000/auth/callback"
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo" GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"
@router.get("/login/google") @router.get("/login/google")
def login_google(): def login_google():
state = secrets.token_urlsafe(16)
url = ( url = (
f"{GOOGLE_AUTH_URL}" f"{GOOGLE_AUTH_URL}"
f"?client_id={GOOGLE_CLIENT_ID}" f"?client_id={GOOGLE_CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}" f"&redirect_uri={GOOGLE_REDIRECT_URI}"
f"&response_type=code" f"&response_type=code"
f"&scope=openid email profile" f"&scope=openid email profile"
f"&state={state}"
) )
return RedirectResponse(url)
response = RedirectResponse(url)
response.set_cookie(
"oauth_state",
state,
httponly=True,
secure=True,
samesite="lax"
)
return response
@router.get("/auth/callback") @router.get("/auth/callback")
async def auth_callback(code: str): async def auth_callback(request: Request, code: str, state: str):
cookie_state = request.cookies.get("oauth_state")
if not cookie_state or cookie_state != state:
raise HTTPException(status_code=400, detail="Invalid state")
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
# 1. 用 code 换 token
token_res = await client.post( token_res = await client.post(
GOOGLE_TOKEN_URL, GOOGLE_TOKEN_URL,
data={ data={
"code": code, "code": code,
"client_id": GOOGLE_CLIENT_ID, "client_id": GOOGLE_CLIENT_ID,
"client_secret": GOOGLE_CLIENT_SECRET, "client_secret": GOOGLE_CLIENT_SECRET,
"redirect_uri": REDIRECT_URI, "redirect_uri": GOOGLE_REDIRECT_URI,
"grant_type": "authorization_code", "grant_type": "authorization_code",
}, },
) )
token_json = token_res.json() token_json = token_res.json()
access_token = token_json["access_token"] access_token = token_json["access_token"]
# 2. 获取用户信息
user_res = await client.get( user_res = await client.get(
GOOGLE_USERINFO_URL, GOOGLE_USERINFO_URL,
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
) )
user_info = user_res.json() 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 { return {
"email": user_info["email"], "access_token": jwt_token,
"name": user_info["name"], "refresh_token": refresh_token,
"picture": user_info["picture"], "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"}
View File
+19
View File
@@ -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")
+53
View File
@@ -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
+3 -2
View File
@@ -24,8 +24,9 @@ services:
- db - db
environment: environment:
DATABASE_URL: postgresql://${SERVICE_USER_POSTGRESQL}:${SERVICE_PASSWORD_POSTGRESQL}@db:5432/roleplay DATABASE_URL: postgresql://${SERVICE_USER_POSTGRESQL}:${SERVICE_PASSWORD_POSTGRESQL}@db:5432/roleplay
GOOGLE_CLIENT_ID: your_client_id GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: your_secret GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI}
ports: ports:
- "8000:8000" - "8000:8000"
+4
View File
@@ -1,5 +1,9 @@
from fastapi import FastAPI from fastapi import FastAPI
from auth import router as auth_router from auth import router as auth_router
from models import Base
from db import engine
Base.metadata.create_all(bind=engine)
app = FastAPI() app = FastAPI()