- 시스템 관리자 화면: 「시스템 설정」을 리소스 현황 아래로 · 제목 줄 오른쪽에 회사 고르기(기본 자기 회사) - 고른 회사 = sessionStorage aislo.adminCompany · 프로젝트 · 사용자 관리 · 가입 요청 · 회사 관리 · 팀원 추가가 그 회사 것 - 카드 해시에 &company=<id> · 회사 관리자 · 개인 화면은 그대로 · 임시 보관함은 사람 것이라 그대로 - 서버: /admin/projects · /admin/join-requests · /admin/members(POST · invite)에 company_id(_scope_company 한 곳 · 시스템 관리자만 남의 회사) · /admin/users 에 회사 거르기 - 라우터 700줄 넘어 나눔: 권한 도우미 B01_Dashboard_Scope · 회사 자산 B01_Dashboard_Router_Assets(본체가 include) - 시험 test_b01_settings_cards 보탬 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
566 lines
22 KiB
Python
566 lines
22 KiB
Python
"""B01_Dashboard 역할별 대시보드 API."""
|
|
|
|
import html
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, Response
|
|
from pymysql.err import IntegrityError
|
|
|
|
from common_util.common_util_auth import require_company, require_system_admin, verify_session
|
|
from common_util.common_util_email import send_email
|
|
from common_util.common_util_project_delete import hard_delete_project
|
|
from config.config_system import APP_PUBLIC_BASE_URL, PROJECT_DELETE_HARD_ENABLED
|
|
|
|
from .B01_Dashboard_Map import fetch_base_tile, geocode_address, search_address
|
|
from .B01_Dashboard_Repository import (
|
|
assign_user_company,
|
|
change_user_role,
|
|
count_company_admins,
|
|
get_dashboard_me,
|
|
get_project,
|
|
get_system_resources,
|
|
get_user_admin_target,
|
|
get_user_ui_prefs,
|
|
list_all_projects,
|
|
list_all_users,
|
|
list_audit_logs,
|
|
list_company_projects,
|
|
list_user_projects,
|
|
save_user_ui_prefs,
|
|
soft_delete_project,
|
|
soft_delete_user,
|
|
update_admin_user,
|
|
update_project,
|
|
update_user_profile,
|
|
)
|
|
from .B01_Dashboard_Repository_Company import (
|
|
create_company,
|
|
get_user_company,
|
|
join_company,
|
|
list_all_companies,
|
|
list_join_requests,
|
|
process_join_request,
|
|
search_companies,
|
|
update_company,
|
|
)
|
|
from .B01_Dashboard_Repository_Members import (
|
|
attach_company_member,
|
|
check_project_refs,
|
|
is_project_member,
|
|
list_company_members,
|
|
list_unassigned_users,
|
|
remove_company_member,
|
|
set_project_members,
|
|
)
|
|
from .B01_Dashboard_Router_Assets import router as assets_router
|
|
from .B01_Dashboard_Schema import (
|
|
AddMemberRequest,
|
|
AdminUpdateUserRequest,
|
|
AssignCompanyRequest,
|
|
ChangeUserRoleRequest,
|
|
CreateCompanyRequest,
|
|
InviteMemberRequest,
|
|
JoinCompanyRequest,
|
|
ProcessJoinRequest,
|
|
UiPrefsRequest,
|
|
UpdateProjectRequest,
|
|
UpdateUserRequest,
|
|
)
|
|
from .B01_Dashboard_Scope import (
|
|
_require_company_id,
|
|
_same_company,
|
|
_scope_company,
|
|
require_company_admin,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/dashboard", tags=["B01_Dashboard"])
|
|
|
|
|
|
async def _can_edit_project(session: dict[str, Any], project: dict[str, Any]) -> bool:
|
|
if session["role"] == "SYSTEM_ADMIN":
|
|
return True
|
|
if session["role"] == "ADMIN":
|
|
return _same_company(session, project.get("company_id"))
|
|
# 참여자로 지정된 일반 사용자도 수정할 수 있다 (2026-09-06 사용자 확정).
|
|
return _same_company(session, project.get("company_id")) and await is_project_member(
|
|
str(project["id"]), int(session["user_id"])
|
|
)
|
|
|
|
|
|
def _can_edit_user(session: dict[str, Any], target: dict[str, Any]) -> bool:
|
|
if session["role"] == "SYSTEM_ADMIN":
|
|
return True
|
|
if session["role"] == "ADMIN":
|
|
return _same_company(session, target.get("company_id"))
|
|
return int(session["user_id"]) == int(target["id"])
|
|
|
|
|
|
@router.get("/me")
|
|
async def dashboard_me(session: dict[str, Any] = Depends(verify_session)):
|
|
user = await get_dashboard_me(int(session["user_id"]))
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="사용자 정보를 찾을 수 없습니다.")
|
|
# 삭제 버튼이 원본까지 지우는 모드인지 화면에 알려 경고 문구를 바꾸게 한다.
|
|
user["project_delete_hard"] = PROJECT_DELETE_HARD_ENABLED
|
|
return {"status": "success", "user": user}
|
|
|
|
|
|
@router.patch("/me")
|
|
async def patch_dashboard_me(
|
|
payload: UpdateUserRequest,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
user = await update_user_profile(int(session["user_id"]), payload.model_dump())
|
|
return {"status": "success", "user": user}
|
|
|
|
|
|
# ── 화면 취향 (2026-09-07 사용자 승인) ───────────────────────────────────────
|
|
# 패널 높이·접힘 같은 **배치 값**만 계정에 붙인다. 설계값은 여기 오지 않는다 — 초안·결과는
|
|
# 브라우저에 두고 [저장]·[확정]에서만 정본으로 간다(CLAUDE.md 5장).
|
|
# 브라우저는 이 값을 **로컬 사본 위에 병합**하고, 서버가 없거나 못 읽으면 로컬 값으로 돈다.
|
|
@router.get("/me/ui-prefs")
|
|
async def read_ui_prefs(session: dict[str, Any] = Depends(verify_session)):
|
|
return {"status": "success", "prefs": await get_user_ui_prefs(int(session["user_id"]))}
|
|
|
|
|
|
@router.put("/me/ui-prefs")
|
|
async def write_ui_prefs(
|
|
payload: UiPrefsRequest,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
await save_user_ui_prefs(int(session["user_id"]), payload.prefs)
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/user/projects")
|
|
async def user_projects(session: dict[str, Any] = Depends(verify_session)):
|
|
# 회사에 속하면 회사 프로젝트 전체를 본다 (2026-09-06 사용자 지시) — 수정 권한은 따로다.
|
|
company_id = session.get("company_id")
|
|
projects = (
|
|
await list_company_projects(int(company_id))
|
|
if company_id
|
|
else await list_user_projects(int(session["user_id"]))
|
|
)
|
|
return {"status": "success", "projects": projects}
|
|
|
|
|
|
@router.get("/user/company")
|
|
async def user_company(session: dict[str, Any] = Depends(verify_session)):
|
|
return {"status": "success", "company": await get_user_company(session.get("company_id"))}
|
|
|
|
|
|
@router.get("/user/companies")
|
|
async def user_company_search(
|
|
q: str = Query(min_length=1, max_length=100),
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
_ = session
|
|
return {"status": "success", "companies": await search_companies(q.strip())}
|
|
|
|
|
|
@router.post("/user/company/create")
|
|
async def user_company_create(
|
|
request: Request,
|
|
payload: CreateCompanyRequest,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
result = await create_company(int(session["user_id"]), payload.model_dump(), request)
|
|
return {"status": "success", **result}
|
|
|
|
|
|
@router.put("/company")
|
|
async def company_update(
|
|
payload: CreateCompanyRequest,
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
"""회사 정보 수정 — 시스템관리자만 남의 회사를 지정할 수 있다."""
|
|
try:
|
|
changed = await update_company(_scope_company(session, company_id), payload.model_dump())
|
|
except IntegrityError as exc:
|
|
raise HTTPException(
|
|
status_code=409, detail="같은 회사명 또는 사업자등록번호가 이미 있습니다."
|
|
) from exc
|
|
if not changed:
|
|
raise HTTPException(status_code=404, detail="회사를 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/company/geocode")
|
|
async def company_geocode(
|
|
address: str = Query(min_length=2, max_length=500),
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
"""주소를 좌표로 바꾼다 — 회사 등록·수정 화면의 지도 미리보기용."""
|
|
_ = session
|
|
point = await geocode_address(address)
|
|
if not point:
|
|
raise HTTPException(status_code=404, detail="주소를 찾지 못했습니다.")
|
|
return {"status": "success", **point}
|
|
|
|
|
|
@router.get("/company/address/search")
|
|
async def company_address_search(
|
|
query: str = Query(min_length=2, max_length=200),
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
"""주소 후보를 찾아 준다 — 회사 등록·수정 화면에서 골라 넣는 용도."""
|
|
_ = session
|
|
return {"status": "success", "items": await search_address(query)}
|
|
|
|
|
|
@router.get("/map/tile/{z}/{x}/{y}")
|
|
async def map_tile(
|
|
z: int = Path(ge=0, le=19),
|
|
x: int = Path(ge=0),
|
|
y: int = Path(ge=0),
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
"""배경지도 타일 — VWorld 키가 화면으로 새지 않게 서버가 받아 넘긴다."""
|
|
_ = session
|
|
if x >= 2**z or y >= 2**z:
|
|
raise HTTPException(status_code=400, detail="지도 타일 번호가 범위를 벗어났습니다.")
|
|
try:
|
|
tile = await fetch_base_tile(z, x, y)
|
|
except Exception as exc: # 지도 한 칸이 비는 것은 화면을 막을 일이 아니다
|
|
raise HTTPException(status_code=502, detail="지도를 불러오지 못했습니다.") from exc
|
|
return Response(content=tile, media_type="image/png", headers={"Cache-Control": "max-age=3600"})
|
|
|
|
|
|
@router.post("/user/company/join")
|
|
async def user_company_join(
|
|
payload: JoinCompanyRequest,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
result = await join_company(int(session["user_id"]), payload.company_id)
|
|
if result is None:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="이미 회사에 소속돼 있습니다. 소속을 옮기려면 회사 관리자에게 요청하십시오.",
|
|
)
|
|
return {"status": "success", **result}
|
|
|
|
|
|
@router.get("/admin/members")
|
|
async def admin_members(
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_company),
|
|
):
|
|
# 읽기는 회사 구성원 누구나 — B02 등록 화면에서 일반 사용자도 담당자를 골라야 한다
|
|
# (2026-09-02 사용자 지시). 쓰기(POST·DELETE)는 관리자 그대로다.
|
|
# company_id 는 시스템관리자가 남의 회사 프로젝트 담당자를 고를 때만 쓴다.
|
|
return {
|
|
"status": "success",
|
|
"members": await list_company_members(_scope_company(session, company_id)),
|
|
}
|
|
|
|
|
|
@router.get("/admin/members/candidates")
|
|
async def admin_member_candidates(
|
|
q: str = Query(min_length=2, max_length=100),
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
"""팀원으로 부를 수 있는 사람 — 소속이 없는 가입자만 (2026-09-06 사용자 확정)."""
|
|
_ = session
|
|
return {"status": "success", "users": await list_unassigned_users(q)}
|
|
|
|
|
|
@router.post("/admin/members")
|
|
async def admin_add_member(
|
|
payload: AddMemberRequest,
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
member = await attach_company_member(_scope_company(session, company_id), payload.user_id)
|
|
if not member:
|
|
raise HTTPException(
|
|
status_code=409, detail="이미 다른 회사 소속이거나 찾을 수 없는 사용자입니다."
|
|
)
|
|
return {"status": "success", "member": member}
|
|
|
|
|
|
@router.post("/admin/members/invite")
|
|
async def admin_invite_member(
|
|
payload: InviteMemberRequest,
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
"""아직 가입하지 않은 사람에게 안내 메일만 보낸다 — 계정을 대신 만들지 않는다."""
|
|
company = await get_user_company(_scope_company(session, company_id))
|
|
company_name = (company or {}).get("name") or "회사"
|
|
# 메일 본문에 사람이 넣은 글자가 그대로 들어가면 남의 편지함에 임의 HTML 을 보낼 수 있다.
|
|
safe_company = html.escape(company_name)
|
|
safe_name = html.escape(payload.name or "")
|
|
sent = await send_email(
|
|
payload.email,
|
|
f"[Aislo] {company_name} 팀원 등록 안내",
|
|
f"<p>{safe_name}님, {safe_company} 에서 Aislo 팀원으로 등록하려 합니다.</p>"
|
|
f"<p>아래 주소에서 가입한 뒤 알려 주시면 회사 관리자가 팀원으로 등록합니다.</p>"
|
|
f'<p><a href="{APP_PUBLIC_BASE_URL}">{APP_PUBLIC_BASE_URL}</a></p>',
|
|
)
|
|
if not sent:
|
|
raise HTTPException(status_code=502, detail="안내 메일을 보내지 못했습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.delete("/admin/members/{user_id}")
|
|
async def admin_remove_member(
|
|
user_id: int, session: dict[str, Any] = Depends(require_company_admin)
|
|
):
|
|
if not await remove_company_member(_require_company_id(session), user_id):
|
|
raise HTTPException(status_code=404, detail="제거할 팀원을 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/admin/join-requests")
|
|
async def admin_join_requests(
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
scoped = _scope_company(session, company_id)
|
|
return {"status": "success", "requests": await list_join_requests(scoped)}
|
|
|
|
|
|
@router.patch("/admin/join-requests/{request_id}")
|
|
async def admin_process_join_request(
|
|
request_id: int,
|
|
payload: ProcessJoinRequest,
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
company_id = None if session["role"] == "SYSTEM_ADMIN" else _require_company_id(session)
|
|
changed = await process_join_request(
|
|
request_id,
|
|
int(session["user_id"]),
|
|
payload.action == "APPROVE",
|
|
company_id,
|
|
)
|
|
if not changed:
|
|
raise HTTPException(status_code=404, detail="처리할 가입 신청을 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/admin/projects")
|
|
async def admin_projects(
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
return {
|
|
"status": "success",
|
|
"projects": await list_company_projects(_scope_company(session, company_id)),
|
|
}
|
|
|
|
|
|
@router.put("/projects/{project_id}")
|
|
async def dashboard_update_project(
|
|
project_id: str,
|
|
request: Request,
|
|
payload: UpdateProjectRequest,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
project = await get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
|
if not await _can_edit_project(session, project):
|
|
raise HTTPException(status_code=403, detail="프로젝트 수정 권한이 없습니다.")
|
|
data = payload.model_dump()
|
|
# 시작이 종료보다 뒤면 남는 구간이 없다 — B02 등록과 같은 규칙 (2026-09-04 사용자 지시).
|
|
start_m, end_m = data.get("route_start_m"), data.get("route_end_m")
|
|
if start_m is not None and end_m is not None and start_m >= end_m:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="노선 시작 누가거리는 종료 누가거리보다 작아야 합니다.",
|
|
)
|
|
await check_project_refs(int(project["company_id"]), data)
|
|
member_ids = data.pop("member_user_ids", None)
|
|
if not await update_project(project_id, data, int(session["user_id"]), request):
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
|
if member_ids is not None:
|
|
# 만든 사람은 늘 참여자로 남는다.
|
|
await set_project_members(project_id, [int(project["user_id"]), *member_ids])
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.delete("/projects/{project_id}")
|
|
async def dashboard_delete_project(
|
|
project_id: str,
|
|
request: Request,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
project = await get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
|
|
|
# 회사 관리자는 자기 회사 프로젝트를 지운다 (2026-09-06 사용자 확정).
|
|
can_del = session["role"] == "SYSTEM_ADMIN" or (
|
|
session["role"] == "ADMIN" and _same_company(session, project.get("company_id"))
|
|
)
|
|
if not can_del:
|
|
raise HTTPException(status_code=403, detail="프로젝트 삭제 권한이 없습니다.")
|
|
|
|
# 개발 PC에서만 하드 삭제. 배포 기본값은 지금까지처럼 소프트 삭제다.
|
|
delete_project = hard_delete_project if PROJECT_DELETE_HARD_ENABLED else soft_delete_project
|
|
if not await delete_project(project_id, int(session["user_id"]), request):
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/admin/companies")
|
|
async def system_companies(session: dict[str, Any] = Depends(require_system_admin)):
|
|
_ = session
|
|
return {"status": "success", "companies": await list_all_companies()}
|
|
|
|
|
|
@router.post("/admin/companies")
|
|
async def system_create_company(
|
|
request: Request,
|
|
payload: CreateCompanyRequest,
|
|
session: dict[str, Any] = Depends(require_system_admin),
|
|
):
|
|
result = await create_company(int(session["user_id"]), payload.model_dump(), request)
|
|
return {"status": "success", **result}
|
|
|
|
|
|
@router.get("/admin/users")
|
|
async def system_users(
|
|
company_id: int | None = Query(None, gt=0),
|
|
session: dict[str, Any] = Depends(require_system_admin),
|
|
):
|
|
"""company_id = 대시보드에서 고른 회사 사람만(29-1) · 없으면 전체."""
|
|
_ = session
|
|
return {"status": "success", "users": await list_all_users(company_id)}
|
|
|
|
|
|
@router.patch("/admin/users/{user_id}/role")
|
|
async def system_change_role(
|
|
user_id: int,
|
|
payload: ChangeUserRoleRequest,
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
target = await get_user_admin_target(user_id)
|
|
if not target:
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
# 회사 관리자는 자기 회사 사람만 (2026-09-06 사용자 확정). 시스템관리자는 전역이다.
|
|
if session["role"] != "SYSTEM_ADMIN" and not _same_company(session, target.get("company_id")):
|
|
raise HTTPException(status_code=403, detail="다른 회사 사용자는 바꿀 수 없습니다.")
|
|
if payload.role == "SYSTEM_ADMIN" or target["role"] == "SYSTEM_ADMIN":
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="시스템 관리자 역할은 시스템 관리 회사 소속 여부로 정해집니다.",
|
|
)
|
|
# 회사에 관리자가 하나도 남지 않게 되는 강등은 막는다 (본인 강등 포함).
|
|
if (
|
|
target["role"] == "ADMIN"
|
|
and payload.role != "ADMIN"
|
|
and target.get("company_id")
|
|
and await count_company_admins(int(target["company_id"])) <= 1
|
|
):
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="회사에 관리자가 한 명뿐입니다. 다른 관리자를 먼저 지정하십시오.",
|
|
)
|
|
if not await change_user_role(user_id, payload.role):
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.put("/admin/users/{user_id}")
|
|
async def admin_update_user(
|
|
user_id: int,
|
|
payload: AdminUpdateUserRequest,
|
|
session: dict[str, Any] = Depends(verify_session),
|
|
):
|
|
target = await get_user_admin_target(user_id)
|
|
if not target:
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
if not _can_edit_user(session, target):
|
|
raise HTTPException(status_code=403, detail="사용자 수정 권한이 없습니다.")
|
|
data = payload.model_dump()
|
|
if session["role"] == "USER":
|
|
data["status"] = None
|
|
if not await update_admin_user(user_id, data):
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.delete("/admin/users/{user_id}")
|
|
async def admin_delete_user(
|
|
user_id: int,
|
|
session: dict[str, Any] = Depends(require_company_admin),
|
|
):
|
|
target = await get_user_admin_target(user_id)
|
|
if not target:
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
if session["role"] != "SYSTEM_ADMIN" and not _same_company(session, target.get("company_id")):
|
|
raise HTTPException(status_code=403, detail="다른 회사 사용자는 지울 수 없습니다.")
|
|
if int(session["user_id"]) == user_id:
|
|
raise HTTPException(status_code=409, detail="본인 계정은 지울 수 없습니다.")
|
|
# 회사에 관리자가 하나도 남지 않게 되는 삭제는 막는다.
|
|
if (
|
|
target["role"] == "ADMIN"
|
|
and target.get("company_id")
|
|
and await count_company_admins(int(target["company_id"])) <= 1
|
|
):
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="회사에 관리자가 한 명뿐입니다. 다른 관리자를 먼저 지정하십시오.",
|
|
)
|
|
if not await soft_delete_user(user_id):
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.patch("/admin/users/{user_id}/company")
|
|
async def system_assign_company(
|
|
user_id: int,
|
|
payload: AssignCompanyRequest,
|
|
session: dict[str, Any] = Depends(require_system_admin),
|
|
):
|
|
_ = session
|
|
if not await assign_user_company(user_id, payload.company_id):
|
|
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/admin/join-requests-all")
|
|
async def system_join_requests(session: dict[str, Any] = Depends(require_system_admin)):
|
|
_ = session
|
|
return {"status": "success", "requests": await list_join_requests()}
|
|
|
|
|
|
@router.patch("/admin/join-requests/{request_id}/approve")
|
|
async def system_approve_join_request(
|
|
request_id: int,
|
|
session: dict[str, Any] = Depends(require_system_admin),
|
|
):
|
|
if not await process_join_request(request_id, int(session["user_id"]), True):
|
|
raise HTTPException(status_code=404, detail="처리할 가입 신청을 찾을 수 없습니다.")
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/admin/audit-logs")
|
|
async def system_audit_logs(
|
|
page: int = Query(1, ge=1),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
session: dict[str, Any] = Depends(require_system_admin),
|
|
):
|
|
_ = session
|
|
return {"status": "success", **await list_audit_logs(limit, (page - 1) * limit)}
|
|
|
|
|
|
@router.get("/admin/resources")
|
|
async def system_resources(
|
|
days: int = Query(30, ge=1, le=90),
|
|
session: dict[str, Any] = Depends(require_system_admin),
|
|
):
|
|
_ = session
|
|
return {"status": "success", **await get_system_resources(days)}
|
|
|
|
|
|
@router.get("/admin/projects-all")
|
|
async def system_projects(session: dict[str, Any] = Depends(require_system_admin)):
|
|
_ = session
|
|
return {"status": "success", "projects": await list_all_projects()}
|
|
|
|
|
|
router.include_router(assets_router)
|