diff --git a/be/app/api/auth/oauth.py b/be/app/api/auth/oauth.py index 13223d6..6a1b624 100644 --- a/be/app/api/auth/oauth.py +++ b/be/app/api/auth/oauth.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, Request -from fastapi.responses import RedirectResponse +from fastapi import APIRouter, Depends, Request, HTTPException, status +from fastapi.responses import RedirectResponse, JSONResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.database import get_transactional_session from app.models import User from app.auth import oauth, fastapi_users, auth_backend, get_jwt_strategy, get_refresh_jwt_strategy from app.config import settings +from fastapi.security import OAuth2PasswordRequestForm router = APIRouter() @@ -92,4 +93,30 @@ async def apple_callback(request: Request, db: AsyncSession = Depends(get_transa # Redirect to frontend with tokens redirect_url = f"{settings.FRONTEND_URL}/auth/callback?access_token={access_token}&refresh_token={refresh_token}" - return RedirectResponse(url=redirect_url) \ No newline at end of file + return RedirectResponse(url=redirect_url) + +@router.post('/jwt/refresh') +async def refresh_jwt_token(request: Request): + data = await request.json() + refresh_token = data.get('refresh_token') + if not refresh_token: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing refresh token") + + refresh_strategy = get_refresh_jwt_strategy() + try: + user = await refresh_strategy.read_token(refresh_token, None) + except Exception: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") + + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") + + access_strategy = get_jwt_strategy() + access_token = await access_strategy.write_token(user) + # Optionally, issue a new refresh token (rotation) + new_refresh_token = await refresh_strategy.write_token(user) + return JSONResponse({ + "access_token": access_token, + "refresh_token": new_refresh_token, + "token_type": "bearer" + }) \ No newline at end of file diff --git a/be/app/api/v1/api.py b/be/app/api/v1/api.py index adc28cc..4d0599e 100644 --- a/be/app/api/v1/api.py +++ b/be/app/api/v1/api.py @@ -9,6 +9,7 @@ from app.api.v1.endpoints import ocr from app.api.v1.endpoints import costs from app.api.v1.endpoints import financials from app.api.v1.endpoints import chores +from app.api.auth import oauth api_router_v1 = APIRouter() @@ -21,5 +22,6 @@ api_router_v1.include_router(ocr.router, prefix="/ocr", tags=["OCR"]) api_router_v1.include_router(costs.router, prefix="/costs", tags=["Costs"]) api_router_v1.include_router(financials.router, prefix="/financials", tags=["Financials"]) api_router_v1.include_router(chores.router, prefix="/chores", tags=["Chores"]) +api_router_v1.include_router(oauth.router, prefix="/auth", tags=["Auth"]) # Add other v1 endpoint routers here later # e.g., api_router_v1.include_router(users.router, prefix="/users", tags=["Users"]) \ No newline at end of file diff --git a/fe/package-lock.json b/fe/package-lock.json index ff478d6..31a8204 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -34,6 +34,7 @@ "@types/date-fns": "^2.5.3", "@types/jsdom": "^21.1.7", "@types/node": "^22.15.17", + "@types/qs": "^6.14.0", "@vitejs/plugin-vue": "^5.2.3", "@vitest/eslint-plugin": "^1.1.39", "@vue/eslint-config-prettier": "^10.2.0", @@ -4291,6 +4292,13 @@ "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", diff --git a/fe/package.json b/fe/package.json index b9253fb..6e9b709 100644 --- a/fe/package.json +++ b/fe/package.json @@ -45,6 +45,7 @@ "@types/date-fns": "^2.5.3", "@types/jsdom": "^21.1.7", "@types/node": "^22.15.17", + "@types/qs": "^6.14.0", "@vitejs/plugin-vue": "^5.2.3", "@vitest/eslint-plugin": "^1.1.39", "@vue/eslint-config-prettier": "^10.2.0", diff --git a/fe/src/services/api.ts b/fe/src/services/api.ts index 9b16cbf..6215ce8 100644 --- a/fe/src/services/api.ts +++ b/fe/src/services/api.ts @@ -58,10 +58,10 @@ api.interceptors.response.use( return Promise.reject(error) } - // Send refresh token in Authorization header as expected by backend - const response = await api.post(API_ENDPOINTS.AUTH.REFRESH, {}, { + // Send refresh token in request body as expected by backend + const response = await api.post(API_ENDPOINTS.AUTH.REFRESH, { refresh_token: refreshTokenValue }, { headers: { - Authorization: `Bearer ${refreshTokenValue}` + 'Content-Type': 'application/json', } })