mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
fix(users):expose current user session routes
This commit is contained in:
parent
a547cfe3c3
commit
2b6bf504ec
3 changed files with 90 additions and 43 deletions
|
|
@ -28,6 +28,7 @@ from app.agents.chat.runtime.checkpointer import (
|
||||||
setup_checkpointer_tables,
|
setup_checkpointer_tables,
|
||||||
)
|
)
|
||||||
from app.auth.context import AuthContext
|
from app.auth.context import AuthContext
|
||||||
|
from app.auth.csrf import CsrfOriginMiddleware
|
||||||
from app.config import (
|
from app.config import (
|
||||||
config,
|
config,
|
||||||
initialize_image_gen_router,
|
initialize_image_gen_router,
|
||||||
|
|
@ -54,7 +55,10 @@ from app.observability.bootstrap import init_otel, shutdown_otel
|
||||||
from app.rate_limiter import get_real_client_ip, limiter
|
from app.rate_limiter import get_real_client_ip, limiter
|
||||||
from app.routes import router as crud_router
|
from app.routes import router as crud_router
|
||||||
from app.routes.auth_routes import router as auth_router
|
from app.routes.auth_routes import router as auth_router
|
||||||
from app.schemas import UserCreate, UserRead, UserUpdate
|
from app.routes.auth_routes import session_router
|
||||||
|
from app.routes.users_routes import router as users_router
|
||||||
|
from app.routes.zero_context_routes import router as zero_context_router
|
||||||
|
from app.schemas import UserCreate, UserRead
|
||||||
from app.session_events import register_session_hooks
|
from app.session_events import register_session_hooks
|
||||||
from app.users import SECRET, allow_any_principal, auth_backend, fastapi_users
|
from app.users import SECRET, allow_any_principal, auth_backend, fastapi_users
|
||||||
from app.utils.perf import log_system_snapshot
|
from app.utils.perf import log_system_snapshot
|
||||||
|
|
@ -817,6 +821,7 @@ app.add_middleware(
|
||||||
# FRONTEND_URL to BACKEND_URL.
|
# FRONTEND_URL to BACKEND_URL.
|
||||||
max_age=86400,
|
max_age=86400,
|
||||||
)
|
)
|
||||||
|
app.add_middleware(CsrfOriginMiddleware)
|
||||||
|
|
||||||
# Password / email-based auth routers are only mounted when not running in
|
# Password / email-based auth routers are only mounted when not running in
|
||||||
# Google-OAuth-only mode. Mounting them in OAuth-only prod previously left
|
# Google-OAuth-only mode. Mounting them in OAuth-only prod previously left
|
||||||
|
|
@ -855,16 +860,14 @@ if config.AUTH_TYPE != "GOOGLE":
|
||||||
tags=["auth"],
|
tags=["auth"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# /users/me (read/update profile) is needed in every auth mode, so it stays
|
# /users/me uses the unified auth resolver so web cookie sessions, desktop bearer
|
||||||
# mounted unconditionally.
|
# sessions, and PAT principals all resolve through the same authority.
|
||||||
app.include_router(
|
app.include_router(users_router)
|
||||||
fastapi_users.get_users_router(UserRead, UserUpdate),
|
|
||||||
prefix="/users",
|
|
||||||
tags=["users"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include custom auth routes (refresh token, logout)
|
# Include custom auth routes (refresh token, logout)
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
app.include_router(session_router)
|
||||||
|
app.include_router(zero_context_router)
|
||||||
|
|
||||||
if config.AUTH_TYPE == "GOOGLE":
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
|
||||||
27
surfsense_backend/app/routes/users_routes.py
Normal file
27
surfsense_backend/app/routes/users_routes.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Cookie-aware user profile routes."""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
|
||||||
|
from app.auth.context import AuthContext
|
||||||
|
from app.schemas import UserRead, UserUpdate
|
||||||
|
from app.users import UserManager, get_auth_context, get_user_manager, require_session_context
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/users", tags=["users"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserRead)
|
||||||
|
async def get_current_user_profile(
|
||||||
|
auth: AuthContext = Depends(get_auth_context),
|
||||||
|
):
|
||||||
|
return auth.user
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/me", response_model=UserRead)
|
||||||
|
async def update_current_user_profile(
|
||||||
|
update: UserUpdate,
|
||||||
|
request: Request,
|
||||||
|
auth: AuthContext = Depends(require_session_context),
|
||||||
|
user_manager: UserManager = Depends(get_user_manager),
|
||||||
|
):
|
||||||
|
updated_user = await user_manager.update(update, auth.user, safe=True, request=request)
|
||||||
|
return updated_user
|
||||||
|
|
@ -17,6 +17,7 @@ from sqlalchemy import update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.auth.context import AuthContext
|
from app.auth.context import AuthContext
|
||||||
|
from app.auth.session_cookies import write_session
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.db import (
|
from app.db import (
|
||||||
Prompt,
|
Prompt,
|
||||||
|
|
@ -40,6 +41,7 @@ class BearerResponse(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
refresh_token: str
|
refresh_token: str
|
||||||
token_type: str
|
token_type: str
|
||||||
|
access_expires_at: int
|
||||||
|
|
||||||
|
|
||||||
SECRET = config.SECRET_KEY
|
SECRET = config.SECRET_KEY
|
||||||
|
|
@ -263,10 +265,12 @@ class CustomBearerTransport(BearerTransport):
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
# Decode JWT to get user_id for refresh token creation
|
# Decode JWT to get user_id for refresh token creation
|
||||||
|
access_expires_at = 0
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token, SECRET, algorithms=["HS256"], options={"verify_aud": False}
|
token, SECRET, algorithms=["HS256"], options={"verify_aud": False}
|
||||||
)
|
)
|
||||||
|
access_expires_at = int(payload["exp"])
|
||||||
user_id = uuid.UUID(payload.get("sub"))
|
user_id = uuid.UUID(payload.get("sub"))
|
||||||
refresh_token = await create_refresh_token(user_id)
|
refresh_token = await create_refresh_token(user_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -278,17 +282,28 @@ class CustomBearerTransport(BearerTransport):
|
||||||
access_token=token,
|
access_token=token,
|
||||||
refresh_token=refresh_token,
|
refresh_token=refresh_token,
|
||||||
token_type="bearer",
|
token_type="bearer",
|
||||||
|
access_expires_at=access_expires_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.AUTH_TYPE == "GOOGLE":
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
redirect_url = (
|
response = RedirectResponse(
|
||||||
f"{config.NEXT_FRONTEND_URL}/auth/callback"
|
f"{config.NEXT_FRONTEND_URL}/auth/callback",
|
||||||
f"?token={bearer_response.access_token}"
|
status_code=302,
|
||||||
f"&refresh_token={bearer_response.refresh_token}"
|
|
||||||
)
|
)
|
||||||
return RedirectResponse(redirect_url, status_code=302)
|
write_session(
|
||||||
|
response,
|
||||||
|
bearer_response.access_token,
|
||||||
|
bearer_response.refresh_token,
|
||||||
|
)
|
||||||
|
return response
|
||||||
else:
|
else:
|
||||||
return JSONResponse(bearer_response.model_dump())
|
response = JSONResponse(bearer_response.model_dump())
|
||||||
|
write_session(
|
||||||
|
response,
|
||||||
|
bearer_response.access_token,
|
||||||
|
bearer_response.refresh_token,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
bearer_transport = CustomBearerTransport(tokenUrl="auth/jwt/login")
|
bearer_transport = CustomBearerTransport(tokenUrl="auth/jwt/login")
|
||||||
|
|
@ -315,38 +330,42 @@ async def get_auth_context(
|
||||||
receives the full SurfSense principal instead of a bare User.
|
receives the full SurfSense principal instead of a bare User.
|
||||||
"""
|
"""
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if not auth_header:
|
if auth_header:
|
||||||
raise HTTPException(
|
scheme, _, credential = auth_header.partition(" ")
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
is_bearer = scheme.lower() == "bearer" and bool(credential)
|
||||||
detail="Unauthorized",
|
token = credential if is_bearer else auth_header.strip()
|
||||||
)
|
|
||||||
|
|
||||||
scheme, _, token = auth_header.partition(" ")
|
if token.startswith(PAT_PREFIX):
|
||||||
if scheme.lower() != "bearer" or not token:
|
pat = await resolve_pat(session, token)
|
||||||
raise HTTPException(
|
if pat and pat.user and pat.user.is_active:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
maybe_touch_last_used(pat)
|
||||||
detail="Unauthorized",
|
return AuthContext.pat_auth(pat.user, pat)
|
||||||
)
|
|
||||||
|
|
||||||
if token.startswith(PAT_PREFIX):
|
if is_bearer:
|
||||||
pat = await resolve_pat(session, token)
|
try:
|
||||||
if pat and pat.user and pat.user.is_active:
|
user = await get_jwt_strategy().read_token(token, user_manager)
|
||||||
maybe_touch_last_used(pat)
|
except Exception:
|
||||||
return AuthContext.pat_auth(pat.user, pat)
|
logger.exception("Failed to read bearer access token")
|
||||||
|
user = None
|
||||||
|
|
||||||
try:
|
if user and user.is_active:
|
||||||
user = await get_jwt_strategy().read_token(token, user_manager)
|
return AuthContext.session(user)
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to read access token")
|
|
||||||
user = None
|
|
||||||
|
|
||||||
if not user or not user.is_active:
|
cookie_token = request.cookies.get(config.SESSION_COOKIE_NAME)
|
||||||
raise HTTPException(
|
if cookie_token:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
try:
|
||||||
detail="Unauthorized",
|
user = await get_jwt_strategy().read_token(cookie_token, user_manager)
|
||||||
)
|
except Exception:
|
||||||
|
logger.exception("Failed to read session cookie access token")
|
||||||
|
user = None
|
||||||
|
|
||||||
return AuthContext.session(user)
|
if user and user.is_active:
|
||||||
|
return AuthContext.session(user)
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Unauthorized",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def allow_any_principal(
|
async def allow_any_principal(
|
||||||
|
|
@ -372,5 +391,3 @@ async def require_session_context(
|
||||||
)
|
)
|
||||||
return auth
|
return auth
|
||||||
|
|
||||||
|
|
||||||
current_optional_user = fastapi_users.current_user(active=True, optional=True)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue