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>
158 lines
4.7 KiB
Python
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
|