Initial commit - Project start (July 17, 2025)
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m28s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Has been skipped
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m28s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Has been skipped
Original: feat: enhance security logging and ensure rate limit blocks are properly tracked - Add comprehensive logging for rate limit blocks with full request details - IP address (with proper proxy detection), user agent, headers, timestamps - Rate limit info (current count, limit, remaining, reset time) - Separate tracking for auth vs general endpoints - Enhance authentication failure logging - JWT validation failures with detailed error info - Admin auth attempts without token - Failed token validation with user context - All events include IP, path, method, user agent - Improve Winston logger configuration for production - Add automatic log rotation (10MB errors, 50MB combined) - Create separate security.log for auth/rate limit events - Ensure logs directory exists automatically - Add structured JSON format for log aggregation - Support container logging with LOG_TO_CONSOLE env var - Create comprehensive documentation - Security logging guide with examples - Monitoring recommendations - Configuration reference - Add test script to verify logging functionality All rate limit settings remain configurable via admin panel: - Window duration, max requests, auth limits - Skip authenticated requests option - Public endpoints only option 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface DashboardStats {
|
||||
activeEvents: number;
|
||||
expiringEvents: number;
|
||||
totalPhotos: number;
|
||||
storageUsed: number;
|
||||
totalViews: number;
|
||||
totalDownloads: number;
|
||||
viewsTrend: number;
|
||||
downloadsTrend: number;
|
||||
archivedEvents: number;
|
||||
}
|
||||
|
||||
export interface SystemHealth {
|
||||
overall: 'healthy' | 'warning' | 'error';
|
||||
services: {
|
||||
database: 'healthy' | 'warning' | 'error';
|
||||
email: 'healthy' | 'warning' | 'error';
|
||||
storage: 'healthy' | 'warning' | 'error';
|
||||
memory: 'healthy' | 'warning' | 'error';
|
||||
};
|
||||
details: {
|
||||
emailQueue: {
|
||||
pending: number;
|
||||
processable: number;
|
||||
stuck: number;
|
||||
sent: number;
|
||||
failed: number;
|
||||
};
|
||||
memory: {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
percentage: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: number;
|
||||
type: string;
|
||||
actorType: string;
|
||||
actorName: string;
|
||||
eventName?: string;
|
||||
metadata: Record<string, any>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsData {
|
||||
chartData: Array<{
|
||||
date: string;
|
||||
views: number;
|
||||
downloads: number;
|
||||
uniqueVisitors: number;
|
||||
}>;
|
||||
topGalleries: Array<{
|
||||
event_name: string;
|
||||
slug: string;
|
||||
views: number;
|
||||
}>;
|
||||
devices: {
|
||||
desktop: number;
|
||||
mobile: number;
|
||||
tablet: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const adminService = {
|
||||
// Dashboard statistics
|
||||
async getDashboardStats(): Promise<DashboardStats> {
|
||||
const response = await api.get<DashboardStats>('/admin/dashboard/stats');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Recent activity
|
||||
async getRecentActivity(limit: number = 10): Promise<Activity[]> {
|
||||
const response = await api.get<Activity[]>('/admin/dashboard/activity', {
|
||||
params: { limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Analytics data
|
||||
async getAnalytics(days: number = 7): Promise<AnalyticsData> {
|
||||
const response = await api.get<AnalyticsData>('/admin/dashboard/analytics', {
|
||||
params: { days }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// System health check
|
||||
async getSystemHealth(): Promise<SystemHealth> {
|
||||
const response = await api.get<SystemHealth>('/admin/dashboard/health');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Format activity message
|
||||
formatActivityMessage(activity: Activity): string {
|
||||
const messages: Record<string, string> = {
|
||||
'event_created': `New event created: ${activity.eventName || 'Unknown'}`,
|
||||
'photos_uploaded': `${activity.metadata.count || 0} photos uploaded to ${activity.eventName || 'Unknown'}`,
|
||||
'event_archived': `Event archived: ${activity.eventName || 'Unknown'}`,
|
||||
'archive_restored': `Archive restored: ${activity.eventName || 'Unknown'}`,
|
||||
'archive_deleted': `Archive deleted: ${activity.metadata.event_name || 'Unknown'}`,
|
||||
'archive_downloaded': `Archive downloaded: ${activity.eventName || 'Unknown'}`,
|
||||
'email_config_updated': 'Email configuration updated',
|
||||
'email_template_updated': `Email template updated: ${activity.metadata.template_key || ''}`,
|
||||
'branding_updated': 'Branding settings updated',
|
||||
'theme_updated': 'Theme settings updated',
|
||||
'bulk_download': `${activity.metadata.photo_count || 0} photos downloaded from ${activity.eventName || 'Unknown'}`,
|
||||
'gallery_password_entry': `Password entered for ${activity.eventName || 'Unknown'}`,
|
||||
'expiration_warning_viewed': `Expiration warning viewed for ${activity.eventName || 'Unknown'}`
|
||||
};
|
||||
|
||||
return messages[activity.type] || activity.type;
|
||||
},
|
||||
|
||||
// Format bytes to human readable
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
// Change password
|
||||
async changePassword(data: { currentPassword: string; newPassword: string }): Promise<void> {
|
||||
await api.post('/admin/auth/change-password', data);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
// Umami Analytics Service
|
||||
// Provides integration with Umami for tracking page views and events
|
||||
|
||||
interface UmamiConfig {
|
||||
websiteId?: string;
|
||||
hostUrl?: string;
|
||||
autoTrack?: boolean;
|
||||
doNotTrack?: boolean;
|
||||
domains?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: {
|
||||
track: (eventName: string, eventData?: any) => void;
|
||||
trackView: (url?: string, referrer?: string, websiteId?: string) => void;
|
||||
trackEvent: (
|
||||
eventValue: string,
|
||||
eventType: string,
|
||||
url?: string,
|
||||
websiteId?: string
|
||||
) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsService {
|
||||
private initialized = false;
|
||||
private websiteId: string | null = null;
|
||||
// private hostUrl: string | null = null;
|
||||
|
||||
initialize(config: UmamiConfig) {
|
||||
if (this.initialized) return;
|
||||
|
||||
const { websiteId, hostUrl, autoTrack = true, doNotTrack = true } = config;
|
||||
|
||||
if (!websiteId || !hostUrl) {
|
||||
console.warn('Umami Analytics: Missing websiteId or hostUrl');
|
||||
return;
|
||||
}
|
||||
|
||||
this.websiteId = websiteId;
|
||||
// this.hostUrl = hostUrl;
|
||||
|
||||
// Create and inject Umami script
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.src = `${hostUrl}/script.js`;
|
||||
script.setAttribute('data-website-id', websiteId);
|
||||
|
||||
if (!autoTrack) {
|
||||
script.setAttribute('data-auto-track', 'false');
|
||||
}
|
||||
|
||||
if (doNotTrack) {
|
||||
script.setAttribute('data-do-not-track', 'true');
|
||||
}
|
||||
|
||||
if (config.domains && config.domains.length > 0) {
|
||||
script.setAttribute('data-domains', config.domains.join(','));
|
||||
}
|
||||
|
||||
document.head.appendChild(script);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
// Check if analytics is initialized
|
||||
isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
// Track custom events
|
||||
track(eventName: string, eventData?: Record<string, any>) {
|
||||
if (!this.initialized || !window.umami) {
|
||||
// Silently ignore if not initialized
|
||||
return;
|
||||
}
|
||||
|
||||
// Umami expects flat event data
|
||||
window.umami.track(eventName, eventData);
|
||||
}
|
||||
|
||||
// Track page views manually
|
||||
trackPageView(url?: string, referrer?: string) {
|
||||
if (!this.initialized || !window.umami) {
|
||||
// Silently ignore if not initialized
|
||||
return;
|
||||
}
|
||||
|
||||
window.umami.trackView(url, referrer, this.websiteId || undefined);
|
||||
}
|
||||
|
||||
// Gallery-specific tracking events
|
||||
trackGalleryEvent(eventType: 'password_entry' | 'photo_view' | 'photo_download' | 'gallery_expired' | 'bulk_download', data?: any) {
|
||||
this.track(`gallery_${eventType}`, data);
|
||||
}
|
||||
|
||||
// Admin-specific tracking events
|
||||
trackAdminEvent(eventType: 'login' | 'event_created' | 'event_archived' | 'event_deleted' | 'settings_updated', data?: any) {
|
||||
this.track(`admin_${eventType}`, data);
|
||||
}
|
||||
|
||||
// Track download events with more context
|
||||
trackDownload(photoId: string | number, gallerySlug: string, isBulk: boolean = false) {
|
||||
this.track('photo_download', {
|
||||
photo_id: photoId,
|
||||
gallery: gallerySlug,
|
||||
bulk: isBulk,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Track expiration warning views
|
||||
trackExpirationWarning(gallerySlug: string, daysRemaining: number) {
|
||||
this.track('expiration_warning_viewed', {
|
||||
gallery: gallerySlug,
|
||||
days_remaining: daysRemaining,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Track search usage
|
||||
trackSearch(query: string, resultsCount: number, context: 'gallery' | 'admin') {
|
||||
this.track('search_performed', {
|
||||
query_length: query.length,
|
||||
results_count: resultsCount,
|
||||
context,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const analyticsService = new AnalyticsService();
|
||||
|
||||
// Helper hook for React components
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const useAnalytics = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// Track page views on route change
|
||||
analyticsService.trackPageView(location.pathname + location.search);
|
||||
}, [location]);
|
||||
|
||||
return analyticsService;
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface Archive {
|
||||
id: number;
|
||||
slug: string;
|
||||
eventName: string;
|
||||
eventDate: string;
|
||||
eventType: string;
|
||||
hostEmail: string;
|
||||
archivedAt: string;
|
||||
expiresAt: string;
|
||||
photoCount: number;
|
||||
originalSize: number;
|
||||
archiveSize: number;
|
||||
archivePath?: string;
|
||||
}
|
||||
|
||||
export interface ArchiveDetails extends Archive {
|
||||
adminEmail: string;
|
||||
welcomeMessage?: string;
|
||||
colorTheme?: string;
|
||||
createdAt: string;
|
||||
photos: Array<{
|
||||
filename: string;
|
||||
type: string;
|
||||
size_bytes: number;
|
||||
uploaded_at: string;
|
||||
}>;
|
||||
archiveFile?: {
|
||||
size: number;
|
||||
createdAt: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ArchivesResponse {
|
||||
archives: Archive[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const archiveService = {
|
||||
// Get all archives with pagination
|
||||
async getArchives(page: number = 1, limit: number = 20): Promise<ArchivesResponse> {
|
||||
const response = await api.get<ArchivesResponse>('/admin/archives', {
|
||||
params: { page, limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single archive details
|
||||
async getArchiveDetails(id: number): Promise<ArchiveDetails> {
|
||||
const response = await api.get<ArchiveDetails>(`/admin/archives/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Restore archive
|
||||
async restoreArchive(id: number): Promise<void> {
|
||||
await api.post(`/admin/archives/${id}/restore`);
|
||||
},
|
||||
|
||||
// Download archive
|
||||
async downloadArchive(id: number, filename: string): Promise<void> {
|
||||
const response = await api.get(`/admin/archives/${id}/download`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
// Delete archive permanently
|
||||
async deleteArchive(id: number): Promise<void> {
|
||||
await api.delete(`/admin/archives/${id}`);
|
||||
},
|
||||
|
||||
// Format bytes to human readable
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { api, setAuthToken, clearAuthToken } from '../config/api';
|
||||
import type { LoginResponse, GalleryAuthResponse } from '../types';
|
||||
|
||||
export const authService = {
|
||||
// Admin authentication
|
||||
async adminLogin(credentials: { email: string; password: string; recaptchaToken?: string | null }): Promise<LoginResponse> {
|
||||
// Backend expects 'username' field, but we accept email
|
||||
const response = await api.post<LoginResponse>('/auth/admin/login', {
|
||||
username: credentials.email,
|
||||
password: credentials.password,
|
||||
recaptchaToken: credentials.recaptchaToken
|
||||
});
|
||||
|
||||
setAuthToken(response.data.token, true);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
adminLogout() {
|
||||
clearAuthToken(true);
|
||||
window.location.href = '/admin/login';
|
||||
},
|
||||
|
||||
// Gallery authentication
|
||||
async verifyGalleryPassword(slug: string, password: string, recaptchaToken?: string | null): Promise<GalleryAuthResponse> {
|
||||
const response = await api.post<GalleryAuthResponse>('/auth/gallery/verify', {
|
||||
slug,
|
||||
password,
|
||||
recaptchaToken
|
||||
});
|
||||
|
||||
// Token is now handled by GalleryAuthContext with slug-specific storage
|
||||
return response.data;
|
||||
},
|
||||
|
||||
galleryLogout() {
|
||||
// Logout is now handled by GalleryAuthContext
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface PhotoCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
is_global: boolean;
|
||||
event_id: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateCategoryData {
|
||||
name: string;
|
||||
slug?: string;
|
||||
is_global?: boolean;
|
||||
event_id?: number;
|
||||
}
|
||||
|
||||
export const categoriesService = {
|
||||
// Get all global categories
|
||||
async getGlobalCategories(): Promise<PhotoCategory[]> {
|
||||
const response = await api.get<PhotoCategory[]>('/admin/categories/global');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get categories for a specific event (global + event-specific)
|
||||
async getEventCategories(eventId: number): Promise<PhotoCategory[]> {
|
||||
const response = await api.get<PhotoCategory[]>(`/admin/categories/event/${eventId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create a new category
|
||||
async createCategory(data: CreateCategoryData): Promise<PhotoCategory> {
|
||||
const response = await api.post<PhotoCategory>('/admin/categories', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update a category
|
||||
async updateCategory(id: number, name: string): Promise<PhotoCategory> {
|
||||
const response = await api.put<PhotoCategory>(`/admin/categories/${id}`, { name });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete a category
|
||||
async deleteCategory(id: number): Promise<void> {
|
||||
await api.delete(`/admin/categories/${id}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface CMSPage {
|
||||
id: number;
|
||||
slug: string;
|
||||
title_en: string;
|
||||
title_de: string;
|
||||
content_en: string;
|
||||
content_de: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export const cmsService = {
|
||||
// Get all CMS pages
|
||||
async getPages(): Promise<CMSPage[]> {
|
||||
const response = await api.get<CMSPage[]>('/admin/cms/pages');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get a single CMS page
|
||||
async getPage(slug: string): Promise<CMSPage> {
|
||||
const response = await api.get<CMSPage>(`/admin/cms/pages/${slug}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update a CMS page
|
||||
async updatePage(slug: string, data: Partial<CMSPage>): Promise<CMSPage> {
|
||||
const response = await api.put<CMSPage>(`/admin/cms/pages/${slug}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get public CMS page (no auth required)
|
||||
async getPublicPage(slug: string, lang: string = 'en'): Promise<{ title: string; content: string }> {
|
||||
const response = await api.get<{ title: string; content: string }>(`/public/pages/${slug}`, {
|
||||
params: { lang }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface EmailConfig {
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
smtp_secure: boolean;
|
||||
smtp_user: string;
|
||||
smtp_pass: string;
|
||||
from_email: string;
|
||||
from_name: string;
|
||||
}
|
||||
|
||||
export interface EmailTemplate {
|
||||
id: number;
|
||||
template_key: string;
|
||||
subject: string; // For backward compatibility
|
||||
body_html: string; // For backward compatibility
|
||||
body_text?: string; // For backward compatibility
|
||||
subject_en: string;
|
||||
subject_de: string;
|
||||
body_html_en: string;
|
||||
body_html_de: string;
|
||||
body_text_en?: string;
|
||||
body_text_de?: string;
|
||||
variables: string[];
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface EmailPreview {
|
||||
subject: string;
|
||||
body_html: string;
|
||||
body_text: string;
|
||||
}
|
||||
|
||||
export const emailService = {
|
||||
// Get email configuration
|
||||
async getConfig(): Promise<EmailConfig> {
|
||||
const response = await api.get<EmailConfig>('/admin/email/config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update email configuration
|
||||
async updateConfig(config: EmailConfig): Promise<void> {
|
||||
await api.post('/admin/email/config', config);
|
||||
},
|
||||
|
||||
// Test email configuration
|
||||
async testEmail(testEmail: string): Promise<void> {
|
||||
await api.post('/admin/email/test', { test_email: testEmail });
|
||||
},
|
||||
|
||||
// Get all email templates
|
||||
async getTemplates(): Promise<EmailTemplate[]> {
|
||||
const response = await api.get<EmailTemplate[]>('/admin/email/templates');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single template
|
||||
async getTemplate(key: string): Promise<EmailTemplate> {
|
||||
const response = await api.get<EmailTemplate>(`/admin/email/templates/${key}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update email template
|
||||
async updateTemplate(key: string, template: Partial<EmailTemplate>): Promise<void> {
|
||||
await api.put(`/admin/email/templates/${key}`, template);
|
||||
},
|
||||
|
||||
// Preview email template
|
||||
async previewTemplate(key: string, previewData: Record<string, string>, language: 'en' | 'de' = 'en'): Promise<EmailPreview> {
|
||||
const response = await api.post<EmailPreview>(
|
||||
`/admin/email/templates/${key}/preview`,
|
||||
{ preview_data: previewData, language }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { api } from '../config/api';
|
||||
import type { Event } from '../types';
|
||||
|
||||
interface CreateEventData {
|
||||
event_type: string;
|
||||
event_name: string;
|
||||
event_date: string;
|
||||
host_email: string;
|
||||
admin_email: string;
|
||||
password: string;
|
||||
welcome_message?: string;
|
||||
color_theme?: string;
|
||||
expiration_days: number;
|
||||
allow_user_uploads?: boolean;
|
||||
upload_category_id?: number | null;
|
||||
}
|
||||
|
||||
interface UpdateEventData {
|
||||
event_name?: string;
|
||||
event_date?: string;
|
||||
host_email?: string;
|
||||
admin_email?: string;
|
||||
password?: string;
|
||||
welcome_message?: string;
|
||||
color_theme?: string;
|
||||
expires_at?: string;
|
||||
is_active?: boolean;
|
||||
allow_user_uploads?: boolean;
|
||||
upload_category_id?: number | null;
|
||||
hero_photo_id?: number | null;
|
||||
}
|
||||
|
||||
interface EventsListResponse {
|
||||
events: Event[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export const eventsService = {
|
||||
// Get all events (admin)
|
||||
async getEvents(
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
status?: 'active' | 'inactive' | 'archived'
|
||||
): Promise<EventsListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: limit.toString(),
|
||||
});
|
||||
|
||||
if (status) {
|
||||
params.append('status', status);
|
||||
}
|
||||
|
||||
const response = await api.get<EventsListResponse>(`/admin/events?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single event details (admin)
|
||||
async getEvent(id: number): Promise<Event> {
|
||||
const response = await api.get<Event>(`/admin/events/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create new event (admin)
|
||||
async createEvent(data: CreateEventData): Promise<Event> {
|
||||
const response = await api.post<Event>('/admin/events', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update event (admin)
|
||||
async updateEvent(id: number, data: UpdateEventData): Promise<Event> {
|
||||
const response = await api.put<Event>(`/admin/events/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete/deactivate event (admin)
|
||||
async deleteEvent(id: number): Promise<void> {
|
||||
await api.delete(`/admin/events/${id}`);
|
||||
},
|
||||
|
||||
// Force archive event (admin)
|
||||
async archiveEvent(id: number): Promise<void> {
|
||||
await api.post(`/admin/events/${id}/archive`);
|
||||
},
|
||||
|
||||
// Bulk archive events (admin)
|
||||
async bulkArchiveEvents(eventIds: number[]): Promise<{
|
||||
message: string;
|
||||
results: {
|
||||
successful: Array<{ id: number; name: string }>;
|
||||
failed: Array<{ id: number; name: string; error: string }>;
|
||||
};
|
||||
}> {
|
||||
const response = await api.post('/admin/events/bulk-archive', {
|
||||
eventIds,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Extend event expiration (admin)
|
||||
async extendExpiration(id: number, days: number): Promise<Event> {
|
||||
const response = await api.post<Event>(`/events/${id}/extend`, {
|
||||
days,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get event categories
|
||||
async getEventCategories(eventId: number): Promise<Array<{ id: number; name: string; slug: string }>> {
|
||||
const response = await api.get(`/admin/categories/event/${eventId}`);
|
||||
return response.data || [];
|
||||
},
|
||||
|
||||
// Reset event password
|
||||
async resetPassword(eventId: number, sendEmail: boolean = true): Promise<{ message: string; newPassword: string; emailSent: boolean }> {
|
||||
const response = await api.post(`/admin/events/${eventId}/reset-password`, { sendEmail });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Resend creation email
|
||||
async resendCreationEmail(eventId: number): Promise<{ success: boolean; message: string }> {
|
||||
const response = await api.post(`/admin/events/${eventId}/resend-email`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { api } from '../config/api';
|
||||
import type { GalleryInfo, GalleryData, GalleryStats } from '../types';
|
||||
|
||||
export const galleryService = {
|
||||
// Verify share token
|
||||
async verifyToken(slug: string, token: string): Promise<{ valid: boolean }> {
|
||||
const response = await api.get<{ valid: boolean }>(`/gallery/${slug}/verify-token/${token}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get basic gallery info (no auth required)
|
||||
async getGalleryInfo(slug: string, token?: string): Promise<GalleryInfo> {
|
||||
const params = token ? { token } : {};
|
||||
const response = await api.get<GalleryInfo>(`/gallery/${slug}/info`, { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get gallery photos (requires auth)
|
||||
async getGalleryPhotos(slug: string): Promise<GalleryData> {
|
||||
const response = await api.get<GalleryData>(`/gallery/${slug}/photos`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Download single photo
|
||||
async downloadPhoto(slug: string, photoId: number, filename: string): Promise<void> {
|
||||
const response = await api.get(`/gallery/${slug}/download/${photoId}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
// Download all photos as ZIP
|
||||
async downloadAllPhotos(slug: string): Promise<void> {
|
||||
const response = await api.get(`/gallery/${slug}/download-all`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${slug}.zip`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
// Get gallery statistics
|
||||
async getGalleryStats(slug: string): Promise<GalleryStats> {
|
||||
const response = await api.get<GalleryStats>(`/gallery/${slug}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export { authService } from './auth.service';
|
||||
export { galleryService } from './gallery.service';
|
||||
export { eventsService } from './events.service';
|
||||
export { adminService } from './admin.service';
|
||||
export { analyticsService } from './analytics.service';
|
||||
export { archiveService } from './archive.service';
|
||||
export { emailService } from './email.service';
|
||||
export { settingsService } from './settings.service';
|
||||
export { cmsService } from './cms.service';
|
||||
export { notificationsService } from './notifications.service';
|
||||
@@ -0,0 +1,212 @@
|
||||
import { api } from '../config/api';
|
||||
import i18n from '../i18n/config';
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
type: string;
|
||||
actorType: string;
|
||||
actorName: string;
|
||||
eventName?: string;
|
||||
eventId?: number;
|
||||
metadata: Record<string, any>;
|
||||
createdAt: string;
|
||||
readAt?: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationsResponse {
|
||||
notifications: Notification[];
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export const notificationsService = {
|
||||
// Get notifications
|
||||
async getNotifications(includeRead: boolean = false, limit: number = 20): Promise<NotificationsResponse> {
|
||||
const response = await api.get('/admin/notifications', {
|
||||
params: { includeRead, limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Mark single notification as read
|
||||
async markAsRead(notificationId: number): Promise<void> {
|
||||
await api.put(`/admin/notifications/${notificationId}/read`);
|
||||
},
|
||||
|
||||
// Mark all notifications as read
|
||||
async markAllAsRead(): Promise<void> {
|
||||
await api.put('/admin/notifications/read-all');
|
||||
},
|
||||
|
||||
// Clear old notifications
|
||||
async clearOldNotifications(): Promise<{ deletedCount: number }> {
|
||||
const response = await api.delete('/admin/notifications/clear-old');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Format notification message
|
||||
formatNotificationMessage(notification: Notification): string {
|
||||
const t = i18n.t;
|
||||
switch (notification.type) {
|
||||
case 'event_created':
|
||||
return t('admin.notificationMessages.eventCreated', { eventName: notification.eventName });
|
||||
case 'event_archived':
|
||||
return t('admin.notificationMessages.eventArchived', { eventName: notification.eventName });
|
||||
case 'event_updated':
|
||||
return t('admin.notificationMessages.eventUpdated', {
|
||||
eventName: notification.eventName || notification.metadata.eventName
|
||||
});
|
||||
case 'event_deleted':
|
||||
return t('admin.notificationMessages.eventDeleted', {
|
||||
eventName: notification.metadata.event_name
|
||||
});
|
||||
case 'photos_uploaded':
|
||||
return t('admin.notificationMessages.photosUploaded', {
|
||||
count: notification.metadata.count || 0,
|
||||
eventName: notification.eventName
|
||||
});
|
||||
case 'photo_deleted':
|
||||
return t('admin.notificationMessages.photoDeleted', { eventName: notification.eventName });
|
||||
case 'photos_bulk_deleted':
|
||||
return t('admin.notificationMessages.photosBulkDeleted', {
|
||||
count: notification.metadata.count || 0,
|
||||
eventName: notification.eventName
|
||||
});
|
||||
case 'event_expiring':
|
||||
return t('admin.notificationMessages.eventExpiring', {
|
||||
eventName: notification.eventName,
|
||||
days: notification.metadata.days || 0
|
||||
});
|
||||
case 'event_expired':
|
||||
return t('admin.notificationMessages.eventExpired', { eventName: notification.eventName });
|
||||
case 'password_changed':
|
||||
return t('admin.notificationMessages.passwordChanged', { actorName: notification.actorName });
|
||||
case 'password_reset':
|
||||
return t('admin.notificationMessages.passwordReset', {
|
||||
eventName: notification.metadata.eventName
|
||||
});
|
||||
case 'settings_updated':
|
||||
return t('admin.notificationMessages.settingsUpdated', {
|
||||
type: notification.metadata.type || 'System'
|
||||
});
|
||||
case 'email_template_updated':
|
||||
return t('admin.notificationMessages.emailTemplateUpdated', {
|
||||
template: notification.metadata.template
|
||||
});
|
||||
case 'bulk_download':
|
||||
return t('admin.notificationMessages.bulkDownload', {
|
||||
count: notification.metadata.count || 0,
|
||||
eventName: notification.eventName
|
||||
});
|
||||
case 'storage_warning':
|
||||
return t('admin.notificationMessages.storageWarning', {
|
||||
percentage: notification.metadata.percentage || 0
|
||||
});
|
||||
case 'admin_logout':
|
||||
return t('admin.notificationMessages.adminLogout', { actorName: notification.actorName });
|
||||
case 'category_created':
|
||||
return t('admin.notificationMessages.categoryCreated', {
|
||||
name: notification.metadata.name,
|
||||
eventName: notification.eventName
|
||||
});
|
||||
case 'category_updated':
|
||||
return t('admin.notificationMessages.categoryUpdated', {
|
||||
name: notification.metadata.name,
|
||||
eventName: notification.eventName
|
||||
});
|
||||
case 'category_deleted':
|
||||
return t('admin.notificationMessages.categoryDeleted', {
|
||||
name: notification.metadata.name,
|
||||
eventName: notification.eventName
|
||||
});
|
||||
case 'cms_page_updated':
|
||||
return t('admin.notificationMessages.cmsPageUpdated', {
|
||||
slug: notification.metadata.slug
|
||||
});
|
||||
case 'email_config_updated':
|
||||
return t('admin.notificationMessages.emailConfigUpdated');
|
||||
case 'favicon_uploaded':
|
||||
return t('admin.notificationMessages.faviconUploaded');
|
||||
case 'branding_updated':
|
||||
return t('admin.notificationMessages.brandingUpdated');
|
||||
case 'general_settings_updated':
|
||||
return t('admin.notificationMessages.generalSettingsUpdated');
|
||||
case 'security_settings_updated':
|
||||
return t('admin.notificationMessages.securitySettingsUpdated');
|
||||
case 'theme_updated':
|
||||
return t('admin.notificationMessages.themeUpdated');
|
||||
case 'archive_downloaded':
|
||||
return t('admin.notificationMessages.archiveDownloaded', {
|
||||
eventName: notification.metadata.event_name
|
||||
});
|
||||
case 'archive_deleted':
|
||||
return t('admin.notificationMessages.archiveDeleted', {
|
||||
eventName: notification.metadata.event_name
|
||||
});
|
||||
case 'archive_restored':
|
||||
return t('admin.notificationMessages.archiveRestored', {
|
||||
eventName: notification.metadata.event_name
|
||||
});
|
||||
default:
|
||||
// Log unknown notification types for debugging
|
||||
console.warn('Unknown notification type:', notification.type, notification);
|
||||
return notification.metadata.message || t('admin.notificationMessages.systemActivity', {
|
||||
type: notification.type.replace(/_/g, ' ')
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Get notification icon and color
|
||||
getNotificationStyle(type: string): { icon: string; color: string } {
|
||||
switch (type) {
|
||||
case 'event_created':
|
||||
return { icon: 'Calendar', color: 'text-blue-600' };
|
||||
case 'event_archived':
|
||||
return { icon: 'Archive', color: 'text-green-600' };
|
||||
case 'event_updated':
|
||||
case 'event_deleted':
|
||||
return { icon: 'Calendar', color: 'text-gray-600' };
|
||||
case 'photos_uploaded':
|
||||
return { icon: 'Image', color: 'text-purple-600' };
|
||||
case 'photo_deleted':
|
||||
case 'photos_bulk_deleted':
|
||||
return { icon: 'Image', color: 'text-red-600' };
|
||||
case 'event_expiring':
|
||||
return { icon: 'AlertCircle', color: 'text-amber-600' };
|
||||
case 'event_expired':
|
||||
return { icon: 'Clock', color: 'text-red-600' };
|
||||
case 'password_changed':
|
||||
case 'password_reset':
|
||||
return { icon: 'Lock', color: 'text-indigo-600' };
|
||||
case 'settings_updated':
|
||||
case 'branding_updated':
|
||||
case 'general_settings_updated':
|
||||
case 'security_settings_updated':
|
||||
case 'theme_updated':
|
||||
return { icon: 'Settings', color: 'text-gray-600' };
|
||||
case 'email_template_updated':
|
||||
case 'email_config_updated':
|
||||
return { icon: 'Mail', color: 'text-teal-600' };
|
||||
case 'bulk_download':
|
||||
return { icon: 'Download', color: 'text-cyan-600' };
|
||||
case 'storage_warning':
|
||||
return { icon: 'Database', color: 'text-orange-600' };
|
||||
case 'admin_logout':
|
||||
return { icon: 'LogOut', color: 'text-gray-600' };
|
||||
case 'category_created':
|
||||
case 'category_updated':
|
||||
case 'category_deleted':
|
||||
return { icon: 'Folder', color: 'text-indigo-600' };
|
||||
case 'cms_page_updated':
|
||||
return { icon: 'FileText', color: 'text-green-600' };
|
||||
case 'favicon_uploaded':
|
||||
return { icon: 'Globe', color: 'text-purple-600' };
|
||||
case 'archive_downloaded':
|
||||
case 'archive_deleted':
|
||||
case 'archive_restored':
|
||||
return { icon: 'Archive', color: 'text-blue-600' };
|
||||
default:
|
||||
return { icon: 'Bell', color: 'text-gray-600' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface AdminPhoto {
|
||||
id: number;
|
||||
filename: string;
|
||||
path: string;
|
||||
url: string;
|
||||
thumbnail_url: string | null;
|
||||
type: string;
|
||||
category_id: number | null;
|
||||
category_name: string | null;
|
||||
category_slug: string | null;
|
||||
size: number;
|
||||
uploaded_at: string;
|
||||
view_count?: number;
|
||||
download_count?: number;
|
||||
}
|
||||
|
||||
export interface PhotoFilters {
|
||||
category_id?: number | null;
|
||||
type?: string;
|
||||
search?: string;
|
||||
sort?: 'date' | 'name' | 'size';
|
||||
order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
class PhotosService {
|
||||
async getEventPhotos(eventId: number, filters?: PhotoFilters): Promise<AdminPhoto[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters) {
|
||||
if (filters.category_id !== undefined) {
|
||||
params.append('category_id', filters.category_id?.toString() || '');
|
||||
}
|
||||
if (filters.type) params.append('type', filters.type);
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.sort) params.append('sort', filters.sort);
|
||||
if (filters.order) params.append('order', filters.order);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/admin/events/${eventId}/photos${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await api.get(url);
|
||||
|
||||
// Return photos as-is, URLs are already relative API paths
|
||||
return response.data.photos;
|
||||
}
|
||||
|
||||
async deletePhoto(eventId: number, photoId: number): Promise<void> {
|
||||
await api.delete(`/admin/events/${eventId}/photos/${photoId}`);
|
||||
}
|
||||
|
||||
async deletePhotos(eventId: number, photoIds: number[]): Promise<void> {
|
||||
await api.post(`/admin/events/${eventId}/photos/bulk-delete`, { photoIds });
|
||||
}
|
||||
|
||||
async updatePhotoCategory(eventId: number, photoId: number, categoryId: number | null): Promise<void> {
|
||||
await api.patch(`/admin/events/${eventId}/photos/${photoId}`, { category_id: categoryId });
|
||||
}
|
||||
|
||||
async updatePhotosCategory(eventId: number, photoIds: number[], categoryId: number | null): Promise<void> {
|
||||
await api.post(`/admin/events/${eventId}/photos/bulk-update`, {
|
||||
photoIds,
|
||||
updates: { category_id: categoryId }
|
||||
});
|
||||
}
|
||||
|
||||
async downloadPhoto(eventId: number, photoId: number, filename: string): Promise<void> {
|
||||
const response = await api.get(`/admin/events/${eventId}/photos/${photoId}/download`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
export const photosService = new PhotosService();
|
||||
@@ -0,0 +1,210 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface BrandingSettings {
|
||||
company_name: string;
|
||||
company_tagline: string;
|
||||
support_email: string;
|
||||
footer_text: string;
|
||||
watermark_enabled: boolean;
|
||||
watermark_position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'center';
|
||||
watermark_opacity?: number;
|
||||
watermark_size?: number;
|
||||
watermark_logo_url?: string;
|
||||
logo_url?: string;
|
||||
favicon_url?: string;
|
||||
}
|
||||
|
||||
export interface ThemeSettings {
|
||||
name?: string;
|
||||
primaryColor?: string;
|
||||
accentColor?: string;
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
fontFamily?: string;
|
||||
borderRadius?: 'none' | 'sm' | 'md' | 'lg';
|
||||
customCss?: string;
|
||||
}
|
||||
|
||||
export interface StorageInfo {
|
||||
total_used: number;
|
||||
archive_storage: number;
|
||||
storage_by_event: Array<{
|
||||
event_name: string;
|
||||
id: number;
|
||||
size: number;
|
||||
}>;
|
||||
storage_limit: number;
|
||||
}
|
||||
|
||||
export interface SystemStatus {
|
||||
database: {
|
||||
size: number;
|
||||
tables: {
|
||||
events: number;
|
||||
photos: number;
|
||||
admins: number;
|
||||
categories: number;
|
||||
activityLogs: number;
|
||||
};
|
||||
};
|
||||
storage: {
|
||||
totalUsed: number;
|
||||
photoStorage: number;
|
||||
archiveStorage: number;
|
||||
};
|
||||
emailQueue: {
|
||||
pending: number;
|
||||
processable: number;
|
||||
stuck: number;
|
||||
sent: number;
|
||||
failed: number;
|
||||
};
|
||||
system: {
|
||||
platform: string;
|
||||
arch: string;
|
||||
hostname: string;
|
||||
uptime: number;
|
||||
nodeVersion: string;
|
||||
memory: {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
};
|
||||
cpu: {
|
||||
model: string;
|
||||
cores: number;
|
||||
};
|
||||
};
|
||||
services: {
|
||||
fileWatcher: { status: string };
|
||||
expirationChecker: { status: string };
|
||||
emailProcessor: { status: string };
|
||||
};
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export const settingsService = {
|
||||
// Get all settings
|
||||
async getAllSettings(): Promise<Record<string, any>> {
|
||||
const response = await api.get<Record<string, any>>('/admin/settings');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get settings by type
|
||||
async getSettingsByType(type: 'branding' | 'theme' | 'general'): Promise<Record<string, any>> {
|
||||
const response = await api.get<Record<string, any>>(`/admin/settings/${type}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update branding settings
|
||||
async updateBranding(settings: BrandingSettings): Promise<void> {
|
||||
await api.put('/admin/settings/branding', settings);
|
||||
},
|
||||
|
||||
// Upload logo
|
||||
async uploadLogo(file: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('logo', file);
|
||||
|
||||
const response = await api.post<{ logoUrl: string }>(
|
||||
'/admin/settings/logo',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data.logoUrl;
|
||||
},
|
||||
|
||||
// Upload favicon
|
||||
async uploadFavicon(file: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('favicon', file);
|
||||
|
||||
const response = await api.post<{ faviconUrl: string }>(
|
||||
'/admin/settings/favicon',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data.faviconUrl;
|
||||
},
|
||||
|
||||
// Upload watermark logo
|
||||
async uploadWatermarkLogo(file: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('watermarkLogo', file);
|
||||
|
||||
const response = await api.post<{ watermarkLogoUrl: string }>(
|
||||
'/admin/settings/branding/watermark-logo',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data.watermarkLogoUrl;
|
||||
},
|
||||
|
||||
// Update theme settings
|
||||
async updateTheme(settings: ThemeSettings): Promise<void> {
|
||||
await api.put('/admin/settings/theme', settings);
|
||||
},
|
||||
|
||||
// Update multiple settings at once
|
||||
async updateSettings(settings: Record<string, any>): Promise<void> {
|
||||
await api.put('/admin/settings/general', settings);
|
||||
},
|
||||
|
||||
// Get storage information
|
||||
async getStorageInfo(): Promise<StorageInfo> {
|
||||
const response = await api.get<StorageInfo>('/admin/settings/storage/info');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get system status
|
||||
async getSystemStatus(): Promise<SystemStatus> {
|
||||
const response = await api.get<SystemStatus>('/admin/system/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Format branding settings from raw data
|
||||
formatBrandingSettings(rawSettings: Record<string, any>): BrandingSettings {
|
||||
return {
|
||||
company_name: rawSettings.branding_company_name || '',
|
||||
company_tagline: rawSettings.branding_company_tagline || '',
|
||||
support_email: rawSettings.branding_support_email || '',
|
||||
footer_text: rawSettings.branding_footer_text || '',
|
||||
watermark_enabled: rawSettings.branding_watermark_enabled || false,
|
||||
watermark_position: rawSettings.branding_watermark_position || 'bottom-right',
|
||||
watermark_opacity: rawSettings.branding_watermark_opacity || 50,
|
||||
watermark_size: rawSettings.branding_watermark_size || 15,
|
||||
watermark_logo_url: rawSettings.branding_watermark_logo_url || undefined,
|
||||
logo_url: rawSettings.branding_logo_url || undefined,
|
||||
favicon_url: rawSettings.branding_favicon_url || undefined
|
||||
};
|
||||
},
|
||||
|
||||
// Format theme settings from raw data
|
||||
formatThemeSettings(rawSettings: Record<string, any>): ThemeSettings {
|
||||
return rawSettings.theme_config || {};
|
||||
},
|
||||
|
||||
// Format bytes to human readable
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { toast as toastify } from 'react-toastify';
|
||||
import i18n from '../i18n/config';
|
||||
|
||||
export const toast = {
|
||||
success: (messageKey: string, interpolations?: Record<string, any>) => {
|
||||
const message = i18n.t(messageKey, interpolations);
|
||||
toastify.success(message);
|
||||
},
|
||||
|
||||
error: (messageKey: string, interpolations?: Record<string, any>) => {
|
||||
const message = i18n.t(messageKey, interpolations);
|
||||
toastify.error(message);
|
||||
},
|
||||
|
||||
info: (messageKey: string, interpolations?: Record<string, any>) => {
|
||||
const message = i18n.t(messageKey, interpolations);
|
||||
toastify.info(message);
|
||||
},
|
||||
|
||||
warning: (messageKey: string, interpolations?: Record<string, any>) => {
|
||||
const message = i18n.t(messageKey, interpolations);
|
||||
toastify.warning(message);
|
||||
},
|
||||
|
||||
// For direct messages (not translation keys)
|
||||
successDirect: (message: string) => toastify.success(message),
|
||||
errorDirect: (message: string) => toastify.error(message),
|
||||
infoDirect: (message: string) => toastify.info(message),
|
||||
warningDirect: (message: string) => toastify.warning(message),
|
||||
};
|
||||
Reference in New Issue
Block a user