mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-15 16:32:03 +00:00
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:
1 parent
ca23dfa318
commit
7f159d5111
2 files changed
+62
No files matched your search
@@ -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' })
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user