feat(immich): read Immich through a per-user read-only proxy
The browser cannot talk to Immich directly: the key must stay out of it, COEP blocks the origin, and the app has no place to keep a key per user. So the backend keeps it. `src/immich.ts` holds the whole surface — the user's servers live in a JSON column on `users` (additive migration), and every route reads the key from there and never takes a URL from the browser except when probing one. Albums, a page of assets, a thumbnail and an original, all behind the normal session check. `probe` is the only route that touches a URL the client named, and it validates it first (http/https only, no credentials, no path, no query, no hash) so the browser cannot turn the backend into a proxy to an arbitrary host. The key is masked down to its last four characters everywhere it comes back out, and no log line carries it. The share-link path is the same routes with `type: 'share'`, whose key travels as `?key=`, so there is one code path per call rather than two. test/immich.mjs runs a fake Immich on loopback — two keys with different albums, one of them without `asset.download` — and checks 59 things including that neither the responses nor the log leak a key.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { recipeFile } from './recipeFile';
|
||||
import { sendVerificationMail } from './mailer';
|
||||
import { placeName } from './place';
|
||||
import { immichRoutes } from './immich';
|
||||
import Fastify, { type FastifyReply, type FastifyRequest } from 'fastify';
|
||||
import { exec, spawn, spawnSync } from 'node:child_process';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
@@ -1164,6 +1165,15 @@ app.patch<{ Params: { id: string } }>('/api/admin/photos/:id', async (req, reply
|
||||
return reply.status(200).send({ id, slots: set });
|
||||
});
|
||||
|
||||
// ---- Immich, as a second photo source ---------------------------------------
|
||||
// The LIBRARY module may carry Immich albums beside the folders on disk. The
|
||||
// browser cannot talk to Immich itself (the SPA is served with COEP require-corp
|
||||
// and an Immich key must not reach it), so the routes behind /api/immich/*
|
||||
// proxy it, read-only, with each account's own key kept in the users table.
|
||||
// Nothing here is required for the rest of the API: with no server added, the
|
||||
// dialog is the only thing that ever calls these.
|
||||
immichRoutes(app, requireMember);
|
||||
|
||||
// ---- backup / restore -----------------------------------------------------
|
||||
// The deployment's whole state is DATA_DIR: one SQLite file plus the two media
|
||||
// folders. The archive is a plain tar.gz of exactly those three, which makes
|
||||
|
||||
Reference in New Issue
Block a user