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 import logging
from contextlib import suppress from contextlib import suppress
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from uuid import UUID
from fastapi import ( from fastapi import (
Body,
Cookie, Cookie,
Depends, Depends,
FastAPI, FastAPI,
@@ -21,8 +19,6 @@ from passkey.util import frontend, useragent
from .. import aaguid from .. import aaguid
from ..authsession import ( from ..authsession import (
EXPIRES, EXPIRES,
delete_credential,
expires,
get_reset, get_reset,
get_session, get_session,
refresh_session_token, refresh_session_token,
@@ -30,14 +26,16 @@ from ..authsession import (
) )
from ..globals import db from ..globals import db
from ..globals import passkey as global_passkey from ..globals import passkey as global_passkey
from ..util import hostutil, passphrase, permutil, tokens from ..util import hostutil, passphrase, permutil
from ..util.tokens import decode_session_key, encode_session_key, session_key from ..util.tokens import encode_session_key, session_key
from . import authz, session from . import authz, session, user
bearer_auth = HTTPBearer(auto_error=True) bearer_auth = HTTPBearer(auto_error=True)
app = FastAPI() app = FastAPI()
app.mount("/user", user.app)
@app.exception_handler(HTTPException) @app.exception_handler(HTTPException)
async def http_exception_handler(_request: Request, exc: 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") @app.post("/logout")
async def api_logout( async def api_logout(
request: Request, response: Response, auth=Cookie(None, alias="__Host-auth") request: Request, response: Response, auth=Cookie(None, alias="__Host-auth")
@@ -399,53 +375,6 @@ async def api_logout(
return {"message": "Logged out successfully"} 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") @app.post("/set-session")
async def api_set_session( async def api_set_session(
request: Request, response: Response, auth=Depends(bearer_auth) request: Request, response: Response, auth=Depends(bearer_auth)
@@ -456,49 +385,3 @@ async def api_set_session(
"message": "Session cookie set successfully", "message": "Session cookie set successfully",
"user_uuid": str(user.user_uuid), "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")
),
}