docs: fix deployment/admin routing and CORS guidance; add AGENTS.md; ignore AGENTS.md (refs #18)
@@ -72,3 +72,6 @@ logs/
|
||||
storage/
|
||||
data/
|
||||
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
|
||||
- `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;
|
||||
}
|
||||
}
|
||||
@@ -735,4 +746,4 @@ For issues and questions:
|
||||
- Error messages
|
||||
- Log output
|
||||
- Environment details (without secrets)
|
||||
- Steps to reproduce
|
||||
- Steps to reproduce
|
||||
|
||||
@@ -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
|
||||
@@ -516,4 +540,4 @@ sudo ./setup.sh --native \
|
||||
|
||||
---
|
||||
|
||||
**PicPeak Setup v1.0** | [Documentation](https://github.com/yourusername/wedding-photo-sharing) | [Support](https://github.com/yourusername/wedding-photo-sharing/issues)
|
||||
**PicPeak Setup v1.0** | [Documentation](https://github.com/yourusername/wedding-photo-sharing) | [Support](https://github.com/yourusername/wedding-photo-sharing/issues)
|
||||
|
||||
@@ -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">
|
||||
|
||||