docs: fix deployment/admin routing and CORS guidance; add AGENTS.md; ignore AGENTS.md (refs #18)

This commit is contained in:
2025-09-05 14:28:03 +02:00
parent 909e760447
commit dad1787aad
22 changed files with 1112 additions and 515 deletions
File diff suppressed because it is too large Load Diff
+3
View File
@@ -72,3 +72,6 @@ logs/
storage/
data/
certbot/
# Ignore local contributor guide copy
AGENTS.md
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+30 -19
View File
@@ -170,8 +170,15 @@ Update `.env` with:
- `REDIS_PASSWORD` - Redis password
- `SMTP_*` - Email configuration
- **URL Configuration** (for backend CORS):
- `FRONTEND_URL` - Frontend URL (e.g., `http://localhost:3000` for Docker)
- `ADMIN_URL` - Admin URL (e.g., `http://localhost:3000` for Docker)
- `FRONTEND_URL` - Frontend origin (use full URL with scheme, no trailing slash)
- Example (Docker): `http://localhost:3000`
- `ADMIN_URL` - Admin origin (same as `FRONTEND_URL` for Docker; full URL, no trailing slash)
- Example (Docker): `http://localhost:3000`
Notes:
- Do not include trailing `/` (e.g., use `http://host:3000`, not `http://host:3000/`).
- Always include the scheme (`http://` or `https://`).
- The backend compares origins strictly for CORS; malformed values will cause login requests to fail with 500.
### Frontend Configuration (frontend/.env)
Create `frontend/.env` from `frontend/.env.example`:
@@ -181,9 +188,10 @@ cp frontend/.env.example frontend/.env
Update `frontend/.env` with:
- `VITE_API_URL` - Backend API URL
- For Docker deployment: `http://localhost:3001/api`
- For non-Docker local dev: `http://localhost:3001`
- For production with reverse proxy: `/api`
- Docker (pre-built images) and production behind reverse proxy: `/api` (recommended; avoids CORS and matches the frontend Nginx proxy in the image)
- Local dev (Vite): `http://localhost:3001` or `/api` if proxying through a dev proxy
Note: When using pre-built frontend images, runtime container env does not change the already-built JS. Prefer the default `/api` and let the frontend Nginx proxy forward to the backend.
⚠️ **IMPORTANT PORT CONFIGURATION**:
- The frontend runs on port **3000** in Docker (exposed via nginx)
@@ -246,8 +254,8 @@ docker compose ps
### Access Points
By default, services are exposed on:
- Frontend: http://localhost:3000
- Backend/API: http://localhost:3001
- Frontend (UI + Admin): http://localhost:3000 (admin at `/admin`)
- Backend/API: http://localhost:3001 (API only; no UI routes)
- PostgreSQL: localhost:5432 (if needed)
- Redis: localhost:6379 (if needed)
@@ -318,7 +326,11 @@ After deployment, you must complete the first login process which includes manda
### Step 2: Access Admin Panel
1. Navigate to your admin panel URL (e.g., `http://your-domain.com:3001/admin` or `https://your-domain.com/admin`)
1. Navigate to your frontend domain and open the admin section:
- `http://your-domain.com/admin` (behind reverse proxy)
- `http://localhost:3000/admin` (Docker local)
The backend at `:3001` serves API only and does not serve the admin UI.
2. Login using:
- **Email**: `admin@example.com` (or your custom admin email)
- **Password**: The auto-generated password from the logs
@@ -392,7 +404,16 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API
# Frontend (serves UI and /admin/*)
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API and protected resources
location /api {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
@@ -400,21 +421,11 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Protected photos and uploads
location ~ ^/(photos|thumbnails|uploads) {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Admin routes
location /admin {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+27 -3
View File
@@ -166,8 +166,10 @@ sudo ./setup.sh --native --unattended \
## 🌐 Access Methods
### Direct Access (Simplest)
- **Docker**: `http://your-server:3000` (frontend), `http://your-server:3001/admin` (admin)
- **Native**: `http://your-server:3001/admin` (admin panel)
- Docker: `http://your-server:3000` (frontend and admin at `/admin`)
- Backend/API: `http://your-server:3001` (API only; no UI routes)
For native installs, serve the built frontend (e.g., with nginx or Caddy) and access the admin at `/admin` on the frontend domain.
### With Domain & HTTPS
If configured during setup:
@@ -175,9 +177,23 @@ If configured during setup:
- `https://your-domain.com/admin` - Admin panel
### Behind Existing Proxy
Add to your Nginx/Apache configuration:
Add to your Nginx/Apache configuration (split frontend vs backend):
```nginx
# Frontend (UI + /admin/*)
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API and protected resources
location /api {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -189,6 +205,14 @@ location / {
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100M;
}
location ~ ^/(photos|thumbnails|uploads) {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
## 📁 Managing Galleries
Binary file not shown.
View File
+63
View File
@@ -0,0 +1,63 @@
// Test script for filter functionality
const axios = require('axios');
const API_URL = 'http://localhost:3001/api';
const TEST_SLUG = 'wedding-test-feedback-event-2025-09-02';
const TEST_PASSWORD = 'StrongTiger3610%';
async function testFilterFunctionality() {
try {
console.log('Testing filter functionality...\n');
// 1. Authenticate to get JWT token
console.log('1. Authenticating with gallery...');
const authResponse = await axios.post(`${API_URL}/auth/gallery-login`, {
slug: TEST_SLUG,
password: TEST_PASSWORD
});
const token = authResponse.data.token;
console.log('✅ Authentication successful\n');
// 2. Test fetching all photos (no filter)
console.log('2. Fetching all photos (no filter)...');
const allPhotosResponse = await axios.get(`${API_URL}/gallery/${TEST_SLUG}/photos`, {
headers: { Authorization: `Bearer ${token}` }
});
console.log(`✅ Found ${allPhotosResponse.data.photos.length} total photos\n`);
// 3. Test fetching with liked filter
console.log('3. Testing filter for liked photos...');
const guestId = 'test_guest_123';
const likedPhotosResponse = await axios.get(`${API_URL}/gallery/${TEST_SLUG}/photos`, {
params: { filter: 'liked', guest_id: guestId },
headers: { Authorization: `Bearer ${token}` }
});
console.log(`✅ Found ${likedPhotosResponse.data.photos.length} liked photos for guest ${guestId}\n`);
// 4. Test fetching with favorited filter
console.log('4. Testing filter for favorited photos...');
const favoritedPhotosResponse = await axios.get(`${API_URL}/gallery/${TEST_SLUG}/photos`, {
params: { filter: 'favorited', guest_id: guestId },
headers: { Authorization: `Bearer ${token}` }
});
console.log(`✅ Found ${favoritedPhotosResponse.data.photos.length} favorited photos for guest ${guestId}\n`);
// 5. Test combined filter
console.log('5. Testing combined filter (liked OR favorited)...');
const combinedPhotosResponse = await axios.get(`${API_URL}/gallery/${TEST_SLUG}/photos`, {
params: { filter: 'liked,favorited', guest_id: guestId },
headers: { Authorization: `Bearer ${token}` }
});
console.log(`✅ Found ${combinedPhotosResponse.data.photos.length} photos that are liked OR favorited\n`);
console.log('🎉 All filter tests passed successfully!');
} catch (error) {
console.error('❌ Test failed:', error.response?.data || error.message);
process.exit(1);
}
}
// Run the test
testFilterFunctionality();
@@ -3,6 +3,7 @@ import { X, Download, Filter, SortAsc, Search, Calendar, Type, HardDrive, Check,
import { Button } from '../common';
import { PhotoCategory } from '../../types';
import { useTranslation } from 'react-i18next';
import { GalleryFilter, type FilterType } from './GalleryFilter';
interface GallerySidebarProps {
isOpen: boolean;
@@ -28,6 +29,11 @@ interface GallerySidebarProps {
galleryLayout?: string;
allowUploads?: boolean;
onUploadClick?: () => void;
feedbackEnabled?: boolean;
filterType?: FilterType;
onFilterChange?: (filter: FilterType) => void;
likeCount?: number;
favoriteCount?: number;
}
export const GallerySidebar: React.FC<GallerySidebarProps> = ({
@@ -53,7 +59,12 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
isMobile,
galleryLayout,
allowUploads,
onUploadClick
onUploadClick,
feedbackEnabled = false,
filterType = 'all',
onFilterChange,
likeCount = 0,
favoriteCount = 0
}) => {
const { t } = useTranslation();
const sidebarRef = useRef<HTMLDivElement>(null);
@@ -201,6 +212,23 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
</div>
)}
{/* Feedback Filter Section */}
{feedbackEnabled && onFilterChange && (
<div className="p-4 border-b border-neutral-200">
<GalleryFilter
currentFilter={filterType}
onFilterChange={(filter) => {
onFilterChange(filter);
if (isMobile) onClose();
}}
feedbackEnabled={feedbackEnabled}
likeCount={likeCount}
favoriteCount={favoriteCount}
className="w-full"
/>
</div>
)}
{/* Categories Section - Hidden for carousel layout */}
{galleryLayout !== 'carousel' && categories.length > 0 && (
<div className="p-4 border-b border-neutral-200">