Refactor /api/user/* to its own module.

This commit is contained in:
Leo Vasanko
2025-10-04 18:41:14 -06:00
parent 876215f1c1
commit 1ad1644b64
2 changed files with 143 additions and 122 deletions

View File

@@ -1,10 +1,8 @@
import logging
from contextlib import suppress
from datetime import datetime, timedelta, timezone
from uuid import UUID
from fastapi import (
Body,
Cookie,
Depends,
FastAPI,
@@ -21,8 +19,6 @@ from passkey.util import frontend, useragent
from .. import aaguid
from ..authsession import (
EXPIRES,
delete_credential,
expires,
get_reset,
get_session,
refresh_session_token,
@@ -30,14 +26,16 @@ from ..authsession import (
)
from ..globals import db
from ..globals import passkey as global_passkey
from ..util import hostutil, passphrase, permutil, tokens
from ..util.tokens import decode_session_key, encode_session_key, session_key
from . import authz, session
from ..util import hostutil, passphrase, permutil
from ..util.tokens import encode_session_key, session_key
from . import authz, session, user
bearer_auth = HTTPBearer(auto_error=True)
app = FastAPI()
app.mount("/user", user.app)
@app.exception_handler(HTTPException)
async def http_exception_handler(_request: Request, exc: HTTPException):
@@ -361,28 +359,6 @@ async def api_user_info(
}
@app.put("/user/display-name")
async def user_update_display_name(
request: Request,
response: Response,
payload: dict = Body(...),
auth=Cookie(None, alias="__Host-auth"),
):
if not auth:
raise HTTPException(status_code=401, detail="Authentication Required")
try:
s = await get_session(auth, host=request.headers.get("host"))
except ValueError as e:
raise HTTPException(status_code=401, detail="Session expired") from e
new_name = (payload.get("display_name") or "").strip()
if not new_name:
raise HTTPException(status_code=400, detail="display_name required")
if len(new_name) > 64:
raise HTTPException(status_code=400, detail="display_name too long")
await db.instance.update_user_display_name(s.user_uuid, new_name)
return {"status": "ok"}
@app.post("/logout")
async def api_logout(
request: Request, response: Response, auth=Cookie(None, alias="__Host-auth")
@@ -399,53 +375,6 @@ async def api_logout(
return {"message": "Logged out successfully"}
@app.post("/user/logout-all")
async def api_logout_all(
request: Request, response: Response, auth=Cookie(None, alias="__Host-auth")
):
if not auth:
return {"message": "Already logged out"}
try:
s = await get_session(auth, host=request.headers.get("host"))
except ValueError:
raise HTTPException(status_code=401, detail="Session expired")
await db.instance.delete_sessions_for_user(s.user_uuid)
session.clear_session_cookie(response)
return {"message": "Logged out from all hosts"}
@app.delete("/user/session/{session_id}")
async def api_delete_session(
request: Request,
response: Response,
session_id: str,
auth=Cookie(None, alias="__Host-auth"),
):
if not auth:
raise HTTPException(status_code=401, detail="Authentication Required")
try:
current_session = await get_session(auth, host=request.headers.get("host"))
except ValueError as exc:
raise HTTPException(status_code=401, detail="Session expired") from exc
try:
target_key = decode_session_key(session_id)
except ValueError as exc:
raise HTTPException(
status_code=400, detail="Invalid session identifier"
) from exc
target_session = await db.instance.get_session(target_key)
if not target_session or target_session.user_uuid != current_session.user_uuid:
raise HTTPException(status_code=404, detail="Session not found")
await db.instance.delete_session(target_key)
current_terminated = target_key == session_key(auth)
if current_terminated:
session.clear_session_cookie(response) # explicit because 200
return {"status": "ok", "current_session_terminated": current_terminated}
@app.post("/set-session")
async def api_set_session(
request: Request, response: Response, auth=Depends(bearer_auth)
@@ -456,49 +385,3 @@ async def api_set_session(
"message": "Session cookie set successfully",
"user_uuid": str(user.user_uuid),
}
@app.delete("/user/credential/{uuid}")
async def api_delete_credential(
request: Request,
response: Response,
uuid: UUID,
auth: str = Cookie(None, alias="__Host-auth"),
):
try:
await delete_credential(uuid, auth, host=request.headers.get("host"))
except ValueError as e:
raise HTTPException(status_code=401, detail="Session expired") from e
return {"message": "Credential deleted successfully"}
@app.post("/user/create-link")
async def api_create_link(
request: Request,
response: Response,
auth=Cookie(None, alias="__Host-auth"),
):
try:
s = await get_session(auth, host=request.headers.get("host"))
except ValueError as e:
raise HTTPException(status_code=401, detail="Session expired") from e
token = passphrase.generate()
expiry = expires()
await db.instance.create_reset_token(
user_uuid=s.user_uuid,
key=tokens.reset_key(token),
expiry=expiry,
token_type="device addition",
)
url = hostutil.reset_link_url(
token, request.url.scheme, request.headers.get("host")
)
return {
"message": "Registration link generated successfully",
"url": url,
"expires": (
expiry.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
if expiry.tzinfo
else expiry.replace(tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
),
}

138
passkey/fastapi/user.py Normal file
View File

@@ -0,0 +1,138 @@
from datetime import timezone
from uuid import UUID
from fastapi import (
Body,
Cookie,
FastAPI,
HTTPException,
Request,
Response,
)
from ..authsession import (
delete_credential,
expires,
get_session,
)
from ..globals import db
from ..util import hostutil, passphrase, tokens
from ..util.tokens import decode_session_key, session_key
from . import session
app = FastAPI()
@app.put("/display-name")
async def user_update_display_name(
request: Request,
response: Response,
payload: dict = Body(...),
auth=Cookie(None, alias="__Host-auth"),
):
if not auth:
raise HTTPException(status_code=401, detail="Authentication Required")
try:
s = await get_session(auth, host=request.headers.get("host"))
except ValueError as e:
raise HTTPException(status_code=401, detail="Session expired") from e
new_name = (payload.get("display_name") or "").strip()
if not new_name:
raise HTTPException(status_code=400, detail="display_name required")
if len(new_name) > 64:
raise HTTPException(status_code=400, detail="display_name too long")
await db.instance.update_user_display_name(s.user_uuid, new_name)
return {"status": "ok"}
@app.post("/logout-all")
async def api_logout_all(
request: Request, response: Response, auth=Cookie(None, alias="__Host-auth")
):
if not auth:
return {"message": "Already logged out"}
try:
s = await get_session(auth, host=request.headers.get("host"))
except ValueError:
raise HTTPException(status_code=401, detail="Session expired")
await db.instance.delete_sessions_for_user(s.user_uuid)
session.clear_session_cookie(response)
return {"message": "Logged out from all hosts"}
@app.delete("/session/{session_id}")
async def api_delete_session(
request: Request,
response: Response,
session_id: str,
auth=Cookie(None, alias="__Host-auth"),
):
if not auth:
raise HTTPException(status_code=401, detail="Authentication Required")
try:
current_session = await get_session(auth, host=request.headers.get("host"))
except ValueError as exc:
raise HTTPException(status_code=401, detail="Session expired") from exc
try:
target_key = decode_session_key(session_id)
except ValueError as exc:
raise HTTPException(
status_code=400, detail="Invalid session identifier"
) from exc
target_session = await db.instance.get_session(target_key)
if not target_session or target_session.user_uuid != current_session.user_uuid:
raise HTTPException(status_code=404, detail="Session not found")
await db.instance.delete_session(target_key)
current_terminated = target_key == session_key(auth)
if current_terminated:
session.clear_session_cookie(response) # explicit because 200
return {"status": "ok", "current_session_terminated": current_terminated}
@app.delete("/credential/{uuid}")
async def api_delete_credential(
request: Request,
response: Response,
uuid: UUID,
auth: str = Cookie(None, alias="__Host-auth"),
):
try:
await delete_credential(uuid, auth, host=request.headers.get("host"))
except ValueError as e:
raise HTTPException(status_code=401, detail="Session expired") from e
return {"message": "Credential deleted successfully"}
@app.post("/create-link")
async def api_create_link(
request: Request,
response: Response,
auth=Cookie(None, alias="__Host-auth"),
):
try:
s = await get_session(auth, host=request.headers.get("host"))
except ValueError as e:
raise HTTPException(status_code=401, detail="Session expired") from e
token = passphrase.generate()
expiry = expires()
await db.instance.create_reset_token(
user_uuid=s.user_uuid,
key=tokens.reset_key(token),
expiry=expiry,
token_type="device addition",
)
url = hostutil.reset_link_url(
token, request.url.scheme, request.headers.get("host")
)
return {
"message": "Registration link generated successfully",
"url": url,
"expires": (
expiry.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
if expiry.tzinfo
else expiry.replace(tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
),
}