Init first files
This commit is contained in:
1
backend/app/api/routes/__init__.py
Normal file
1
backend/app/api/routes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
65
backend/app/api/routes/admin_users.py
Normal file
65
backend/app/api/routes/admin_users.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.db import get_db
|
||||
from app.core.deps import require_roles
|
||||
from app.core.security import hash_password
|
||||
from app.models.models import User
|
||||
from app.schemas.user import UserCreate, UserOut, UserUpdate
|
||||
from app.services.audit import write_audit_log
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
async def list_users(admin: User = Depends(require_roles("admin")), db: AsyncSession = Depends(get_db)) -> list[UserOut]:
|
||||
users = (await db.scalars(select(User).order_by(User.id.asc()))).all()
|
||||
_ = admin
|
||||
return [UserOut.model_validate(user) for user in users]
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(payload: UserCreate, admin: User = Depends(require_roles("admin")), db: AsyncSession = Depends(get_db)) -> UserOut:
|
||||
exists = await db.scalar(select(User).where(User.email == payload.email))
|
||||
if exists:
|
||||
raise HTTPException(status_code=409, detail="Email already exists")
|
||||
user = User(email=payload.email, password_hash=hash_password(payload.password), role=payload.role)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
await write_audit_log(db, "admin.user.create", admin.id, {"created_user_id": user.id})
|
||||
return UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserOut)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
payload: UserUpdate,
|
||||
admin: User = Depends(require_roles("admin")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> UserOut:
|
||||
user = await db.scalar(select(User).where(User.id == user_id))
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
if "password" in update_data and update_data["password"]:
|
||||
user.password_hash = hash_password(update_data.pop("password"))
|
||||
for key, value in update_data.items():
|
||||
setattr(user, key, value)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
await write_audit_log(db, "admin.user.update", admin.id, {"updated_user_id": user.id})
|
||||
return UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user(user_id: int, admin: User = Depends(require_roles("admin")), db: AsyncSession = Depends(get_db)) -> dict:
|
||||
if user_id == admin.id:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete yourself")
|
||||
user = await db.scalar(select(User).where(User.id == user_id))
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
await write_audit_log(db, "admin.user.delete", admin.id, {"deleted_user_id": user_id})
|
||||
return {"status": "deleted"}
|
||||
54
backend/app/api/routes/auth.py
Normal file
54
backend/app/api/routes/auth.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.config import get_settings
|
||||
from app.core.db import get_db
|
||||
from app.core.deps import get_current_user
|
||||
from app.core.security import create_access_token, create_refresh_token, verify_password
|
||||
from app.models.models import User
|
||||
from app.schemas.auth import LoginRequest, RefreshRequest, TokenResponse
|
||||
from app.schemas.user import UserOut
|
||||
from app.services.audit import write_audit_log
|
||||
|
||||
router = APIRouter()
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(payload: LoginRequest, db: AsyncSession = Depends(get_db)) -> TokenResponse:
|
||||
user = await db.scalar(select(User).where(User.email == payload.email))
|
||||
if not user or not verify_password(payload.password, user.password_hash):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
|
||||
await write_audit_log(db, action="auth.login", user_id=user.id, payload={"email": user.email})
|
||||
return TokenResponse(access_token=create_access_token(str(user.id)), refresh_token=create_refresh_token(str(user.id)))
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
async def refresh(payload: RefreshRequest, db: AsyncSession = Depends(get_db)) -> TokenResponse:
|
||||
try:
|
||||
token_payload = jwt.decode(payload.refresh_token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
||||
except JWTError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") from exc
|
||||
|
||||
if token_payload.get("type") != "refresh":
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token type")
|
||||
user_id = token_payload.get("sub")
|
||||
user = await db.scalar(select(User).where(User.id == int(user_id)))
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
||||
|
||||
await write_audit_log(db, action="auth.refresh", user_id=user.id, payload={})
|
||||
return TokenResponse(access_token=create_access_token(str(user.id)), refresh_token=create_refresh_token(str(user.id)))
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> dict:
|
||||
await write_audit_log(db, action="auth.logout", user_id=user.id, payload={})
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def me(user: User = Depends(get_current_user)) -> UserOut:
|
||||
return UserOut.model_validate(user)
|
||||
17
backend/app/api/routes/health.py
Normal file
17
backend/app/api/routes/health.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
from sqlalchemy import text
|
||||
from app.core.db import SessionLocal
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/healthz")
|
||||
async def healthz() -> dict:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/readyz")
|
||||
async def readyz() -> dict:
|
||||
async with SessionLocal() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
return {"status": "ready"}
|
||||
11
backend/app/api/routes/me.py
Normal file
11
backend/app/api/routes/me.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.deps import get_current_user
|
||||
from app.models.models import User
|
||||
from app.schemas.user import UserOut
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def me(user: User = Depends(get_current_user)) -> UserOut:
|
||||
return UserOut.model_validate(user)
|
||||
181
backend/app/api/routes/targets.py
Normal file
181
backend/app/api/routes/targets.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from datetime import datetime
|
||||
import asyncpg
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.db import get_db
|
||||
from app.core.deps import get_current_user, require_roles
|
||||
from app.models.models import Metric, QueryStat, Target, User
|
||||
from app.schemas.metric import MetricOut, QueryStatOut
|
||||
from app.schemas.target import TargetCreate, TargetOut, TargetUpdate
|
||||
from app.services.audit import write_audit_log
|
||||
from app.services.collector import build_target_dsn
|
||||
from app.services.crypto import encrypt_secret
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[TargetOut])
|
||||
async def list_targets(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> list[TargetOut]:
|
||||
targets = (await db.scalars(select(Target).order_by(Target.id.desc()))).all()
|
||||
return [TargetOut.model_validate(item) for item in targets]
|
||||
|
||||
|
||||
@router.post("", response_model=TargetOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_target(
|
||||
payload: TargetCreate,
|
||||
user: User = Depends(require_roles("admin", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> TargetOut:
|
||||
target = Target(
|
||||
name=payload.name,
|
||||
host=payload.host,
|
||||
port=payload.port,
|
||||
dbname=payload.dbname,
|
||||
username=payload.username,
|
||||
encrypted_password=encrypt_secret(payload.password),
|
||||
sslmode=payload.sslmode,
|
||||
tags=payload.tags,
|
||||
)
|
||||
db.add(target)
|
||||
await db.commit()
|
||||
await db.refresh(target)
|
||||
await write_audit_log(db, "target.create", user.id, {"target_id": target.id, "name": target.name})
|
||||
return TargetOut.model_validate(target)
|
||||
|
||||
|
||||
@router.get("/{target_id}", response_model=TargetOut)
|
||||
async def get_target(target_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> TargetOut:
|
||||
target = await db.scalar(select(Target).where(Target.id == target_id))
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Target not found")
|
||||
return TargetOut.model_validate(target)
|
||||
|
||||
|
||||
@router.put("/{target_id}", response_model=TargetOut)
|
||||
async def update_target(
|
||||
target_id: int,
|
||||
payload: TargetUpdate,
|
||||
user: User = Depends(require_roles("admin", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> TargetOut:
|
||||
target = await db.scalar(select(Target).where(Target.id == target_id))
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Target not found")
|
||||
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
if "password" in updates:
|
||||
target.encrypted_password = encrypt_secret(updates.pop("password"))
|
||||
for key, value in updates.items():
|
||||
setattr(target, key, value)
|
||||
await db.commit()
|
||||
await db.refresh(target)
|
||||
await write_audit_log(db, "target.update", user.id, {"target_id": target.id})
|
||||
return TargetOut.model_validate(target)
|
||||
|
||||
|
||||
@router.delete("/{target_id}")
|
||||
async def delete_target(
|
||||
target_id: int,
|
||||
user: User = Depends(require_roles("admin", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
target = await db.scalar(select(Target).where(Target.id == target_id))
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Target not found")
|
||||
await db.delete(target)
|
||||
await db.commit()
|
||||
await write_audit_log(db, "target.delete", user.id, {"target_id": target_id})
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.get("/{target_id}/metrics", response_model=list[MetricOut])
|
||||
async def get_metrics(
|
||||
target_id: int,
|
||||
metric: str = Query(...),
|
||||
from_ts: datetime = Query(alias="from"),
|
||||
to_ts: datetime = Query(alias="to"),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[MetricOut]:
|
||||
_ = user
|
||||
rows = (
|
||||
await db.scalars(
|
||||
select(Metric).where(
|
||||
and_(Metric.target_id == target_id, Metric.metric_name == metric, Metric.ts >= from_ts, Metric.ts <= to_ts)
|
||||
).order_by(Metric.ts.asc())
|
||||
)
|
||||
).all()
|
||||
return [MetricOut(ts=r.ts, metric_name=r.metric_name, value=r.value, labels=r.labels) for r in rows]
|
||||
|
||||
|
||||
async def _live_conn(target: Target) -> asyncpg.Connection:
|
||||
return await asyncpg.connect(dsn=build_target_dsn(target))
|
||||
|
||||
|
||||
@router.get("/{target_id}/locks")
|
||||
async def get_locks(target_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> list[dict]:
|
||||
_ = user
|
||||
target = await db.scalar(select(Target).where(Target.id == target_id))
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Target not found")
|
||||
conn = await _live_conn(target)
|
||||
try:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT locktype, mode, granted, relation::regclass::text AS relation, pid
|
||||
FROM pg_locks
|
||||
ORDER BY granted ASC, mode
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
@router.get("/{target_id}/activity")
|
||||
async def get_activity(target_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> list[dict]:
|
||||
_ = user
|
||||
target = await db.scalar(select(Target).where(Target.id == target_id))
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Target not found")
|
||||
conn = await _live_conn(target)
|
||||
try:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT pid, usename, application_name, client_addr::text, state, wait_event_type, wait_event, now() - query_start AS running_for, left(query, 300) AS query
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database()
|
||||
ORDER BY query_start NULLS LAST
|
||||
LIMIT 200
|
||||
"""
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
@router.get("/{target_id}/top-queries", response_model=list[QueryStatOut])
|
||||
async def get_top_queries(target_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)) -> list[QueryStatOut]:
|
||||
_ = user
|
||||
rows = (
|
||||
await db.scalars(
|
||||
select(QueryStat)
|
||||
.where(QueryStat.target_id == target_id)
|
||||
.order_by(desc(QueryStat.ts))
|
||||
.limit(100)
|
||||
)
|
||||
).all()
|
||||
return [
|
||||
QueryStatOut(
|
||||
ts=r.ts,
|
||||
queryid=r.queryid,
|
||||
calls=r.calls,
|
||||
total_time=r.total_time,
|
||||
mean_time=r.mean_time,
|
||||
rows=r.rows,
|
||||
query_text=r.query_text,
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
Reference in New Issue
Block a user