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:
@@ -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",
|
||||
]
|
||||
|
||||
55
backend/app/schemas/api_key.py
Normal file
55
backend/app/schemas/api_key.py
Normal 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
|
||||
65
backend/app/schemas/audit_log.py
Normal file
65
backend/app/schemas/audit_log.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
87
backend/app/schemas/file.py
Normal file
87
backend/app/schemas/file.py
Normal 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
|
||||
75
backend/app/schemas/notification.py
Normal file
75
backend/app/schemas/notification.py
Normal 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
|
||||
}
|
||||
)
|
||||
55
backend/app/schemas/session.py
Normal file
55
backend/app/schemas/session.py
Normal 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]
|
||||
98
backend/app/schemas/webhook.py
Normal file
98
backend/app/schemas/webhook.py
Normal 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
|
||||
Reference in New Issue
Block a user