diff --git a/TRAEFIK_DEPLOYMENT.md b/TRAEFIK_DEPLOYMENT.md new file mode 100644 index 0000000..668b6ed --- /dev/null +++ b/TRAEFIK_DEPLOYMENT.md @@ -0,0 +1,280 @@ +# Traefik Deployment Guide + +This guide explains how to deploy the PicPeak application with Traefik as the reverse proxy. + +## Overview + +The application consists of: +- **Frontend**: React app served by nginx (port 80) +- **Backend**: Node.js API (port 3000) +- **Database**: PostgreSQL (port 5432, internal only) + +## Traefik Configuration + +### 1. Docker Labels for Traefik + +Add these labels to your `docker-compose.prod.yml` services: + +```yaml +services: + frontend: + labels: + - "traefik.enable=true" + - "traefik.http.routers.picpeak-frontend.rule=Host(`picpeak.yourdomain.com`)" + - "traefik.http.routers.picpeak-frontend.entrypoints=websecure" + - "traefik.http.routers.picpeak-frontend.tls.certresolver=letsencrypt" + - "traefik.http.services.picpeak-frontend.loadbalancer.server.port=80" + # Priority for catch-all route + - "traefik.http.routers.picpeak-frontend.priority=1" + + backend: + labels: + - "traefik.enable=true" + - "traefik.http.routers.picpeak-api.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/api`)" + - "traefik.http.routers.picpeak-api.entrypoints=websecure" + - "traefik.http.routers.picpeak-api.tls.certresolver=letsencrypt" + - "traefik.http.services.picpeak-api.loadbalancer.server.port=3000" + # Higher priority for API routes + - "traefik.http.routers.picpeak-api.priority=10" + + # Additional routes for backend static files + - "traefik.http.routers.picpeak-uploads.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/uploads`)" + - "traefik.http.routers.picpeak-uploads.entrypoints=websecure" + - "traefik.http.routers.picpeak-uploads.tls.certresolver=letsencrypt" + - "traefik.http.routers.picpeak-uploads.service=picpeak-api" + - "traefik.http.routers.picpeak-uploads.priority=10" + + - "traefik.http.routers.picpeak-images.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/images`)" + - "traefik.http.routers.picpeak-images.entrypoints=websecure" + - "traefik.http.routers.picpeak-images.tls.certresolver=letsencrypt" + - "traefik.http.routers.picpeak-images.service=picpeak-api" + - "traefik.http.routers.picpeak-images.priority=10" +``` + +### 2. Network Configuration + +Ensure your services are on the Traefik network: + +```yaml +networks: + picpeak: + external: false + traefik: + external: true + +services: + frontend: + networks: + - picpeak + - traefik + + backend: + networks: + - picpeak + - traefik + + db: + networks: + - picpeak # Don't expose to traefik +``` + +### 3. Remove Nginx Service + +Since you're using Traefik, remove the nginx service from `docker-compose.prod.yml`: + +```yaml +# Remove this entire service: +# nginx: +# image: nginx:alpine +# ... +``` + +## Frontend Configuration + +The frontend is built with the API URL set to `/api`. This is important because: + +1. All API calls will be relative to the same domain +2. Traefik will route `/api/*` to the backend service +3. No CORS issues since everything is on the same domain + +## Environment Variables + +Ensure these are set correctly: + +```bash +# Backend needs to know the public URLs +ADMIN_URL=https://picpeak.yourdomain.com +FRONTEND_URL=https://picpeak.yourdomain.com + +# Backend API is accessed via /api path +API_URL=https://picpeak.yourdomain.com/api +``` + +## Complete Example + +Here's a complete `docker-compose.prod.yml` for Traefik: + +```yaml +version: '3.8' + +networks: + picpeak: + external: false + traefik: + external: true + +services: + backend: + image: picpeak-backend:latest + build: + context: ./backend + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + - db + environment: + - NODE_ENV=production + - PORT=3000 + - JWT_SECRET=${JWT_SECRET} + - ADMIN_URL=https://picpeak.yourdomain.com + - FRONTEND_URL=https://picpeak.yourdomain.com + - DATABASE_CLIENT=pg + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=${DB_USER:-picpeak} + - DB_PASSWORD=${DB_PASSWORD} + - DB_NAME=${DB_NAME:-picpeak} + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_SECURE=${SMTP_SECURE} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - EMAIL_FROM=${EMAIL_FROM} + - STORAGE_PATH=/app/storage + - EVENTS_PATH=/app/storage/events + - ARCHIVE_PATH=/app/storage/events/archived + volumes: + - ./storage:/app/storage + - ./data:/app/data + - ./logs:/app/logs + networks: + - picpeak + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.picpeak-api.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/api`)" + - "traefik.http.routers.picpeak-api.entrypoints=websecure" + - "traefik.http.routers.picpeak-api.tls.certresolver=letsencrypt" + - "traefik.http.services.picpeak-api.loadbalancer.server.port=3000" + - "traefik.http.routers.picpeak-api.priority=10" + + - "traefik.http.routers.picpeak-uploads.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/uploads`)" + - "traefik.http.routers.picpeak-uploads.entrypoints=websecure" + - "traefik.http.routers.picpeak-uploads.tls.certresolver=letsencrypt" + - "traefik.http.routers.picpeak-uploads.service=picpeak-api" + - "traefik.http.routers.picpeak-uploads.priority=10" + + - "traefik.http.routers.picpeak-images.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/images`)" + - "traefik.http.routers.picpeak-images.entrypoints=websecure" + - "traefik.http.routers.picpeak-images.tls.certresolver=letsencrypt" + - "traefik.http.routers.picpeak-images.service=picpeak-api" + - "traefik.http.routers.picpeak-images.priority=10" + + frontend: + image: picpeak-frontend:latest + build: + context: ./frontend + dockerfile: Dockerfile + args: + - VITE_API_URL=/api + restart: unless-stopped + depends_on: + - backend + networks: + - picpeak + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.picpeak-frontend.rule=Host(`picpeak.yourdomain.com`)" + - "traefik.http.routers.picpeak-frontend.entrypoints=websecure" + - "traefik.http.routers.picpeak-frontend.tls.certresolver=letsencrypt" + - "traefik.http.services.picpeak-frontend.loadbalancer.server.port=80" + - "traefik.http.routers.picpeak-frontend.priority=1" + + db: + image: postgres:14-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=${DB_USER:-picpeak} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME:-picpeak} + - POSTGRES_HOST_AUTH_METHOD=scram-sha-256 + - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - picpeak + command: postgres -c ssl=off + +volumes: + postgres_data: +``` + +## Troubleshooting + +### 502 Bad Gateway Errors + +1. **Check if backend is running**: + ```bash + docker-compose -f docker-compose.prod.yml ps + docker-compose -f docker-compose.prod.yml logs backend + ``` + +2. **Verify Traefik can reach the backend**: + - Ensure both services are on the same Docker network + - Check Traefik logs: `docker logs traefik` + +3. **Check backend health**: + ```bash + docker-compose -f docker-compose.prod.yml exec backend curl http://localhost:3000/api/health + ``` + +### Frontend Can't Reach API + +1. **Verify API paths don't have double `/api`**: + - Frontend should call `/auth/admin/login`, not `/api/auth/admin/login` + - The base URL in axios should be `/api` + +2. **Check browser console for actual URLs being called** + +3. **Ensure Traefik routing rules are correct**: + - API routes should have higher priority than frontend catch-all + +### CORS Issues + +Should not occur since everything is on the same domain. If you see CORS errors: +1. Check that `FRONTEND_URL` and `ADMIN_URL` match your actual domain +2. Ensure you're not mixing HTTP and HTTPS + +## Testing the Setup + +1. **Test API directly**: + ```bash + curl https://picpeak.yourdomain.com/api/health + ``` + +2. **Test frontend**: + ```bash + curl https://picpeak.yourdomain.com/ + ``` + +3. **Test admin login**: + - Navigate to https://picpeak.yourdomain.com/admin/login + - Check browser console for any errors + +## Important Notes + +1. **SSL/TLS**: Traefik handles SSL termination, so the backend doesn't need SSL +2. **Port Exposure**: Don't expose backend ports directly - let Traefik handle routing +3. **Health Checks**: Configure Traefik health checks for better reliability +4. **Rate Limiting**: Consider adding Traefik rate limiting middleware for API routes \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e7c533d..d8cd1a8 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -49,6 +49,8 @@ services: build: context: ./frontend dockerfile: Dockerfile + args: + - VITE_API_URL=/api restart: unless-stopped depends_on: - backend diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 0000000..2e0c6bb --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,65 @@ +# Build stage with dynamic API URL +FROM node:20-alpine AS builder + +# Accept build args for API URL +ARG VITE_API_URL=/api + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --legacy-peer-deps + +# Copy source files +COPY . . + +# Set environment variable for build +ENV VITE_API_URL=$VITE_API_URL + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Install runtime dependencies +RUN apk add --no-cache curl + +# Remove default nginx config +RUN rm -rf /etc/nginx/conf.d/* + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built application from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Create a script to inject runtime config +RUN cat > /usr/share/nginx/html/config.js << 'EOF' +window.__RUNTIME_CONFIG__ = { + API_URL: '/api' +}; +EOF + +# Set permissions +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 + +# Switch to non-root user +USER nginx + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/src/components/GlobalThemeProvider.tsx b/frontend/src/components/GlobalThemeProvider.tsx index a6625fa..d3fb8b3 100644 --- a/frontend/src/components/GlobalThemeProvider.tsx +++ b/frontend/src/components/GlobalThemeProvider.tsx @@ -15,7 +15,7 @@ export const GlobalThemeProvider: React.FC = ({ childr const { data: settingsData } = useQuery({ queryKey: ['global-theme-settings'], queryFn: async () => { - const response = await api.get('/api/public/settings'); + const response = await api.get('/public/settings'); return response.data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes diff --git a/frontend/src/components/MaintenanceMode.tsx b/frontend/src/components/MaintenanceMode.tsx index 0019824..83d2ef4 100644 --- a/frontend/src/components/MaintenanceMode.tsx +++ b/frontend/src/components/MaintenanceMode.tsx @@ -23,7 +23,7 @@ export const MaintenanceMode: React.FC = () => { queryKey: ['public-settings-maintenance'], queryFn: async () => { try { - const response = await api.get('/api/public/settings'); + const response = await api.get('/public/settings'); return response.data; } catch (error) { // Return empty object if settings can't be fetched diff --git a/frontend/src/components/MaintenanceWrapper.tsx b/frontend/src/components/MaintenanceWrapper.tsx index d993e7c..c6b34f0 100644 --- a/frontend/src/components/MaintenanceWrapper.tsx +++ b/frontend/src/components/MaintenanceWrapper.tsx @@ -30,7 +30,7 @@ export const MaintenanceWrapper: React.FC = ({ children queryFn: async () => { try { // Make a lightweight request to check maintenance status - await api.get('/api/public/settings'); + await api.get('/public/settings'); // If successful, maintenance mode is off setMaintenanceMode(false); return { maintenance: false }; diff --git a/frontend/src/components/admin/PhotoUpload.tsx b/frontend/src/components/admin/PhotoUpload.tsx index 97bcc09..c8c7042 100644 --- a/frontend/src/components/admin/PhotoUpload.tsx +++ b/frontend/src/components/admin/PhotoUpload.tsx @@ -62,7 +62,7 @@ export const PhotoUpload: React.FC = ({ eventId, onUploadCompl } try { - const response = await api.post(`/api/admin/events/${eventId}/upload`, formData, { + const response = await api.post(`/admin/events/${eventId}/upload`, formData, { // Don't set Content-Type header - axios will set it with the boundary onUploadProgress: (progressEvent) => { if (progressEvent.total) { diff --git a/frontend/src/components/admin/VersionInfo.tsx b/frontend/src/components/admin/VersionInfo.tsx index 53619b8..e2e3891 100644 --- a/frontend/src/components/admin/VersionInfo.tsx +++ b/frontend/src/components/admin/VersionInfo.tsx @@ -15,7 +15,7 @@ interface SystemVersion { } async function fetchSystemVersion(): Promise { - const response = await api.get('/api/admin/system/version'); + const response = await api.get('/admin/system/version'); return response.data; } diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 9d977bf..5607695 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -79,7 +79,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { const { data: settingsData } = useQuery({ queryKey: ['gallery-settings'], queryFn: async () => { - const response = await api.get('/api/public/settings'); + const response = await api.get('/public/settings'); return response.data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes @@ -195,8 +195,8 @@ export const GalleryView: React.FC = ({ slug, event }) => { if (watermarkEnabled) { photos = photos.map(photo => ({ ...photo, - url: `/api/gallery/${slug}/photo/${photo.id}`, - thumbnail_url: `/api/gallery/${slug}/photo/${photo.id}` // Use watermarked version for thumbnails too + url: `/gallery/${slug}/photo/${photo.id}`, + thumbnail_url: `/gallery/${slug}/photo/${photo.id}` // Use watermarked version for thumbnails too })); } diff --git a/frontend/src/components/gallery/UserPhotoUpload.tsx b/frontend/src/components/gallery/UserPhotoUpload.tsx index e9dfbe6..c0bac77 100644 --- a/frontend/src/components/gallery/UserPhotoUpload.tsx +++ b/frontend/src/components/gallery/UserPhotoUpload.tsx @@ -63,7 +63,7 @@ export const UserPhotoUpload: React.FC = ({ } try { - await api.post(`/api/gallery/${eventId}/upload`, formData, { + await api.post(`/gallery/${eventId}/upload`, formData, { headers: { 'Content-Type': 'multipart/form-data', }, diff --git a/frontend/src/pages/GalleryPage.tsx b/frontend/src/pages/GalleryPage.tsx index b39dfc3..9f48e4c 100644 --- a/frontend/src/pages/GalleryPage.tsx +++ b/frontend/src/pages/GalleryPage.tsx @@ -33,7 +33,7 @@ export const GalleryPage: React.FC = () => { const { data: settingsData } = useQuery({ queryKey: ['gallery-settings'], queryFn: async () => { - const response = await api.get('/api/public/settings'); + const response = await api.get('/public/settings'); return response.data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes diff --git a/frontend/src/pages/admin/AdminLoginPage.tsx b/frontend/src/pages/admin/AdminLoginPage.tsx index 92625de..accb4a8 100644 --- a/frontend/src/pages/admin/AdminLoginPage.tsx +++ b/frontend/src/pages/admin/AdminLoginPage.tsx @@ -27,7 +27,7 @@ export const AdminLoginPage: React.FC = () => { const { data: settingsData } = useQuery({ queryKey: ['admin-login-settings'], queryFn: async () => { - const response = await api.get('/api/public/settings'); + const response = await api.get('/public/settings'); return response.data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes diff --git a/frontend/src/pages/public/LegalPage.tsx b/frontend/src/pages/public/LegalPage.tsx index 600670a..e736bb2 100644 --- a/frontend/src/pages/public/LegalPage.tsx +++ b/frontend/src/pages/public/LegalPage.tsx @@ -21,7 +21,7 @@ export const LegalPage: React.FC = () => { const { data: settingsData } = useQuery({ queryKey: ['public-settings'], queryFn: async () => { - const response = await api.get('/api/public/settings'); + const response = await api.get('/public/settings'); return response.data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes diff --git a/frontend/src/services/admin.service.ts b/frontend/src/services/admin.service.ts index 9fe4910..520963f 100644 --- a/frontend/src/services/admin.service.ts +++ b/frontend/src/services/admin.service.ts @@ -66,13 +66,13 @@ export interface AnalyticsData { export const adminService = { // Dashboard statistics async getDashboardStats(): Promise { - const response = await api.get('/api/admin/dashboard/stats'); + const response = await api.get('/admin/dashboard/stats'); return response.data; }, // Recent activity async getRecentActivity(limit: number = 10): Promise { - const response = await api.get('/api/admin/dashboard/activity', { + const response = await api.get('/admin/dashboard/activity', { params: { limit } }); return response.data; @@ -80,7 +80,7 @@ export const adminService = { // Analytics data async getAnalytics(days: number = 7): Promise { - const response = await api.get('/api/admin/dashboard/analytics', { + const response = await api.get('/admin/dashboard/analytics', { params: { days } }); return response.data; @@ -88,7 +88,7 @@ export const adminService = { // System health check async getSystemHealth(): Promise { - const response = await api.get('/api/admin/dashboard/health'); + const response = await api.get('/admin/dashboard/health'); return response.data; }, @@ -124,6 +124,6 @@ export const adminService = { // Change password async changePassword(data: { currentPassword: string; newPassword: string }): Promise { - await api.post('/api/admin/auth/change-password', data); + await api.post('/admin/auth/change-password', data); } }; \ No newline at end of file diff --git a/frontend/src/services/archive.service.ts b/frontend/src/services/archive.service.ts index dede8dd..3e6c97b 100644 --- a/frontend/src/services/archive.service.ts +++ b/frontend/src/services/archive.service.ts @@ -46,7 +46,7 @@ export interface ArchivesResponse { export const archiveService = { // Get all archives with pagination async getArchives(page: number = 1, limit: number = 20): Promise { - const response = await api.get('/api/admin/archives', { + const response = await api.get('/admin/archives', { params: { page, limit } }); return response.data; @@ -54,18 +54,18 @@ export const archiveService = { // Get single archive details async getArchiveDetails(id: number): Promise { - const response = await api.get(`/api/admin/archives/${id}`); + const response = await api.get(`/admin/archives/${id}`); return response.data; }, // Restore archive async restoreArchive(id: number): Promise { - await api.post(`/api/admin/archives/${id}/restore`); + await api.post(`/admin/archives/${id}/restore`); }, // Download archive async downloadArchive(id: number, filename: string): Promise { - const response = await api.get(`/api/admin/archives/${id}/download`, { + const response = await api.get(`/admin/archives/${id}/download`, { responseType: 'blob' }); @@ -82,7 +82,7 @@ export const archiveService = { // Delete archive permanently async deleteArchive(id: number): Promise { - await api.delete(`/api/admin/archives/${id}`); + await api.delete(`/admin/archives/${id}`); }, // Format bytes to human readable diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 18750df..0ecc759 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -5,7 +5,7 @@ export const authService = { // Admin authentication async adminLogin(credentials: { email: string; password: string; recaptchaToken?: string | null }): Promise { // Backend expects 'username' field, but we accept email - const response = await api.post('/api/auth/admin/login', { + const response = await api.post('/auth/admin/login', { username: credentials.email, password: credentials.password, recaptchaToken: credentials.recaptchaToken @@ -22,7 +22,7 @@ export const authService = { // Gallery authentication async verifyGalleryPassword(slug: string, password: string, recaptchaToken?: string | null): Promise { - const response = await api.post('/api/auth/gallery/verify', { + const response = await api.post('/auth/gallery/verify', { slug, password, recaptchaToken diff --git a/frontend/src/services/categories.service.ts b/frontend/src/services/categories.service.ts index 67185af..56c5bed 100644 --- a/frontend/src/services/categories.service.ts +++ b/frontend/src/services/categories.service.ts @@ -19,30 +19,30 @@ export interface CreateCategoryData { export const categoriesService = { // Get all global categories async getGlobalCategories(): Promise { - const response = await api.get('/api/admin/categories/global'); + const response = await api.get('/admin/categories/global'); return response.data; }, // Get categories for a specific event (global + event-specific) async getEventCategories(eventId: number): Promise { - const response = await api.get(`/api/admin/categories/event/${eventId}`); + const response = await api.get(`/admin/categories/event/${eventId}`); return response.data; }, // Create a new category async createCategory(data: CreateCategoryData): Promise { - const response = await api.post('/api/admin/categories', data); + const response = await api.post('/admin/categories', data); return response.data; }, // Update a category async updateCategory(id: number, name: string): Promise { - const response = await api.put(`/api/admin/categories/${id}`, { name }); + const response = await api.put(`/admin/categories/${id}`, { name }); return response.data; }, // Delete a category async deleteCategory(id: number): Promise { - await api.delete(`/api/admin/categories/${id}`); + await api.delete(`/admin/categories/${id}`); } }; \ No newline at end of file diff --git a/frontend/src/services/cms.service.ts b/frontend/src/services/cms.service.ts index 072ba77..bd20910 100644 --- a/frontend/src/services/cms.service.ts +++ b/frontend/src/services/cms.service.ts @@ -13,25 +13,25 @@ export interface CMSPage { export const cmsService = { // Get all CMS pages async getPages(): Promise { - const response = await api.get('/api/admin/cms/pages'); + const response = await api.get('/admin/cms/pages'); return response.data; }, // Get a single CMS page async getPage(slug: string): Promise { - const response = await api.get(`/api/admin/cms/pages/${slug}`); + const response = await api.get(`/admin/cms/pages/${slug}`); return response.data; }, // Update a CMS page async updatePage(slug: string, data: Partial): Promise { - const response = await api.put(`/api/admin/cms/pages/${slug}`, data); + const response = await api.put(`/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 }>(`/api/public/pages/${slug}`, { + const response = await api.get<{ title: string; content: string }>(`/public/pages/${slug}`, { params: { lang } }); return response.data; diff --git a/frontend/src/services/email.service.ts b/frontend/src/services/email.service.ts index 382229b..196f14f 100644 --- a/frontend/src/services/email.service.ts +++ b/frontend/src/services/email.service.ts @@ -35,41 +35,41 @@ export interface EmailPreview { export const emailService = { // Get email configuration async getConfig(): Promise { - const response = await api.get('/api/admin/email/config'); + const response = await api.get('/admin/email/config'); return response.data; }, // Update email configuration async updateConfig(config: EmailConfig): Promise { - await api.post('/api/admin/email/config', config); + await api.post('/admin/email/config', config); }, // Test email configuration async testEmail(testEmail: string): Promise { - await api.post('/api/admin/email/test', { test_email: testEmail }); + await api.post('/admin/email/test', { test_email: testEmail }); }, // Get all email templates async getTemplates(): Promise { - const response = await api.get('/api/admin/email/templates'); + const response = await api.get('/admin/email/templates'); return response.data; }, // Get single template async getTemplate(key: string): Promise { - const response = await api.get(`/api/admin/email/templates/${key}`); + const response = await api.get(`/admin/email/templates/${key}`); return response.data; }, // Update email template async updateTemplate(key: string, template: Partial): Promise { - await api.put(`/api/admin/email/templates/${key}`, template); + await api.put(`/admin/email/templates/${key}`, template); }, // Preview email template async previewTemplate(key: string, previewData: Record, language: 'en' | 'de' = 'en'): Promise { const response = await api.post( - `/api/admin/email/templates/${key}/preview`, + `/admin/email/templates/${key}/preview`, { preview_data: previewData, language } ); return response.data; diff --git a/frontend/src/services/events.service.ts b/frontend/src/services/events.service.ts index 11d5ffe..09c176e 100644 --- a/frontend/src/services/events.service.ts +++ b/frontend/src/services/events.service.ts @@ -53,36 +53,36 @@ export const eventsService = { params.append('status', status); } - const response = await api.get(`/api/admin/events?${params}`); + const response = await api.get(`/admin/events?${params}`); return response.data; }, // Get single event details (admin) async getEvent(id: number): Promise { - const response = await api.get(`/api/admin/events/${id}`); + const response = await api.get(`/admin/events/${id}`); return response.data; }, // Create new event (admin) async createEvent(data: CreateEventData): Promise { - const response = await api.post('/api/admin/events', data); + const response = await api.post('/admin/events', data); return response.data; }, // Update event (admin) async updateEvent(id: number, data: UpdateEventData): Promise { - const response = await api.put(`/api/admin/events/${id}`, data); + const response = await api.put(`/admin/events/${id}`, data); return response.data; }, // Delete/deactivate event (admin) async deleteEvent(id: number): Promise { - await api.delete(`/api/admin/events/${id}`); + await api.delete(`/admin/events/${id}`); }, // Force archive event (admin) async archiveEvent(id: number): Promise { - await api.post(`/api/admin/events/${id}/archive`); + await api.post(`/admin/events/${id}/archive`); }, // Bulk archive events (admin) @@ -93,7 +93,7 @@ export const eventsService = { failed: Array<{ id: number; name: string; error: string }>; }; }> { - const response = await api.post('/api/admin/events/bulk-archive', { + const response = await api.post('/admin/events/bulk-archive', { eventIds, }); return response.data; @@ -101,7 +101,7 @@ export const eventsService = { // Extend event expiration (admin) async extendExpiration(id: number, days: number): Promise { - const response = await api.post(`/api/events/${id}/extend`, { + const response = await api.post(`/events/${id}/extend`, { days, }); return response.data; @@ -109,13 +109,13 @@ export const eventsService = { // Get event categories async getEventCategories(eventId: number): Promise> { - const response = await api.get(`/api/admin/categories/event/${eventId}`); + 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(`/api/admin/events/${eventId}/reset-password`, { sendEmail }); + const response = await api.post(`/admin/events/${eventId}/reset-password`, { sendEmail }); return response.data; }, }; \ No newline at end of file diff --git a/frontend/src/services/gallery.service.ts b/frontend/src/services/gallery.service.ts index 3b01b1a..eea2227 100644 --- a/frontend/src/services/gallery.service.ts +++ b/frontend/src/services/gallery.service.ts @@ -4,26 +4,26 @@ 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 }>(`/api/gallery/${slug}/verify-token/${token}`); + 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 { const params = token ? { token } : {}; - const response = await api.get(`/api/gallery/${slug}/info`, { params }); + const response = await api.get(`/gallery/${slug}/info`, { params }); return response.data; }, // Get gallery photos (requires auth) async getGalleryPhotos(slug: string): Promise { - const response = await api.get(`/api/gallery/${slug}/photos`); + const response = await api.get(`/gallery/${slug}/photos`); return response.data; }, // Download single photo async downloadPhoto(slug: string, photoId: number, filename: string): Promise { - const response = await api.get(`/api/gallery/${slug}/download/${photoId}`, { + const response = await api.get(`/gallery/${slug}/download/${photoId}`, { responseType: 'blob', }); @@ -40,7 +40,7 @@ export const galleryService = { // Download all photos as ZIP async downloadAllPhotos(slug: string): Promise { - const response = await api.get(`/api/gallery/${slug}/download-all`, { + const response = await api.get(`/gallery/${slug}/download-all`, { responseType: 'blob', }); @@ -57,7 +57,7 @@ export const galleryService = { // Get gallery statistics async getGalleryStats(slug: string): Promise { - const response = await api.get(`/api/gallery/${slug}/stats`); + const response = await api.get(`/gallery/${slug}/stats`); return response.data; }, }; \ No newline at end of file diff --git a/frontend/src/services/notifications.service.ts b/frontend/src/services/notifications.service.ts index 29e86b7..d80465c 100644 --- a/frontend/src/services/notifications.service.ts +++ b/frontend/src/services/notifications.service.ts @@ -22,7 +22,7 @@ export interface NotificationsResponse { export const notificationsService = { // Get notifications async getNotifications(includeRead: boolean = false, limit: number = 20): Promise { - const response = await api.get('/api/admin/notifications', { + const response = await api.get('/admin/notifications', { params: { includeRead, limit } }); return response.data; @@ -30,17 +30,17 @@ export const notificationsService = { // Mark single notification as read async markAsRead(notificationId: number): Promise { - await api.put(`/api/admin/notifications/${notificationId}/read`); + await api.put(`/admin/notifications/${notificationId}/read`); }, // Mark all notifications as read async markAllAsRead(): Promise { - await api.put('/api/admin/notifications/read-all'); + await api.put('/admin/notifications/read-all'); }, // Clear old notifications async clearOldNotifications(): Promise<{ deletedCount: number }> { - const response = await api.delete('/api/admin/notifications/clear-old'); + const response = await api.delete('/admin/notifications/clear-old'); return response.data; }, diff --git a/frontend/src/services/photos.service.ts b/frontend/src/services/photos.service.ts index 24c0f94..c670893 100644 --- a/frontend/src/services/photos.service.ts +++ b/frontend/src/services/photos.service.ts @@ -39,7 +39,7 @@ class PhotosService { } const queryString = params.toString(); - const url = `/api/admin/events/${eventId}/photos${queryString ? `?${queryString}` : ''}`; + const url = `/admin/events/${eventId}/photos${queryString ? `?${queryString}` : ''}`; const response = await api.get(url); @@ -48,26 +48,26 @@ class PhotosService { } async deletePhoto(eventId: number, photoId: number): Promise { - await api.delete(`/api/admin/events/${eventId}/photos/${photoId}`); + await api.delete(`/admin/events/${eventId}/photos/${photoId}`); } async deletePhotos(eventId: number, photoIds: number[]): Promise { - await api.post(`/api/admin/events/${eventId}/photos/bulk-delete`, { photoIds }); + await api.post(`/admin/events/${eventId}/photos/bulk-delete`, { photoIds }); } async updatePhotoCategory(eventId: number, photoId: number, categoryId: number | null): Promise { - await api.patch(`/api/admin/events/${eventId}/photos/${photoId}`, { category_id: categoryId }); + await api.patch(`/admin/events/${eventId}/photos/${photoId}`, { category_id: categoryId }); } async updatePhotosCategory(eventId: number, photoIds: number[], categoryId: number | null): Promise { - await api.post(`/api/admin/events/${eventId}/photos/bulk-update`, { + await api.post(`/admin/events/${eventId}/photos/bulk-update`, { photoIds, updates: { category_id: categoryId } }); } async downloadPhoto(eventId: number, photoId: number, filename: string): Promise { - const response = await api.get(`/api/admin/events/${eventId}/photos/${photoId}/download`, { + const response = await api.get(`/admin/events/${eventId}/photos/${photoId}/download`, { responseType: 'blob' }); diff --git a/frontend/src/services/settings.service.ts b/frontend/src/services/settings.service.ts index b0fc672..5f9397f 100644 --- a/frontend/src/services/settings.service.ts +++ b/frontend/src/services/settings.service.ts @@ -79,19 +79,19 @@ export interface SystemStatus { export const settingsService = { // Get all settings async getAllSettings(): Promise> { - const response = await api.get>('/api/admin/settings'); + const response = await api.get>('/admin/settings'); return response.data; }, // Get settings by type async getSettingsByType(type: 'branding' | 'theme' | 'general'): Promise> { - const response = await api.get>(`/api/admin/settings/${type}`); + const response = await api.get>(`/admin/settings/${type}`); return response.data; }, // Update branding settings async updateBranding(settings: BrandingSettings): Promise { - await api.put('/api/admin/settings/branding', settings); + await api.put('/admin/settings/branding', settings); }, // Upload logo @@ -150,23 +150,23 @@ export const settingsService = { // Update theme settings async updateTheme(settings: ThemeSettings): Promise { - await api.put('/api/admin/settings/theme', settings); + await api.put('/admin/settings/theme', settings); }, // Update multiple settings at once async updateSettings(settings: Record): Promise { - await api.put('/api/admin/settings/general', settings); + await api.put('/admin/settings/general', settings); }, // Get storage information async getStorageInfo(): Promise { - const response = await api.get('/api/admin/settings/storage/info'); + const response = await api.get('/admin/settings/storage/info'); return response.data; }, // Get system status async getSystemStatus(): Promise { - const response = await api.get('/api/admin/system/status'); + const response = await api.get('/admin/system/status'); return response.data; }, diff --git a/frontend/src/utils/photoUrl.ts b/frontend/src/utils/photoUrl.ts index 01aba71..2bce36b 100644 --- a/frontend/src/utils/photoUrl.ts +++ b/frontend/src/utils/photoUrl.ts @@ -13,7 +13,7 @@ interface PhotoUrlOptions { export function getPhotoUrl({ slug, photo, watermarkEnabled = false, token }: PhotoUrlOptions): string { if (watermarkEnabled && token) { // Use the watermarked photo endpoint - return `/api/gallery/${slug}/photo/${photo.id}`; + return `/gallery/${slug}/photo/${photo.id}`; } // Use the static photo URL @@ -24,5 +24,5 @@ export function getPhotoUrl({ slug, photo, watermarkEnabled = false, token }: Ph * Get the download URL for a photo */ export function getPhotoDownloadUrl(slug: string, photoId: number): string { - return `/api/gallery/${slug}/download/${photoId}`; + return `/gallery/${slug}/download/${photoId}`; } \ No newline at end of file