fix: P1安全加固 — 全局JWT保护 + 审计日志补全 + 敏感字段过滤
Settings API: - 所有端点添加JWT认证(get_current_admin) - 不可改项(secret_key/api_key/encryption_key/database_url)禁止PUT修改 - 敏感字段GET响应自动脱敏(前8位+...) - Schedules/Presets/Retry CRUD添加JWT+审计日志 Servers API: - POST/PUT/DELETE添加JWT认证 - 服务器增删改写审计日志 Assets API: - Platform/Node写操作添加JWT认证 - Platform创建添加审计日志
This commit is contained in:
+44
-7
@@ -1,13 +1,16 @@
|
|||||||
"""Nexus — Platform & Node API Routes (Asset organization layer)
|
"""Nexus — Platform & Node API Routes (Asset organization layer)
|
||||||
|
All write operations require JWT authentication.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
|
|
||||||
from server.api.dependencies import get_db
|
from server.api.dependencies import get_db
|
||||||
|
from server.api.auth_jwt import get_current_admin
|
||||||
from server.api.schemas import PlatformCreate, PlatformUpdate, NodeCreate, NodeUpdate
|
from server.api.schemas import PlatformCreate, PlatformUpdate, NodeCreate, NodeUpdate
|
||||||
from server.infrastructure.database.platform_node_repo import PlatformRepositoryImpl, NodeRepositoryImpl
|
from server.infrastructure.database.platform_node_repo import PlatformRepositoryImpl, NodeRepositoryImpl
|
||||||
from server.domain.models import Platform, Node
|
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
||||||
|
from server.domain.models import Platform, Node, Admin, AuditLog
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -35,16 +38,33 @@ async def get_platform(id: int, db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/platforms", response_model=dict, status_code=201)
|
@router.post("/platforms", response_model=dict, status_code=201)
|
||||||
async def create_platform(payload: PlatformCreate, db: AsyncSession = Depends(get_db)):
|
async def create_platform(
|
||||||
|
payload: PlatformCreate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Create a new platform template"""
|
"""Create a new platform template"""
|
||||||
repo = PlatformRepositoryImpl(db)
|
repo = PlatformRepositoryImpl(db)
|
||||||
platform = Platform(**payload.model_dump(exclude_none=True))
|
platform = Platform(**payload.model_dump(exclude_none=True))
|
||||||
created = await repo.create(platform)
|
created = await repo.create(platform)
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username, action="create_platform",
|
||||||
|
target_type="platform", target_id=created.id,
|
||||||
|
detail=f"name={created.name}", ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return _platform_to_dict(created)
|
return _platform_to_dict(created)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/platforms/{id}", response_model=dict)
|
@router.put("/platforms/{id}", response_model=dict)
|
||||||
async def update_platform(id: int, payload: PlatformUpdate, db: AsyncSession = Depends(get_db)):
|
async def update_platform(
|
||||||
|
id: int,
|
||||||
|
payload: PlatformUpdate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Update a platform template"""
|
"""Update a platform template"""
|
||||||
repo = PlatformRepositoryImpl(db)
|
repo = PlatformRepositoryImpl(db)
|
||||||
platform = await repo.get_by_id(id)
|
platform = await repo.get_by_id(id)
|
||||||
@@ -58,7 +78,11 @@ async def update_platform(id: int, payload: PlatformUpdate, db: AsyncSession = D
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/platforms/{id}", status_code=204)
|
@router.delete("/platforms/{id}", status_code=204)
|
||||||
async def delete_platform(id: int, db: AsyncSession = Depends(get_db)):
|
async def delete_platform(
|
||||||
|
id: int,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Delete a platform template"""
|
"""Delete a platform template"""
|
||||||
repo = PlatformRepositoryImpl(db)
|
repo = PlatformRepositoryImpl(db)
|
||||||
result = await repo.delete(id)
|
result = await repo.delete(id)
|
||||||
@@ -91,7 +115,11 @@ async def get_node_tree(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/nodes", response_model=dict, status_code=201)
|
@router.post("/nodes", response_model=dict, status_code=201)
|
||||||
async def create_node(payload: NodeCreate, db: AsyncSession = Depends(get_db)):
|
async def create_node(
|
||||||
|
payload: NodeCreate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Create a new node"""
|
"""Create a new node"""
|
||||||
repo = NodeRepositoryImpl(db)
|
repo = NodeRepositoryImpl(db)
|
||||||
node = Node(**payload.model_dump(exclude_none=True))
|
node = Node(**payload.model_dump(exclude_none=True))
|
||||||
@@ -100,7 +128,12 @@ async def create_node(payload: NodeCreate, db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@router.put("/nodes/{id}", response_model=dict)
|
@router.put("/nodes/{id}", response_model=dict)
|
||||||
async def update_node(id: int, payload: NodeUpdate, db: AsyncSession = Depends(get_db)):
|
async def update_node(
|
||||||
|
id: int,
|
||||||
|
payload: NodeUpdate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Update a node"""
|
"""Update a node"""
|
||||||
repo = NodeRepositoryImpl(db)
|
repo = NodeRepositoryImpl(db)
|
||||||
node = await repo.get_by_id(id)
|
node = await repo.get_by_id(id)
|
||||||
@@ -114,7 +147,11 @@ async def update_node(id: int, payload: NodeUpdate, db: AsyncSession = Depends(g
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/nodes/{id}", status_code=204)
|
@router.delete("/nodes/{id}", status_code=204)
|
||||||
async def delete_node(id: int, db: AsyncSession = Depends(get_db)):
|
async def delete_node(
|
||||||
|
id: int,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Delete a node"""
|
"""Delete a node"""
|
||||||
repo = NodeRepositoryImpl(db)
|
repo = NodeRepositoryImpl(db)
|
||||||
result = await repo.delete(id)
|
result = await repo.delete(id)
|
||||||
|
|||||||
+44
-1
@@ -2,6 +2,7 @@
|
|||||||
Presentation layer — receives HTTP requests, delegates to ServerService/SyncService.
|
Presentation layer — receives HTTP requests, delegates to ServerService/SyncService.
|
||||||
|
|
||||||
Live server status comes from Redis (real-time), base info from MySQL.
|
Live server status comes from Redis (real-time), base info from MySQL.
|
||||||
|
All write operations require JWT authentication and produce audit logs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -15,8 +16,9 @@ from server.api.auth_jwt import get_current_admin
|
|||||||
from server.api.schemas import ServerCreate, ServerUpdate, ServerPush, ServerCheck
|
from server.api.schemas import ServerCreate, ServerUpdate, ServerPush, ServerCheck
|
||||||
from server.application.services.server_service import ServerService
|
from server.application.services.server_service import ServerService
|
||||||
from server.application.services.sync_service import SyncService
|
from server.application.services.sync_service import SyncService
|
||||||
from server.domain.models import Server, SyncLog, Admin
|
from server.domain.models import Server, SyncLog, Admin, AuditLog
|
||||||
from server.infrastructure.redis.client import get_redis
|
from server.infrastructure.redis.client import get_redis
|
||||||
|
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -158,11 +160,24 @@ async def get_server(
|
|||||||
@router.post("/", response_model=dict, status_code=201)
|
@router.post("/", response_model=dict, status_code=201)
|
||||||
async def create_server(
|
async def create_server(
|
||||||
payload: ServerCreate,
|
payload: ServerCreate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
service: ServerService = Depends(get_server_service),
|
service: ServerService = Depends(get_server_service),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Create a new server"""
|
"""Create a new server"""
|
||||||
server = Server(**payload.model_dump(exclude_none=True))
|
server = Server(**payload.model_dump(exclude_none=True))
|
||||||
created = await service.create_server(server)
|
created = await service.create_server(server)
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="create_server",
|
||||||
|
target_type="server",
|
||||||
|
target_id=created.id,
|
||||||
|
detail=f"name={created.name} domain={created.domain}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return _server_to_dict(created)
|
return _server_to_dict(created)
|
||||||
|
|
||||||
|
|
||||||
@@ -170,7 +185,9 @@ async def create_server(
|
|||||||
async def update_server(
|
async def update_server(
|
||||||
id: int,
|
id: int,
|
||||||
payload: ServerUpdate,
|
payload: ServerUpdate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
service: ServerService = Depends(get_server_service),
|
service: ServerService = Depends(get_server_service),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update an existing server"""
|
"""Update an existing server"""
|
||||||
server = await service.get_server(id)
|
server = await service.get_server(id)
|
||||||
@@ -180,19 +197,45 @@ async def update_server(
|
|||||||
if hasattr(server, key) and key != "id":
|
if hasattr(server, key) and key != "id":
|
||||||
setattr(server, key, value)
|
setattr(server, key, value)
|
||||||
updated = await service.update_server(server)
|
updated = await service.update_server(server)
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="update_server",
|
||||||
|
target_type="server",
|
||||||
|
target_id=id,
|
||||||
|
detail=f"name={updated.name}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return _server_to_dict(updated)
|
return _server_to_dict(updated)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}", status_code=204)
|
@router.delete("/{id}", status_code=204)
|
||||||
async def delete_server(
|
async def delete_server(
|
||||||
id: int,
|
id: int,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
service: ServerService = Depends(get_server_service),
|
service: ServerService = Depends(get_server_service),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Delete a server"""
|
"""Delete a server"""
|
||||||
|
server = await service.get_server(id)
|
||||||
|
if not server:
|
||||||
|
raise HTTPException(status_code=404, detail="Server not found")
|
||||||
result = await service.delete_server(id)
|
result = await service.delete_server(id)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="Server not found")
|
raise HTTPException(status_code=404, detail="Server not found")
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="delete_server",
|
||||||
|
target_type="server",
|
||||||
|
target_id=id,
|
||||||
|
detail=f"name={server.name}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
# ── Health Check ──
|
# ── Health Check ──
|
||||||
|
|
||||||
|
|||||||
+132
-17
@@ -1,48 +1,86 @@
|
|||||||
"""Nexus — Settings & Schedules API Routes
|
"""Nexus — Settings & Schedules API Routes
|
||||||
Presentation layer — system settings + push schedules + password presets + audit logs.
|
Presentation layer — system settings + push schedules + password presets + audit logs.
|
||||||
|
|
||||||
|
Security: All write operations require JWT authentication.
|
||||||
|
Immutable settings (secret_key, api_key, encryption_key, database_url) cannot be modified via API.
|
||||||
|
Sensitive values are masked in GET responses.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from server.api.dependencies import get_db
|
from server.api.dependencies import get_db
|
||||||
|
from server.api.auth_jwt import get_current_admin
|
||||||
from server.api.schemas import ScheduleCreate, ScheduleUpdate, PresetCreate, SettingUpdatePayload
|
from server.api.schemas import ScheduleCreate, ScheduleUpdate, PresetCreate, SettingUpdatePayload
|
||||||
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
|
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
|
||||||
from server.infrastructure.database.push_schedule_repo import PushScheduleRepositoryImpl, PushRetryJobRepositoryImpl
|
from server.infrastructure.database.push_schedule_repo import PushScheduleRepositoryImpl, PushRetryJobRepositoryImpl
|
||||||
from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl
|
from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl
|
||||||
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
||||||
from server.domain.models import Setting, PushSchedule, PushRetryJob, PasswordPreset, AuditLog
|
from server.domain.models import Setting, PushSchedule, PushRetryJob, PasswordPreset, AuditLog, Admin
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
# Settings that cannot be modified via API (encryption consistency)
|
||||||
|
IMMUTABLE_KEYS = {"secret_key", "api_key", "encryption_key", "database_url"}
|
||||||
|
|
||||||
|
# Settings whose values are masked in GET responses
|
||||||
|
SENSITIVE_KEYS = {"secret_key", "api_key", "encryption_key", "redis_url"}
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||||
|
|
||||||
|
|
||||||
# ── System Settings ──
|
# ── System Settings ──
|
||||||
|
|
||||||
@router.get("/", response_model=list)
|
@router.get("/", response_model=list)
|
||||||
async def list_settings(db: AsyncSession = Depends(get_db)):
|
async def list_settings(admin: Admin = Depends(get_current_admin), db: AsyncSession = Depends(get_db)):
|
||||||
"""List all system settings"""
|
"""List all system settings (sensitive values masked)"""
|
||||||
repo = SettingRepositoryImpl(db)
|
repo = SettingRepositoryImpl(db)
|
||||||
settings = await repo.get_all()
|
settings = await repo.get_all()
|
||||||
return [{"key": s.key, "value": s.value, "updated_at": str(s.updated_at)} for s in settings]
|
result = []
|
||||||
|
for s in settings:
|
||||||
|
val = s.value
|
||||||
|
if s.key in SENSITIVE_KEYS and val:
|
||||||
|
val = val[:8] + "..." if len(val) > 8 else "***"
|
||||||
|
result.append({"key": s.key, "value": val, "updated_at": str(s.updated_at)})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{key}", response_model=dict)
|
@router.get("/{key}", response_model=dict)
|
||||||
async def get_setting(key: str, db: AsyncSession = Depends(get_db)):
|
async def get_setting(key: str, admin: Admin = Depends(get_current_admin), db: AsyncSession = Depends(get_db)):
|
||||||
"""Get a single setting by key"""
|
"""Get a single setting by key (sensitive values masked)"""
|
||||||
repo = SettingRepositoryImpl(db)
|
repo = SettingRepositoryImpl(db)
|
||||||
value = await repo.get(key)
|
value = await repo.get(key)
|
||||||
if value is None:
|
if value is None:
|
||||||
raise HTTPException(status_code=404, detail="Setting not found")
|
raise HTTPException(status_code=404, detail="Setting not found")
|
||||||
|
if key in SENSITIVE_KEYS and value:
|
||||||
|
value = value[:8] + "..." if len(value) > 8 else "***"
|
||||||
return {"key": key, "value": value}
|
return {"key": key, "value": value}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{key}", response_model=dict)
|
@router.put("/{key}", response_model=dict)
|
||||||
async def set_setting(key: str, payload: SettingUpdatePayload, db: AsyncSession = Depends(get_db)):
|
async def set_setting(
|
||||||
"""Set a system setting value"""
|
key: str,
|
||||||
|
payload: SettingUpdatePayload,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Set a system setting value (immutable keys are rejected)"""
|
||||||
|
if key in IMMUTABLE_KEYS:
|
||||||
|
raise HTTPException(status_code=403, detail=f"Setting '{key}' is immutable and cannot be modified via API")
|
||||||
|
|
||||||
repo = SettingRepositoryImpl(db)
|
repo = SettingRepositoryImpl(db)
|
||||||
setting = await repo.set(key, str(payload.value))
|
setting = await repo.set(key, str(payload.value))
|
||||||
|
|
||||||
|
# Audit log
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="update_setting",
|
||||||
|
target_type="setting",
|
||||||
|
detail=f"key={key}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return {"key": setting.key, "value": setting.value}
|
return {"key": setting.key, "value": setting.value}
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +90,7 @@ schedule_router = APIRouter(prefix="/api/schedules", tags=["schedules"])
|
|||||||
|
|
||||||
|
|
||||||
@schedule_router.get("/", response_model=list)
|
@schedule_router.get("/", response_model=list)
|
||||||
async def list_schedules(db: AsyncSession = Depends(get_db)):
|
async def list_schedules(admin: Admin = Depends(get_current_admin), db: AsyncSession = Depends(get_db)):
|
||||||
"""List all push schedules"""
|
"""List all push schedules"""
|
||||||
repo = PushScheduleRepositoryImpl(db)
|
repo = PushScheduleRepositoryImpl(db)
|
||||||
schedules = await repo.get_all()
|
schedules = await repo.get_all()
|
||||||
@@ -60,16 +98,36 @@ async def list_schedules(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@schedule_router.post("/", response_model=dict, status_code=201)
|
@schedule_router.post("/", response_model=dict, status_code=201)
|
||||||
async def create_schedule(payload: ScheduleCreate, db: AsyncSession = Depends(get_db)):
|
async def create_schedule(
|
||||||
|
payload: ScheduleCreate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Create a new push schedule"""
|
"""Create a new push schedule"""
|
||||||
repo = PushScheduleRepositoryImpl(db)
|
repo = PushScheduleRepositoryImpl(db)
|
||||||
schedule = PushSchedule(**payload.model_dump())
|
schedule = PushSchedule(**payload.model_dump())
|
||||||
created = await repo.create(schedule)
|
created = await repo.create(schedule)
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="create_schedule",
|
||||||
|
target_type="schedule",
|
||||||
|
target_id=created.id,
|
||||||
|
detail=f"name={created.name}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return _schedule_to_dict(created)
|
return _schedule_to_dict(created)
|
||||||
|
|
||||||
|
|
||||||
@schedule_router.put("/{id}", response_model=dict)
|
@schedule_router.put("/{id}", response_model=dict)
|
||||||
async def update_schedule(id: int, payload: ScheduleUpdate, db: AsyncSession = Depends(get_db)):
|
async def update_schedule(
|
||||||
|
id: int,
|
||||||
|
payload: ScheduleUpdate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Update a push schedule"""
|
"""Update a push schedule"""
|
||||||
repo = PushScheduleRepositoryImpl(db)
|
repo = PushScheduleRepositoryImpl(db)
|
||||||
schedule = await repo.get_by_id(id)
|
schedule = await repo.get_by_id(id)
|
||||||
@@ -79,17 +137,41 @@ async def update_schedule(id: int, payload: ScheduleUpdate, db: AsyncSession = D
|
|||||||
if hasattr(schedule, key) and key != "id":
|
if hasattr(schedule, key) and key != "id":
|
||||||
setattr(schedule, key, value)
|
setattr(schedule, key, value)
|
||||||
updated = await repo.update(schedule)
|
updated = await repo.update(schedule)
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="update_schedule",
|
||||||
|
target_type="schedule",
|
||||||
|
target_id=id,
|
||||||
|
detail=f"name={updated.name}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return _schedule_to_dict(updated)
|
return _schedule_to_dict(updated)
|
||||||
|
|
||||||
|
|
||||||
@schedule_router.delete("/{id}", status_code=204)
|
@schedule_router.delete("/{id}", status_code=204)
|
||||||
async def delete_schedule(id: int, db: AsyncSession = Depends(get_db)):
|
async def delete_schedule(
|
||||||
|
id: int,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Delete a push schedule"""
|
"""Delete a push schedule"""
|
||||||
repo = PushScheduleRepositoryImpl(db)
|
repo = PushScheduleRepositoryImpl(db)
|
||||||
result = await repo.delete(id)
|
result = await repo.delete(id)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="delete_schedule",
|
||||||
|
target_type="schedule",
|
||||||
|
target_id=id,
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
# ── Password Presets ──
|
# ── Password Presets ──
|
||||||
|
|
||||||
@@ -97,7 +179,7 @@ preset_router = APIRouter(prefix="/api/presets", tags=["presets"])
|
|||||||
|
|
||||||
|
|
||||||
@preset_router.get("/", response_model=list)
|
@preset_router.get("/", response_model=list)
|
||||||
async def list_presets(db: AsyncSession = Depends(get_db)):
|
async def list_presets(admin: Admin = Depends(get_current_admin), db: AsyncSession = Depends(get_db)):
|
||||||
"""List all password presets"""
|
"""List all password presets"""
|
||||||
repo = PasswordPresetRepositoryImpl(db)
|
repo = PasswordPresetRepositoryImpl(db)
|
||||||
presets = await repo.get_all()
|
presets = await repo.get_all()
|
||||||
@@ -105,24 +187,52 @@ async def list_presets(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@preset_router.post("/", response_model=dict, status_code=201)
|
@preset_router.post("/", response_model=dict, status_code=201)
|
||||||
async def create_preset(payload: PresetCreate, db: AsyncSession = Depends(get_db)):
|
async def create_preset(
|
||||||
|
payload: PresetCreate,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Create a password preset (password will be encrypted)"""
|
"""Create a password preset (password will be encrypted)"""
|
||||||
from server.infrastructure.database.crypto import encrypt_value
|
from server.infrastructure.database.crypto import encrypt_value
|
||||||
repo = PasswordPresetRepositoryImpl(db)
|
repo = PasswordPresetRepositoryImpl(db)
|
||||||
encrypted = encrypt_value(payload.encrypted_pw)
|
encrypted = encrypt_value(payload.encrypted_pw)
|
||||||
preset = PasswordPreset(name=payload.name, encrypted_pw=encrypted)
|
preset = PasswordPreset(name=payload.name, encrypted_pw=encrypted)
|
||||||
created = await repo.create(preset)
|
created = await repo.create(preset)
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="create_preset",
|
||||||
|
target_type="preset",
|
||||||
|
target_id=created.id,
|
||||||
|
detail=f"name={created.name}",
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
return {"id": created.id, "name": created.name}
|
return {"id": created.id, "name": created.name}
|
||||||
|
|
||||||
|
|
||||||
@preset_router.delete("/{id}", status_code=204)
|
@preset_router.delete("/{id}", status_code=204)
|
||||||
async def delete_preset(id: int, db: AsyncSession = Depends(get_db)):
|
async def delete_preset(
|
||||||
|
id: int,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""Delete a password preset"""
|
"""Delete a password preset"""
|
||||||
repo = PasswordPresetRepositoryImpl(db)
|
repo = PasswordPresetRepositoryImpl(db)
|
||||||
result = await repo.delete(id)
|
result = await repo.delete(id)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="Preset not found")
|
raise HTTPException(status_code=404, detail="Preset not found")
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="delete_preset",
|
||||||
|
target_type="preset",
|
||||||
|
target_id=id,
|
||||||
|
ip_address="",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
# ── Audit Logs ──
|
# ── Audit Logs ──
|
||||||
|
|
||||||
@@ -133,6 +243,7 @@ audit_router = APIRouter(prefix="/api/audit", tags=["audit"])
|
|||||||
async def list_audit_logs(
|
async def list_audit_logs(
|
||||||
action: Optional[str] = None,
|
action: Optional[str] = None,
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""List recent audit logs, optionally filtered by action"""
|
"""List recent audit logs, optionally filtered by action"""
|
||||||
@@ -150,7 +261,11 @@ retry_router = APIRouter(prefix="/api/retries", tags=["retries"])
|
|||||||
|
|
||||||
|
|
||||||
@retry_router.get("/", response_model=list)
|
@retry_router.get("/", response_model=list)
|
||||||
async def list_retry_jobs(limit: int = 100, db: AsyncSession = Depends(get_db)):
|
async def list_retry_jobs(
|
||||||
|
limit: int = 100,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
"""List pending retry jobs"""
|
"""List pending retry jobs"""
|
||||||
repo = PushRetryJobRepositoryImpl(db)
|
repo = PushRetryJobRepositoryImpl(db)
|
||||||
jobs = await repo.get_pending(limit)
|
jobs = await repo.get_pending(limit)
|
||||||
@@ -199,4 +314,4 @@ def _retry_to_dict(job: PushRetryJob) -> dict:
|
|||||||
"next_retry_at": str(job.next_retry_at) if job.next_retry_at else None,
|
"next_retry_at": str(job.next_retry_at) if job.next_retry_at else None,
|
||||||
"last_error": job.last_error,
|
"last_error": job.last_error,
|
||||||
"created_at": str(job.created_at) if job.created_at else None,
|
"created_at": str(job.created_at) if job.created_at else None,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user