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",
"version": "1.0.10",
"version": "1.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.10",
"version": "1.0.12",
"dependencies": {
"adm-zip": "^0.5.16",
"archiver": "^5.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.10",
"version": "1.0.12",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
+1 -1
View File
@@ -8,7 +8,7 @@
*/
require('dotenv').config();
const bcrypt = require('bcryptjs');
const bcrypt = require('bcrypt');
const { db } = require('../src/database/db');
const crypto = require('crypto');
+2
View File
@@ -49,6 +49,8 @@ services:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- VITE_API_URL=/api
restart: unless-stopped
depends_on:
- 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",
"version": "1.0.10",
"version": "1.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.10",
"version": "1.0.12",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-link": "^2.25.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.10",
"version": "1.0.12",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,7 +15,7 @@ export const GlobalThemeProvider: React.FC<GlobalThemeProviderProps> = ({ 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
+1 -1
View File
@@ -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
@@ -30,7 +30,7 @@ export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ 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 };
@@ -62,7 +62,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ 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) {
@@ -15,7 +15,7 @@ interface 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;
}
@@ -79,7 +79,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ 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<GalleryViewProps> = ({ 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
}));
}
@@ -63,7 +63,7 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
}
try {
await api.post(`/api/gallery/${eventId}/upload`, formData, {
await api.post(`/gallery/${eventId}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+5 -5
View File
@@ -66,13 +66,13 @@ export interface AnalyticsData {
export const adminService = {
// Dashboard statistics
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;
},
// Recent 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 }
});
return response.data;
@@ -80,7 +80,7 @@ export const adminService = {
// Analytics data
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 }
});
return response.data;
@@ -88,7 +88,7 @@ export const adminService = {
// System health check
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;
},
@@ -124,6 +124,6 @@ export const adminService = {
// Change password
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 = {
// Get all archives with pagination
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 }
});
return response.data;
@@ -54,18 +54,18 @@ export const archiveService = {
// Get single archive details
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;
},
// Restore archive
async restoreArchive(id: number): Promise<void> {
await api.post(`/api/admin/archives/${id}/restore`);
await api.post(`/admin/archives/${id}/restore`);
},
// Download archive
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'
});
@@ -82,7 +82,7 @@ export const archiveService = {
// Delete archive permanently
async deleteArchive(id: number): Promise<void> {
await api.delete(`/api/admin/archives/${id}`);
await api.delete(`/admin/archives/${id}`);
},
// Format bytes to human readable
+2 -2
View File
@@ -5,7 +5,7 @@ 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>('/api/auth/admin/login', {
const response = await api.post<LoginResponse>('/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<GalleryAuthResponse> {
const response = await api.post<GalleryAuthResponse>('/api/auth/gallery/verify', {
const response = await api.post<GalleryAuthResponse>('/auth/gallery/verify', {
slug,
password,
recaptchaToken
+5 -5
View File
@@ -19,30 +19,30 @@ export interface CreateCategoryData {
export const categoriesService = {
// Get all global categories
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;
},
// Get categories for a specific event (global + event-specific)
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;
},
// Create a new category
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;
},
// Update a category
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;
},
// Delete a category
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 = {
// Get all CMS pages
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;
},
// Get a single CMS page
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;
},
// Update a CMS page
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;
},
// 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;
+7 -7
View File
@@ -35,41 +35,41 @@ export interface EmailPreview {
export const emailService = {
// Get email configuration
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;
},
// Update email configuration
async updateConfig(config: EmailConfig): Promise<void> {
await api.post('/api/admin/email/config', config);
await api.post('/admin/email/config', config);
},
// Test email configuration
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
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;
},
// Get single template
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;
},
// Update email template
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
async previewTemplate(key: string, previewData: Record<string, string>, language: 'en' | 'de' = 'en'): Promise<EmailPreview> {
const response = await api.post<EmailPreview>(
`/api/admin/email/templates/${key}/preview`,
`/admin/email/templates/${key}/preview`,
{ preview_data: previewData, language }
);
return response.data;
+10 -10
View File
@@ -53,36 +53,36 @@ export const eventsService = {
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;
},
// Get single event details (admin)
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;
},
// Create new event (admin)
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;
},
// Update event (admin)
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;
},
// Delete/deactivate event (admin)
async deleteEvent(id: number): Promise<void> {
await api.delete(`/api/admin/events/${id}`);
await api.delete(`/admin/events/${id}`);
},
// Force archive event (admin)
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)
@@ -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<Event> {
const response = await api.post<Event>(`/api/events/${id}/extend`, {
const response = await api.post<Event>(`/events/${id}/extend`, {
days,
});
return response.data;
@@ -109,13 +109,13 @@ export const eventsService = {
// Get event categories
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 || [];
},
// 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;
},
};
+6 -6
View File
@@ -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<GalleryInfo> {
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;
},
// Get gallery photos (requires auth)
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;
},
// Download single photo
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',
});
@@ -40,7 +40,7 @@ export const galleryService = {
// Download all photos as ZIP
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',
});
@@ -57,7 +57,7 @@ export const galleryService = {
// Get gallery statistics
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;
},
};
@@ -22,7 +22,7 @@ export interface NotificationsResponse {
export const notificationsService = {
// Get notifications
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 }
});
return response.data;
@@ -30,17 +30,17 @@ export const notificationsService = {
// Mark single notification as read
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
async markAllAsRead(): Promise<void> {
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;
},
+6 -6
View File
@@ -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<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> {
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> {
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> {
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<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'
});
+7 -7
View File
@@ -79,19 +79,19 @@ export interface SystemStatus {
export const settingsService = {
// Get all settings
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;
},
// Get settings by type
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;
},
// Update branding settings
async updateBranding(settings: BrandingSettings): Promise<void> {
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<void> {
await api.put('/api/admin/settings/theme', settings);
await api.put('/admin/settings/theme', settings);
},
// Update multiple settings at once
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
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;
},
// Get system status
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;
},
+2 -2
View File
@@ -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}`;
}