Immich : route admin pour lire le lien d'une fiche

Le sélecteur doit marquer la personne Immich déjà associée. L'identifiant ne
figure pas dans le graphe public : GET /trees/:id/immich/links/:personId le
renvoie aux seuls administrateurs de l'arbre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Opus 5.5 committed 2026-10-16 18:13:26 +02:00
1 parent ca23dfa318
commit 7f159d5111
2 files changed
+62

No files matched your search

+28
View File
@@ -26,6 +26,7 @@ import { exceedsStorageQuota, getStorageRoot, prepareUploadedFile, requireAdminM
// administrateurs, et ne parlent jamais à l'instance d'un autre arbre.
const treeParamSchema = z.object({ id: z.string().min(1) })
const treePersonParamsSchema = z.object({ id: z.string().min(1), personId: z.string().min(1) })
const assignSchema = z.object({
personId: z.string().min(1),
@@ -324,6 +325,33 @@ export const immichRoutes: FastifyPluginAsync = async (app) => {
return reply.send(thumbnail)
})
// Personne Immich déjà associée à une fiche, pour la marquer dans le sélecteur. Réservé aux
// administrateurs : l'identifiant n'entre jamais dans le graphe public.
app.get('/trees/:id/immich/links/:personId', async (request, reply) => {
if (!request.actor || request.actor.kind !== 'user') {
return reply.code(401).send({ error: 'authentication_required' })
}
const params = treePersonParamsSchema.safeParse(request.params)
if (!params.success) {
return reply.code(400).send({ error: 'invalid_params' })
}
if (!(await requireAdminMembership(app, request.actor.userId, params.data.id))) {
return reply.code(403).send({ error: 'forbidden' })
}
const person = await app.prisma.person.findFirst({
where: { id: params.data.personId, treeId: params.data.id, deletedAt: null },
select: { immichPersonId: true },
})
if (!person) {
return reply.code(404).send({ error: 'person_not_found' })
}
return reply.send({ immichPersonId: person.immichPersonId })
})
app.post('/trees/:id/immich/assign', async (request, reply) => {
if (!request.actor || request.actor.kind !== 'user') {
return reply.code(401).send({ error: 'authentication_required' })
+34
View File
@@ -997,3 +997,37 @@ describe('Immich integration: durcissement des vignettes et des réponses', () =
await app.close()
})
})
describe('Immich integration: lien d’une fiche', () => {
it('tells an administrator which Immich person a file is linked to, and refuses others', async () => {
const app = createApp()
const token = await createUserToken(app)
prismaMock.person.findFirst.mockResolvedValue({ immichPersonId })
const linked = await app.inject({
method: 'GET',
url: '/trees/tree-1/immich/links/person-1',
headers: { authorization: `Bearer ${token}` },
})
expect(linked.statusCode).toBe(200)
expect(linked.json()).toEqual({ immichPersonId })
const memberToken = await createUserToken(app, 'user-member')
prismaMock.treeMembership.findUnique.mockResolvedValueOnce({ treeId: 'tree-1', userId: 'user-member', role: 'member', tree: { deletedAt: null } })
const member = await app.inject({
method: 'GET',
url: '/trees/tree-1/immich/links/person-1',
headers: { authorization: `Bearer ${memberToken}` },
})
expect(member.statusCode).toBe(403)
prismaMock.person.findFirst.mockResolvedValue(null)
const missing = await app.inject({
method: 'GET',
url: '/trees/tree-1/immich/links/person-unknown',
headers: { authorization: `Bearer ${token}` },
})
expect(missing.statusCode).toBe(404)
await app.close()
})
})