Late night hacking...?
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
API endpoints for user management and session handling.
|
||||
|
||||
This module contains all the HTTP API endpoints for:
|
||||
- User information retrieval
|
||||
- User credentials management
|
||||
- Session token validation and refresh
|
||||
- Login/logout functionality
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
|
||||
from .. import aaguid
|
||||
from ..db import sql
|
||||
from ..util.session import refresh_session_token, validate_session_token
|
||||
from .session import (
|
||||
clear_session_cookie,
|
||||
get_current_user,
|
||||
get_session_token_from_bearer,
|
||||
get_session_token_from_cookie,
|
||||
set_session_cookie,
|
||||
)
|
||||
|
||||
|
||||
def register_api_routes(app: FastAPI):
|
||||
"""Register all API routes on the FastAPI app."""
|
||||
|
||||
@app.post("/auth/user-info")
|
||||
async def api_user_info(request: Request, response: Response):
|
||||
"""Get user information and credentials from session cookie."""
|
||||
try:
|
||||
user = await get_current_user(request)
|
||||
if not user:
|
||||
return {"error": "Not authenticated"}
|
||||
|
||||
# Get current session credential ID
|
||||
current_credential_id = None
|
||||
session_token = get_session_token_from_cookie(request)
|
||||
if session_token:
|
||||
token_data = await validate_session_token(session_token)
|
||||
if token_data:
|
||||
current_credential_id = token_data.get("credential_id")
|
||||
|
||||
# Get all credentials for the user
|
||||
credential_ids = await sql.get_user_credentials(user.user_id)
|
||||
|
||||
credentials = []
|
||||
user_aaguids = set()
|
||||
|
||||
for cred_id in credential_ids:
|
||||
stored_cred = await sql.get_credential_by_id(cred_id)
|
||||
|
||||
# Convert AAGUID to string format
|
||||
aaguid_str = str(stored_cred.aaguid)
|
||||
user_aaguids.add(aaguid_str)
|
||||
|
||||
# Check if this is the current session credential
|
||||
is_current_session = current_credential_id == stored_cred.credential_id
|
||||
|
||||
credentials.append(
|
||||
{
|
||||
"credential_id": stored_cred.credential_id.hex(),
|
||||
"aaguid": aaguid_str,
|
||||
"created_at": stored_cred.created_at.isoformat(),
|
||||
"last_used": stored_cred.last_used.isoformat()
|
||||
if stored_cred.last_used
|
||||
else None,
|
||||
"last_verified": stored_cred.last_verified.isoformat()
|
||||
if stored_cred.last_verified
|
||||
else None,
|
||||
"sign_count": stored_cred.sign_count,
|
||||
"is_current_session": is_current_session,
|
||||
}
|
||||
)
|
||||
|
||||
# Get AAGUID information for only the AAGUIDs that the user has
|
||||
aaguid_info = aaguid.filter(user_aaguids)
|
||||
|
||||
# Sort credentials by creation date (earliest first, most recently created last)
|
||||
credentials.sort(key=lambda cred: cred["created_at"])
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"user": {
|
||||
"user_id": str(user.user_id),
|
||||
"user_name": user.user_name,
|
||||
"created_at": user.created_at.isoformat()
|
||||
if user.created_at
|
||||
else None,
|
||||
"last_seen": user.last_seen.isoformat() if user.last_seen else None,
|
||||
"visits": user.visits,
|
||||
},
|
||||
"credentials": credentials,
|
||||
"aaguid_info": aaguid_info,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to get user info: {str(e)}"}
|
||||
|
||||
@app.post("/auth/logout")
|
||||
async def api_logout(request: Request, response: Response):
|
||||
"""Log out the current user by clearing the session cookie and deleting from database."""
|
||||
# Get the session token before clearing the cookie
|
||||
session_token = get_session_token_from_cookie(request)
|
||||
|
||||
# Clear the cookie
|
||||
clear_session_cookie(response)
|
||||
|
||||
# Delete the session from the database if it exists
|
||||
if session_token:
|
||||
from ..util.session import logout_session
|
||||
|
||||
try:
|
||||
await logout_session(session_token)
|
||||
except Exception:
|
||||
# Continue even if session deletion fails
|
||||
pass
|
||||
|
||||
return {"status": "success", "message": "Logged out successfully"}
|
||||
|
||||
@app.post("/auth/set-session")
|
||||
async def api_set_session(request: Request, response: Response):
|
||||
"""Set session cookie using JWT token from request body or Authorization header."""
|
||||
try:
|
||||
session_token = await get_session_token_from_bearer(request)
|
||||
|
||||
if not session_token:
|
||||
return {"error": "No session token provided"}
|
||||
|
||||
# Validate the session token
|
||||
token_data = await validate_session_token(session_token)
|
||||
if not token_data:
|
||||
return {"error": "Invalid or expired session token"}
|
||||
|
||||
# Set the HTTP-only cookie
|
||||
set_session_cookie(response, session_token)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Session cookie set successfully",
|
||||
"user_id": str(token_data["user_id"]),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to set session: {str(e)}"}
|
||||
|
||||
@app.post("/auth/delete-credential")
|
||||
async def api_delete_credential(request: Request):
|
||||
"""Delete a specific credential for the current user."""
|
||||
try:
|
||||
user = await get_current_user(request)
|
||||
if not user:
|
||||
return {"error": "Not authenticated"}
|
||||
|
||||
# Get the credential ID from the request body
|
||||
try:
|
||||
body = await request.json()
|
||||
credential_id = body.get("credential_id")
|
||||
if not credential_id:
|
||||
return {"error": "credential_id is required"}
|
||||
except Exception:
|
||||
return {"error": "Invalid request body"}
|
||||
|
||||
# Convert credential_id from hex string to bytes
|
||||
try:
|
||||
credential_id_bytes = bytes.fromhex(credential_id)
|
||||
except ValueError:
|
||||
return {"error": "Invalid credential_id format"}
|
||||
|
||||
# First, verify the credential belongs to the current user
|
||||
try:
|
||||
stored_cred = await sql.get_credential_by_id(credential_id_bytes)
|
||||
if stored_cred.user_id != user.user_id:
|
||||
return {"error": "Credential not found or access denied"}
|
||||
except ValueError:
|
||||
return {"error": "Credential not found"}
|
||||
|
||||
# Check if this is the current session credential
|
||||
session_token = get_session_token_from_cookie(request)
|
||||
if session_token:
|
||||
token_data = await validate_session_token(session_token)
|
||||
if (
|
||||
token_data
|
||||
and token_data.get("credential_id") == credential_id_bytes
|
||||
):
|
||||
return {"error": "Cannot delete current session credential"}
|
||||
|
||||
# Get user's remaining credentials count
|
||||
remaining_credentials = await sql.get_user_credentials(user.user_id)
|
||||
if len(remaining_credentials) <= 1:
|
||||
return {"error": "Cannot delete last remaining credential"}
|
||||
|
||||
# Delete the credential
|
||||
await sql.delete_user_credential(credential_id_bytes)
|
||||
|
||||
return {"status": "success", "message": "Credential deleted successfully"}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to delete credential: {str(e)}"}
|
||||
|
||||
@app.get("/auth/sessions")
|
||||
async def api_get_sessions(request: Request):
|
||||
"""Get all active sessions for the current user."""
|
||||
try:
|
||||
user = await get_current_user(request)
|
||||
if not user:
|
||||
return {"error": "Authentication required"}
|
||||
|
||||
# Get all sessions for this user
|
||||
from sqlalchemy import select
|
||||
|
||||
from ..db.sql import SessionModel, connect
|
||||
|
||||
async with connect() as db:
|
||||
stmt = select(SessionModel).where(
|
||||
SessionModel.user_id == user.user_id.bytes
|
||||
)
|
||||
result = await db.session.execute(stmt)
|
||||
session_models = result.scalars().all()
|
||||
|
||||
sessions = []
|
||||
current_token = get_session_token_from_cookie(request)
|
||||
|
||||
for session in session_models:
|
||||
# Check if session is expired
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
expiry_time = session.created_at + timedelta(hours=24)
|
||||
is_expired = datetime.now() > expiry_time
|
||||
|
||||
sessions.append(
|
||||
{
|
||||
"token": session.token[:8]
|
||||
+ "...", # Only show first 8 chars for security
|
||||
"created_at": session.created_at.isoformat(),
|
||||
"client_ip": session.info.get("client_ip")
|
||||
if session.info
|
||||
else None,
|
||||
"user_agent": session.info.get("user_agent")
|
||||
if session.info
|
||||
else None,
|
||||
"connection_type": session.info.get(
|
||||
"connection_type", "http"
|
||||
)
|
||||
if session.info
|
||||
else "http",
|
||||
"is_current": session.token == current_token,
|
||||
"is_reset_token": session.credential_id is None,
|
||||
"is_expired": is_expired,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"sessions": sessions,
|
||||
"total_sessions": len(sessions),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to get sessions: {str(e)}"}
|
||||
|
||||
|
||||
async def validate_token(request: Request, response: Response) -> dict:
|
||||
"""Validate a session token and return user info. Also refreshes the token if valid."""
|
||||
try:
|
||||
session_token = get_session_token_from_cookie(request)
|
||||
if not session_token:
|
||||
return {"error": "No session token found"}
|
||||
|
||||
# Validate the session token
|
||||
token_data = await validate_session_token(session_token)
|
||||
if not token_data:
|
||||
clear_session_cookie(response)
|
||||
return {"error": "Invalid or expired session token"}
|
||||
|
||||
# Refresh the token if valid
|
||||
new_token = await refresh_session_token(session_token)
|
||||
if new_token:
|
||||
set_session_cookie(response, new_token)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"valid": True,
|
||||
"refreshed": bool(new_token),
|
||||
"user_id": str(token_data["user_id"]),
|
||||
"credential_id": token_data["credential_id"].hex()
|
||||
if token_data["credential_id"]
|
||||
else None,
|
||||
"created_at": token_data["created_at"].isoformat(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to validate token: {str(e)}"}
|
||||
Reference in New Issue
Block a user