Harden auth cookies and fix native schema for event creation
Mirror to GitHub / mirror (push) Successful in 45s
Test and Lint / backend-test (push) Successful in 1m37s
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 1m0s
Version and Release / trigger-drone (push) Successful in 3s
Mirror to GitHub / mirror (push) Successful in 45s
Test and Lint / backend-test (push) Successful in 1m37s
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 1m0s
Version and Release / trigger-drone (push) Successful in 3s
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MaintenanceMode } from './MaintenanceMode';
|
||||
import { useMaintenanceMode } from '../contexts/MaintenanceContext';
|
||||
import { setMaintenanceModeCallback, api, getAuthToken } from '../config/api';
|
||||
import { setMaintenanceModeCallback, api } from '../config/api';
|
||||
|
||||
interface MaintenanceWrapperProps {
|
||||
children: React.ReactNode;
|
||||
@@ -12,10 +12,38 @@ interface MaintenanceWrapperProps {
|
||||
export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const { isMaintenanceMode, setMaintenanceMode } = useMaintenanceMode();
|
||||
const [hasAdminSession, setHasAdminSession] = useState(false);
|
||||
|
||||
// Check if current route is admin route
|
||||
const isAdminRoute = location.pathname.startsWith('/admin');
|
||||
const hasAdminAuth = !!getAuthToken(true);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const checkAdminSession = async () => {
|
||||
if (!isAdminRoute) {
|
||||
setHasAdminSession(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get<{ valid: boolean; type: string }>('/auth/session');
|
||||
if (isMounted) {
|
||||
setHasAdminSession(Boolean(response.data?.valid && response.data.type === 'admin'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setHasAdminSession(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAdminSession();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [isAdminRoute]);
|
||||
|
||||
// Register the maintenance mode callback
|
||||
useEffect(() => {
|
||||
@@ -37,7 +65,7 @@ export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 503) {
|
||||
// Only set maintenance mode for non-admin routes or unauthenticated admin routes
|
||||
if (!isAdminRoute || !hasAdminAuth) {
|
||||
if (!isAdminRoute || !hasAdminSession) {
|
||||
setMaintenanceMode(true);
|
||||
return { maintenance: true };
|
||||
}
|
||||
@@ -47,13 +75,13 @@ export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children
|
||||
},
|
||||
staleTime: 30000, // Check every 30 seconds
|
||||
retry: false, // Don't retry on failure
|
||||
enabled: (!isAdminRoute || !hasAdminAuth) && !isMaintenanceMode, // Don't check if already in maintenance
|
||||
enabled: (!isAdminRoute || !hasAdminSession) && !isMaintenanceMode, // Don't check if already in maintenance
|
||||
});
|
||||
|
||||
// Show maintenance page if in maintenance mode and not on admin route with auth
|
||||
if (isMaintenanceMode && (!isAdminRoute || !hasAdminAuth)) {
|
||||
if (isMaintenanceMode && (!isAdminRoute || !hasAdminSession)) {
|
||||
return <MaintenanceMode />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getAuthToken } from '../../config/api';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
@@ -25,33 +24,12 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
let objectUrl: string | null = null;
|
||||
|
||||
// Determine which token to use based on context
|
||||
let token: string | undefined;
|
||||
|
||||
if (isGallery) {
|
||||
// For gallery images, get the gallery-specific token
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
if (pathParts[1] === 'gallery' && pathParts[2]) {
|
||||
const gallerySlug = pathParts[2];
|
||||
token = localStorage.getItem(`gallery_token_${gallerySlug}`) || undefined;
|
||||
}
|
||||
} else {
|
||||
// For admin images, use the admin token
|
||||
token = getAuthToken(true);
|
||||
}
|
||||
|
||||
if (!src) {
|
||||
setImageSrc(fallbackSrc || '');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
// No auth token - use fallback
|
||||
setImageSrc(fallbackSrc || '');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(false);
|
||||
|
||||
@@ -71,9 +49,7 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
|
||||
// Fetch authenticated image
|
||||
const response = await fetch(fullImageUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -119,4 +95,4 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
}
|
||||
|
||||
return <img src={imageSrc} alt={alt} {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
+5
-100
@@ -1,9 +1,4 @@
|
||||
import axios, { AxiosHeaders } from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
// Cookie keys
|
||||
export const ADMIN_TOKEN_KEY = 'admin_token';
|
||||
export const GALLERY_TOKEN_KEY = 'gallery_token';
|
||||
import axios from 'axios';
|
||||
|
||||
// Maintenance mode callback
|
||||
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
||||
@@ -18,80 +13,12 @@ export const api = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: false, // Ensure we're not relying on cookies
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
// Request interceptor: drop Content-Type for FormData payloads so the browser can set boundaries
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// Don't process if headers are already set by the component
|
||||
const existingAuth = config.headers?.['Authorization'] || config.headers?.get?.('Authorization');
|
||||
|
||||
// If authorization is already set by the component, don't override it
|
||||
if (existingAuth) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// Check if it's an admin route or gallery route
|
||||
const isAdminRoute = config.url?.includes('/admin');
|
||||
|
||||
if (isAdminRoute) {
|
||||
const token = Cookies.get(ADMIN_TOKEN_KEY);
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
} else {
|
||||
// For gallery routes, try to extract slug from the request URL first
|
||||
const galleryMatch = config.url?.match(/gallery\/([^\/]+)/);
|
||||
|
||||
if (galleryMatch && galleryMatch[1]) {
|
||||
const galleryIdOrSlug = galleryMatch[1];
|
||||
// Remove any query parameters from the slug
|
||||
const cleanIdOrSlug = galleryIdOrSlug.split('?')[0];
|
||||
|
||||
// Check if it's a numeric ID (for upload endpoints)
|
||||
let token = null;
|
||||
if (/^\d+$/.test(cleanIdOrSlug)) {
|
||||
// It's an event ID - try to find the token from current page slug
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
if (pathParts[1] === 'gallery' && pathParts[2]) {
|
||||
const gallerySlug = pathParts[2];
|
||||
const cleanSlug = gallerySlug.split('?')[0];
|
||||
token = localStorage.getItem(`gallery_token_${cleanSlug}`);
|
||||
}
|
||||
} else {
|
||||
// It's a slug - use it directly
|
||||
token = localStorage.getItem(`gallery_token_${cleanIdOrSlug}`);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
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];
|
||||
// Remove any query parameters from the slug
|
||||
const cleanSlug = gallerySlug.split('?')[0];
|
||||
const token = localStorage.getItem(`gallery_token_${cleanSlug}`);
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't set Content-Type for FormData - let browser set it with boundary
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers?.['Content-Type'];
|
||||
}
|
||||
@@ -110,10 +37,9 @@ api.interceptors.response.use(
|
||||
// Handle maintenance mode (503)
|
||||
if (error.response?.status === 503) {
|
||||
const isAdminRoute = error.config?.url?.includes('/admin');
|
||||
const hasAdminAuth = error.config?.headers?.Authorization?.startsWith('Bearer ');
|
||||
|
||||
// Only trigger maintenance mode for non-admin routes or unauthenticated admin routes
|
||||
if (!isAdminRoute || !hasAdminAuth) {
|
||||
if (!isAdminRoute) {
|
||||
if (maintenanceModeCallback) {
|
||||
maintenanceModeCallback(true);
|
||||
}
|
||||
@@ -126,8 +52,6 @@ api.interceptors.response.use(
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (isAdminRoute) {
|
||||
// Clear admin token on unauthorized
|
||||
Cookies.remove(ADMIN_TOKEN_KEY);
|
||||
// Only redirect if we're not already on the admin login page
|
||||
if (!currentPath.includes('/admin/login')) {
|
||||
window.location.href = '/admin/login';
|
||||
@@ -144,8 +68,7 @@ api.interceptors.response.use(
|
||||
// Don't clear tokens for image requests - they might just need a retry
|
||||
if (!isImageRequest && galleryMatch && galleryMatch[1]) {
|
||||
const gallerySlug = galleryMatch[1];
|
||||
localStorage.removeItem(`gallery_token_${gallerySlug}`);
|
||||
localStorage.removeItem(`gallery_event_${gallerySlug}`);
|
||||
sessionStorage.removeItem(`gallery_event_${gallerySlug}`);
|
||||
}
|
||||
// Don't redirect - let the component handle the auth state
|
||||
} else if (galleryMatch) {
|
||||
@@ -159,21 +82,3 @@ api.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Helper to set auth tokens
|
||||
export const setAuthToken = (token: string, isAdmin: boolean = false) => {
|
||||
const key = isAdmin ? ADMIN_TOKEN_KEY : GALLERY_TOKEN_KEY;
|
||||
Cookies.set(key, token, { expires: 1 }); // 1 day expiry
|
||||
};
|
||||
|
||||
// Helper to clear auth tokens
|
||||
export const clearAuthToken = (isAdmin: boolean = false) => {
|
||||
const key = isAdmin ? ADMIN_TOKEN_KEY : GALLERY_TOKEN_KEY;
|
||||
Cookies.remove(key);
|
||||
};
|
||||
|
||||
// Helper to get auth tokens
|
||||
export const getAuthToken = (isAdmin: boolean = false) => {
|
||||
const key = isAdmin ? ADMIN_TOKEN_KEY : GALLERY_TOKEN_KEY;
|
||||
return Cookies.get(key);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { getAuthToken } from '../config/api';
|
||||
import { api } from '../config/api';
|
||||
import { authService } from '../services';
|
||||
import type { AdminUser } from '../types';
|
||||
|
||||
@@ -40,15 +40,31 @@ export const AdminAuthProvider: React.FC<AdminAuthProviderProps> = ({ children }
|
||||
// Check if user has a valid token on mount
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const token = getAuthToken(true);
|
||||
if (token) {
|
||||
// For now, just assume the token is valid
|
||||
// TODO: Validate token with backend and get user info
|
||||
const storedUser = sessionStorage.getItem('admin_user');
|
||||
if (storedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser));
|
||||
} catch (err) {
|
||||
sessionStorage.removeItem('admin_user');
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.get<{ valid: boolean; type: string; adminUsername?: string; user?: string }>(
|
||||
'/auth/session'
|
||||
);
|
||||
|
||||
if (response.data?.valid && response.data.type === 'admin') {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
sessionStorage.removeItem('admin_user');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
// Auth check failed - user needs to login
|
||||
setError('Failed to check authentication');
|
||||
sessionStorage.removeItem('admin_user');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -63,9 +79,11 @@ export const AdminAuthProvider: React.FC<AdminAuthProviderProps> = ({ children }
|
||||
setError(null);
|
||||
setIsAuthenticated(true);
|
||||
setMustChangePassword(user.mustChangePassword || false);
|
||||
sessionStorage.setItem('admin_user', JSON.stringify(user));
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
sessionStorage.removeItem('admin_user');
|
||||
authService.adminLogout();
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
@@ -79,6 +97,10 @@ export const AdminAuthProvider: React.FC<AdminAuthProviderProps> = ({ children }
|
||||
...user,
|
||||
mustChangePassword: false
|
||||
});
|
||||
sessionStorage.setItem('admin_user', JSON.stringify({
|
||||
...user,
|
||||
mustChangePassword: false
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,4 +120,4 @@ export const AdminAuthProvider: React.FC<AdminAuthProviderProps> = ({ children }
|
||||
{children}
|
||||
</AdminAuthContext.Provider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { api } from '../config/api';
|
||||
import { authService, galleryService } from '../services';
|
||||
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
|
||||
|
||||
@@ -52,65 +53,81 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Clean up old authentication data on mount
|
||||
cleanupOldGalleryAuth();
|
||||
|
||||
// Check if user has a valid token on mount
|
||||
const currentSlug = getCurrentGallerySlug();
|
||||
if (currentSlug) {
|
||||
// Try to restore event data from localStorage with slug-specific key
|
||||
const storedEvent = localStorage.getItem(`gallery_event_${currentSlug}`);
|
||||
const storedToken = localStorage.getItem(`gallery_token_${currentSlug}`);
|
||||
|
||||
if (storedEvent && storedToken) {
|
||||
|
||||
const initialise = async () => {
|
||||
const currentSlug = getCurrentGallerySlug();
|
||||
|
||||
if (!currentSlug) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const storedEvent = sessionStorage.getItem(`gallery_event_${currentSlug}`);
|
||||
if (storedEvent) {
|
||||
try {
|
||||
const eventData = JSON.parse(storedEvent);
|
||||
// Verify the stored event matches the current gallery slug
|
||||
if (eventData && eventData.id) {
|
||||
setEvent(eventData);
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
// Clear invalid data
|
||||
localStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
localStorage.removeItem(`gallery_token_${currentSlug}`);
|
||||
const parsed = JSON.parse(storedEvent);
|
||||
if (parsed && parsed.id) {
|
||||
setEvent(parsed);
|
||||
}
|
||||
} catch (error) {
|
||||
// Invalid stored data - clear it
|
||||
localStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
localStorage.removeItem(`gallery_token_${currentSlug}`);
|
||||
}
|
||||
} else {
|
||||
// No stored auth; check for token in URL and auto-authenticate
|
||||
const parts = window.location.pathname.split('/');
|
||||
const urlToken = parts.length >= 5 ? parts[4] : (parts.length >= 4 ? parts[3] : undefined);
|
||||
if (urlToken) {
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Verify token against backend
|
||||
const verify = await galleryService.verifyToken(currentSlug, urlToken);
|
||||
if (verify?.valid) {
|
||||
// Store token and fetch event via photos endpoint to get full event object
|
||||
localStorage.setItem(`gallery_token_${currentSlug}`, urlToken);
|
||||
const data = await galleryService.getGalleryPhotos(currentSlug);
|
||||
if (data?.event) {
|
||||
setEvent(data.event);
|
||||
setIsAuthenticated(true);
|
||||
localStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(data.event));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid token; ensure any residual storage is cleared
|
||||
localStorage.removeItem(`gallery_token_${currentSlug}`);
|
||||
localStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
} catch (err) {
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const sessionResponse = await api.get<{ valid: boolean; type: string; eventSlug?: string }>(
|
||||
'/auth/session',
|
||||
{ params: { slug: currentSlug } }
|
||||
);
|
||||
|
||||
if (sessionResponse.data?.valid && sessionResponse.data.type === 'gallery' && sessionResponse.data.eventSlug === currentSlug) {
|
||||
setIsAuthenticated(true);
|
||||
|
||||
if (!storedEvent) {
|
||||
// Fetch gallery details to hydrate context
|
||||
const galleryData = await galleryService.getGalleryPhotos(currentSlug);
|
||||
if (galleryData?.event) {
|
||||
setEvent(galleryData.event);
|
||||
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(galleryData.event));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If no active session, check for share token in URL
|
||||
const parts = window.location.pathname.split('/');
|
||||
const urlToken = parts.length >= 5 ? parts[4] : (parts.length >= 4 ? parts[3] : undefined);
|
||||
|
||||
if (urlToken) {
|
||||
const verify = await galleryService.verifyToken(currentSlug, urlToken);
|
||||
if (verify?.valid) {
|
||||
const response = await authService.shareLinkLogin(currentSlug, urlToken);
|
||||
if (response?.event) {
|
||||
setEvent(response.event);
|
||||
setIsAuthenticated(true);
|
||||
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(response.event));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No valid session found
|
||||
setIsAuthenticated(false);
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
setEvent(null);
|
||||
} catch (error) {
|
||||
setIsAuthenticated(false);
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
setEvent(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initialise();
|
||||
}, []);
|
||||
|
||||
const login = async (slug: string, password: string, recaptchaToken?: string | null) => {
|
||||
@@ -121,9 +138,8 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
setEvent(response.event);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
// Store event data and token in localStorage with slug-specific key
|
||||
localStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
||||
localStorage.setItem(`gallery_token_${slug}`, response.token);
|
||||
// Store event data for quick reloads (non-sensitive)
|
||||
sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Invalid password');
|
||||
throw err;
|
||||
@@ -135,13 +151,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
const logout = () => {
|
||||
const currentSlug = getCurrentGallerySlug();
|
||||
if (currentSlug) {
|
||||
localStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
localStorage.removeItem(`gallery_token_${currentSlug}`);
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
}
|
||||
authService.galleryLogout();
|
||||
authService.galleryLogout(currentSlug || undefined);
|
||||
setIsAuthenticated(false);
|
||||
setEvent(null);
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
return (
|
||||
<GalleryAuthContext.Provider
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button, Input, Card, ReCaptcha } from '../../components/common';
|
||||
import { useAdminAuth } from '../../contexts';
|
||||
import { authService } from '../../services/auth.service';
|
||||
import { getAuthToken, api } from '../../config/api';
|
||||
import { api } from '../../config/api';
|
||||
|
||||
export const AdminLoginPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -84,17 +84,20 @@ export const AdminLoginPage: React.FC = () => {
|
||||
login(response.token, response.user);
|
||||
toast.success(t('adminLogin.loginSuccess'));
|
||||
setLoginSuccess(true);
|
||||
} catch (error: any) {
|
||||
// Login error handled by UI notification
|
||||
|
||||
// Handle network errors gracefully
|
||||
if (error.code === 'ERR_NETWORK' || error.code === 'ERR_CONNECTION_RESET') {
|
||||
} catch (error: any) {
|
||||
// Login error handled by UI notification
|
||||
|
||||
// Handle network errors gracefully
|
||||
if (error.code === 'ERR_NETWORK' || error.code === 'ERR_CONNECTION_RESET') {
|
||||
// Check if we actually got logged in despite the error
|
||||
const token = getAuthToken(true);
|
||||
if (token) {
|
||||
// Login was successful, just had a connection issue
|
||||
setLoginSuccess(true);
|
||||
return;
|
||||
try {
|
||||
const sessionResponse = await api.get<{ valid: boolean; type: string }>('/auth/session');
|
||||
if (sessionResponse.data?.valid && sessionResponse.data.type === 'admin') {
|
||||
setLoginSuccess(true);
|
||||
return;
|
||||
}
|
||||
} catch (sessionError) {
|
||||
// Ignore secondary failure, we'll surface the original network error
|
||||
}
|
||||
toast.error(t('adminLogin.networkError'));
|
||||
} else if (error.response?.status === 429) {
|
||||
@@ -259,4 +262,4 @@ export const AdminLoginPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
AdminLoginPage.displayName = 'AdminLoginPage';
|
||||
AdminLoginPage.displayName = 'AdminLoginPage';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api, setAuthToken, clearAuthToken } from '../config/api';
|
||||
import { api } from '../config/api';
|
||||
import type { LoginResponse, GalleryAuthResponse } from '../types';
|
||||
|
||||
export const authService = {
|
||||
@@ -10,14 +10,17 @@ export const authService = {
|
||||
password: credentials.password,
|
||||
recaptchaToken: credentials.recaptchaToken
|
||||
});
|
||||
|
||||
setAuthToken(response.data.token, true);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
adminLogout() {
|
||||
clearAuthToken(true);
|
||||
window.location.href = '/admin/login';
|
||||
async adminLogout() {
|
||||
try {
|
||||
await api.post('/auth/logout');
|
||||
} catch (err) {
|
||||
// Ignore logout errors; fallback to redirect
|
||||
} finally {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
},
|
||||
|
||||
// Gallery authentication
|
||||
@@ -32,7 +35,19 @@ export const authService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
galleryLogout() {
|
||||
// Logout is now handled by GalleryAuthContext
|
||||
async shareLinkLogin(slug: string, token: string): Promise<GalleryAuthResponse> {
|
||||
const response = await api.post<GalleryAuthResponse>('/auth/gallery/share-login', {
|
||||
slug,
|
||||
token,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
async galleryLogout(slug?: string | null) {
|
||||
try {
|
||||
await api.post('/auth/gallery/logout', { slug });
|
||||
} catch (err) {
|
||||
// Ignore; cookie will naturally expire if removal fails
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,21 +49,10 @@ class SecureTokenService {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the gallery token from localStorage
|
||||
const galleryToken = localStorage.getItem(`gallery_token_${slug}`);
|
||||
if (!galleryToken) {
|
||||
throw new Error('No gallery authentication token found');
|
||||
}
|
||||
|
||||
// Generate new token from backend with explicit auth header
|
||||
// Generate new token from backend – authentication handled via cookies
|
||||
const response = await api.post<SecureToken>(
|
||||
`/secure-images/${slug}/generate-token`,
|
||||
{ photoId, accessType },
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${galleryToken}`
|
||||
}
|
||||
}
|
||||
{ photoId, accessType }
|
||||
);
|
||||
|
||||
const tokenData: SecureToken = {
|
||||
@@ -190,4 +179,4 @@ if (typeof window !== 'undefined') {
|
||||
setInterval(() => {
|
||||
secureTokenService.clearExpiredTokens();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,12 @@ export const cleanupOldGalleryAuth = () => {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && (key.startsWith('gallery_token') || key.startsWith('gallery_event'))) {
|
||||
// Check if it's an old format token that might be corrupted
|
||||
const value = localStorage.getItem(key);
|
||||
if (value && (value.length < 100 || !value.includes('.'))) {
|
||||
// Token is too short or doesn't contain dots (not a valid JWT)
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
// Silently remove corrupted tokens
|
||||
});
|
||||
|
||||
// Remove old gallery token from cookies if it exists
|
||||
@@ -29,4 +23,4 @@ export const cleanupOldGalleryAuth = () => {
|
||||
// Also clear session storage
|
||||
sessionStorage.removeItem('gallery_event');
|
||||
sessionStorage.removeItem('gallery_token');
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user