All checks were successful
Depuis /admin/messages, un admin peut créer une issue Forgejo à partir
d'un message utilisateur : bouton « Créer un ticket » → modale pré-remplie
éditable → l'issue est créée via l'API Forgejo et le lien (#N) est
mémorisé sur le message. Première intégration Forgejo d'apps/api.
- Modèle : `forgejoIssueNumber` / `forgejoIssueUrl` sur `ContactMessage`
(migration `20260908130000_contact_message_forgejo_link`, 2 colonnes
nullables, aucun backfill).
- Config : `FORGEJO_BASE_URL` / `FORGEJO_TOKEN` / `FORGEJO_REPO`, toutes
`.optional()` — **fail-closed** (même posture que `ADMIN_JWT_SECRET`) :
bouton masqué + endpoint `503` tant que les trois ne sont pas là.
Documentées dans `.env.example` ; valeurs de test dans `ci.yml`.
- `ForgejoService` (`lib/forgejo/forgejo.service.ts`, singleton `forgejo`) :
`isConfigured()` + `createIssue({ title, body })` → `POST .../issues`,
en-tête `Authorization: token <pat>`, lit `{ number, html_url }` ;
erreur réseau / non-2xx / JSON invalide ⇒ `HttpError 502
FORGEJO_REQUEST_FAILED`.
- `admin-contact` : `POST /admin/api/contact/:id/forgejo-issue`
(`createForgejoIssueFromMessage` — ordre 503 → 404 → 409 → Forgejo →
écriture BDD ; un échec Forgejo ne touche pas la BDD) et
`GET /admin/api/contact/forgejo/status`.
- Contrat partagé : `createForgejoIssueSchema` (title 1–255, body 1–60000),
`ContactMessageAdminView` étendu, `ForgejoIntegrationStatus`, nouveaux
`ErrorCode` (`FORGEJO_NOT_CONFIGURED`=5001, `FORGEJO_REQUEST_FAILED`=5002,
`CONTACT_MESSAGE_ALREADY_LINKED`=4026, `CONTACT_MESSAGE_NOT_FOUND`=4055)
+ labels i18n.
- Front : `MessagesPage` — sonde `forgejo/status` au montage, colonne
« Ticket » (lien `#N` si lié, bouton sinon, « non configurée » sinon),
`CreateForgejoIssueModal` + helpers purs `messages.ts`
(`buildIssueTitle` / `buildIssueBody`).
- Tests : `forgejo.service.test.ts` (stub `globalThis.fetch`),
`admin-contact.test.ts` étendu (nominal / 503 / 404 / 409 / 502 +
ligne intacte / 400), `admin-messages.cy.ts` étendu (modale pré-remplie,
lien après création, masquage si non configuré).
- Docs : `backend-architecture.md`, `batch-cooking-modele.md`.
Closes #81.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
59 lines
1.7 KiB
TypeScript
59 lines
1.7 KiB
TypeScript
import { HttpError } from "@batch-cooking/error-tools";
|
|
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import {
|
|
createForgejoIssueSchema,
|
|
ErrorCode,
|
|
listContactMessagesQuerySchema,
|
|
} from "@batch-cooking/shared";
|
|
import { Router } from "express";
|
|
import { requireAdmin } from "../../middlewares/require-admin.js";
|
|
import {
|
|
createForgejoIssueFromMessage,
|
|
getForgejoStatus,
|
|
listContactMessages,
|
|
} from "./admin-contact.service.js";
|
|
|
|
/**
|
|
* Router mounted at `/admin/api/contact` (via `admin.routes.ts`) — behind
|
|
* {@link requireAdmin}. Backs the `/admin/messages` page: the read-only list
|
|
* of contact-form messages (issue #80), plus "create a Forgejo issue from
|
|
* this message" (issue #81). Still no message-mutation routes — the message
|
|
* itself stays read-only.
|
|
*/
|
|
export const adminContactRouter = Router();
|
|
|
|
adminContactRouter.get(
|
|
"/",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
res
|
|
.status(200)
|
|
.json(await listContactMessages(listContactMessagesQuerySchema.parse(req.query)));
|
|
}),
|
|
);
|
|
|
|
// Static path — declared before `/:id/...` so it's never parsed as an id.
|
|
adminContactRouter.get(
|
|
"/forgejo/status",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (_req, res) => {
|
|
res.status(200).json(getForgejoStatus());
|
|
}),
|
|
);
|
|
|
|
adminContactRouter.post(
|
|
"/:id/forgejo-issue",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id) || id <= 0) {
|
|
throw new HttpError(
|
|
400,
|
|
ErrorCode.VALIDATION_ERROR,
|
|
`Not a valid contact message id: ${req.params.id}`,
|
|
);
|
|
}
|
|
const input = createForgejoIssueSchema.parse(req.body);
|
|
res.status(200).json(await createForgejoIssueFromMessage(id, input));
|
|
}),
|
|
);
|