From cdda70988664a177b351abc6a259ec39664d17ff Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sat, 17 Jan 2026 15:48:15 +0100 Subject: [PATCH 1/4] fix: add STORAGE_PATH to production docker-compose Ensures STORAGE_PATH environment variable is explicitly set in production deployments to prevent path resolution issues when serving thumbnails and other storage-related operations. --- docker-compose.production.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.production.yml b/docker-compose.production.yml index fc53952d..1bbfe487 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -46,6 +46,7 @@ services: - NODE_ENV=production - DB_HOST=${DB_HOST:-postgres} - REDIS_HOST=redis + - STORAGE_PATH=/app/storage - PHOTOS_DIR=/app/storage/events - PICPEAK_RELEASE_CHANNEL=${PICPEAK_CHANNEL:-stable} volumes: From 3397807670784e02cbe34a7a60db43c95d64f19c Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sat, 17 Jan 2026 15:48:24 +0100 Subject: [PATCH 2/4] docs: emphasize importance of STORAGE_PATH in env example --- backend/.env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/.env.example b/backend/.env.example index fb1ce982..7d7a12d2 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -44,6 +44,7 @@ SMTP_PASS=your-sendgrid-api-key EMAIL_FROM=noreply@example.com # Storage Paths +# IMPORTANT: STORAGE_PATH must be set to avoid file path resolution issues # Docker deployment: STORAGE_PATH=/app/storage EVENTS_PATH=/app/storage/events From 86fa1046d5439cb451feb164175c919c49ca219a Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sun, 18 Jan 2026 12:55:56 +0100 Subject: [PATCH 3/4] fix: correct invitation email link URL path The invitation email was generating links to /admin/accept-invite/{token} but the frontend route is configured at /invite/{token}. This caused invited users to see a blank page when clicking the email link. Fixes #129 --- backend/src/services/userManagementService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/userManagementService.js b/backend/src/services/userManagementService.js index 69b99228..e212dba1 100644 --- a/backend/src/services/userManagementService.js +++ b/backend/src/services/userManagementService.js @@ -60,7 +60,7 @@ async function createInvitation({ email, roleId, invitedById }) { // Queue invitation email const frontendUrl = process.env.FRONTEND_URL || process.env.ADMIN_URL || 'http://localhost:3005'; await queueEmail(null, email, 'admin_invitation', { - invite_link: `${frontendUrl}/admin/accept-invite/${token}`, + invite_link: `${frontendUrl}/invite/${token}`, role_name: role.display_name, expires_at: expiresAt.toISOString() }); From 991aa98f98cffd1d7785c272726615325e2c0208 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sun, 18 Jan 2026 15:00:38 +0100 Subject: [PATCH 4/4] fix: correct invitation activation validation and add missing translations - Fix password minimum length validation: frontend now correctly requires 12 characters to match backend validation (was incorrectly checking for 8) - Fix translation key references in AcceptInvitePage to use correct paths (e.g., acceptInvitation.errors.* instead of acceptInvitation.*) - Add missing translations for both EN and DE: - contactAdminMessage - passwordsMatch - alreadyHaveAccount - signIn Fixes #129 --- frontend/src/i18n/locales/de.json | 4 ++ frontend/src/i18n/locales/en.json | 4 ++ .../src/pages/public/AcceptInvitePage.tsx | 40 +++++++++---------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index ab8161cd..93d56e89 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1523,6 +1523,10 @@ "successMessage": "Ihr Konto wurde erfolgreich erstellt. Sie können sich jetzt mit Ihren Zugangsdaten anmelden.", "redirecting": "Weiterleitung zur Anmeldung in {{seconds}}...", "goToLogin": "Zur Anmeldung", + "contactAdminMessage": "Bitte kontaktieren Sie Ihren Administrator, um eine neue Einladung anzufordern.", + "passwordsMatch": "Passwörter stimmen überein", + "alreadyHaveAccount": "Bereits ein Konto?", + "signIn": "Anmelden", "errors": { "usernameRequired": "Benutzername ist erforderlich", "usernameTooShort": "Benutzername muss mindestens 3 Zeichen lang sein", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f11e78bf..78523323 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1284,6 +1284,10 @@ "successMessage": "Your account has been created successfully. You can now log in with your credentials.", "redirecting": "Redirecting to login in {{seconds}}...", "goToLogin": "Go to Login", + "contactAdminMessage": "Please contact your administrator to request a new invitation.", + "passwordsMatch": "Passwords match", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in", "errors": { "usernameRequired": "Username is required", "usernameTooShort": "Username must be at least 3 characters", diff --git a/frontend/src/pages/public/AcceptInvitePage.tsx b/frontend/src/pages/public/AcceptInvitePage.tsx index 08722d49..150c7750 100644 --- a/frontend/src/pages/public/AcceptInvitePage.tsx +++ b/frontend/src/pages/public/AcceptInvitePage.tsx @@ -94,16 +94,16 @@ export const AcceptInvitePage: React.FC = () => { }); setErrors(validationErrors); } else { - toast.error(errorMessage || t('acceptInvitation.validationError')); + toast.error(errorMessage || t('acceptInvitation.errors.genericError')); } } else if (error.response?.status === 422) { - toast.error(errorMessage || t('acceptInvitation.validationError')); + toast.error(errorMessage || t('acceptInvitation.errors.genericError')); } else if (error.response?.status === 404) { - toast.error(t('acceptInvitation.invalidOrExpired')); + toast.error(t('acceptInvitation.invalidTokenMessage')); } else if (error.response?.status === 409) { - toast.error(errorMessage || t('acceptInvitation.alreadyUsed')); + toast.error(errorMessage || t('acceptInvitation.alreadyUsedMessage')); } else { - toast.error(t('acceptInvitation.generalError')); + toast.error(t('acceptInvitation.errors.genericError')); } }, }); @@ -113,7 +113,7 @@ export const AcceptInvitePage: React.FC = () => { { label: t('acceptInvitation.requirements.minLength'), met: false, - test: (pwd: string) => pwd.length >= 8, + test: (pwd: string) => pwd.length >= 12, }, { label: t('acceptInvitation.requirements.uppercase'), @@ -166,16 +166,16 @@ export const AcceptInvitePage: React.FC = () => { // Validate username const validateUsername = (username: string): string | null => { if (!username) { - return t('acceptInvitation.usernameRequired'); + return t('acceptInvitation.errors.usernameRequired'); } if (username.length < 3) { - return t('acceptInvitation.usernameTooShort'); + return t('acceptInvitation.errors.usernameTooShort'); } if (username.length > 50) { - return t('acceptInvitation.usernameTooLong'); + return t('acceptInvitation.errors.usernameTooLong'); } if (!/^[a-zA-Z0-9_-]+$/.test(username)) { - return t('acceptInvitation.usernameInvalid'); + return t('acceptInvitation.errors.usernameInvalid'); } return null; }; @@ -190,18 +190,18 @@ export const AcceptInvitePage: React.FC = () => { } if (!formData.password) { - newErrors.password = t('acceptInvitation.passwordRequired'); + newErrors.password = t('acceptInvitation.errors.passwordRequired'); } else { const allRequirementsMet = passwordRequirements.every(req => req.test(formData.password)); if (!allRequirementsMet) { - newErrors.password = t('acceptInvitation.passwordRequirements'); + newErrors.password = t('acceptInvitation.errors.passwordTooShort'); } } if (!formData.confirmPassword) { - newErrors.confirmPassword = t('acceptInvitation.confirmPasswordRequired'); + newErrors.confirmPassword = t('acceptInvitation.errors.confirmPasswordRequired'); } else if (formData.password !== formData.confirmPassword) { - newErrors.confirmPassword = t('acceptInvitation.passwordsDoNotMatch'); + newErrors.confirmPassword = t('acceptInvitation.errors.passwordsDoNotMatch'); } setErrors(newErrors); @@ -269,7 +269,7 @@ export const AcceptInvitePage: React.FC = () => { // Error state - invalid or expired token if (isError || !invitation?.valid) { - const errorMessage = (validationError as any)?.response?.data?.error || t('acceptInvitation.invalidOrExpired'); + const errorMessage = (validationError as any)?.response?.data?.error || t('acceptInvitation.invalidTokenMessage'); return (
@@ -280,13 +280,13 @@ export const AcceptInvitePage: React.FC = () => {

- {t('acceptInvitation.invalidTitle')} + {t('acceptInvitation.invalidToken')}

{errorMessage}

- {t('acceptInvitation.contactAdmin')} + {t('acceptInvitation.contactAdminMessage')}

@@ -466,7 +466,7 @@ export const AcceptInvitePage: React.FC = () => { {/* Password Requirements */}
-

{t('acceptInvitation.requirementsTitle')}

+

{t('acceptInvitation.requirements.title')}

{passwordRequirements.map((req, index) => { const isMet = req.test(formData.password); return (