Compare commits

...

6 Commits

Author SHA1 Message Date
Gitea Actions Bot b9c28e52cd chore: bump version to 1.0.35
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-15 07:15:42 +00:00
paul c94b6268cf fix: resolve gallery authentication and redirect issues
Test and Lint / backend-test (push) Failing after 32s
Test and Lint / frontend-test (push) Successful in 2m15s
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 4s
- Fix useWatermarkSettings hook to use public API endpoint instead of admin endpoint
- Add hero_photo_id to gallery authentication response
- Prevent 401 errors on gallery pages from redirecting to admin login
- Gallery pages now correctly fetch settings without requiring admin auth

The main issue was that gallery pages were calling admin-only endpoints
which triggered 401 errors and caused redirects to the admin login page.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 09:11:50 +02:00
Gitea Actions Bot 439c743fd1 chore: bump version to 1.0.34
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 06:53:59 +00:00
paul 74144f1fc6 fix: handle both share_link formats in gallery token verification
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Support both full URL and token-only formats in share_link column
- Fix gallery info endpoint to correctly validate share tokens
- Prevents "gallery not found" errors for valid share links

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:49:45 +02:00
Gitea Actions Bot 99a0376657 chore: bump version to 1.0.33
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 06:39:40 +00:00
paul 21b1e79672 fix: resolve gallery login redirect issue
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Updated API interceptor to better handle gallery authentication
- Fixed 401 error handling to prevent redirect loops on gallery pages
- Improved token extraction logic for gallery API requests
- Consolidated duplicate verifyGalleryAccess middleware
- Added proper error handling in GalleryView component
- Gallery authentication now properly distinguishes from admin routes

