Merge pull request #290 from jubnl/main
This commit is contained in:
@@ -25,7 +25,7 @@ apiClient.interceptors.request.use(
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
if (error.response?.status === 401 && (error.response?.data as { code?: string } | undefined)?.code === 'AUTH_REQUIRED') {
|
||||
if (!window.location.pathname.includes('/login') && !window.location.pathname.includes('/register')) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import apiClient from '../../api/client'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { getAuthUrl } from '../../api/authUrl'
|
||||
import { useToast } from '../shared/Toast'
|
||||
|
||||
function ImmichImg({ baseUrl, style, loading }: { baseUrl: string; style?: React.CSSProperties; loading?: 'lazy' | 'eager' }) {
|
||||
const [src, setSrc] = useState('')
|
||||
@@ -40,6 +41,7 @@ interface MemoriesPanelProps {
|
||||
|
||||
export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
const toast = useToast()
|
||||
const currentUser = useAuthStore(s => s.user)
|
||||
|
||||
const [connected, setConnected] = useState(false)
|
||||
@@ -81,7 +83,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
try {
|
||||
const res = await apiClient.get('/integrations/immich/albums')
|
||||
setAlbums(res.data.albums || [])
|
||||
} catch { setAlbums([]) }
|
||||
} catch { setAlbums([]); toast.error(t('memories.error.loadAlbums')) }
|
||||
finally { setAlbumsLoading(false) }
|
||||
}
|
||||
|
||||
@@ -94,14 +96,14 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
const linksRes = await apiClient.get(`/integrations/immich/trips/${tripId}/album-links`)
|
||||
const newLink = (linksRes.data.links || []).find((l: any) => l.immich_album_id === albumId)
|
||||
if (newLink) await syncAlbum(newLink.id)
|
||||
} catch {}
|
||||
} catch { toast.error(t('memories.error.linkAlbum')) }
|
||||
}
|
||||
|
||||
const unlinkAlbum = async (linkId: number) => {
|
||||
try {
|
||||
await apiClient.delete(`/integrations/immich/trips/${tripId}/album-links/${linkId}`)
|
||||
loadAlbumLinks()
|
||||
} catch {}
|
||||
} catch { toast.error(t('memories.error.unlinkAlbum')) }
|
||||
}
|
||||
|
||||
const syncAlbum = async (linkId: number) => {
|
||||
@@ -110,7 +112,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
await apiClient.post(`/integrations/immich/trips/${tripId}/album-links/${linkId}/sync`)
|
||||
await loadAlbumLinks()
|
||||
await loadPhotos()
|
||||
} catch {}
|
||||
} catch { toast.error(t('memories.error.syncAlbum')) }
|
||||
finally { setSyncing(null) }
|
||||
}
|
||||
|
||||
@@ -178,6 +180,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
setPickerPhotos(res.data.assets || [])
|
||||
} catch {
|
||||
setPickerPhotos([])
|
||||
toast.error(t('memories.error.loadPhotos'))
|
||||
} finally {
|
||||
setPickerLoading(false)
|
||||
}
|
||||
@@ -206,7 +209,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
})
|
||||
setShowPicker(false)
|
||||
loadInitial()
|
||||
} catch {}
|
||||
} catch { toast.error(t('memories.error.addPhotos')) }
|
||||
}
|
||||
|
||||
// ── Remove photo ──────────────────────────────────────────────────────────
|
||||
@@ -215,7 +218,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
try {
|
||||
await apiClient.delete(`/integrations/immich/trips/${tripId}/photos/${assetId}`)
|
||||
setTripPhotos(prev => prev.filter(p => p.immich_asset_id !== assetId))
|
||||
} catch {}
|
||||
} catch { toast.error(t('memories.error.removePhoto')) }
|
||||
}
|
||||
|
||||
// ── Toggle sharing ────────────────────────────────────────────────────────
|
||||
@@ -226,7 +229,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
||||
setTripPhotos(prev => prev.map(p =>
|
||||
p.immich_asset_id === assetId ? { ...p, shared: shared ? 1 : 0 } : p
|
||||
))
|
||||
} catch {}
|
||||
} catch { toast.error(t('memories.error.toggleSharing')) }
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1363,6 +1363,14 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'memories.confirmShareTitle': 'Share with trip members?',
|
||||
'memories.confirmShareHint': '{count} photos will be visible to all members of this trip. You can make individual photos private later.',
|
||||
'memories.confirmShareButton': 'Share photos',
|
||||
'memories.error.loadAlbums': 'Failed to load albums',
|
||||
'memories.error.linkAlbum': 'Failed to link album',
|
||||
'memories.error.unlinkAlbum': 'Failed to unlink album',
|
||||
'memories.error.syncAlbum': 'Failed to sync album',
|
||||
'memories.error.loadPhotos': 'Failed to load photos',
|
||||
'memories.error.addPhotos': 'Failed to add photos',
|
||||
'memories.error.removePhoto': 'Failed to remove photo',
|
||||
'memories.error.toggleSharing': 'Failed to update sharing',
|
||||
|
||||
// Collab Addon
|
||||
'collab.tabs.chat': 'Chat',
|
||||
|
||||
@@ -16,7 +16,7 @@ const authenticate = (req: Request, res: Response, next: NextFunction): void =>
|
||||
const token = extractToken(req);
|
||||
|
||||
if (!token) {
|
||||
res.status(401).json({ error: 'Access token required' });
|
||||
res.status(401).json({ error: 'Access token required', code: 'AUTH_REQUIRED' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ const authenticate = (req: Request, res: Response, next: NextFunction): void =>
|
||||
'SELECT id, username, email, role FROM users WHERE id = ?'
|
||||
).get(decoded.id) as User | undefined;
|
||||
if (!user) {
|
||||
res.status(401).json({ error: 'User not found' });
|
||||
res.status(401).json({ error: 'User not found', code: 'AUTH_REQUIRED' });
|
||||
return;
|
||||
}
|
||||
(req as AuthRequest).user = user;
|
||||
next();
|
||||
} catch (err: unknown) {
|
||||
res.status(401).json({ error: 'Invalid or expired token' });
|
||||
res.status(401).json({ error: 'Invalid or expired token', code: 'AUTH_REQUIRED' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -359,12 +359,12 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res:
|
||||
// List user's Immich albums
|
||||
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any;
|
||||
if (!user?.immich_url || !user?.immich_api_key) return res.status(400).json({ error: 'Immich not configured' });
|
||||
const creds = getImmichCredentials(authReq.user.id);
|
||||
if (!creds) return res.status(400).json({ error: 'Immich not configured' });
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${user.immich_url}/api/albums`, {
|
||||
headers: { 'x-api-key': user.immich_api_key, 'Accept': 'application/json' },
|
||||
const resp = await fetch(`${creds.immich_url}/api/albums`, {
|
||||
headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch albums' });
|
||||
@@ -431,12 +431,12 @@ router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req:
|
||||
.get(linkId, tripId, authReq.user.id) as any;
|
||||
if (!link) return res.status(404).json({ error: 'Album link not found' });
|
||||
|
||||
const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any;
|
||||
if (!user?.immich_url || !user?.immich_api_key) return res.status(400).json({ error: 'Immich not configured' });
|
||||
const creds = getImmichCredentials(authReq.user.id);
|
||||
if (!creds) return res.status(400).json({ error: 'Immich not configured' });
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${user.immich_url}/api/albums/${link.immich_album_id}`, {
|
||||
headers: { 'x-api-key': user.immich_api_key, 'Accept': 'application/json' },
|
||||
const resp = await fetch(`${creds.immich_url}/api/albums/${link.immich_album_id}`, {
|
||||
headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch album' });
|
||||
|
||||
Reference in New Issue
Block a user