Add comprehensive backend features and mobile UI improvements

Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

View File

@@ -1,8 +1,27 @@
"""Schemas package - exports all Pydantic schemas."""
from app.schemas.user import User, UserCreate, UserUpdate, UserInDB
from app.schemas.auth import Token, TokenData, LoginRequest, RegisterRequest
from app.schemas.auth import Token, TokenData, LoginRequest, RegisterRequest, TokenWith2FA, Verify2FARequest
from app.schemas.settings import Setting, SettingUpdate
from app.schemas.audit_log import AuditLog as AuditLogSchema, AuditLogCreate, AuditLogList, AuditLogStats
from app.schemas.webhook import (
Webhook as WebhookSchema,
WebhookCreate,
WebhookUpdate,
WebhookWithSecret,
WebhookDelivery as WebhookDeliverySchema,
WebhookTest,
WEBHOOK_EVENTS,
)
from app.schemas.file import (
StoredFile as StoredFileSchema,
FileCreate,
FileUpdate,
FileUploadResponse,
FileListResponse,
ALLOWED_CONTENT_TYPES,
MAX_FILE_SIZE,
)
__all__ = [
"User",
@@ -13,6 +32,26 @@ __all__ = [
"TokenData",
"LoginRequest",
"RegisterRequest",
"TokenWith2FA",
"Verify2FARequest",
"Setting",
"SettingUpdate",
"AuditLogSchema",
"AuditLogCreate",
"AuditLogList",
"AuditLogStats",
"WebhookSchema",
"WebhookCreate",
"WebhookUpdate",
"WebhookWithSecret",
"WebhookDeliverySchema",
"WebhookTest",
"WEBHOOK_EVENTS",
"StoredFileSchema",
"FileCreate",
"FileUpdate",
"FileUploadResponse",
"FileListResponse",
"ALLOWED_CONTENT_TYPES",
"MAX_FILE_SIZE",
]

View File

@@ -0,0 +1,55 @@
"""Pydantic schemas for API Key requests/responses."""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
class APIKeyBase(BaseModel):
"""Base API key schema."""
name: str = Field(..., min_length=1, max_length=100)
scopes: Optional[List[str]] = None
expires_at: Optional[datetime] = None
class APIKeyCreate(APIKeyBase):
"""Schema for creating an API key."""
pass
class APIKeyUpdate(BaseModel):
"""Schema for updating an API key."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
scopes: Optional[List[str]] = None
is_active: Optional[bool] = None
expires_at: Optional[datetime] = None
class APIKey(BaseModel):
"""Schema for API key response (without the actual key)."""
id: str
user_id: str
name: str
key_prefix: str
scopes: Optional[List[str]] = None
is_active: bool
last_used_at: Optional[datetime] = None
last_used_ip: Optional[str] = None
usage_count: int = 0
expires_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class APIKeyWithSecret(APIKey):
"""Schema for API key response with the actual key (only on creation)."""
key: str # The actual API key - only shown once
class APIKeyList(BaseModel):
"""Schema for paginated API key list."""
items: List[APIKey]
total: int

View File

@@ -0,0 +1,65 @@
"""Pydantic schemas for Audit Log API requests/responses."""
from datetime import datetime
from typing import Optional, List, Any
from pydantic import BaseModel, Field
class AuditLogBase(BaseModel):
"""Base audit log schema."""
action: str = Field(..., max_length=50)
resource_type: Optional[str] = Field(None, max_length=50)
resource_id: Optional[str] = Field(None, max_length=255)
details: Optional[str] = None
ip_address: Optional[str] = Field(None, max_length=45)
user_agent: Optional[str] = Field(None, max_length=500)
status: str = Field(default="success", max_length=20)
class AuditLogCreate(AuditLogBase):
"""Schema for creating an audit log entry."""
user_id: Optional[str] = None
username: Optional[str] = None
class AuditLog(AuditLogBase):
"""Schema for audit log response."""
id: str
user_id: Optional[str] = None
username: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
class AuditLogList(BaseModel):
"""Schema for paginated audit log list."""
items: List[AuditLog]
total: int
page: int
page_size: int
total_pages: int
class AuditLogFilter(BaseModel):
"""Schema for filtering audit logs."""
user_id: Optional[str] = None
username: Optional[str] = None
action: Optional[str] = None
resource_type: Optional[str] = None
resource_id: Optional[str] = None
status: Optional[str] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
class AuditLogStats(BaseModel):
"""Schema for audit log statistics."""
total_entries: int
entries_today: int
entries_this_week: int
entries_this_month: int
actions_breakdown: dict[str, int]
top_users: List[dict[str, Any]]
recent_failures: int

View File

@@ -11,6 +11,15 @@ class Token(BaseModel):
token_type: str = "bearer"
class TokenWith2FA(BaseModel):
"""JWT token response with 2FA requirement indicator."""
access_token: Optional[str] = None
token_type: str = "bearer"
requires_2fa: bool = False
temp_token: Optional[str] = None # Temporary token for 2FA verification
class TokenData(BaseModel):
"""Token payload data schema."""
@@ -22,6 +31,14 @@ class LoginRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=100)
password: str = Field(..., min_length=1)
totp_code: Optional[str] = Field(None, min_length=6, max_length=8) # 6 digits or 8-char backup code
class Verify2FARequest(BaseModel):
"""2FA verification request schema."""
temp_token: str
code: str = Field(..., min_length=6, max_length=8)
class RegisterRequest(BaseModel):

View File

@@ -0,0 +1,87 @@
"""File storage schemas."""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
class FileBase(BaseModel):
"""Base file schema."""
description: Optional[str] = None
tags: Optional[List[str]] = None
is_public: bool = False
class FileCreate(FileBase):
"""Schema for file upload metadata."""
pass
class FileUpdate(BaseModel):
"""Schema for updating file metadata."""
description: Optional[str] = None
tags: Optional[List[str]] = None
is_public: Optional[bool] = None
class StoredFile(FileBase):
"""File response schema."""
id: str
original_filename: str
content_type: Optional[str] = None
size_bytes: int
storage_type: str
uploaded_by: Optional[str] = None
file_hash: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class FileUploadResponse(BaseModel):
"""Response after successful file upload."""
id: str
original_filename: str
content_type: Optional[str] = None
size_bytes: int
download_url: str
class FileListResponse(BaseModel):
"""Response for file listing."""
files: List[StoredFile]
total: int
page: int
page_size: int
# Allowed file types for upload
ALLOWED_CONTENT_TYPES = [
# Images
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
# Documents
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
"text/csv",
# Archives
"application/zip",
"application/x-tar",
"application/gzip",
# JSON/XML
"application/json",
"application/xml",
"text/xml",
]
# Maximum file size (10 MB by default)
MAX_FILE_SIZE = 10 * 1024 * 1024

View File

@@ -0,0 +1,75 @@
"""Pydantic schemas for Notification API requests/responses."""
from datetime import datetime
from typing import Optional, List, Any
from pydantic import BaseModel, Field
class NotificationBase(BaseModel):
"""Base notification schema."""
title: str = Field(..., min_length=1, max_length=200)
message: Optional[str] = None
type: str = Field(default="info", pattern="^(info|success|warning|error|system)$")
link: Optional[str] = Field(None, max_length=500)
extra_data: Optional[dict] = None
class NotificationCreate(NotificationBase):
"""Schema for creating a notification."""
user_id: str # Required for system/admin notifications
class NotificationCreateForUser(NotificationBase):
"""Schema for creating a notification for a specific user (admin use)."""
pass
class Notification(BaseModel):
"""Schema for notification response."""
id: str
user_id: str
title: str
message: Optional[str] = None
type: str
link: Optional[str] = None
extra_data: Optional[dict] = None
is_read: bool
created_at: datetime
read_at: Optional[datetime] = None
class Config:
from_attributes = True
class NotificationList(BaseModel):
"""Schema for paginated notification list."""
items: List[Notification]
total: int
unread_count: int
class NotificationStats(BaseModel):
"""Schema for notification statistics."""
total: int
unread: int
by_type: dict[str, int]
class NotificationBulkAction(BaseModel):
"""Schema for bulk notification actions."""
notification_ids: List[str]
class NotificationPreferences(BaseModel):
"""Schema for user notification preferences."""
email_notifications: bool = True
push_notifications: bool = True
notification_types: dict[str, bool] = Field(
default_factory=lambda: {
"info": True,
"success": True,
"warning": True,
"error": True,
"system": True
}
)

View File

@@ -0,0 +1,55 @@
"""Pydantic schemas for User Session API requests/responses."""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
class SessionBase(BaseModel):
"""Base session schema."""
device_name: Optional[str] = Field(None, max_length=200)
device_type: Optional[str] = Field(None, max_length=50)
browser: Optional[str] = Field(None, max_length=100)
os: Optional[str] = Field(None, max_length=100)
ip_address: Optional[str] = Field(None, max_length=45)
location: Optional[str] = Field(None, max_length=200)
class SessionCreate(SessionBase):
"""Schema for creating a session."""
user_id: str
token_hash: str
user_agent: Optional[str] = None
expires_at: Optional[datetime] = None
class Session(BaseModel):
"""Schema for session response."""
id: str
user_id: str
device_name: Optional[str] = None
device_type: Optional[str] = None
browser: Optional[str] = None
os: Optional[str] = None
ip_address: Optional[str] = None
location: Optional[str] = None
is_active: bool
is_current: bool
created_at: datetime
last_active_at: Optional[datetime] = None
expires_at: Optional[datetime] = None
class Config:
from_attributes = True
class SessionList(BaseModel):
"""Schema for session list."""
items: List[Session]
total: int
active_count: int
class SessionRevokeRequest(BaseModel):
"""Schema for revoking sessions."""
session_ids: List[str]

View File

@@ -0,0 +1,98 @@
"""Webhook schemas."""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, HttpUrl, Field
# Webhook Base
class WebhookBase(BaseModel):
"""Base webhook schema."""
name: str = Field(..., min_length=1, max_length=100)
url: str = Field(..., min_length=1, max_length=500)
events: List[str] = Field(default=["*"])
is_active: bool = True
retry_count: int = Field(default=3, ge=0, le=10)
timeout_seconds: int = Field(default=30, ge=5, le=120)
class WebhookCreate(WebhookBase):
"""Schema for creating a webhook."""
pass
class WebhookUpdate(BaseModel):
"""Schema for updating a webhook."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
url: Optional[str] = Field(None, min_length=1, max_length=500)
events: Optional[List[str]] = None
is_active: Optional[bool] = None
retry_count: Optional[int] = Field(None, ge=0, le=10)
timeout_seconds: Optional[int] = Field(None, ge=5, le=120)
class Webhook(WebhookBase):
"""Webhook response schema."""
id: str
secret: Optional[str] = None
created_by: Optional[str] = None
created_at: datetime
updated_at: datetime
last_triggered_at: Optional[datetime] = None
success_count: int
failure_count: int
class Config:
from_attributes = True
class WebhookWithSecret(Webhook):
"""Webhook response with secret (for creation)."""
secret: str
# Webhook Delivery
class WebhookDeliveryBase(BaseModel):
"""Base webhook delivery schema."""
webhook_id: str
event_type: str
payload: str
class WebhookDelivery(WebhookDeliveryBase):
"""Webhook delivery response schema."""
id: str
status: str
status_code: Optional[int] = None
response_body: Optional[str] = None
error_message: Optional[str] = None
attempt_count: int
next_retry_at: Optional[datetime] = None
created_at: datetime
delivered_at: Optional[datetime] = None
class Config:
from_attributes = True
# Event types
WEBHOOK_EVENTS = [
"user.created",
"user.updated",
"user.deleted",
"user.login",
"user.logout",
"user.password_changed",
"user.2fa_enabled",
"user.2fa_disabled",
"settings.updated",
"api_key.created",
"api_key.revoked",
"*", # All events
]
class WebhookTest(BaseModel):
"""Schema for testing a webhook."""
event_type: str = "test.ping"
payload: Optional[dict] = None