Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cab5b0d795 | |||
| ba95aad3c6 | |||
| c1be7d6785 | |||
| 0024686dc2 | |||
| 96b8b77792 | |||
| 9d2726b3d3 | |||
| 8d6ddd257d |
@@ -128,10 +128,17 @@ jobs:
|
|||||||
MINOR="${version_parts[1]}"
|
MINOR="${version_parts[1]}"
|
||||||
PATCH="${version_parts[2]}"
|
PATCH="${version_parts[2]}"
|
||||||
|
|
||||||
# Increment patch version
|
# Increment patch version and ensure tag uniqueness
|
||||||
|
git fetch --tags --quiet || true
|
||||||
NEW_PATCH=$((PATCH + 1))
|
NEW_PATCH=$((PATCH + 1))
|
||||||
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
|
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
|
||||||
|
|
||||||
|
while git rev-parse "v${NEW_VERSION}" >/dev/null 2>&1; do
|
||||||
|
echo "Tag v${NEW_VERSION} already exists, bumping patch version again"
|
||||||
|
NEW_PATCH=$((NEW_PATCH + 1))
|
||||||
|
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
|
||||||
|
done
|
||||||
|
|
||||||
echo "New version: $NEW_VERSION"
|
echo "New version: $NEW_VERSION"
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "component_changed=$COMPONENT_CHANGED" >> $GITHUB_OUTPUT
|
echo "component_changed=$COMPONENT_CHANGED" >> $GITHUB_OUTPUT
|
||||||
@@ -260,4 +267,3 @@ jobs:
|
|||||||
echo "Component(s) changed: ${{ needs.version-bump.outputs.component_changed }}"
|
echo "Component(s) changed: ${{ needs.version-bump.outputs.component_changed }}"
|
||||||
echo "Drone will automatically trigger on the new tag"
|
echo "Drone will automatically trigger on the new tag"
|
||||||
# Drone CI will automatically trigger on the tag push event
|
# Drone CI will automatically trigger on the tag push event
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
# Add build arguments
|
# Add build arguments
|
||||||
ARG CACHEBUST=1
|
ARG CACHEBUST=1
|
||||||
@@ -23,7 +23,7 @@ RUN npm ci --only=production
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:18-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.850.0",
|
"@aws-sdk/client-s3": "^3.850.0",
|
||||||
"@aws-sdk/lib-storage": "^3.850.0",
|
"@aws-sdk/lib-storage": "^3.850.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { resolveExternalPath } = require('./externalMediaService');
|
const { resolveExternalPath } = require('./externalMediaService');
|
||||||
|
const { safePathJoin } = require('../utils/fileSecurityUtils');
|
||||||
|
|
||||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||||
|
|
||||||
@@ -33,10 +34,15 @@ function resolvePhotoFilePath(event, photo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const storagePath = getStoragePath();
|
const storagePath = getStoragePath();
|
||||||
|
const eventsRoot = path.join(storagePath, 'events/active');
|
||||||
|
|
||||||
if (photo.path && photo.path.startsWith('events/active/')) {
|
if (photo.path && photo.path.startsWith('events/active/')) {
|
||||||
return path.join(storagePath, photo.path);
|
// Legacy paths already include prefix; normalize via safe join
|
||||||
|
return safePathJoin(storagePath, photo.path.replace(/^events\/active\/?/, 'events/active/'));
|
||||||
}
|
}
|
||||||
return path.join(storagePath, 'events/active', photo.path || '');
|
|
||||||
|
const relativeSegment = photo.path ? photo.path.replace(/^\/+/, '') : '';
|
||||||
|
return safePathJoin(eventsRoot, relativeSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-character-count": "^2.26.1",
|
"@tiptap/extension-character-count": "^2.26.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { feedbackService } from '../../services/feedback.service';
|
import { feedbackService } from '../../services/feedback.service';
|
||||||
import { PhotoRating } from './PhotoRating';
|
import { PhotoRating } from './PhotoRating';
|
||||||
import { PhotoLikes } from './PhotoLikes';
|
import { PhotoLikes } from './PhotoLikes';
|
||||||
|
import { PhotoFavorites } from './PhotoFavorites';
|
||||||
import { PhotoComments } from './PhotoComments';
|
import { PhotoComments } from './PhotoComments';
|
||||||
import { Skeleton } from '../common';
|
import { Skeleton } from '../common';
|
||||||
|
|
||||||
@@ -42,13 +43,17 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
const [currentRating, setCurrentRating] = useState(0);
|
const [currentRating, setCurrentRating] = useState(0);
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
const [likeCount, setLikeCount] = useState(0);
|
const [likeCount, setLikeCount] = useState(0);
|
||||||
|
const [isFavorited, setIsFavorited] = useState(false);
|
||||||
|
const [favoriteCount, setFavoriteCount] = useState(0);
|
||||||
|
|
||||||
// Update local state when data loads
|
// Update local state when data loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (feedbackData) {
|
if (feedbackData) {
|
||||||
setCurrentRating(feedbackData.my_feedback.rating || 0);
|
setCurrentRating(feedbackData.my_feedback.rating || 0);
|
||||||
setIsLiked(feedbackData.my_feedback.liked);
|
setIsLiked(Boolean(feedbackData.my_feedback.liked));
|
||||||
setLikeCount(feedbackData.summary.like_count);
|
setLikeCount(Number(feedbackData.summary.like_count) || 0);
|
||||||
|
setIsFavorited(Boolean(feedbackData.my_feedback.favorited));
|
||||||
|
setFavoriteCount(Number(feedbackData.summary.favorite_count) || 0);
|
||||||
}
|
}
|
||||||
}, [feedbackData]);
|
}, [feedbackData]);
|
||||||
|
|
||||||
@@ -64,6 +69,12 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
if (onFeedbackUpdate) onFeedbackUpdate();
|
if (onFeedbackUpdate) onFeedbackUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFavoriteChange = (favorited: boolean) => {
|
||||||
|
setIsFavorited(favorited);
|
||||||
|
setFavoriteCount(prev => favorited ? prev + 1 : Math.max(0, prev - 1));
|
||||||
|
if (onFeedbackUpdate) onFeedbackUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
if (settingsLoading) {
|
if (settingsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-3 ${className}`}>
|
<div className={`space-y-3 ${className}`}>
|
||||||
@@ -77,8 +88,8 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAnyFeedbackType = settings.allow_ratings || settings.allow_likes ||
|
const hasAnyFeedbackType = settings.allow_ratings || settings.allow_likes ||
|
||||||
settings.allow_comments;
|
settings.allow_comments || settings.allow_favorites;
|
||||||
|
|
||||||
if (!hasAnyFeedbackType) {
|
if (!hasAnyFeedbackType) {
|
||||||
return null;
|
return null;
|
||||||
@@ -101,8 +112,8 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
{settings.allow_likes && (
|
{(settings.allow_likes || settings.allow_favorites) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{settings.allow_likes && (
|
{settings.allow_likes && (
|
||||||
<PhotoLikes
|
<PhotoLikes
|
||||||
photoId={photoId}
|
photoId={photoId}
|
||||||
@@ -114,6 +125,18 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
onLikeChange={handleLikeChange}
|
onLikeChange={handleLikeChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{settings.allow_favorites && (
|
||||||
|
<PhotoFavorites
|
||||||
|
photoId={photoId}
|
||||||
|
gallerySlug={gallerySlug}
|
||||||
|
isFavorited={isFavorited}
|
||||||
|
favoriteCount={favoriteCount}
|
||||||
|
isEnabled={true}
|
||||||
|
requireNameEmail={settings.require_name_email || false}
|
||||||
|
onFavoriteChange={handleFavoriteChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosHeaders } from 'axios';
|
||||||
|
import {
|
||||||
|
getActiveGallerySlug,
|
||||||
|
getGalleryToken,
|
||||||
|
inferGallerySlugFromLocation,
|
||||||
|
resolveSlugFromRequestUrl,
|
||||||
|
} from '../utils/galleryAuthStorage';
|
||||||
|
|
||||||
// Maintenance mode callback
|
// Maintenance mode callback
|
||||||
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
||||||
@@ -23,6 +29,60 @@ api.interceptors.request.use(
|
|||||||
delete config.headers?.['Content-Type'];
|
delete config.headers?.['Content-Type'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const pathSlug = resolveSlugFromRequestUrl(config.url || '');
|
||||||
|
const params = config.params as Record<string, unknown> | undefined;
|
||||||
|
const paramSlug = typeof params?.slug === 'string' ? (params.slug as string) : null;
|
||||||
|
|
||||||
|
const rawPath = (() => {
|
||||||
|
if (!config.url) return '';
|
||||||
|
try {
|
||||||
|
if (config.url.startsWith('http://') || config.url.startsWith('https://')) {
|
||||||
|
return new URL(config.url).pathname;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return config.url;
|
||||||
|
}
|
||||||
|
return config.url;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const pathname = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
||||||
|
|
||||||
|
const isGalleryEndpoint = /^\/gallery\//.test(pathname)
|
||||||
|
|| /^\/secure-images\//.test(pathname)
|
||||||
|
|| /^\/auth\/gallery\//.test(pathname);
|
||||||
|
|
||||||
|
const isGallerySessionCheck = pathname === '/auth/session'
|
||||||
|
&& (!!paramSlug || window.location.pathname.startsWith('/gallery/'));
|
||||||
|
|
||||||
|
if (isGalleryEndpoint || isGallerySessionCheck) {
|
||||||
|
const fallbackSlug = getActiveGallerySlug()
|
||||||
|
|| inferGallerySlugFromLocation();
|
||||||
|
const slug = pathSlug || paramSlug || fallbackSlug;
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
const token = getGalleryToken(slug);
|
||||||
|
if (token) {
|
||||||
|
if (!config.headers) {
|
||||||
|
config.headers = new AxiosHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.headers instanceof AxiosHeaders) {
|
||||||
|
const existing = config.headers.get('Authorization');
|
||||||
|
if (!existing) {
|
||||||
|
config.headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const headersRecord = config.headers as Record<string, string | undefined>;
|
||||||
|
if (!headersRecord.Authorization) {
|
||||||
|
headersRecord.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import type { ReactNode } from 'react';
|
|||||||
import { api } from '../config/api';
|
import { api } from '../config/api';
|
||||||
import { authService, galleryService } from '../services';
|
import { authService, galleryService } from '../services';
|
||||||
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
|
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
|
||||||
|
import {
|
||||||
|
clearActiveGallerySlug,
|
||||||
|
clearGalleryToken,
|
||||||
|
setActiveGallerySlug,
|
||||||
|
storeGalleryToken,
|
||||||
|
} from '../utils/galleryAuthStorage';
|
||||||
|
|
||||||
interface GalleryEvent {
|
interface GalleryEvent {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,6 +61,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cleanupOldGalleryAuth();
|
cleanupOldGalleryAuth();
|
||||||
|
|
||||||
|
const slugAtMount = getCurrentGallerySlug();
|
||||||
|
if (slugAtMount) {
|
||||||
|
setActiveGallerySlug(slugAtMount);
|
||||||
|
} else {
|
||||||
|
clearActiveGallerySlug();
|
||||||
|
}
|
||||||
|
|
||||||
const initialise = async () => {
|
const initialise = async () => {
|
||||||
const currentSlug = getCurrentGallerySlug();
|
const currentSlug = getCurrentGallerySlug();
|
||||||
|
|
||||||
@@ -63,6 +76,8 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveGallerySlug(currentSlug);
|
||||||
|
|
||||||
const storedEvent = sessionStorage.getItem(`gallery_event_${currentSlug}`);
|
const storedEvent = sessionStorage.getItem(`gallery_event_${currentSlug}`);
|
||||||
if (storedEvent) {
|
if (storedEvent) {
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +124,10 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
setEvent(response.event);
|
setEvent(response.event);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(response.event));
|
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(response.event));
|
||||||
|
if (response.token) {
|
||||||
|
storeGalleryToken(currentSlug, response.token);
|
||||||
|
}
|
||||||
|
setActiveGallerySlug(currentSlug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,16 +137,21 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
|
clearGalleryToken(currentSlug);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
|
clearGalleryToken(currentSlug);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initialise();
|
initialise();
|
||||||
|
return () => {
|
||||||
|
clearActiveGallerySlug();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async (slug: string, password: string, recaptchaToken?: string | null) => {
|
const login = async (slug: string, password: string, recaptchaToken?: string | null) => {
|
||||||
@@ -137,7 +161,11 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
const response = await authService.verifyGalleryPassword(slug, password, recaptchaToken);
|
const response = await authService.verifyGalleryPassword(slug, password, recaptchaToken);
|
||||||
setEvent(response.event);
|
setEvent(response.event);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
|
if (response.token) {
|
||||||
|
storeGalleryToken(slug, response.token);
|
||||||
|
}
|
||||||
|
setActiveGallerySlug(slug);
|
||||||
|
|
||||||
// Store event data for quick reloads (non-sensitive)
|
// Store event data for quick reloads (non-sensitive)
|
||||||
sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -152,12 +180,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
const currentSlug = getCurrentGallerySlug();
|
const currentSlug = getCurrentGallerySlug();
|
||||||
if (currentSlug) {
|
if (currentSlug) {
|
||||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||||
|
clearGalleryToken(currentSlug);
|
||||||
}
|
}
|
||||||
authService.galleryLogout(currentSlug || undefined);
|
authService.galleryLogout(currentSlug || undefined);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
}
|
clearActiveGallerySlug();
|
||||||
;
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryAuthContext.Provider
|
<GalleryAuthContext.Provider
|
||||||
|
|||||||
@@ -225,7 +225,9 @@ export const CreateEventPageEnhanced: React.FC = () => {
|
|||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const feedbackSettings = formData.feedback_settings;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
event_type: formData.event_type,
|
event_type: formData.event_type,
|
||||||
event_name: formData.event_name,
|
event_name: formData.event_name,
|
||||||
@@ -239,9 +241,16 @@ export const CreateEventPageEnhanced: React.FC = () => {
|
|||||||
expiration_days: formData.expires_in_days,
|
expiration_days: formData.expires_in_days,
|
||||||
allow_user_uploads: formData.allow_user_uploads,
|
allow_user_uploads: formData.allow_user_uploads,
|
||||||
upload_category_id: formData.upload_category_id,
|
upload_category_id: formData.upload_category_id,
|
||||||
feedback_settings: formData.feedback_settings,
|
feedback_enabled: feedbackSettings.feedback_enabled,
|
||||||
|
allow_ratings: feedbackSettings.allow_ratings,
|
||||||
|
allow_likes: feedbackSettings.allow_likes,
|
||||||
|
allow_comments: feedbackSettings.allow_comments,
|
||||||
|
allow_favorites: feedbackSettings.allow_favorites,
|
||||||
|
require_name_email: feedbackSettings.require_name_email,
|
||||||
|
moderate_comments: feedbackSettings.moderate_comments,
|
||||||
|
show_feedback_to_guests: feedbackSettings.show_feedback_to_guests,
|
||||||
};
|
};
|
||||||
|
|
||||||
createMutation.mutate(payload);
|
createMutation.mutate(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ interface CreateEventData {
|
|||||||
expiration_days: number;
|
expiration_days: number;
|
||||||
allow_user_uploads?: boolean;
|
allow_user_uploads?: boolean;
|
||||||
upload_category_id?: number | null;
|
upload_category_id?: number | null;
|
||||||
|
feedback_enabled?: boolean;
|
||||||
|
allow_ratings?: boolean;
|
||||||
|
allow_likes?: boolean;
|
||||||
|
allow_comments?: boolean;
|
||||||
|
allow_favorites?: boolean;
|
||||||
|
require_name_email?: boolean;
|
||||||
|
moderate_comments?: boolean;
|
||||||
|
show_feedback_to_guests?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateEventData {
|
interface UpdateEventData {
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export const cleanupOldGalleryAuth = () => {
|
|||||||
// Also clear session storage
|
// Also clear session storage
|
||||||
sessionStorage.removeItem('gallery_event');
|
sessionStorage.removeItem('gallery_event');
|
||||||
sessionStorage.removeItem('gallery_token');
|
sessionStorage.removeItem('gallery_token');
|
||||||
|
sessionStorage.removeItem('gallery_active_slug');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
const TOKEN_STORAGE_PREFIX = 'gallery_token_';
|
||||||
|
const ACTIVE_SLUG_KEY = 'gallery_active_slug';
|
||||||
|
|
||||||
|
const isBrowser = typeof window !== 'undefined';
|
||||||
|
|
||||||
|
const getSessionStorage = (): Storage | null => {
|
||||||
|
if (!isBrowser) return null;
|
||||||
|
try {
|
||||||
|
return window.sessionStorage;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Session storage unavailable', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractSlugFromPath = (path: string): string | null => {
|
||||||
|
if (!path) return null;
|
||||||
|
const match = path.match(/\/gallery\/([^\/?#]+)/);
|
||||||
|
return match ? decodeURIComponent(match[1]) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inferGallerySlugFromLocation = (): string | null => {
|
||||||
|
if (!isBrowser) return null;
|
||||||
|
return extractSlugFromPath(window.location.pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setActiveGallerySlug = (slug: string | null) => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) return;
|
||||||
|
if (slug) {
|
||||||
|
storage.setItem(ACTIVE_SLUG_KEY, slug);
|
||||||
|
} else {
|
||||||
|
storage.removeItem(ACTIVE_SLUG_KEY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveGallerySlug = (): string | null => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) return null;
|
||||||
|
return storage.getItem(ACTIVE_SLUG_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearActiveGallerySlug = () => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) return;
|
||||||
|
storage.removeItem(ACTIVE_SLUG_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const storeGalleryToken = (slug: string, token: string) => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage || !slug) return;
|
||||||
|
storage.setItem(`${TOKEN_STORAGE_PREFIX}${slug}`, token);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGalleryToken = (slug?: string | null): string | null => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) return null;
|
||||||
|
const resolvedSlug = slug || getActiveGallerySlug() || inferGallerySlugFromLocation();
|
||||||
|
if (!resolvedSlug) return null;
|
||||||
|
return storage.getItem(`${TOKEN_STORAGE_PREFIX}${resolvedSlug}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearGalleryToken = (slug?: string | null) => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) return;
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
storage.removeItem(`${TOKEN_STORAGE_PREFIX}${slug}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = storage.getItem(ACTIVE_SLUG_KEY);
|
||||||
|
if (active) {
|
||||||
|
storage.removeItem(`${TOKEN_STORAGE_PREFIX}${active}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearAllGalleryTokens = () => {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) return;
|
||||||
|
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
for (let i = 0; i < storage.length; i += 1) {
|
||||||
|
const key = storage.key(i);
|
||||||
|
if (key && key.startsWith(TOKEN_STORAGE_PREFIX)) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysToRemove.forEach((key) => storage.removeItem(key));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveSlugFromRequestUrl = (url?: string | null): string | null => {
|
||||||
|
if (!url) return null;
|
||||||
|
let pathname = url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
pathname = new URL(url).pathname;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Leave pathname as provided if URL parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pathname.startsWith('/')) {
|
||||||
|
pathname = `/${pathname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractSlugFromPath(pathname);
|
||||||
|
};
|
||||||
+45
-2
@@ -132,6 +132,37 @@ command_exists() {
|
|||||||
command -v "$1" >/dev/null 2>&1
|
command -v "$1" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_storage_layout() {
|
||||||
|
local base_dir="$1"
|
||||||
|
local storage_root="$base_dir/storage"
|
||||||
|
local storage_events_dir="$storage_root/events"
|
||||||
|
|
||||||
|
mkdir -p "$storage_events_dir/active" \
|
||||||
|
"$storage_events_dir/archived" \
|
||||||
|
"$storage_root/thumbnails" \
|
||||||
|
"$storage_root/tmp"
|
||||||
|
|
||||||
|
local legacy_dir="$base_dir/events"
|
||||||
|
if [[ -d "$legacy_dir" ]]; then
|
||||||
|
log_step "Migrating legacy events directory to storage/events..."
|
||||||
|
mkdir -p "$storage_events_dir"
|
||||||
|
|
||||||
|
local existing=""
|
||||||
|
if [[ -d "$storage_events_dir" ]]; then
|
||||||
|
existing=$(ls -A "$storage_events_dir" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$storage_events_dir" || -z "$existing" ]]; then
|
||||||
|
rm -rf "$storage_events_dir"
|
||||||
|
mv "$legacy_dir" "$storage_events_dir"
|
||||||
|
else
|
||||||
|
cp -a "$legacy_dir/." "$storage_events_dir/"
|
||||||
|
rm -rf "$legacy_dir"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
mkdir -p "$storage_events_dir/active" "$storage_events_dir/archived"
|
||||||
|
}
|
||||||
|
|
||||||
generate_password() {
|
generate_password() {
|
||||||
openssl rand -base64 32 | tr -d "=+/" | cut -c1-16
|
openssl rand -base64 32 | tr -d "=+/" | cut -c1-16
|
||||||
}
|
}
|
||||||
@@ -602,7 +633,8 @@ setup_native_installation() {
|
|||||||
|
|
||||||
# Create application directory
|
# Create application directory
|
||||||
log_step "Creating application directory..."
|
log_step "Creating application directory..."
|
||||||
mkdir -p "$NATIVE_APP_DIR"/{app,events/{active,archived},logs,config}
|
mkdir -p "$NATIVE_APP_DIR"/{app,logs,config}
|
||||||
|
ensure_storage_layout "$NATIVE_APP_DIR"
|
||||||
chown -R $NATIVE_APP_USER:$NATIVE_APP_USER "$NATIVE_APP_DIR"
|
chown -R $NATIVE_APP_USER:$NATIVE_APP_USER "$NATIVE_APP_DIR"
|
||||||
|
|
||||||
# Clone repository
|
# Clone repository
|
||||||
@@ -668,7 +700,7 @@ DATABASE_CLIENT=sqlite3
|
|||||||
DATABASE_PATH=$NATIVE_APP_DIR/app/backend/data/photo_sharing.db
|
DATABASE_PATH=$NATIVE_APP_DIR/app/backend/data/photo_sharing.db
|
||||||
|
|
||||||
# Storage root (thumbnails/uploads live under this path)
|
# Storage root (thumbnails/uploads live under this path)
|
||||||
STORAGE_PATH=$NATIVE_APP_DIR
|
STORAGE_PATH=$NATIVE_APP_DIR/storage
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
SMTP_ENABLED=${SMTP_HOST:+true}
|
SMTP_ENABLED=${SMTP_HOST:+true}
|
||||||
@@ -1101,6 +1133,17 @@ update_native_installation() {
|
|||||||
if ! grep -q '^FRONTEND_DIR=' "$NATIVE_APP_DIR/app/backend/.env"; then
|
if ! grep -q '^FRONTEND_DIR=' "$NATIVE_APP_DIR/app/backend/.env"; then
|
||||||
echo "FRONTEND_DIR=$NATIVE_APP_DIR/app/frontend/dist" >> "$NATIVE_APP_DIR/app/backend/.env"
|
echo "FRONTEND_DIR=$NATIVE_APP_DIR/app/frontend/dist" >> "$NATIVE_APP_DIR/app/backend/.env"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ensure_storage_layout "$NATIVE_APP_DIR"
|
||||||
|
chown -R $NATIVE_APP_USER:$NATIVE_APP_USER "$NATIVE_APP_DIR/storage"
|
||||||
|
|
||||||
|
if [[ -f "$NATIVE_APP_DIR/app/backend/.env" ]]; then
|
||||||
|
if grep -q '^STORAGE_PATH=' "$NATIVE_APP_DIR/app/backend/.env"; then
|
||||||
|
sed -i "s|^STORAGE_PATH=.*|STORAGE_PATH=$NATIVE_APP_DIR/storage|" "$NATIVE_APP_DIR/app/backend/.env"
|
||||||
|
else
|
||||||
|
echo "STORAGE_PATH=$NATIVE_APP_DIR/storage" >> "$NATIVE_APP_DIR/app/backend/.env"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Restart services
|
# Restart services
|
||||||
systemctl restart picpeak-backend
|
systemctl restart picpeak-backend
|
||||||
|
|||||||
Reference in New Issue
Block a user