mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-11 17:27:54 +02:00
Affiche les coordonnées du référent aux visiteurs connectés
L'annuaire, les pages métier et la fiche adhérent montrent désormais la personne à joindre chez l'adhérent — nom, prénom, ligne directe — mais uniquement à un visiteur connecté. Trois colonnes sur `members` (`contact_first_name`, `contact_last_name`, `contact_phone`), saisies dans les deux formulaires de fiche via le composant partagé `MemberContactFields`. Le filtrage ne repose pas sur du CSS ni sur un rendu conditionnel tardif : aucune requête publique ne lit ces colonnes. Seules `getMemberContacts()` et `getMemberContact()` les rapatrient, et les pages ne les appellent qu'après un `getSession()` positif. Un visiteur anonyme ne reçoit donc rien — ni dans le HTML, ni dans le JSON-LD, ni dans les props du composant client de l'annuaire. `tests/security.test.ts` verrouille cette séparation. `src/lib/member-contact.ts` reste pur (composition du nom, lien `tel:`) et est couvert par `tests/member-contact.test.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012TAK3c4jAUqxVMzy746wWQ
This commit is contained in:
20 files changed
+1854
-7
No files matched your search
@@ -0,0 +1,35 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { contactName, memberContact, telHref } from "../src/lib/member-contact";
|
||||
|
||||
test("le référent joint prénom et nom", () => {
|
||||
assert.equal(contactName({ contactFirstName: "Marie", contactLastName: "Durand" }), "Marie Durand");
|
||||
assert.equal(contactName({ contactFirstName: " Marie ", contactLastName: " Durand " }), "Marie Durand");
|
||||
assert.equal(contactName({ contactFirstName: "Marie" }), "Marie");
|
||||
assert.equal(contactName({ contactLastName: "Durand" }), "Durand");
|
||||
assert.equal(contactName({}), null);
|
||||
assert.equal(contactName({ contactFirstName: " ", contactLastName: null }), null);
|
||||
});
|
||||
|
||||
test("un référent sans nom ni téléphone n'existe pas", () => {
|
||||
assert.equal(memberContact({}), null);
|
||||
assert.equal(memberContact({ contactFirstName: " ", contactPhone: "" }), null);
|
||||
assert.deepEqual(memberContact({ contactPhone: "03 83 24 10 10" }), {
|
||||
name: null,
|
||||
phone: "03 83 24 10 10",
|
||||
});
|
||||
assert.deepEqual(memberContact({ contactLastName: "Durand", contactPhone: null }), {
|
||||
name: "Durand",
|
||||
phone: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("le lien tel: ne garde que les chiffres et l'indicatif", () => {
|
||||
assert.equal(telHref("03 83 24 10 10"), "tel:0383241010");
|
||||
assert.equal(telHref("+33 3 83 24 10 10"), "tel:+33383241010");
|
||||
assert.equal(telHref("03.83.24.10.10"), "tel:0383241010");
|
||||
assert.equal(telHref("sur rendez-vous"), null);
|
||||
assert.equal(telHref(null), null);
|
||||
assert.equal(telHref(" "), null);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { activityNodes, sanitizeActivityMessage } from "../src/lib/activity";
|
||||
@@ -109,3 +110,46 @@ describe("Chiffrement des secrets réseaux", () => {
|
||||
assert.equal(tryDecryptSecret("pas-un-secret-chiffré"), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Coordonnées du référent — jamais dans les lectures publiques", () => {
|
||||
const source = readFileSync(new URL("../src/lib/queries.ts", import.meta.url), "utf8");
|
||||
|
||||
/** Corps d'une fonction exportée de `queries.ts`, jusqu'à la suivante. */
|
||||
function bodyOf(name: string): string {
|
||||
const start = source.indexOf(`export async function ${name}(`);
|
||||
assert.notEqual(start, -1, `${name} introuvable dans src/lib/queries.ts`);
|
||||
const next = source.indexOf("\nexport ", start + 1);
|
||||
return source.slice(start, next === -1 ? source.length : next);
|
||||
}
|
||||
|
||||
// Nom, prénom et ligne directe de l'adhérent ne sont montrés qu'à un visiteur
|
||||
// connecté. La garantie tient à une seule chose : aucune requête servant une
|
||||
// page publique ne les rapatrie — sinon ils partiraient dans le HTML, le
|
||||
// JSON-LD ou les props du composant client de l'annuaire.
|
||||
const publicReads = [
|
||||
"getActiveMembersWithCategory",
|
||||
"getActiveMembersByCategory",
|
||||
"getRotatingActiveMembers",
|
||||
"getPublicMember",
|
||||
"getLivePromotions",
|
||||
];
|
||||
|
||||
for (const name of publicReads) {
|
||||
it(`${name} ne lit pas le référent`, () => {
|
||||
const body = bodyOf(name);
|
||||
for (const column of ["contactFirstName", "contactLastName", "contactPhone"]) {
|
||||
assert.ok(
|
||||
!body.includes(column),
|
||||
`${name} sélectionne ${column} : ces coordonnées deviendraient publiques.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("les lectures réservées existent et sont séparées", () => {
|
||||
for (const name of ["getMemberContacts", "getMemberContact"]) {
|
||||
const body = bodyOf(name);
|
||||
assert.ok(body.includes("contactPhone"), `${name} devrait lire le référent`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user