fix(users): repair create delete and duplicate email handling
This commit is contained in:
parent
155fdbb16a
commit
0bbcaba211
22 changed files with 773 additions and 107 deletions
Binary file not shown.
|
|
@ -6,13 +6,14 @@ import json
|
|||
import logging
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, Response, UploadFile
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Response, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.exc import IntegrityError, ProgrammingError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.dependencies import current_admin, current_user
|
||||
|
|
@ -560,7 +561,7 @@ def _user_payload(user: User) -> dict:
|
|||
"email": user.email,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"role": user.role,
|
||||
"role": UserRole(user.role),
|
||||
"is_active": user.is_active,
|
||||
"must_change_password": user.must_change_password,
|
||||
"last_login_at": user.last_login_at,
|
||||
|
|
@ -581,6 +582,7 @@ def list_users(
|
|||
_: User = Depends(current_admin),
|
||||
):
|
||||
query = select(User)
|
||||
query = query.where(User.deleted_at.is_(None))
|
||||
if search:
|
||||
term = f"%{search.strip()}%"
|
||||
query = query.where(
|
||||
|
|
@ -605,8 +607,9 @@ def list_users(
|
|||
sort_column = sort_columns.get(sort_by, User.created_at)
|
||||
query = query.order_by(sort_column.asc() if sort_order.lower() == "asc" else sort_column.desc())
|
||||
total = session.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||
pages = max((total + page_size - 1) // page_size, 1)
|
||||
items = list(session.scalars(query.offset((page - 1) * page_size).limit(page_size)))
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size, "pages": pages}
|
||||
|
||||
|
||||
@router.post("/users", status_code=201)
|
||||
|
|
@ -615,26 +618,47 @@ def create_user(payload: UserCreate, session: Session = Depends(get_session), _:
|
|||
import secrets
|
||||
from app.models.user import User
|
||||
|
||||
normalized_email = payload.email.strip().lower()
|
||||
logger.debug("create_user request email=%s normalized_email=%s", payload.email, normalized_email)
|
||||
existing = session.scalar(select(User).where(func.lower(User.email) == normalized_email))
|
||||
logger.debug("create_user select result=%s count=%s", bool(existing), 1 if existing else 0)
|
||||
if existing is not None:
|
||||
raise HTTPException(status_code=409, detail="Die E-Mail-Adresse wird bereits verwendet.")
|
||||
|
||||
temporary_password = payload.temporary_password or secrets.token_urlsafe(12)
|
||||
user = User(
|
||||
email=payload.email.lower(),
|
||||
first_name=payload.first_name,
|
||||
last_name=payload.last_name,
|
||||
role=payload.role.value,
|
||||
password_hash=hash_password(temporary_password),
|
||||
is_active=payload.is_active,
|
||||
must_change_password=payload.must_change_password,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return JSONResponse(content={"temporary_password": temporary_password, "user": jsonable_encoder(user)}, status_code=201)
|
||||
try:
|
||||
user = User(
|
||||
email=normalized_email,
|
||||
first_name=payload.first_name,
|
||||
last_name=payload.last_name,
|
||||
role=payload.role.value,
|
||||
password_hash=hash_password(temporary_password),
|
||||
is_active=payload.is_active,
|
||||
must_change_password=payload.must_change_password,
|
||||
)
|
||||
logger.debug("create_user insert email=%s", normalized_email)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
logger.debug("create_user commit ok email=%s id=%s", normalized_email, user.id)
|
||||
session.refresh(user)
|
||||
logger.debug("create_user response email=%s status=201", normalized_email)
|
||||
return JSONResponse(
|
||||
content={"temporary_password": temporary_password, "user": jsonable_encoder(user)},
|
||||
status_code=201,
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
logger.exception("create_user integrity error email=%s", normalized_email)
|
||||
raise HTTPException(status_code=409, detail="Die E-Mail-Adresse wird bereits verwendet.") from exc
|
||||
except ProgrammingError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=422, detail="Die Benutzerrolle ist ungültig.") from exc
|
||||
|
||||
|
||||
@router.get("/users/{item_id}", response_model=UserRead)
|
||||
def get_user(item_id: str, session: Session = Depends(get_session), _: User = Depends(current_admin)):
|
||||
user = session.get(User, item_id)
|
||||
if user is None:
|
||||
if user is None or user.deleted_at is not None:
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
return user
|
||||
|
||||
|
|
@ -642,7 +666,7 @@ def get_user(item_id: str, session: Session = Depends(get_session), _: User = De
|
|||
@router.put("/users/{item_id}", response_model=UserRead)
|
||||
def update_user(item_id: str, payload: UserUpdate, session: Session = Depends(get_session), me: User = Depends(current_admin)):
|
||||
user = session.get(User, item_id)
|
||||
if user is None:
|
||||
if user is None or user.deleted_at is not None:
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
if user.id == me.id and not payload.is_active:
|
||||
raise HTTPException(status_code=409, detail="Own admin account cannot be deactivated")
|
||||
|
|
@ -651,44 +675,58 @@ def update_user(item_id: str, payload: UserUpdate, session: Session = Depends(ge
|
|||
) or 0
|
||||
if user.role == UserRole.ADMIN.value and active_admins <= 1 and (not payload.is_active or payload.role != UserRole.ADMIN.value):
|
||||
raise HTTPException(status_code=409, detail="Der letzte aktive ADMIN darf nicht deaktiviert oder herabgestuft werden.")
|
||||
user.first_name = payload.first_name
|
||||
user.last_name = payload.last_name
|
||||
user.email = payload.email.lower()
|
||||
user.role = payload.role.value
|
||||
user.is_active = payload.is_active
|
||||
user.must_change_password = payload.must_change_password
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
try:
|
||||
normalized_email = payload.email.strip().lower()
|
||||
logger.debug("update_user request id=%s email=%s normalized_email=%s", item_id, payload.email, normalized_email)
|
||||
existing = session.scalar(
|
||||
select(User).where(func.lower(User.email) == normalized_email, User.id != user.id)
|
||||
)
|
||||
logger.debug("update_user select result=%s count=%s", bool(existing), 1 if existing else 0)
|
||||
if existing is not None:
|
||||
raise HTTPException(status_code=409, detail="Die E-Mail-Adresse wird bereits verwendet.")
|
||||
user.first_name = payload.first_name
|
||||
user.last_name = payload.last_name
|
||||
user.email = normalized_email
|
||||
user.role = payload.role.value
|
||||
user.is_active = payload.is_active
|
||||
user.must_change_password = payload.must_change_password
|
||||
logger.debug("update_user commit start id=%s email=%s", user.id, normalized_email)
|
||||
session.commit()
|
||||
logger.debug("update_user commit ok id=%s email=%s", user.id, normalized_email)
|
||||
session.refresh(user)
|
||||
return user
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
logger.exception("update_user integrity error id=%s email=%s", item_id, payload.email)
|
||||
raise HTTPException(status_code=409, detail="Die E-Mail-Adresse wird bereits verwendet.") from exc
|
||||
except ProgrammingError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=422, detail="Die Benutzerrolle ist ungültig.") from exc
|
||||
|
||||
|
||||
@router.post("/users/{item_id}/deactivate", response_model=UserRead)
|
||||
def deactivate_user(item_id: str, session: Session = Depends(get_session), me: User = Depends(current_admin)):
|
||||
@router.delete("/users/{item_id}", status_code=204)
|
||||
def delete_user(item_id: str, session: Session = Depends(get_session), me: User = Depends(current_admin)):
|
||||
user = session.get(User, item_id)
|
||||
if user is None:
|
||||
if user is None or user.deleted_at is not None:
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
if user.id == me.id:
|
||||
raise HTTPException(status_code=409, detail="Own admin account cannot be deactivated")
|
||||
raise HTTPException(status_code=409, detail="Das eigene Benutzerkonto kann nicht geloescht werden.")
|
||||
active_admins = session.scalar(
|
||||
select(func.count()).select_from(User).where(User.role == UserRole.ADMIN.value, User.is_active.is_(True))
|
||||
) or 0
|
||||
if user.role == UserRole.ADMIN.value and active_admins <= 1:
|
||||
raise HTTPException(status_code=409, detail="Der letzte aktive ADMIN darf nicht deaktiviert werden.")
|
||||
raise HTTPException(status_code=409, detail="Der letzte aktive ADMIN darf nicht geloescht werden.")
|
||||
referenced_validations = session.scalar(
|
||||
select(func.count()).select_from(Validation).where(Validation.examiner_id == user.id)
|
||||
) or 0
|
||||
if referenced_validations == 0:
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return Response(status_code=204)
|
||||
user.is_active = False
|
||||
user.deleted_at = datetime.now(UTC)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/users/{item_id}/activate", response_model=UserRead)
|
||||
def activate_user(item_id: str, session: Session = Depends(get_session), _: User = Depends(current_admin)):
|
||||
user = session.get(User, item_id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
user.is_active = True
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@router.post("/users/{item_id}/reset-password")
|
||||
|
|
@ -697,7 +735,7 @@ def reset_user_password(item_id: str, session: Session = Depends(get_session), _
|
|||
from app.core.security import hash_password
|
||||
|
||||
user = session.get(User, item_id)
|
||||
if user is None:
|
||||
if user is None or user.deleted_at is not None:
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
temporary_password = secrets.token_urlsafe(12)
|
||||
user.password_hash = hash_password(temporary_password)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue