Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9c28e52cd | |||
| c94b6268cf | |||
| 439c743fd1 | |||
| 74144f1fc6 | |||
| 99a0376657 | |||
| 21b1e79672 |
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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,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' });
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -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,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
@@ -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 = '/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user