97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""Administrator endpoints: see who has an account, and turn them off."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
from fastapi import (APIRouter, BackgroundTasks, Depends, HTTPException, Request,
|
|
status)
|
|
from sqlalchemy import delete, func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.config import Settings, get_settings
|
|
from app.db import get_db
|
|
from app.deps import client_ip, require_admin
|
|
from app.models import VERIFY_EMAIL, AuthSession, User
|
|
from app.routers.auth import _mint, _send_verification
|
|
from app.schemas import Ok, UserOut
|
|
from app.services.store import store_of
|
|
|
|
log = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/admin", tags=["admin"],
|
|
dependencies=[Depends(require_admin)])
|
|
|
|
|
|
@router.get("/users")
|
|
def list_users(db: Session = Depends(get_db)) -> dict:
|
|
users = db.scalars(select(User).order_by(User.created_at.desc())).all()
|
|
live = {uid for (uid,) in db.execute(
|
|
select(AuthSession.user_id).group_by(AuthSession.user_id))}
|
|
return {
|
|
"users": [
|
|
{**UserOut.of(u).model_dump(mode="json"), "signed_in": u.id in live}
|
|
for u in users
|
|
],
|
|
"sessions": db.scalar(select(func.count()).select_from(AuthSession)) or 0,
|
|
"workspaces": len(store_of()),
|
|
}
|
|
|
|
|
|
def _target(db: Session, user_id: int) -> User:
|
|
user = db.get(User, user_id)
|
|
if user is None:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "No such account.")
|
|
return user
|
|
|
|
|
|
@router.post("/users/{user_id}/deactivate", response_model=Ok)
|
|
def deactivate(user_id: int, admin: User = Depends(require_admin),
|
|
db: Session = Depends(get_db)) -> Ok:
|
|
user = _target(db, user_id)
|
|
if user.id == admin.id:
|
|
raise HTTPException(status.HTTP_400_BAD_REQUEST,
|
|
"You cannot deactivate your own account.")
|
|
user.is_active = False
|
|
# Signing them out is the point; leaving live sessions would make the
|
|
# button look like it worked while they carried on using the dashboard.
|
|
db.execute(delete(AuthSession).where(AuthSession.user_id == user.id))
|
|
db.commit()
|
|
store_of().drop(user.id)
|
|
log.info("admin %s deactivated %s", admin.email, user.email)
|
|
return Ok(message=f"{user.email} can no longer sign in.")
|
|
|
|
|
|
@router.post("/users/{user_id}/activate", response_model=Ok)
|
|
def activate(user_id: int, admin: User = Depends(require_admin),
|
|
db: Session = Depends(get_db)) -> Ok:
|
|
user = _target(db, user_id)
|
|
user.is_active = True
|
|
user.failed_login_count = 0
|
|
user.locked_until = None
|
|
db.commit()
|
|
log.info("admin %s reactivated %s", admin.email, user.email)
|
|
return Ok(message=f"{user.email} can sign in again.")
|
|
|
|
|
|
@router.post("/users/{user_id}/resend-verification", response_model=Ok)
|
|
def resend(user_id: int, request: Request, bg: BackgroundTasks,
|
|
db: Session = Depends(get_db),
|
|
s: Settings = Depends(get_settings)) -> Ok:
|
|
user = _target(db, user_id)
|
|
if user.is_verified:
|
|
return Ok(message=f"{user.email} is already confirmed.")
|
|
raw = _mint(db, user, VERIFY_EMAIL, timedelta(hours=s.VERIFY_TOKEN_TTL_HOURS),
|
|
client_ip(request))
|
|
bg.add_task(_send_verification, user, raw, s)
|
|
return Ok(message=f"A new confirmation link is on its way to {user.email}.")
|
|
|
|
|
|
@router.post("/users/{user_id}/sign-out", response_model=Ok)
|
|
def sign_out(user_id: int, db: Session = Depends(get_db)) -> Ok:
|
|
user = _target(db, user_id)
|
|
db.execute(delete(AuthSession).where(AuthSession.user_id == user.id))
|
|
db.commit()
|
|
store_of().drop(user.id)
|
|
return Ok(message=f"{user.email} has been signed out everywhere.")
|