Compare commits

...

4 Commits

Author SHA1 Message Date
Gitea Actions Bot 97bbb3c8e1 chore: bump version to 1.0.12
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2025-07-13 21:22:28 +00:00
paul ac1cd96ecd fix: resolve frontend API routing issues for Traefik deployment
Test and Lint / backend-test (push) Successful in 1m16s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Successful in 2m18s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Successful in 4s
Major fixes for production deployment with Traefik:

1. API Path Fixes:
   - Remove double /api prefix from all frontend service calls
   - Fix auth.service.ts to use correct paths (/auth/admin/login)
   - Update all services to use single /api prefix from base URL
   - Fix template literal paths in photo services

2. Docker Configuration:
   - Add build args for VITE_API_URL in docker-compose.prod.yml
   - Create Dockerfile.prod with proper API URL configuration
   - Ensure frontend is built with correct API base path

3. Documentation:
   - Add comprehensive TRAEFIK_DEPLOYMENT.md guide
   - Document proper Traefik labels and routing configuration
   - Include troubleshooting steps for common issues
   - Explain network configuration and SSL handling

This resolves:
- 502 Bad Gateway errors
- Double /api/api paths in requests
- Frontend unable to communicate with backend
- Login functionality not working

The frontend now correctly calls the backend API through Traefik's
routing, with all requests going to /api/* being forwarded to the
backend service on port 3000.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 23:18:00 +02:00
Gitea Actions Bot 689861f671 chore: bump version to 1.0.11
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 21:12:57 +00:00
paul 41fb575e80 fix: use correct bcrypt package in create-admin script
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m5s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Change from bcryptjs to bcrypt to match installed dependency
- Fixes "Cannot find module 'bcryptjs'" error when creating admin user

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 23:09:02 +02:00
30 changed files with 429 additions and 82 deletions
+280
View File
@@ -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
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.10", "version": "1.0.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.10", "version": "1.0.12",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"archiver": "^5.3.1", "archiver": "^5.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.10", "version": "1.0.12",
"description": "Backend for PicPeak event photo sharing platform", "description": "Backend for PicPeak event photo sharing platform",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -8,7 +8,7 @@
*/ */
require('dotenv').config(); require('dotenv').config();
const bcrypt = require('bcryptjs'); const bcrypt = require('bcrypt');
const { db } = require('../src/database/db'); const { db } = require('../src/database/db');
const crypto = require('crypto'); const crypto = require('crypto');
+2
View File
@@ -49,6 +49,8 @@ services:
build: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
args:
- VITE_API_URL=/api
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- backend - backend
+65
View File
@@ -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;"]
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"version": "1.0.10", "version": "1.0.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"version": "1.0.10", "version": "1.0.12",
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.0.0", "@tanstack/react-query": "^5.0.0",
"@tiptap/extension-link": "^2.25.0", "@tiptap/extension-link": "^2.25.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"private": true, "private": true,
"version": "1.0.10", "version": "1.0.12",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -15,7 +15,7 @@ export const GlobalThemeProvider: React.FC<GlobalThemeProviderProps> = ({ childr
const { data: settingsData } = useQuery({ const { data: settingsData } = useQuery({
queryKey: ['global-theme-settings'], queryKey: ['global-theme-settings'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/api/public/settings'); const response = await api.get('/public/settings');
return response.data; return response.data;
}, },
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+1 -1
View File
@@ -23,7 +23,7 @@ export const MaintenanceMode: React.FC = () => {
queryKey: ['public-settings-maintenance'], queryKey: ['public-settings-maintenance'],
queryFn: async () => { queryFn: async () => {
try { try {
const response = await api.get('/api/public/settings'); const response = await api.get('/public/settings');
return response.data; return response.data;
} catch (error) { } catch (error) {
// Return empty object if settings can't be fetched // Return empty object if settings can't be fetched
@@ -30,7 +30,7 @@ export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children
queryFn: async () => { queryFn: async () => {
try { try {
// Make a lightweight request to check maintenance status // 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 // If successful, maintenance mode is off
setMaintenanceMode(false); setMaintenanceMode(false);
return { maintenance: false }; return { maintenance: false };
@@ -62,7 +62,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
} }
try { 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 // Don't set Content-Type header - axios will set it with the boundary
onUploadProgress: (progressEvent) => { onUploadProgress: (progressEvent) => {
if (progressEvent.total) { if (progressEvent.total) {
@@ -15,7 +15,7 @@ interface SystemVersion {
} }
async function fetchSystemVersion(): Promise<SystemVersion> { async function fetchSystemVersion(): Promise<SystemVersion> {
const response = await api.get<SystemVersion>('/api/admin/system/version'); const response = await api.get<SystemVersion>('/admin/system/version');
return response.data; return response.data;
} }
@@ -79,7 +79,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const { data: settingsData } = useQuery({ const { data: settingsData } = useQuery({
queryKey: ['gallery-settings'], queryKey: ['gallery-settings'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/api/public/settings'); const response = await api.get('/public/settings');
return response.data; return response.data;
}, },
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
@@ -195,8 +195,8 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
if (watermarkEnabled) { if (watermarkEnabled) {
photos = photos.map(photo => ({ photos = photos.map(photo => ({
...photo, ...photo,
url: `/api/gallery/${slug}/photo/${photo.id}`, url: `/gallery/${slug}/photo/${photo.id}`,
thumbnail_url: `/api/gallery/${slug}/photo/${photo.id}` // Use watermarked version for thumbnails too thumbnail_url: `/gallery/${slug}/photo/${photo.id}` // Use watermarked version for thumbnails too
})); }));
} }
@@ -63,7 +63,7 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
} }
try { try {
await api.post(`/api/gallery/${eventId}/upload`, formData, { await api.post(`/gallery/${eventId}/upload`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
+1 -1
View File
@@ -33,7 +33,7 @@ export const GalleryPage: React.FC = () => {
const { data: settingsData } = useQuery({ const { data: settingsData } = useQuery({
queryKey: ['gallery-settings'], queryKey: ['gallery-settings'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/api/public/settings'); const response = await api.get('/public/settings');
return response.data; return response.data;
}, },
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+1 -1
View File
@@ -27,7 +27,7 @@ export const AdminLoginPage: React.FC = () => {
const { data: settingsData } = useQuery({ const { data: settingsData } = useQuery({
queryKey: ['admin-login-settings'], queryKey: ['admin-login-settings'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/api/public/settings'); const response = await api.get('/public/settings');
return response.data; return response.data;
}, },
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+1 -1
View File
@@ -21,7 +21,7 @@ export const LegalPage: React.FC = () => {
const { data: settingsData } = useQuery({ const { data: settingsData } = useQuery({
queryKey: ['public-settings'], queryKey: ['public-settings'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/api/public/settings'); const response = await api.get('/public/settings');
return response.data; return response.data;
}, },
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+5 -5
View File
@@ -66,13 +66,13 @@ export interface AnalyticsData {
export const adminService = { export const adminService = {
// Dashboard statistics // Dashboard statistics
async getDashboardStats(): Promise<DashboardStats> { async getDashboardStats(): Promise<DashboardStats> {
const response = await api.get<DashboardStats>('/api/admin/dashboard/stats'); const response = await api.get<DashboardStats>('/admin/dashboard/stats');
return response.data; return response.data;
}, },
// Recent activity // Recent activity
async getRecentActivity(limit: number = 10): Promise<Activity[]> { async getRecentActivity(limit: number = 10): Promise<Activity[]> {
const response = await api.get<Activity[]>('/api/admin/dashboard/activity', { const response = await api.get<Activity[]>('/admin/dashboard/activity', {
params: { limit } params: { limit }
}); });
return response.data; return response.data;
@@ -80,7 +80,7 @@ export const adminService = {
// Analytics data // Analytics data
async getAnalytics(days: number = 7): Promise<AnalyticsData> { async getAnalytics(days: number = 7): Promise<AnalyticsData> {
const response = await api.get<AnalyticsData>('/api/admin/dashboard/analytics', { const response = await api.get<AnalyticsData>('/admin/dashboard/analytics', {
params: { days } params: { days }
}); });
return response.data; return response.data;
@@ -88,7 +88,7 @@ export const adminService = {
// System health check // System health check
async getSystemHealth(): Promise<SystemHealth> { async getSystemHealth(): Promise<SystemHealth> {
const response = await api.get<SystemHealth>('/api/admin/dashboard/health'); const response = await api.get<SystemHealth>('/admin/dashboard/health');
return response.data; return response.data;
}, },
@@ -124,6 +124,6 @@ export const adminService = {
// Change password // Change password
async changePassword(data: { currentPassword: string; newPassword: string }): Promise<void> { async changePassword(data: { currentPassword: string; newPassword: string }): Promise<void> {
await api.post('/api/admin/auth/change-password', data); await api.post('/admin/auth/change-password', data);
} }
}; };
+5 -5
View File
@@ -46,7 +46,7 @@ export interface ArchivesResponse {
export const archiveService = { export const archiveService = {
// Get all archives with pagination // Get all archives with pagination
async getArchives(page: number = 1, limit: number = 20): Promise<ArchivesResponse> { async getArchives(page: number = 1, limit: number = 20): Promise<ArchivesResponse> {
const response = await api.get<ArchivesResponse>('/api/admin/archives', { const response = await api.get<ArchivesResponse>('/admin/archives', {
params: { page, limit } params: { page, limit }
}); });
return response.data; return response.data;
@@ -54,18 +54,18 @@ export const archiveService = {
// Get single archive details // Get single archive details
async getArchiveDetails(id: number): Promise<ArchiveDetails> { async getArchiveDetails(id: number): Promise<ArchiveDetails> {
const response = await api.get<ArchiveDetails>(`/api/admin/archives/${id}`); const response = await api.get<ArchiveDetails>(`/admin/archives/${id}`);
return response.data; return response.data;
}, },
// Restore archive // Restore archive
async restoreArchive(id: number): Promise<void> { async restoreArchive(id: number): Promise<void> {
await api.post(`/api/admin/archives/${id}/restore`); await api.post(`/admin/archives/${id}/restore`);
}, },
// Download archive // Download archive
async downloadArchive(id: number, filename: string): Promise<void> { async downloadArchive(id: number, filename: string): Promise<void> {
const response = await api.get(`/api/admin/archives/${id}/download`, { const response = await api.get(`/admin/archives/${id}/download`, {
responseType: 'blob' responseType: 'blob'
}); });
@@ -82,7 +82,7 @@ export const archiveService = {
// Delete archive permanently // Delete archive permanently
async deleteArchive(id: number): Promise<void> { async deleteArchive(id: number): Promise<void> {
await api.delete(`/api/admin/archives/${id}`); await api.delete(`/admin/archives/${id}`);
}, },
// Format bytes to human readable // Format bytes to human readable
+2 -2
View File
@@ -5,7 +5,7 @@ export const authService = {
// Admin authentication // Admin authentication
async adminLogin(credentials: { email: string; password: string; recaptchaToken?: string | null }): Promise<LoginResponse> { async adminLogin(credentials: { email: string; password: string; recaptchaToken?: string | null }): Promise<LoginResponse> {
// Backend expects 'username' field, but we accept email // Backend expects 'username' field, but we accept email
const response = await api.post<LoginResponse>('/api/auth/admin/login', { const response = await api.post<LoginResponse>('/auth/admin/login', {
username: credentials.email, username: credentials.email,
password: credentials.password, password: credentials.password,
recaptchaToken: credentials.recaptchaToken recaptchaToken: credentials.recaptchaToken
@@ -22,7 +22,7 @@ export const authService = {
// Gallery authentication // Gallery authentication
async verifyGalleryPassword(slug: string, password: string, recaptchaToken?: string | null): Promise<GalleryAuthResponse> { async verifyGalleryPassword(slug: string, password: string, recaptchaToken?: string | null): Promise<GalleryAuthResponse> {
const response = await api.post<GalleryAuthResponse>('/api/auth/gallery/verify', { const response = await api.post<GalleryAuthResponse>('/auth/gallery/verify', {
slug, slug,
password, password,
recaptchaToken recaptchaToken
+5 -5
View File
@@ -19,30 +19,30 @@ export interface CreateCategoryData {
export const categoriesService = { export const categoriesService = {
// Get all global categories // Get all global categories
async getGlobalCategories(): Promise<PhotoCategory[]> { async getGlobalCategories(): Promise<PhotoCategory[]> {
const response = await api.get<PhotoCategory[]>('/api/admin/categories/global'); const response = await api.get<PhotoCategory[]>('/admin/categories/global');
return response.data; return response.data;
}, },
// Get categories for a specific event (global + event-specific) // Get categories for a specific event (global + event-specific)
async getEventCategories(eventId: number): Promise<PhotoCategory[]> { async getEventCategories(eventId: number): Promise<PhotoCategory[]> {
const response = await api.get<PhotoCategory[]>(`/api/admin/categories/event/${eventId}`); const response = await api.get<PhotoCategory[]>(`/admin/categories/event/${eventId}`);
return response.data; return response.data;
}, },
// Create a new category // Create a new category
async createCategory(data: CreateCategoryData): Promise<PhotoCategory> { async createCategory(data: CreateCategoryData): Promise<PhotoCategory> {
const response = await api.post<PhotoCategory>('/api/admin/categories', data); const response = await api.post<PhotoCategory>('/admin/categories', data);
return response.data; return response.data;
}, },
// Update a category // Update a category
async updateCategory(id: number, name: string): Promise<PhotoCategory> { async updateCategory(id: number, name: string): Promise<PhotoCategory> {
const response = await api.put<PhotoCategory>(`/api/admin/categories/${id}`, { name }); const response = await api.put<PhotoCategory>(`/admin/categories/${id}`, { name });
return response.data; return response.data;
}, },
// Delete a category // Delete a category
async deleteCategory(id: number): Promise<void> { async deleteCategory(id: number): Promise<void> {
await api.delete(`/api/admin/categories/${id}`); await api.delete(`/admin/categories/${id}`);
} }
}; };
+4 -4
View File
@@ -13,25 +13,25 @@ export interface CMSPage {
export const cmsService = { export const cmsService = {
// Get all CMS pages // Get all CMS pages
async getPages(): Promise<CMSPage[]> { async getPages(): Promise<CMSPage[]> {
const response = await api.get<CMSPage[]>('/api/admin/cms/pages'); const response = await api.get<CMSPage[]>('/admin/cms/pages');
return response.data; return response.data;
}, },
// Get a single CMS page // Get a single CMS page
async getPage(slug: string): Promise<CMSPage> { async getPage(slug: string): Promise<CMSPage> {
const response = await api.get<CMSPage>(`/api/admin/cms/pages/${slug}`); const response = await api.get<CMSPage>(`/admin/cms/pages/${slug}`);
return response.data; return response.data;
}, },
// Update a CMS page // Update a CMS page
async updatePage(slug: string, data: Partial<CMSPage>): Promise<CMSPage> { async updatePage(slug: string, data: Partial<CMSPage>): Promise<CMSPage> {
const response = await api.put<CMSPage>(`/api/admin/cms/pages/${slug}`, data); const response = await api.put<CMSPage>(`/admin/cms/pages/${slug}`, data);
return response.data; return response.data;
}, },
// Get public CMS page (no auth required) // Get public CMS page (no auth required)
async getPublicPage(slug: string, lang: string = 'en'): Promise<{ title: string; content: string }> { 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 } params: { lang }
}); });
return response.data; return response.data;
+7 -7
View File
@@ -35,41 +35,41 @@ export interface EmailPreview {
export const emailService = { export const emailService = {
// Get email configuration // Get email configuration
async getConfig(): Promise<EmailConfig> { async getConfig(): Promise<EmailConfig> {
const response = await api.get<EmailConfig>('/api/admin/email/config'); const response = await api.get<EmailConfig>('/admin/email/config');
return response.data; return response.data;
}, },
// Update email configuration // Update email configuration
async updateConfig(config: EmailConfig): Promise<void> { async updateConfig(config: EmailConfig): Promise<void> {
await api.post('/api/admin/email/config', config); await api.post('/admin/email/config', config);
}, },
// Test email configuration // Test email configuration
async testEmail(testEmail: string): Promise<void> { async testEmail(testEmail: string): Promise<void> {
await api.post('/api/admin/email/test', { test_email: testEmail }); await api.post('/admin/email/test', { test_email: testEmail });
}, },
// Get all email templates // Get all email templates
async getTemplates(): Promise<EmailTemplate[]> { async getTemplates(): Promise<EmailTemplate[]> {
const response = await api.get<EmailTemplate[]>('/api/admin/email/templates'); const response = await api.get<EmailTemplate[]>('/admin/email/templates');
return response.data; return response.data;
}, },
// Get single template // Get single template
async getTemplate(key: string): Promise<EmailTemplate> { async getTemplate(key: string): Promise<EmailTemplate> {
const response = await api.get<EmailTemplate>(`/api/admin/email/templates/${key}`); const response = await api.get<EmailTemplate>(`/admin/email/templates/${key}`);
return response.data; return response.data;
}, },
// Update email template // Update email template
async updateTemplate(key: string, template: Partial<EmailTemplate>): Promise<void> { async updateTemplate(key: string, template: Partial<EmailTemplate>): Promise<void> {
await api.put(`/api/admin/email/templates/${key}`, template); await api.put(`/admin/email/templates/${key}`, template);
}, },
// Preview email template // Preview email template
async previewTemplate(key: string, previewData: Record<string, string>, language: 'en' | 'de' = 'en'): Promise<EmailPreview> { async previewTemplate(key: string, previewData: Record<string, string>, language: 'en' | 'de' = 'en'): Promise<EmailPreview> {
const response = await api.post<EmailPreview>( const response = await api.post<EmailPreview>(
`/api/admin/email/templates/${key}/preview`, `/admin/email/templates/${key}/preview`,
{ preview_data: previewData, language } { preview_data: previewData, language }
); );
return response.data; return response.data;
+10 -10
View File
@@ -53,36 +53,36 @@ export const eventsService = {
params.append('status', status); params.append('status', status);
} }
const response = await api.get<EventsListResponse>(`/api/admin/events?${params}`); const response = await api.get<EventsListResponse>(`/admin/events?${params}`);
return response.data; return response.data;
}, },
// Get single event details (admin) // Get single event details (admin)
async getEvent(id: number): Promise<Event> { async getEvent(id: number): Promise<Event> {
const response = await api.get<Event>(`/api/admin/events/${id}`); const response = await api.get<Event>(`/admin/events/${id}`);
return response.data; return response.data;
}, },
// Create new event (admin) // Create new event (admin)
async createEvent(data: CreateEventData): Promise<Event> { async createEvent(data: CreateEventData): Promise<Event> {
const response = await api.post<Event>('/api/admin/events', data); const response = await api.post<Event>('/admin/events', data);
return response.data; return response.data;
}, },
// Update event (admin) // Update event (admin)
async updateEvent(id: number, data: UpdateEventData): Promise<Event> { async updateEvent(id: number, data: UpdateEventData): Promise<Event> {
const response = await api.put<Event>(`/api/admin/events/${id}`, data); const response = await api.put<Event>(`/admin/events/${id}`, data);
return response.data; return response.data;
}, },
// Delete/deactivate event (admin) // Delete/deactivate event (admin)
async deleteEvent(id: number): Promise<void> { async deleteEvent(id: number): Promise<void> {
await api.delete(`/api/admin/events/${id}`); await api.delete(`/admin/events/${id}`);
}, },
// Force archive event (admin) // Force archive event (admin)
async archiveEvent(id: number): Promise<void> { async archiveEvent(id: number): Promise<void> {
await api.post(`/api/admin/events/${id}/archive`); await api.post(`/admin/events/${id}/archive`);
}, },
// Bulk archive events (admin) // Bulk archive events (admin)
@@ -93,7 +93,7 @@ export const eventsService = {
failed: Array<{ id: number; name: string; error: string }>; 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, eventIds,
}); });
return response.data; return response.data;
@@ -101,7 +101,7 @@ export const eventsService = {
// Extend event expiration (admin) // Extend event expiration (admin)
async extendExpiration(id: number, days: number): Promise<Event> { async extendExpiration(id: number, days: number): Promise<Event> {
const response = await api.post<Event>(`/api/events/${id}/extend`, { const response = await api.post<Event>(`/events/${id}/extend`, {
days, days,
}); });
return response.data; return response.data;
@@ -109,13 +109,13 @@ export const eventsService = {
// Get event categories // Get event categories
async getEventCategories(eventId: number): Promise<Array<{ id: number; name: string; slug: string }>> { async getEventCategories(eventId: number): Promise<Array<{ id: number; name: string; slug: string }>> {
const response = await api.get(`/api/admin/categories/event/${eventId}`); const response = await api.get(`/admin/categories/event/${eventId}`);
return response.data || []; return response.data || [];
}, },
// Reset event password // Reset event password
async resetPassword(eventId: number, sendEmail: boolean = true): Promise<{ message: string; newPassword: string; emailSent: boolean }> { 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; return response.data;
}, },
}; };
+6 -6
View File
@@ -4,26 +4,26 @@ import type { GalleryInfo, GalleryData, GalleryStats } from '../types';
export const galleryService = { export const galleryService = {
// Verify share token // Verify share token
async verifyToken(slug: string, token: string): Promise<{ valid: boolean }> { 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; return response.data;
}, },
// Get basic gallery info (no auth required) // Get basic gallery info (no auth required)
async getGalleryInfo(slug: string, token?: string): Promise<GalleryInfo> { async getGalleryInfo(slug: string, token?: string): Promise<GalleryInfo> {
const params = token ? { token } : {}; const params = token ? { token } : {};
const response = await api.get<GalleryInfo>(`/api/gallery/${slug}/info`, { params }); const response = await api.get<GalleryInfo>(`/gallery/${slug}/info`, { params });
return response.data; return response.data;
}, },
// Get gallery photos (requires auth) // Get gallery photos (requires auth)
async getGalleryPhotos(slug: string): Promise<GalleryData> { async getGalleryPhotos(slug: string): Promise<GalleryData> {
const response = await api.get<GalleryData>(`/api/gallery/${slug}/photos`); const response = await api.get<GalleryData>(`/gallery/${slug}/photos`);
return response.data; return response.data;
}, },
// Download single photo // Download single photo
async downloadPhoto(slug: string, photoId: number, filename: string): Promise<void> { async downloadPhoto(slug: string, photoId: number, filename: string): Promise<void> {
const response = await api.get(`/api/gallery/${slug}/download/${photoId}`, { const response = await api.get(`/gallery/${slug}/download/${photoId}`, {
responseType: 'blob', responseType: 'blob',
}); });
@@ -40,7 +40,7 @@ export const galleryService = {
// Download all photos as ZIP // Download all photos as ZIP
async downloadAllPhotos(slug: string): Promise<void> { async downloadAllPhotos(slug: string): Promise<void> {
const response = await api.get(`/api/gallery/${slug}/download-all`, { const response = await api.get(`/gallery/${slug}/download-all`, {
responseType: 'blob', responseType: 'blob',
}); });
@@ -57,7 +57,7 @@ export const galleryService = {
// Get gallery statistics // Get gallery statistics
async getGalleryStats(slug: string): Promise<GalleryStats> { async getGalleryStats(slug: string): Promise<GalleryStats> {
const response = await api.get<GalleryStats>(`/api/gallery/${slug}/stats`); const response = await api.get<GalleryStats>(`/gallery/${slug}/stats`);
return response.data; return response.data;
}, },
}; };
@@ -22,7 +22,7 @@ export interface NotificationsResponse {
export const notificationsService = { export const notificationsService = {
// Get notifications // Get notifications
async getNotifications(includeRead: boolean = false, limit: number = 20): Promise<NotificationsResponse> { async getNotifications(includeRead: boolean = false, limit: number = 20): Promise<NotificationsResponse> {
const response = await api.get('/api/admin/notifications', { const response = await api.get('/admin/notifications', {
params: { includeRead, limit } params: { includeRead, limit }
}); });
return response.data; return response.data;
@@ -30,17 +30,17 @@ export const notificationsService = {
// Mark single notification as read // Mark single notification as read
async markAsRead(notificationId: number): Promise<void> { async markAsRead(notificationId: number): Promise<void> {
await api.put(`/api/admin/notifications/${notificationId}/read`); await api.put(`/admin/notifications/${notificationId}/read`);
}, },
// Mark all notifications as read // Mark all notifications as read
async markAllAsRead(): Promise<void> { async markAllAsRead(): Promise<void> {
await api.put('/api/admin/notifications/read-all'); await api.put('/admin/notifications/read-all');
}, },
// Clear old notifications // Clear old notifications
async clearOldNotifications(): Promise<{ deletedCount: number }> { 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; return response.data;
}, },
+6 -6
View File
@@ -39,7 +39,7 @@ class PhotosService {
} }
const queryString = params.toString(); 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); const response = await api.get(url);
@@ -48,26 +48,26 @@ class PhotosService {
} }
async deletePhoto(eventId: number, photoId: number): Promise<void> { async deletePhoto(eventId: number, photoId: number): Promise<void> {
await api.delete(`/api/admin/events/${eventId}/photos/${photoId}`); await api.delete(`/admin/events/${eventId}/photos/${photoId}`);
} }
async deletePhotos(eventId: number, photoIds: number[]): Promise<void> { async deletePhotos(eventId: number, photoIds: number[]): Promise<void> {
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<void> { async updatePhotoCategory(eventId: number, photoId: number, categoryId: number | null): Promise<void> {
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<void> { async updatePhotosCategory(eventId: number, photoIds: number[], categoryId: number | null): Promise<void> {
await api.post(`/api/admin/events/${eventId}/photos/bulk-update`, { await api.post(`/admin/events/${eventId}/photos/bulk-update`, {
photoIds, photoIds,
updates: { category_id: categoryId } updates: { category_id: categoryId }
}); });
} }
async downloadPhoto(eventId: number, photoId: number, filename: string): Promise<void> { async downloadPhoto(eventId: number, photoId: number, filename: string): Promise<void> {
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' responseType: 'blob'
}); });
+7 -7
View File
@@ -79,19 +79,19 @@ export interface SystemStatus {
export const settingsService = { export const settingsService = {
// Get all settings // Get all settings
async getAllSettings(): Promise<Record<string, any>> { async getAllSettings(): Promise<Record<string, any>> {
const response = await api.get<Record<string, any>>('/api/admin/settings'); const response = await api.get<Record<string, any>>('/admin/settings');
return response.data; return response.data;
}, },
// Get settings by type // Get settings by type
async getSettingsByType(type: 'branding' | 'theme' | 'general'): Promise<Record<string, any>> { async getSettingsByType(type: 'branding' | 'theme' | 'general'): Promise<Record<string, any>> {
const response = await api.get<Record<string, any>>(`/api/admin/settings/${type}`); const response = await api.get<Record<string, any>>(`/admin/settings/${type}`);
return response.data; return response.data;
}, },
// Update branding settings // Update branding settings
async updateBranding(settings: BrandingSettings): Promise<void> { async updateBranding(settings: BrandingSettings): Promise<void> {
await api.put('/api/admin/settings/branding', settings); await api.put('/admin/settings/branding', settings);
}, },
// Upload logo // Upload logo
@@ -150,23 +150,23 @@ export const settingsService = {
// Update theme settings // Update theme settings
async updateTheme(settings: ThemeSettings): Promise<void> { async updateTheme(settings: ThemeSettings): Promise<void> {
await api.put('/api/admin/settings/theme', settings); await api.put('/admin/settings/theme', settings);
}, },
// Update multiple settings at once // Update multiple settings at once
async updateSettings(settings: Record<string, any>): Promise<void> { async updateSettings(settings: Record<string, any>): Promise<void> {
await api.put('/api/admin/settings/general', settings); await api.put('/admin/settings/general', settings);
}, },
// Get storage information // Get storage information
async getStorageInfo(): Promise<StorageInfo> { async getStorageInfo(): Promise<StorageInfo> {
const response = await api.get<StorageInfo>('/api/admin/settings/storage/info'); const response = await api.get<StorageInfo>('/admin/settings/storage/info');
return response.data; return response.data;
}, },
// Get system status // Get system status
async getSystemStatus(): Promise<SystemStatus> { async getSystemStatus(): Promise<SystemStatus> {
const response = await api.get<SystemStatus>('/api/admin/system/status'); const response = await api.get<SystemStatus>('/admin/system/status');
return response.data; return response.data;
}, },
+2 -2
View File
@@ -13,7 +13,7 @@ interface PhotoUrlOptions {
export function getPhotoUrl({ slug, photo, watermarkEnabled = false, token }: PhotoUrlOptions): string { export function getPhotoUrl({ slug, photo, watermarkEnabled = false, token }: PhotoUrlOptions): string {
if (watermarkEnabled && token) { if (watermarkEnabled && token) {
// Use the watermarked photo endpoint // Use the watermarked photo endpoint
return `/api/gallery/${slug}/photo/${photo.id}`; return `/gallery/${slug}/photo/${photo.id}`;
} }
// Use the static photo URL // 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 * Get the download URL for a photo
*/ */
export function getPhotoDownloadUrl(slug: string, photoId: number): string { export function getPhotoDownloadUrl(slug: string, photoId: number): string {
return `/api/gallery/${slug}/download/${photoId}`; return `/gallery/${slug}/download/${photoId}`;
} }