docs: fix deployment/admin routing and CORS guidance; add AGENTS.md; ignore AGENTS.md (refs #18)
@@ -72,3 +72,6 @@ logs/
|
|||||||
storage/
|
storage/
|
||||||
data/
|
data/
|
||||||
certbot/
|
certbot/
|
||||||
|
|
||||||
|
# Ignore local contributor guide copy
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -170,8 +170,15 @@ Update `.env` with:
|
|||||||
- `REDIS_PASSWORD` - Redis password
|
- `REDIS_PASSWORD` - Redis password
|
||||||
- `SMTP_*` - Email configuration
|
- `SMTP_*` - Email configuration
|
||||||
- **URL Configuration** (for backend CORS):
|
- **URL Configuration** (for backend CORS):
|
||||||
- `FRONTEND_URL` - Frontend URL (e.g., `http://localhost:3000` for Docker)
|
- `FRONTEND_URL` - Frontend origin (use full URL with scheme, no trailing slash)
|
||||||
- `ADMIN_URL` - Admin URL (e.g., `http://localhost:3000` for Docker)
|
- 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)
|
### Frontend Configuration (frontend/.env)
|
||||||
Create `frontend/.env` from `frontend/.env.example`:
|
Create `frontend/.env` from `frontend/.env.example`:
|
||||||
@@ -181,9 +188,10 @@ cp frontend/.env.example frontend/.env
|
|||||||
|
|
||||||
Update `frontend/.env` with:
|
Update `frontend/.env` with:
|
||||||
- `VITE_API_URL` - Backend API URL
|
- `VITE_API_URL` - Backend API URL
|
||||||
- For Docker deployment: `http://localhost:3001/api`
|
- Docker (pre-built images) and production behind reverse proxy: `/api` (recommended; avoids CORS and matches the frontend Nginx proxy in the image)
|
||||||
- For non-Docker local dev: `http://localhost:3001`
|
- Local dev (Vite): `http://localhost:3001` or `/api` if proxying through a dev proxy
|
||||||
- For production with reverse proxy: `/api`
|
|
||||||
|
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**:
|
⚠️ **IMPORTANT PORT CONFIGURATION**:
|
||||||
- The frontend runs on port **3000** in Docker (exposed via nginx)
|
- The frontend runs on port **3000** in Docker (exposed via nginx)
|
||||||
@@ -246,8 +254,8 @@ docker compose ps
|
|||||||
### Access Points
|
### Access Points
|
||||||
|
|
||||||
By default, services are exposed on:
|
By default, services are exposed on:
|
||||||
- Frontend: http://localhost:3000
|
- Frontend (UI + Admin): http://localhost:3000 (admin at `/admin`)
|
||||||
- Backend/API: http://localhost:3001
|
- Backend/API: http://localhost:3001 (API only; no UI routes)
|
||||||
- PostgreSQL: localhost:5432 (if needed)
|
- PostgreSQL: localhost:5432 (if needed)
|
||||||
- Redis: localhost:6379 (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
|
### 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:
|
2. Login using:
|
||||||
- **Email**: `admin@example.com` (or your custom admin email)
|
- **Email**: `admin@example.com` (or your custom admin email)
|
||||||
- **Password**: The auto-generated password from the logs
|
- **Password**: The auto-generated password from the logs
|
||||||
@@ -392,7 +404,16 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
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 {
|
location /api {
|
||||||
proxy_pass http://localhost:3001;
|
proxy_pass http://localhost:3001;
|
||||||
proxy_set_header Host $host;
|
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-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Protected photos and uploads
|
|
||||||
location ~ ^/(photos|thumbnails|uploads) {
|
location ~ ^/(photos|thumbnails|uploads) {
|
||||||
proxy_pass http://localhost:3001;
|
proxy_pass http://localhost:3001;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
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;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,8 +166,10 @@ sudo ./setup.sh --native --unattended \
|
|||||||
## 🌐 Access Methods
|
## 🌐 Access Methods
|
||||||
|
|
||||||
### Direct Access (Simplest)
|
### Direct Access (Simplest)
|
||||||
- **Docker**: `http://your-server:3000` (frontend), `http://your-server:3001/admin` (admin)
|
- Docker: `http://your-server:3000` (frontend and admin at `/admin`)
|
||||||
- **Native**: `http://your-server:3001/admin` (admin panel)
|
- 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
|
### With Domain & HTTPS
|
||||||
If configured during setup:
|
If configured during setup:
|
||||||
@@ -175,9 +177,23 @@ If configured during setup:
|
|||||||
- `https://your-domain.com/admin` - Admin panel
|
- `https://your-domain.com/admin` - Admin panel
|
||||||
|
|
||||||
### Behind Existing Proxy
|
### Behind Existing Proxy
|
||||||
Add to your Nginx/Apache configuration:
|
Add to your Nginx/Apache configuration (split frontend vs backend):
|
||||||
```nginx
|
```nginx
|
||||||
|
# Frontend (UI + /admin/*)
|
||||||
location / {
|
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_pass http://localhost:3001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
@@ -189,6 +205,14 @@ location / {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
client_max_body_size 100M;
|
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
|
## 📁 Managing Galleries
|
||||||
|
|||||||
@@ -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 { Button } from '../common';
|
||||||
import { PhotoCategory } from '../../types';
|
import { PhotoCategory } from '../../types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { GalleryFilter, type FilterType } from './GalleryFilter';
|
||||||
|
|
||||||
interface GallerySidebarProps {
|
interface GallerySidebarProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -28,6 +29,11 @@ interface GallerySidebarProps {
|
|||||||
galleryLayout?: string;
|
galleryLayout?: string;
|
||||||
allowUploads?: boolean;
|
allowUploads?: boolean;
|
||||||
onUploadClick?: () => void;
|
onUploadClick?: () => void;
|
||||||
|
feedbackEnabled?: boolean;
|
||||||
|
filterType?: FilterType;
|
||||||
|
onFilterChange?: (filter: FilterType) => void;
|
||||||
|
likeCount?: number;
|
||||||
|
favoriteCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GallerySidebar: React.FC<GallerySidebarProps> = ({
|
export const GallerySidebar: React.FC<GallerySidebarProps> = ({
|
||||||
@@ -53,7 +59,12 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
|
|||||||
isMobile,
|
isMobile,
|
||||||
galleryLayout,
|
galleryLayout,
|
||||||
allowUploads,
|
allowUploads,
|
||||||
onUploadClick
|
onUploadClick,
|
||||||
|
feedbackEnabled = false,
|
||||||
|
filterType = 'all',
|
||||||
|
onFilterChange,
|
||||||
|
likeCount = 0,
|
||||||
|
favoriteCount = 0
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -201,6 +212,23 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
|
|||||||
</div>
|
</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 */}
|
{/* Categories Section - Hidden for carousel layout */}
|
||||||
{galleryLayout !== 'carousel' && categories.length > 0 && (
|
{galleryLayout !== 'carousel' && categories.length > 0 && (
|
||||||
<div className="p-4 border-b border-neutral-200">
|
<div className="p-4 border-b border-neutral-200">
|
||||||
|
|||||||