Refactor /api/user/* to its own module.
This commit is contained in:
@@ -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
138
passkey/fastapi/user.py
Normal 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")
|
||||||
|
),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user