The issue was caused by the API interceptor redirecting to admin login
when gallery API calls failed with 401, even when users were already
on gallery pages attempting to authenticate.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:35:33 +02:00
11 changed files with 77 additions and 54 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.32", "version": "1.0.35",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.32", "version": "1.0.35",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"archiver": "^5.3.1", "archiver": "^5.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.32", "version": "1.0.35",
"description": "Backend for PicPeak event photo sharing platform", "description": "Backend for PicPeak event photo sharing platform",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+7 -1
View File
@@ -11,7 +11,13 @@ async function verifyGalleryAccess(req, res, next) {
} }
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_SECRET);
const event = await db('events').where({ id: decoded.eventId, is_active: formatBoolean(true) }).first(); const event = await db('events')
.where({
id: decoded.eventId,
is_active: formatBoolean(true),
is_archived: formatBoolean(false)
})
.first();
if (!event) { if (!event) {
return res.status(404).json({ error: 'Gallery not found or expired' }); return res.status(404).json({ error: 'Gallery not found or expired' });
+2 -1
View File
@@ -119,7 +119,8 @@ router.post('/gallery/verify', [
color_theme: event.color_theme, color_theme: event.color_theme,
expires_at: event.expires_at, expires_at: event.expires_at,
allow_user_uploads: event.allow_user_uploads, allow_user_uploads: event.allow_user_uploads,
upload_category_id: event.upload_category_id upload_category_id: event.upload_category_id,
hero_photo_id: event.hero_photo_id
} }
}); });
} catch (error) { } catch (error) {
+6 -26
View File
@@ -6,35 +6,11 @@ const archiver = require('archiver');
const path = require('path'); const path = require('path');
const router = express.Router(); const router = express.Router();
const watermarkService = require('../services/watermarkService'); const watermarkService = require('../services/watermarkService');
const { verifyGalleryAccess } = require('../middleware/gallery');
// Get storage path from environment or default // Get storage path from environment or default
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
// Middleware to verify gallery access
async function verifyGalleryAccess(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const event = await db('events')
.where({ id: decoded.eventId, is_active: formatBoolean(true), is_archived: formatBoolean(false) })
.first();
if (!event) {
return res.status(404).json({ error: 'Gallery not found or expired' });
}
req.event = event;
next();
} catch (error) {
console.error('Error verifying gallery access:', error);
res.status(401).json({ error: 'Invalid token', details: error.message });
}
}
// Verify share token // Verify share token
router.get('/:slug/verify-token/:token', async (req, res) => { router.get('/:slug/verify-token/:token', async (req, res) => {
try { try {
@@ -84,7 +60,11 @@ router.get('/:slug/info', async (req, res) => {
// If token provided, verify it matches the share link // If token provided, verify it matches the share link
if (token) { if (token) {
const expectedToken = event.share_link.split('/').pop(); let expectedToken = event.share_link;
// Handle both formats: full URL or just token
if (event.share_link && event.share_link.includes('/')) {
expectedToken = event.share_link.split('/').pop();
}
if (token !== expectedToken) { if (token !== expectedToken) {
return res.status(404).json({ error: 'Invalid gallery link' }); return res.status(404).json({ error: 'Invalid gallery link' });
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"version": "1.0.32", "version": "1.0.35",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"version": "1.0.32", "version": "1.0.35",
"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 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "picpeak-frontend", "name": "picpeak-frontend",
"private": true, "private": true,
"version": "1.0.32", "version": "1.0.35",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -52,7 +52,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const { watermarkEnabled } = useWatermarkSettings(); const { watermarkEnabled } = useWatermarkSettings();
// Fetch photos // Fetch photos
const { data, isLoading, error } = useGalleryPhotos(slug); const { data, isLoading, error, refetch } = useGalleryPhotos(slug);
// Debug logging // Debug logging
useEffect(() => { useEffect(() => {
@@ -294,11 +294,20 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
} }
if (error || !data) { if (error || !data) {
// Check if it's an authentication error (401)
const is401Error = (error as any)?.response?.status === 401;
if (is401Error) {
// Authentication failed - logout and let the parent component handle re-authentication
logout();
return null;
}
return ( return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center"> <div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<p className="text-lg text-neutral-600">{t('gallery.failedToLoad')}</p> <p className="text-lg text-neutral-600">{t('gallery.failedToLoad')}</p>
<Button onClick={() => window.location.reload()} className="mt-4"> <Button onClick={() => refetch()} className="mt-4">
{t('gallery.tryAgain')} {t('gallery.tryAgain')}
</Button> </Button>
</div> </div>
+37 -14
View File
@@ -32,14 +32,24 @@ api.interceptors.request.use(
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
} else { } else {
// For gallery routes, get the slug from the URL path // For gallery routes, try to extract slug from the request URL first
const pathParts = window.location.pathname.split('/'); const galleryMatch = config.url?.match(/\/gallery\/([^\/]+)/);
if (pathParts[1] === 'gallery' && pathParts[2]) { if (galleryMatch && galleryMatch[1]) {
const gallerySlug = pathParts[2]; const gallerySlug = galleryMatch[1];
const token = localStorage.getItem(`gallery_token_${gallerySlug}`); const token = localStorage.getItem(`gallery_token_${gallerySlug}`);
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
} else {
// Fallback to getting slug from the current page URL
const pathParts = window.location.pathname.split('/');
if (pathParts[1] === 'gallery' && pathParts[2]) {
const gallerySlug = pathParts[2];
const token = localStorage.getItem(`gallery_token_${gallerySlug}`);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
} }
} }
@@ -73,21 +83,34 @@ api.interceptors.response.use(
} }
if (error.response?.status === 401) { if (error.response?.status === 401) {
// Redirect to appropriate login // Check if it's an admin route
const isAdminRoute = error.config?.url?.includes('/admin'); const isAdminRoute = error.config?.url?.includes('/admin');
const currentPath = window.location.pathname;
if (isAdminRoute) { if (isAdminRoute) {
// Clear admin token on unauthorized // Clear admin token on unauthorized
Cookies.remove(ADMIN_TOKEN_KEY); Cookies.remove(ADMIN_TOKEN_KEY);
window.location.href = '/admin/login'; // Only redirect if we're not already on the admin login page
if (!currentPath.includes('/admin/login')) {
window.location.href = '/admin/login';
}
} else { } else {
// For gallery routes, clear gallery-specific token and redirect // For gallery routes, check if the error is from a gallery API call
const currentPath = window.location.pathname; const galleryMatch = error.config?.url?.match(/\/gallery\/([^\/]+)/);
const pathParts = currentPath.split('/');
if (pathParts[1] === 'gallery' && pathParts[2]) { // Don't redirect if we're on any gallery page (to avoid redirect loops during login)
const gallerySlug = pathParts[2]; if (currentPath.startsWith('/gallery/')) {
localStorage.removeItem(`gallery_token_${gallerySlug}`); // If we have a gallery match from the API URL, clear that specific gallery's token
localStorage.removeItem(`gallery_event_${gallerySlug}`); if (galleryMatch && galleryMatch[1]) {
window.location.href = `/gallery/${gallerySlug}`; const gallerySlug = galleryMatch[1];
localStorage.removeItem(`gallery_token_${gallerySlug}`);
localStorage.removeItem(`gallery_event_${gallerySlug}`);
}
// Don't redirect - let the component handle the auth state
} else {
// We're not on a gallery page but got a 401 from a gallery API
// This shouldn't happen in normal flow, but if it does, redirect to homepage
window.location.href = '/';
} }
} }
} }
+2
View File
@@ -18,6 +18,8 @@ export const useGalleryPhotos = (slug: string, enabled: boolean = true) => {
enabled, enabled,
retry: 1, retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5 minutes
// Add a small delay to ensure auth token is properly set
retryDelay: 100,
}); });
}; };
+6 -4
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { settingsService } from '../services/settings.service'; import { api } from '../config/api';
export function useWatermarkSettings() { export function useWatermarkSettings() {
const [watermarkEnabled, setWatermarkEnabled] = useState(false); const [watermarkEnabled, setWatermarkEnabled] = useState(false);
@@ -8,11 +8,13 @@ export function useWatermarkSettings() {
useEffect(() => { useEffect(() => {
const fetchSettings = async () => { const fetchSettings = async () => {
try { try {
const settings = await settingsService.getSettingsByType('branding'); // Use public settings endpoint that doesn't require authentication
const brandingSettings = settingsService.formatBrandingSettings(settings); const response = await api.get('/public/settings');
setWatermarkEnabled(brandingSettings.watermark_enabled); setWatermarkEnabled(response.data.branding_watermark_enabled || false);
} catch (error) { } catch (error) {
console.error('Failed to fetch watermark settings:', error); console.error('Failed to fetch watermark settings:', error);
// Default to false if we can't fetch settings
setWatermarkEnabled(false);
} finally { } finally {
setLoading(false); setLoading(false);
} }