fix: resolve Immich 401 passthrough causing spurious login redirects

- Auth middleware now tags its 401s with code: AUTH_REQUIRED so the
  client interceptor only redirects to /login on genuine session failures,
  not on upstream API errors
- Fix /albums and album sync routes using raw encrypted API key instead
  of getImmichCredentials() (which decrypts it), causing Immich to reject
  requests with 401
- Add toast error notifications for all Immich operations in MemoriesPanel
  that previously swallowed errors silently

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jubnl
2026-04-01 21:19:53 +02:00
parent fabf5a7e26
commit 1a4c04e239
5 changed files with 30 additions and 19 deletions

View File

@@ -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' });
}
};

View File

@@ -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' });