URL publique du site réglable depuis le backoffice

Connecter un réseau social imposait de poser NEXT_PUBLIC_SITE_URL dans
docker-compose et de redémarrer le conteneur. L'écran Réseaux sociaux porte
désormais le champ, pré-rempli avec l'adresse par laquelle l'administrateur
consulte la page.

- Nouveau réglage `site_public_url` ; `siteUrl()` lit la base puis retombe sur
  les variables d'environnement, donc les déploiements actuels ne bougent pas.
- `siteUrl()`, `redirectUri()`, `authorizeUrl()`, `promoLink()` et
  `buildPromoMessage()` deviennent asynchrones.
- Seule l'origine est conservée : le schéma est ajouté s'il manque, un chemin
  est retiré, et une adresse invalide revient sur l'écran avec un message au
  lieu d'une page d'erreur brute.
- `saveSiteSettings` saute cette clé : le formulaire Paramètres ne la contient
  pas et l'aurait écrasée à chaque enregistrement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
Claude committed 2026-07-25 08:28:04 +00:00
1 parent db361051e6
commit e86c67c17e
9 files changed
+146 -37

No files matched your search

+42
View File
@@ -952,6 +952,9 @@ export async function saveSiteSettings(formData: FormData) {
const updatedAt = new Date();
for (const key of Object.keys(SITE_SETTING_DEFAULTS)) {
// Réglée sur l'écran Réseaux sociaux : ce formulaire ne la contient pas et
// l'écraserait avec une chaîne vide.
if (key === "site_public_url") continue;
await db
.insert(siteSettings)
.values({ key, value: asString(formData, key), updatedAt })
@@ -1133,6 +1136,45 @@ export async function setContactStatus(formData: FormData) {
revalidatePath("/backend");
}
// ---- URL publique de l'application ----
// Sert à l'adresse de retour OAuth et au lien inséré dans les publications.
export async function saveSitePublicUrl(formData: FormData) {
const { role } = await requireRole();
if (!can(role, "manageSettings")) throw new Error("Accès refusé");
const raw = asString(formData, "sitePublicUrl");
let value = "";
if (raw) {
// Le schéma est facultatif à la saisie ; seul l'origine est conservée, un
// chemin ou des paramètres casseraient l'adresse de retour OAuth.
const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
let parsed: URL | null = null;
try {
parsed = new URL(candidate);
} catch {
parsed = null;
}
if (!parsed || !["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) {
// Message rendu par le bandeau de l'écran, plutôt qu'une page d'erreur brute.
redirect(
`/backend/reseaux?error=${encodeURIComponent(
"Adresse invalide : indiquez par exemple https://pleinr.example.fr"
)}`
);
}
value = parsed.origin;
}
const updatedAt = new Date();
await db
.insert(siteSettings)
.values({ key: "site_public_url", value, updatedAt })
.onConflictDoUpdate({ target: siteSettings.key, set: { value, updatedAt } });
revalidatePath("/backend/reseaux");
revalidatePath("/backend/parametres");
}
// ---- Réseaux sociaux : identifiants d'application et connexion ----
export async function saveSocialApp(formData: FormData) {
const { role } = await requireRole();