diff --git a/decnet/web/router/config/__init__.py b/decnet/web/router/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/decnet/web/router/config/__pycache__/__init__.cpython-314.pyc b/decnet/web/router/config/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..80f5ae2 Binary files /dev/null and b/decnet/web/router/config/__pycache__/__init__.cpython-314.pyc differ diff --git a/decnet/web/router/config/__pycache__/api_get_config.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_get_config.cpython-314.pyc new file mode 100644 index 0000000..2791f5d Binary files /dev/null and b/decnet/web/router/config/__pycache__/api_get_config.cpython-314.pyc differ diff --git a/decnet/web/router/config/__pycache__/api_manage_users.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_manage_users.cpython-314.pyc new file mode 100644 index 0000000..c52e524 Binary files /dev/null and b/decnet/web/router/config/__pycache__/api_manage_users.cpython-314.pyc differ diff --git a/decnet/web/router/config/__pycache__/api_reinit.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_reinit.cpython-314.pyc new file mode 100644 index 0000000..d74c059 Binary files /dev/null and b/decnet/web/router/config/__pycache__/api_reinit.cpython-314.pyc differ diff --git a/decnet/web/router/config/__pycache__/api_update_config.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_update_config.cpython-314.pyc new file mode 100644 index 0000000..79a2ab0 Binary files /dev/null and b/decnet/web/router/config/__pycache__/api_update_config.cpython-314.pyc differ diff --git a/decnet/web/router/config/api_get_config.py b/decnet/web/router/config/api_get_config.py new file mode 100644 index 0000000..397318c --- /dev/null +++ b/decnet/web/router/config/api_get_config.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Depends + +from decnet.env import DECNET_DEVELOPER +from decnet.web.dependencies import require_viewer, repo +from decnet.web.db.models import UserResponse + +router = APIRouter() + +_DEFAULT_DEPLOYMENT_LIMIT = 10 +_DEFAULT_MUTATION_INTERVAL = "30m" + + +@router.get( + "/config", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + }, +) +async def api_get_config(user: dict = Depends(require_viewer)) -> dict: + limits_state = await repo.get_state("config_limits") + globals_state = await repo.get_state("config_globals") + + deployment_limit = ( + limits_state.get("deployment_limit", _DEFAULT_DEPLOYMENT_LIMIT) + if limits_state + else _DEFAULT_DEPLOYMENT_LIMIT + ) + global_mutation_interval = ( + globals_state.get("global_mutation_interval", _DEFAULT_MUTATION_INTERVAL) + if globals_state + else _DEFAULT_MUTATION_INTERVAL + ) + + base = { + "role": user["role"], + "deployment_limit": deployment_limit, + "global_mutation_interval": global_mutation_interval, + } + + if user["role"] == "admin": + all_users = await repo.list_users() + base["users"] = [ + UserResponse( + uuid=u["uuid"], + username=u["username"], + role=u["role"], + must_change_password=u["must_change_password"], + ).model_dump() + for u in all_users + ] + if DECNET_DEVELOPER: + base["developer_mode"] = True + + return base diff --git a/decnet/web/router/config/api_manage_users.py b/decnet/web/router/config/api_manage_users.py new file mode 100644 index 0000000..c1bf9a8 --- /dev/null +++ b/decnet/web/router/config/api_manage_users.py @@ -0,0 +1,123 @@ +import uuid as _uuid + +from fastapi import APIRouter, Depends, HTTPException + +from decnet.web.auth import get_password_hash +from decnet.web.dependencies import require_admin, repo +from decnet.web.db.models import ( + CreateUserRequest, + UpdateUserRoleRequest, + ResetUserPasswordRequest, + UserResponse, +) + +router = APIRouter() + + +@router.post( + "/config/users", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 409: {"description": "Username already exists"}, + 422: {"description": "Validation error"}, + }, +) +async def api_create_user( + req: CreateUserRequest, + admin: dict = Depends(require_admin), +) -> UserResponse: + existing = await repo.get_user_by_username(req.username) + if existing: + raise HTTPException(status_code=409, detail="Username already exists") + + user_uuid = str(_uuid.uuid4()) + await repo.create_user({ + "uuid": user_uuid, + "username": req.username, + "password_hash": get_password_hash(req.password), + "role": req.role, + "must_change_password": True, + }) + return UserResponse( + uuid=user_uuid, + username=req.username, + role=req.role, + must_change_password=True, + ) + + +@router.delete( + "/config/users/{user_uuid}", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required / cannot delete self"}, + 404: {"description": "User not found"}, + }, +) +async def api_delete_user( + user_uuid: str, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + if user_uuid == admin["uuid"]: + raise HTTPException(status_code=403, detail="Cannot delete your own account") + + deleted = await repo.delete_user(user_uuid) + if not deleted: + raise HTTPException(status_code=404, detail="User not found") + return {"message": "User deleted"} + + +@router.put( + "/config/users/{user_uuid}/role", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required / cannot change own role"}, + 404: {"description": "User not found"}, + 422: {"description": "Validation error"}, + }, +) +async def api_update_user_role( + user_uuid: str, + req: UpdateUserRoleRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + if user_uuid == admin["uuid"]: + raise HTTPException(status_code=403, detail="Cannot change your own role") + + target = await repo.get_user_by_uuid(user_uuid) + if not target: + raise HTTPException(status_code=404, detail="User not found") + + await repo.update_user_role(user_uuid, req.role) + return {"message": "User role updated"} + + +@router.put( + "/config/users/{user_uuid}/reset-password", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 404: {"description": "User not found"}, + 422: {"description": "Validation error"}, + }, +) +async def api_reset_user_password( + user_uuid: str, + req: ResetUserPasswordRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + target = await repo.get_user_by_uuid(user_uuid) + if not target: + raise HTTPException(status_code=404, detail="User not found") + + await repo.update_user_password( + user_uuid, + get_password_hash(req.new_password), + must_change_password=True, + ) + return {"message": "Password reset successfully"} diff --git a/decnet/web/router/config/api_reinit.py b/decnet/web/router/config/api_reinit.py new file mode 100644 index 0000000..ced28b1 --- /dev/null +++ b/decnet/web/router/config/api_reinit.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException + +from decnet.env import DECNET_DEVELOPER +from decnet.web.dependencies import require_admin, repo + +router = APIRouter() + + +@router.delete( + "/config/reinit", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required or developer mode not enabled"}, + }, +) +async def api_reinit(admin: dict = Depends(require_admin)) -> dict: + if not DECNET_DEVELOPER: + raise HTTPException(status_code=403, detail="Developer mode is not enabled") + + counts = await repo.purge_logs_and_bounties() + return { + "message": "Data purged", + "deleted": counts, + } diff --git a/decnet/web/router/config/api_update_config.py b/decnet/web/router/config/api_update_config.py new file mode 100644 index 0000000..d5c60f8 --- /dev/null +++ b/decnet/web/router/config/api_update_config.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, Depends + +from decnet.web.dependencies import require_admin, repo +from decnet.web.db.models import DeploymentLimitRequest, GlobalMutationIntervalRequest + +router = APIRouter() + + +@router.put( + "/config/deployment-limit", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 422: {"description": "Validation error"}, + }, +) +async def api_update_deployment_limit( + req: DeploymentLimitRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + await repo.set_state("config_limits", {"deployment_limit": req.deployment_limit}) + return {"message": "Deployment limit updated"} + + +@router.put( + "/config/global-mutation-interval", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 422: {"description": "Validation error"}, + }, +) +async def api_update_global_mutation_interval( + req: GlobalMutationIntervalRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + await repo.set_state( + "config_globals", + {"global_mutation_interval": req.global_mutation_interval}, + ) + return {"message": "Global mutation interval updated"}