Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+363
View File
@@ -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")