Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97bbb3c8e1 | |||
| ac1cd96ecd | |||
| 689861f671 | |||
| 41fb575e80 |
@@ -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
|
||||||
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;"]
|
||||||
Generated
+2
-2
@@ -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,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
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user