2026-06-01 15:54:25 +00:00
|
|
|
"""Auth HTTP routes."""
|
|
|
|
|
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
from backend.auth.deps import get_current_user, require_admin
|
|
|
|
|
from backend.auth.models import ROLES, UserContext
|
|
|
|
|
from backend.auth import database as db
|
|
|
|
|
from backend.auth.service import (
|
|
|
|
|
authenticate,
|
|
|
|
|
create_org_user,
|
|
|
|
|
create_personal_project,
|
|
|
|
|
delete_personal_project,
|
|
|
|
|
get_user_context,
|
|
|
|
|
list_accessible_projects,
|
2026-06-01 16:16:23 +00:00
|
|
|
normalize_project_slug,
|
2026-06-01 15:54:25 +00:00
|
|
|
update_user_projects,
|
|
|
|
|
user_to_dict,
|
|
|
|
|
)
|
|
|
|
|
from src.config import load_config
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
|
|
|
admin_router = APIRouter(prefix="/api/admin", tags=["admin"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
|
|
|
org_slug: str = Field(default="merakom")
|
|
|
|
|
username: str
|
|
|
|
|
password: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateUserRequest(BaseModel):
|
|
|
|
|
username: str
|
|
|
|
|
password: str
|
|
|
|
|
role: str = "user"
|
|
|
|
|
projects: List[str] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateProjectRequest(BaseModel):
|
|
|
|
|
slug: str
|
|
|
|
|
name: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateUserProjectsRequest(BaseModel):
|
|
|
|
|
projects: List[str] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/login")
|
|
|
|
|
async def login(payload: LoginRequest):
|
|
|
|
|
result = authenticate(payload.org_slug, payload.username, payload.password)
|
|
|
|
|
if not result:
|
|
|
|
|
raise HTTPException(status_code=401, detail="Неверная организация, логин или пароль")
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/me")
|
|
|
|
|
async def me(user: UserContext = Depends(get_current_user)):
|
|
|
|
|
return user_to_dict(user)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/projects")
|
|
|
|
|
async def my_projects(user: UserContext = Depends(get_current_user)):
|
|
|
|
|
return {"projects": list_accessible_projects(user)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/projects")
|
|
|
|
|
async def create_my_project(payload: CreateProjectRequest, user: UserContext = Depends(get_current_user)):
|
|
|
|
|
try:
|
|
|
|
|
project = create_personal_project(user, payload.slug, payload.name)
|
|
|
|
|
return {"project": project}
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
|
except Exception as e:
|
|
|
|
|
if "UNIQUE" in str(e):
|
|
|
|
|
raise HTTPException(status_code=409, detail="Проект уже существует") from e
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/projects/{slug}")
|
|
|
|
|
async def delete_my_project(slug: str, user: UserContext = Depends(get_current_user)):
|
|
|
|
|
try:
|
|
|
|
|
delete_personal_project(user, slug)
|
|
|
|
|
return {"deleted": slug}
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/users")
|
|
|
|
|
async def admin_list_users(admin: UserContext = Depends(require_admin)):
|
|
|
|
|
config = load_config()
|
|
|
|
|
rows = db.list_users(admin.org_id, config)
|
|
|
|
|
users = []
|
|
|
|
|
for row in rows:
|
|
|
|
|
ctx = get_user_context(row["id"])
|
|
|
|
|
if ctx:
|
|
|
|
|
users.append(user_to_dict(ctx))
|
|
|
|
|
return {"users": users}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/users")
|
|
|
|
|
async def admin_create_user(payload: CreateUserRequest, admin: UserContext = Depends(require_admin)):
|
|
|
|
|
if payload.role not in ROLES:
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"role must be one of: {', '.join(ROLES)}")
|
|
|
|
|
try:
|
|
|
|
|
user = create_org_user(admin, payload.username, payload.password, payload.role, payload.projects)
|
|
|
|
|
return {"user": user}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
if "UNIQUE" in str(e):
|
|
|
|
|
raise HTTPException(status_code=409, detail="Пользователь уже существует") from e
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.put("/users/{user_id}/projects")
|
|
|
|
|
async def admin_set_user_projects(
|
|
|
|
|
user_id: int,
|
|
|
|
|
payload: UpdateUserProjectsRequest,
|
|
|
|
|
admin: UserContext = Depends(require_admin),
|
|
|
|
|
):
|
|
|
|
|
try:
|
|
|
|
|
user = update_user_projects(admin, user_id, payload.projects)
|
|
|
|
|
return {"user": user}
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/projects")
|
|
|
|
|
async def admin_list_projects(admin: UserContext = Depends(require_admin)):
|
|
|
|
|
config = load_config()
|
|
|
|
|
return {"projects": db.list_projects(admin.org_id, config)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/projects")
|
|
|
|
|
async def admin_create_project(payload: CreateProjectRequest, admin: UserContext = Depends(require_admin)):
|
|
|
|
|
try:
|
2026-06-01 16:16:23 +00:00
|
|
|
slug = normalize_project_slug(payload.slug)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
|
display_name = payload.name.strip() or slug
|
|
|
|
|
try:
|
|
|
|
|
project = db.create_project(admin.org_id, slug, display_name, owner_user_id=None, config=load_config())
|
2026-06-01 15:54:25 +00:00
|
|
|
return {"project": project}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
if "UNIQUE" in str(e):
|
|
|
|
|
raise HTTPException(status_code=409, detail="Проект уже существует") from e
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|