web: the QR card hands out the look that made the photo
A photo's landing section can now be the QR card, and that section is the only one that hands something out: the server writes the photo's own stored look back as the app's .recipe file, at GET /api/photos/:id/preset.recipe, for any row the curator ticked into the qr slot. Nothing new is stored — the file is built from the recipe the upload already carried, so it works for a photo uploaded by the phone too. The admin pane grows a fourth checkbox and a fourth row (QR card); the row draws the download link as a scannable code, and the box is dead for a photo with no stored look. The landing's QR card now encodes the curated photo's own link instead of a mock address. The listing exposes hasPreset, never the recipe itself.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { recipeFile } from './recipeFile';
|
||||
import Fastify, { type FastifyReply, type FastifyRequest } from 'fastify';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
avatarPath,
|
||||
photoFile,
|
||||
photoPath,
|
||||
photoPreset,
|
||||
sessionUser,
|
||||
setPhotoSlots,
|
||||
setPhotoConsent,
|
||||
@@ -644,6 +646,24 @@ app.get<{ Params: { id: string } }>('/api/photos/:id/file', async (req, reply) =
|
||||
.send(data);
|
||||
});
|
||||
|
||||
// The QR card's payload: the `.recipe` file the app reads back on IMPORT, built
|
||||
// from the look the photo was uploaded with. Public like the strip, but only
|
||||
// for a row the curator ticked into the `qr` section — that checkbox is the
|
||||
// whole permission. Everything else is a 404 rather than a 403, so the route
|
||||
// cannot be used to probe which photos carry a look.
|
||||
app.get<{ Params: { id: string } }>('/api/photos/:id/preset.recipe', async (req, reply) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id) || id <= 0) return reply.status(404).send({ error: 'not_found' });
|
||||
const row = photoPreset(id);
|
||||
if (!row || row.recipe === null || !row.slots.includes('qr')) return reply.status(404).send({ error: 'not_found' });
|
||||
return reply
|
||||
.header('content-type', 'application/xml; charset=utf-8')
|
||||
.header('x-content-type-options', 'nosniff')
|
||||
.header('content-disposition', `attachment; filename="recipescam-${id}.recipe"`)
|
||||
.header('cache-control', 'public, max-age=60')
|
||||
.send(recipeFile(row.recipe));
|
||||
});
|
||||
|
||||
// The uploader's own permission switch: may this photo show on the landing
|
||||
// strip? Only the owner may flip it (an admin curates the slot, not the
|
||||
// consent), and only their own row is reachable — the user_id in the WHERE is
|
||||
|
||||
Reference in New Issue
Block a user