Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8594eaf
docs(spec): add admin console full-stack redesign design
AperturePlus May 24, 2026
091b6f7
docs(plans): add admin console backend (Plan A) and frontend (Plan B)…
AperturePlus May 24, 2026
02d95de
feat(web): align api+mock with Plan A admin contracts
AperturePlus May 24, 2026
a4a4cad
feat(web): scaffold Console layout, sidebar, and 9 subpage routes
AperturePlus May 24, 2026
4846456
feat(web): add Console shared components
AperturePlus May 24, 2026
03c1165
feat(web): Console Overview page
AperturePlus May 24, 2026
68f133d
feat(web): Console Users page
AperturePlus May 24, 2026
c34cdb8
feat(web): Console Storage page with trend and quota editing
AperturePlus May 24, 2026
0b12217
feat(web): Console Content audit page
AperturePlus May 24, 2026
89305c9
feat(web): Console Moderation page
AperturePlus May 24, 2026
79bff83
feat(web): Console System page
AperturePlus May 24, 2026
cc8326d
feat(web): Console Logs page
AperturePlus May 24, 2026
15feac3
feat(web): Console Notifications page with broadcast
AperturePlus May 24, 2026
db4e746
feat(web): Console Rules page (registration email domains)
AperturePlus May 24, 2026
518414e
feat(web): wire Console into MainLayout, drop legacy Dashboard, i18n
AperturePlus May 24, 2026
005d7aa
feat(admin): scaffold admin packages and user status mapping
AperturePlus May 24, 2026
51f8b11
feat(admin): users service with list and set_status (last-admin guard)
AperturePlus May 24, 2026
af44fd9
feat(admin): /admin/users list + /admin/users/{id}/status routes
AperturePlus May 24, 2026
7c4d665
FIX viewerjs type error
AperturePlus May 24, 2026
4d0a3c5
feat(admin): /admin/storage summary, users, quota, usage-trend
AperturePlus May 24, 2026
f5b92f7
feat(admin): /admin/files list + /admin/files/{id}/rescan with event
AperturePlus May 24, 2026
915c131
feat(admin): /admin/violations list + resolve via ModerationCase
AperturePlus May 24, 2026
6ac141f
feat(admin): /admin/logs list with filters
AperturePlus May 24, 2026
3bc3688
feat(admin): /admin/notifications list, broadcast, archive
AperturePlus May 24, 2026
4b4adfb
feat(admin): /admin/system health and rate-limit
AperturePlus May 24, 2026
aa1b760
feat(upload): add backend cancel-upload-session endpoint
AperturePlus May 24, 2026
d6ce680
feat(upload): add frontend cancel types, api, and mock
AperturePlus May 24, 2026
89469c8
refactor(upload): add AbortSignal support to hash and uploader utils
AperturePlus May 24, 2026
a978b85
feat(upload): create Pinia upload store with cancel support
AperturePlus May 24, 2026
6fbf985
feat(upload): integrate cancel into components and composable
AperturePlus May 24, 2026
430215d
test(upload): add backend tests for cancel session
AperturePlus May 24, 2026
921205e
feat(upload): add backend recoverable-sessions endpoint
AperturePlus May 24, 2026
fc67161
feat(upload): add frontend types, api, and mock for recoverable uploads
AperturePlus May 24, 2026
cb5c830
feat(upload): add completeUploadSession and onFileHashed to uploader
AperturePlus May 24, 2026
7ba9556
feat(upload): add i18n keys for resume, paused, and session-expired
AperturePlus May 24, 2026
677d00c
feat(upload): add persistence, recovery, and resume to Pinia upload s…
AperturePlus May 24, 2026
7c6f39f
feat(upload): add paused/resume UI to UploadProgressTray component
AperturePlus May 24, 2026
54eaf40
feat(upload): wire resumeUpload into composable and MyFiles page
AperturePlus May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/src/fileflash/core/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
)
from ..services.archive import ArchiveService
from ..services.agent import ExecuteService, McpService, MemoryService, PlanService, SessionService, SettingsService, SkillService
from ..services.admin.users import AdminUsersService
from ..services.admin.storage import AdminStorageService
from ..services.admin.files import AdminFilesService
from ..services.admin.moderation import AdminModerationService
from ..services.admin.logs import AdminLogsService
from ..services.admin.notifications import AdminNotificationsService
from ..services.admin.system import AdminSystemService
from ..services.auth import AuthService
from ..services.background_jobs import BackgroundJobService
from ..services.email_delivery import VerificationEmailDeliveryService
Expand Down Expand Up @@ -120,6 +127,51 @@ def get_registration_email_domain_rule_service(
return RegistrationEmailDomainRuleService(db=db)


def get_admin_users_service(
db: AsyncSession = Depends(get_db),
) -> AdminUsersService:
return AdminUsersService(db=db)


def get_admin_storage_service(
db: AsyncSession = Depends(get_db),
rate_limiter: RedisRateLimiter = Depends(get_rate_limiter),
) -> AdminStorageService:
return AdminStorageService(db=db, redis=getattr(rate_limiter, "_redis", None))


def get_admin_files_service(
db: AsyncSession = Depends(get_db),
event_publisher: InProcessAuthEventPublisher = Depends(get_event_publisher),
) -> AdminFilesService:
return AdminFilesService(db=db, publisher=event_publisher)


def get_admin_moderation_service(
db: AsyncSession = Depends(get_db),
) -> AdminModerationService:
return AdminModerationService(db=db)


def get_admin_logs_service(
db: AsyncSession = Depends(get_db),
) -> AdminLogsService:
return AdminLogsService(db=db)


def get_admin_notifications_service(
db: AsyncSession = Depends(get_db),
) -> AdminNotificationsService:
return AdminNotificationsService(db=db)


def get_admin_system_service(
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(get_settings_dep),
) -> AdminSystemService:
return AdminSystemService(db=db, settings=settings)


def get_upload_service(
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(get_settings_dep),
Expand Down
14 changes: 14 additions & 0 deletions app/src/fileflash/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from fastapi import APIRouter

from .auth import router as auth_router
from .admin_users import router as admin_users_router
from .admin_storage import router as admin_storage_router
from .admin_files import router as admin_files_router
from .admin_moderation import router as admin_moderation_router
from .admin_logs import router as admin_logs_router
from .admin_notifications import router as admin_notifications_router
from .admin_system import router as admin_system_router
from .admin_registration_email_domain_rules import router as admin_registration_email_domain_rules_router
from .files import router as files_router
from .folders import router as folders_router
Expand All @@ -14,6 +21,13 @@

api_router = APIRouter()
api_router.include_router(auth_router)
api_router.include_router(admin_users_router)
api_router.include_router(admin_storage_router)
api_router.include_router(admin_files_router)
api_router.include_router(admin_moderation_router)
api_router.include_router(admin_logs_router)
api_router.include_router(admin_notifications_router)
api_router.include_router(admin_system_router)
api_router.include_router(admin_registration_email_domain_rules_router)
api_router.include_router(files_router)
api_router.include_router(folders_router)
Expand Down
34 changes: 34 additions & 0 deletions app/src/fileflash/routers/admin_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_files_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..schemas.admin.files import ListAdminFilesQuery
from ..services.admin.files import AdminFilesService

router = APIRouter(prefix="/admin/files", tags=["admin"])


@router.get("")
async def list_admin_files(
query: ListAdminFilesQuery = Depends(),
_: User = Depends(require_admin),
service: AdminFilesService = Depends(get_admin_files_service),
):
data = await service.list_files(query=query)
return api_success(data=data.model_dump(by_alias=True), message="Files fetched")


@router.post("/{file_id}/rescan")
async def rescan_admin_file(
file_id: int,
admin: User = Depends(require_admin),
service: AdminFilesService = Depends(get_admin_files_service),
):
result = await service.request_rescan(file_id=file_id, requested_by=admin.user_id)
return api_success(data=result.model_dump(by_alias=True), message="Rescan requested")


__all__ = ["router"]
24 changes: 24 additions & 0 deletions app/src/fileflash/routers/admin_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_logs_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..schemas.admin.logs import ListAdminLogsQuery
from ..services.admin.logs import AdminLogsService

router = APIRouter(prefix="/admin/logs", tags=["admin"])


@router.get("")
async def list_admin_logs(
query: ListAdminLogsQuery = Depends(),
_: User = Depends(require_admin),
service: AdminLogsService = Depends(get_admin_logs_service),
):
data = await service.list_logs(query=query)
return api_success(data=data.model_dump(by_alias=True), message="Logs fetched")


__all__ = ["router"]
34 changes: 34 additions & 0 deletions app/src/fileflash/routers/admin_moderation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_moderation_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..schemas.admin.moderation import ListViolationsQuery
from ..services.admin.moderation import AdminModerationService

router = APIRouter(prefix="/admin/violations", tags=["admin"])


@router.get("")
async def list_violations(
query: ListViolationsQuery = Depends(),
_: User = Depends(require_admin),
service: AdminModerationService = Depends(get_admin_moderation_service),
):
data = await service.list_violations(query=query)
return api_success(data=data.model_dump(by_alias=True), message="Violations fetched")


@router.post("/{case_id}/resolve")
async def resolve_violation(
case_id: int,
admin: User = Depends(require_admin),
service: AdminModerationService = Depends(get_admin_moderation_service),
):
result = await service.resolve_case(case_id=case_id, handled_by=admin.user_id)
return api_success(data=result.model_dump(by_alias=True), message="Violation resolved")


__all__ = ["router"]
47 changes: 47 additions & 0 deletions app/src/fileflash/routers/admin_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_notifications_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..schemas.admin.notifications import BroadcastRequest, ListAdminNotificationsQuery
from ..services.admin.notifications import AdminNotificationsService

router = APIRouter(prefix="/admin/notifications", tags=["admin"])


@router.get("")
async def list_admin_notifications(
query: ListAdminNotificationsQuery = Depends(),
_: User = Depends(require_admin),
service: AdminNotificationsService = Depends(get_admin_notifications_service),
):
data = await service.list_notifications(query=query)
return api_success(data=data.model_dump(by_alias=True), message="Notifications fetched")


@router.post("/broadcast")
async def broadcast_notification(
payload: BroadcastRequest,
admin: User = Depends(require_admin),
service: AdminNotificationsService = Depends(get_admin_notifications_service),
):
result = await service.broadcast(payload=payload, sender_id=admin.user_id)
return api_success(data=result.model_dump(by_alias=True), message="Broadcast sent")


@router.delete("/{notification_id}")
async def archive_admin_notification(
notification_id: int,
_: User = Depends(require_admin),
service: AdminNotificationsService = Depends(get_admin_notifications_service),
):
await service.archive(notification_id=notification_id)
return api_success(
data={"notificationId": str(notification_id), "status": "archived"},
message="Notification archived",
)


__all__ = ["router"]
54 changes: 54 additions & 0 deletions app/src/fileflash/routers/admin_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_storage_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..schemas.admin.storage import ListStorageUsersQuery, UpdateQuotaRequest, UsageTrendQuery
from ..services.admin.storage import AdminStorageService

router = APIRouter(prefix="/admin/storage", tags=["admin"])


@router.get("/summary")
async def get_admin_storage_summary(
_: User = Depends(require_admin),
service: AdminStorageService = Depends(get_admin_storage_service),
):
data = await service.summary()
return api_success(data=data.model_dump(by_alias=True), message="Storage summary fetched")


@router.get("/users")
async def list_admin_storage_users(
query: ListStorageUsersQuery = Depends(),
_: User = Depends(require_admin),
service: AdminStorageService = Depends(get_admin_storage_service),
):
data = await service.list_storage_users(query=query)
return api_success(data=data.model_dump(by_alias=True), message="Storage users fetched")


@router.patch("/users/{user_id}/quota")
async def update_admin_user_quota(
user_id: int,
payload: UpdateQuotaRequest,
_: User = Depends(require_admin),
service: AdminStorageService = Depends(get_admin_storage_service),
):
result = await service.update_quota(user_id=user_id, new_limit=payload.storage_limit)
return api_success(data=result.model_dump(by_alias=True), message="Quota updated")


@router.get("/usage-trend")
async def get_storage_usage_trend(
query: UsageTrendQuery = Depends(),
_: User = Depends(require_admin),
service: AdminStorageService = Depends(get_admin_storage_service),
):
result = await service.usage_trend(query=query)
return api_success(data=result.model_dump(by_alias=True), message="Usage trend fetched")


__all__ = ["router"]
31 changes: 31 additions & 0 deletions app/src/fileflash/routers/admin_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_system_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..services.admin.system import AdminSystemService

router = APIRouter(prefix="/admin/system", tags=["admin"])


@router.get("/health")
async def get_system_health(
_: User = Depends(require_admin),
service: AdminSystemService = Depends(get_admin_system_service),
):
data = await service.health()
return api_success(data=data.model_dump(by_alias=True), message="System health fetched")


@router.get("/rate-limit")
async def get_rate_limit_status(
_: User = Depends(require_admin),
service: AdminSystemService = Depends(get_admin_system_service),
):
data = await service.rate_limit_status()
return api_success(data=data.model_dump(by_alias=True), message="Rate limit fetched")


__all__ = ["router"]
35 changes: 35 additions & 0 deletions app/src/fileflash/routers/admin_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from ..core.deps import get_admin_users_service, require_admin
from ..core.errors import api_success
from ..models.tables_identity import User
from ..schemas.admin.users import ListAdminUsersQuery, UpdateUserStatusRequest
from ..services.admin.users import AdminUsersService

router = APIRouter(prefix="/admin/users", tags=["admin"])


@router.get("")
async def list_admin_users(
query: ListAdminUsersQuery = Depends(),
_: User = Depends(require_admin),
service: AdminUsersService = Depends(get_admin_users_service),
):
data = await service.list_users(query=query)
return api_success(data=data.model_dump(by_alias=True), message="Users fetched")


@router.patch("/{user_id}/status")
async def update_admin_user_status(
user_id: int,
payload: UpdateUserStatusRequest,
_: User = Depends(require_admin),
service: AdminUsersService = Depends(get_admin_users_service),
):
result = await service.set_status(user_id=user_id, external_status=payload.status)
return api_success(data=result.model_dump(by_alias=True), message="Status updated")


__all__ = ["router"]
28 changes: 28 additions & 0 deletions app/src/fileflash/routers/uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ async def preflight_upload(
return api_success(data=response.model_dump(by_alias=True), message="Ready for upload")


@router.get("/recoverable")
async def recoverable_uploads(
current_user: User = Depends(get_current_user),
upload_service: UploadService = Depends(get_upload_service),
):
sessions = await upload_service.list_recoverable_sessions(user_id=current_user.user_id)
return api_success(
data=[session.model_dump(by_alias=True) for session in sessions],
message="Recoverable upload sessions fetched",
)


@router.post("/{upload_id}/chunk")
async def upload_chunk(
upload_id: str,
Expand Down Expand Up @@ -59,3 +71,19 @@ async def merge_chunks(
code=201,
status_code=201,
)


@router.post("/{upload_id}/cancel")
async def cancel_upload(
upload_id: str,
current_user: User = Depends(get_current_user),
upload_service: UploadService = Depends(get_upload_service),
):
response = await upload_service.cancel_upload_session(
user_id=current_user.user_id,
upload_id=upload_id,
)
return api_success(
data=response.model_dump(by_alias=True),
message="Upload session canceled",
)
Loading
Loading