Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.db.deps import get_common_db
|
||||
from app.core.security.passwords import hash_password
|
||||
from app.core.security.session_auth import require_login
|
||||
from app.modules.core.audit.service import model_snapshot, pair_before_after, write_audit_log
|
||||
from app.modules.core.iam.lifecycle import activate_user, deactivate_user, disable_login, enable_login, ensure_manageable_lifecycle, lock_user, restore_user, soft_delete_user, unlock_user, LifecycleError
|
||||
from app.modules.core.iam.models import User
|
||||
from app.modules.core.iam.scope import (
|
||||
build_scope,
|
||||
ensure_assignable_roles,
|
||||
ensure_manageable_existing_user,
|
||||
ensure_users_manage_scope,
|
||||
ensure_users_view_scope,
|
||||
list_scoped_users,
|
||||
resolve_target_tenant_branch,
|
||||
scope_to_http,
|
||||
)
|
||||
from app.modules.core.rbac.deps import require_permission
|
||||
from app.modules.core.rbac.models import UserRole
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
class UserCreateRequest(BaseModel):
|
||||
email: EmailStr
|
||||
full_name: str
|
||||
password: str
|
||||
tenant_id: int | None = None
|
||||
branch_id: int | None = None
|
||||
role_ids: list[int] = []
|
||||
is_active: bool = True
|
||||
allow_login: bool = True
|
||||
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
full_name: str
|
||||
tenant_id: int | None = None
|
||||
branch_id: int | None = None
|
||||
role_ids: list[int] = []
|
||||
is_active: bool = True
|
||||
allow_login: bool = True
|
||||
password: str | None = None
|
||||
|
||||
|
||||
@router.get("", dependencies=[Depends(require_permission("users.view"))])
|
||||
def list_users(current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
try:
|
||||
ensure_users_view_scope(scope)
|
||||
except Exception as exc:
|
||||
raise scope_to_http(exc)
|
||||
|
||||
users = list_scoped_users(db, scope)
|
||||
return [
|
||||
{
|
||||
"id": u.id,
|
||||
"email": u.email,
|
||||
"full_name": u.full_name,
|
||||
"tenant_id": u.tenant_id,
|
||||
"branch_id": u.branch_id,
|
||||
"is_active": u.is_active,
|
||||
"allow_login": getattr(u, "allow_login", True),
|
||||
"is_locked": getattr(u, "is_locked", False),
|
||||
"deleted_at": (u.deleted_at.isoformat() if getattr(u, "deleted_at", None) else None),
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
|
||||
|
||||
@router.post("", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def create_user(
|
||||
payload: UserCreateRequest,
|
||||
current_user: User = Depends(require_login),
|
||||
db: Session = Depends(get_common_db),
|
||||
):
|
||||
scope = build_scope(db, current_user)
|
||||
try:
|
||||
ensure_users_manage_scope(scope)
|
||||
tenant_id, branch_id = resolve_target_tenant_branch(db, scope, payload.tenant_id, payload.branch_id)
|
||||
roles = ensure_assignable_roles(db, scope, payload.role_ids)
|
||||
except Exception as exc:
|
||||
raise scope_to_http(exc)
|
||||
|
||||
email = payload.email.lower().strip()
|
||||
exists = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
|
||||
if exists:
|
||||
raise HTTPException(status_code=400, detail="Email already exists")
|
||||
|
||||
user = User(
|
||||
email=email,
|
||||
full_name=payload.full_name.strip(),
|
||||
password_hash=hash_password(payload.password),
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
is_active=payload.is_active,
|
||||
allow_login=payload.allow_login,
|
||||
is_locked=False,
|
||||
deleted_at=None,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
for role in roles:
|
||||
db.add(UserRole(user_id=user.id, role_id=role.id))
|
||||
db.commit()
|
||||
write_audit_log(
|
||||
db,
|
||||
action="user.create.api",
|
||||
entity_type="user",
|
||||
actor=current_user,
|
||||
entity_id=user.id,
|
||||
entity_name=user.email,
|
||||
target_tenant_id=user.tenant_id,
|
||||
target_branch_id=user.branch_id,
|
||||
details={"after": model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"]), "role_ids": [role.id for role in roles]},
|
||||
)
|
||||
return {"status": "ok", "id": user.id}
|
||||
|
||||
|
||||
@router.get("/{user_id}", dependencies=[Depends(require_permission("users.view"))])
|
||||
def get_user(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
try:
|
||||
ensure_users_view_scope(scope)
|
||||
except Exception as exc:
|
||||
raise scope_to_http(exc)
|
||||
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_existing_user(db, scope, user)
|
||||
except Exception as exc:
|
||||
raise scope_to_http(exc)
|
||||
|
||||
role_ids = db.execute(select(UserRole.role_id).where(UserRole.user_id == user.id)).scalars().all()
|
||||
return {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"tenant_id": user.tenant_id,
|
||||
"branch_id": user.branch_id,
|
||||
"is_active": user.is_active,
|
||||
"allow_login": getattr(user, "allow_login", True),
|
||||
"is_locked": getattr(user, "is_locked", False),
|
||||
"deleted_at": (user.deleted_at.isoformat() if getattr(user, "deleted_at", None) else None),
|
||||
"role_ids": list(role_ids),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{user_id}", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def update_user(
|
||||
user_id: int,
|
||||
payload: UserUpdateRequest,
|
||||
current_user: User = Depends(require_login),
|
||||
db: Session = Depends(get_common_db),
|
||||
):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
try:
|
||||
ensure_users_manage_scope(scope)
|
||||
ensure_manageable_existing_user(db, scope, user)
|
||||
tenant_id, branch_id = resolve_target_tenant_branch(db, scope, payload.tenant_id, payload.branch_id)
|
||||
roles = ensure_assignable_roles(db, scope, payload.role_ids)
|
||||
except Exception as exc:
|
||||
raise scope_to_http(exc)
|
||||
|
||||
before_snapshot = model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"])
|
||||
old_role_ids = list(db.execute(select(UserRole.role_id).where(UserRole.user_id == user.id)).scalars().all())
|
||||
|
||||
user.full_name = payload.full_name.strip()
|
||||
user.tenant_id = tenant_id
|
||||
user.branch_id = branch_id
|
||||
user.is_active = payload.is_active
|
||||
user.allow_login = payload.allow_login
|
||||
if payload.password:
|
||||
user.password_hash = hash_password(payload.password)
|
||||
|
||||
db.execute(UserRole.__table__.delete().where(UserRole.user_id == user.id))
|
||||
for role in roles:
|
||||
db.add(UserRole(user_id=user.id, role_id=role.id))
|
||||
db.commit()
|
||||
write_audit_log(
|
||||
db,
|
||||
action="user.update.api",
|
||||
entity_type="user",
|
||||
actor=current_user,
|
||||
entity_id=user.id,
|
||||
entity_name=user.email,
|
||||
target_tenant_id=user.tenant_id,
|
||||
target_branch_id=user.branch_id,
|
||||
details={**pair_before_after(before_snapshot, model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"])), "old_role_ids": old_role_ids, "new_role_ids": [role.id for role in roles]},
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
|
||||
def _lifecycle_response(user: User) -> dict:
|
||||
return {
|
||||
"status": "ok",
|
||||
"user_id": user.id,
|
||||
"is_active": user.is_active,
|
||||
"allow_login": getattr(user, "allow_login", True),
|
||||
"is_locked": getattr(user, "is_locked", False),
|
||||
"deleted_at": (user.deleted_at.isoformat() if getattr(user, "deleted_at", None) else None),
|
||||
}
|
||||
|
||||
|
||||
def _apply_lifecycle_action(db: Session, current_user: User, user: User, action: str):
|
||||
before = model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"])
|
||||
if action == "activate":
|
||||
activate_user(user)
|
||||
audit_action = "user.activate.api"
|
||||
elif action == "deactivate":
|
||||
deactivate_user(user)
|
||||
audit_action = "user.deactivate.api"
|
||||
elif action == "enable-login":
|
||||
enable_login(user)
|
||||
audit_action = "user.enable_login.api"
|
||||
elif action == "disable-login":
|
||||
disable_login(user)
|
||||
audit_action = "user.disable_login.api"
|
||||
elif action == "lock":
|
||||
lock_user(user)
|
||||
audit_action = "user.lock.api"
|
||||
elif action == "unlock":
|
||||
unlock_user(user)
|
||||
audit_action = "user.unlock.api"
|
||||
elif action == "delete":
|
||||
soft_delete_user(user)
|
||||
audit_action = "user.soft_delete.api"
|
||||
elif action == "restore":
|
||||
restore_user(user)
|
||||
audit_action = "user.restore.api"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unknown action")
|
||||
db.commit()
|
||||
write_audit_log(
|
||||
db,
|
||||
action=audit_action,
|
||||
entity_type="user",
|
||||
actor=current_user,
|
||||
entity_id=user.id,
|
||||
entity_name=user.email,
|
||||
target_tenant_id=user.tenant_id,
|
||||
target_branch_id=user.branch_id,
|
||||
details=pair_before_after(before, model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"])),
|
||||
)
|
||||
return _lifecycle_response(user)
|
||||
|
||||
|
||||
@router.post("/{user_id}/activate", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def activate_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "activate")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "activate")
|
||||
|
||||
|
||||
@router.post("/{user_id}/deactivate", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def deactivate_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "deactivate")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "deactivate")
|
||||
|
||||
|
||||
@router.post("/{user_id}/enable-login", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def enable_login_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "enable login for")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "enable-login")
|
||||
|
||||
|
||||
@router.post("/{user_id}/disable-login", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def disable_login_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "disable login for")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "disable-login")
|
||||
|
||||
|
||||
@router.post("/{user_id}/lock", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def lock_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "lock")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "lock")
|
||||
|
||||
|
||||
@router.post("/{user_id}/unlock", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def unlock_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "unlock")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "unlock")
|
||||
|
||||
|
||||
@router.post("/{user_id}/delete", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def delete_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "delete")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "delete")
|
||||
|
||||
|
||||
@router.post("/{user_id}/restore", dependencies=[Depends(require_permission("users.manage"))])
|
||||
def restore_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)):
|
||||
scope = build_scope(db, current_user)
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
try:
|
||||
ensure_manageable_lifecycle(scope, db, current_user, user, "restore")
|
||||
except (Exception, LifecycleError) as exc:
|
||||
raise scope_to_http(exc)
|
||||
return _apply_lifecycle_action(db, current_user, user, "restore")
|
||||
Reference in New Issue
Block a user