Files
g0v0-server/app/router/private/admin.py
MingxuanGame 40670c094b feat(auth): support trusted device (#52)
New API to maintain sessions and devices:

- GET /api/private/admin/sessions
- DELETE /api/private/admin/sessions/{session_id}
- GET /api/private/admin/trusted-devices
- DELETE /api/private/admin/trusted-devices/{device_id}

Auth:

web clients request `/oauth/token` and `/api/v2/session/verify` with `X-UUID` header to save the client as trusted device.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 11:26:43 +08:00

158 lines
4.7 KiB
Python

from __future__ import annotations
from app.database.auth import OAuthToken
from app.database.verification import LoginSession, LoginSessionResp, TrustedDevice, TrustedDeviceResp
from app.dependencies.database import Database
from app.dependencies.geoip import get_geoip_helper
from app.dependencies.user import UserAndToken, get_client_user_and_token
from app.helpers.geoip_helper import GeoIPHelper
from .router import router
from fastapi import Depends, HTTPException, Security
from pydantic import BaseModel
from sqlmodel import col, select
class SessionsResp(BaseModel):
total: int
current: int = 0
sessions: list[LoginSessionResp]
@router.get(
"/admin/sessions",
name="获取当前用户的登录会话列表",
tags=["用户会话", "g0v0 API", "管理"],
response_model=SessionsResp,
)
async def get_sessions(
session: Database,
user_and_token: UserAndToken = Security(get_client_user_and_token),
geoip: GeoIPHelper = Depends(get_geoip_helper),
):
current_user, token = user_and_token
sessions = (
await session.exec(
select(
LoginSession,
)
.where(LoginSession.user_id == current_user.id, col(LoginSession.is_verified).is_(True))
.order_by(col(LoginSession.created_at).desc())
)
).all()
return SessionsResp(
total=len(sessions),
current=token.id,
sessions=[LoginSessionResp.from_db(s, geoip) for s in sessions],
)
@router.delete(
"/admin/sessions/{session_id}",
name="注销指定的登录会话",
tags=["用户会话", "g0v0 API", "管理"],
status_code=204,
)
async def delete_session(
session: Database,
session_id: int,
user_and_token: UserAndToken = Security(get_client_user_and_token),
):
current_user, token = user_and_token
if session_id == token.id:
raise HTTPException(status_code=400, detail="Cannot delete the current session")
db_session = await session.get(LoginSession, session_id)
if not db_session or db_session.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Session not found")
await session.delete(db_session)
token = await session.get(OAuthToken, db_session.token_id or 0)
if token:
await session.delete(token)
await session.commit()
return
class TrustedDevicesResp(BaseModel):
total: int
current: int = 0
devices: list[TrustedDeviceResp]
@router.get(
"/admin/trusted-devices",
name="获取当前用户的受信任设备列表",
tags=["用户会话", "g0v0 API", "管理"],
response_model=TrustedDevicesResp,
)
async def get_trusted_devices(
session: Database,
user_and_token: UserAndToken = Security(get_client_user_and_token),
geoip: GeoIPHelper = Depends(get_geoip_helper),
):
current_user, token = user_and_token
devices = (
await session.exec(
select(TrustedDevice)
.where(TrustedDevice.user_id == current_user.id)
.order_by(col(TrustedDevice.last_used_at).desc())
)
).all()
current_device_id = (
await session.exec(
select(TrustedDevice.id)
.join(LoginSession, col(LoginSession.device_id) == TrustedDevice.id)
.where(
LoginSession.token_id == token.id,
TrustedDevice.user_id == current_user.id,
)
.limit(1)
)
).first()
return TrustedDevicesResp(
total=len(devices),
current=current_device_id or 0,
devices=[TrustedDeviceResp.from_db(device, geoip) for device in devices],
)
@router.delete(
"/admin/trusted-devices/{device_id}",
name="移除受信任设备",
tags=["用户会话", "g0v0 API", "管理"],
status_code=204,
)
async def delete_trusted_device(
session: Database,
device_id: int,
user_and_token: UserAndToken = Security(get_client_user_and_token),
):
current_user, token = user_and_token
device = await session.get(TrustedDevice, device_id)
current_device_id = (
await session.exec(
select(TrustedDevice.id)
.join(LoginSession, col(LoginSession.device_id) == TrustedDevice.id)
.where(
LoginSession.token_id == token.id,
TrustedDevice.user_id == current_user.id,
)
.limit(1)
)
).first()
if device_id == current_device_id:
raise HTTPException(status_code=400, detail="Cannot delete the current trusted device")
if not device or device.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Trusted device not found")
await session.delete(device)
await session.commit()
return