mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-14 10:49:35 +02:00
Connexion réseaux : adresse de retour déduite de la requête, CSP sans upgrade en HTTP
Cliquer sur « Connecter » sans avoir enregistré l'URL publique du site renvoyait une erreur, et la redirection retombait sur https://0.0.0.0 : la directive upgrade-insecure-requests était posée même sur une page servie en HTTP, le navigateur basculait donc la navigation vers un https:// inexistant. - `publicBaseUrl()` : réglage `site_public_url`, variables d'environnement, puis origine de la requête (X-Forwarded-Host / X-Forwarded-Proto). L'adresse de retour OAuth et le lien des publications l'utilisent ; `seo-server` s'appuie dessus au lieu de dupliquer la déduction. - `src/lib/site-url.ts` : déduction pure et testée de l'origine, HTTP par défaut pour localhost et les IP nues, HTTPS pour un nom de domaine. - Middleware : `upgrade-insecure-requests` uniquement quand la page est servie en HTTPS. - Page Réseaux : l'adresse détectée est annoncée comme valeur par défaut, avec le rappel que Facebook et LinkedIn exigent du HTTPS sur un domaine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
10 files changed
+157
-41
No files matched your search
+4
-19
@@ -1,28 +1,13 @@
|
||||
import { headers } from "next/headers";
|
||||
import { siteUrl } from "@/lib/social-accounts";
|
||||
import { publicBaseUrl } from "@/lib/social-accounts";
|
||||
|
||||
/**
|
||||
* URL publique du site pour le référencement (canoniques, sitemap, JSON-LD).
|
||||
*
|
||||
* Priorité au réglage `site_public_url` (Backend › Réseaux sociaux) puis aux
|
||||
* variables d'environnement, via `siteUrl()`. En dernier recours, l'URL est
|
||||
* déduite des en-têtes de la requête (reverse proxy compris) : le site reste
|
||||
* ainsi correctement référencé tant que le réglage n'est pas renseigné.
|
||||
* variables d'environnement ; en dernier recours, l'URL est déduite des
|
||||
* en-têtes de la requête (reverse proxy compris). Voir `publicBaseUrl()`.
|
||||
*/
|
||||
export async function publicBaseUrl(): Promise<string> {
|
||||
const configured = await siteUrl().catch(() => "");
|
||||
if (configured) return configured;
|
||||
try {
|
||||
const h = await headers();
|
||||
const host = h.get("x-forwarded-host") ?? h.get("host") ?? "";
|
||||
if (!host) return "";
|
||||
const protocol = h.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https");
|
||||
return `${protocol}://${host}`.replace(/\/+$/, "");
|
||||
} catch {
|
||||
// Hors requête (génération de robots/sitemap au build par exemple).
|
||||
return "";
|
||||
}
|
||||
}
|
||||
export { publicBaseUrl };
|
||||
|
||||
/** `metadataBase` pour Next : indéfini si aucune URL n'est connue. */
|
||||
export async function metadataBase(): Promise<URL | undefined> {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Déduction de l'origine publique du site à partir des en-têtes d'une requête.
|
||||
*
|
||||
* Sans dépendance à la base ni à Next : utilisable partout et testable. Sert de
|
||||
* repli quand aucune URL publique n'est enregistrée dans Backend › Réseaux
|
||||
* sociaux : derrière Caddy ou Nginx Proxy Manager, `X-Forwarded-Host` et
|
||||
* `X-Forwarded-Proto` décrivent l'adresse vue par le visiteur.
|
||||
*/
|
||||
|
||||
type HeaderReader = { get(name: string): string | null };
|
||||
|
||||
const LOCAL_HOSTS = /^(localhost|.*\.localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[::1\]|\[::\])$/i;
|
||||
const IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
||||
|
||||
function first(value: string | null): string {
|
||||
return (value ?? "").split(",")[0]?.trim() ?? "";
|
||||
}
|
||||
|
||||
/** Nom d'hôte sans port, en minuscules. */
|
||||
function hostname(host: string): string {
|
||||
const match = /^(\[[^\]]*\]|[^:]+)/.exec(host);
|
||||
return (match?.[1] ?? host).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sans `X-Forwarded-Proto`, on présume HTTPS pour un nom de domaine — c'est le
|
||||
* cas de tout déploiement réel — et HTTP pour une adresse locale ou une IP nue,
|
||||
* qu'aucun certificat ne couvre.
|
||||
*/
|
||||
export function defaultProtocol(host: string): "http" | "https" {
|
||||
const name = hostname(host);
|
||||
if (!name || LOCAL_HOSTS.test(name) || IPV4.test(name)) return "http";
|
||||
return "https";
|
||||
}
|
||||
|
||||
/**
|
||||
* Origine (`https://pleinr.example.fr`) vue par le visiteur, ou chaîne vide si
|
||||
* la requête ne porte aucun hôte.
|
||||
*/
|
||||
export function originFromHeaders(headers: HeaderReader): string {
|
||||
const host = first(headers.get("x-forwarded-host")) || first(headers.get("host"));
|
||||
if (!host || /[\s/\\]/.test(host)) return "";
|
||||
const forwarded = first(headers.get("x-forwarded-proto")).toLowerCase();
|
||||
const protocol = forwarded === "https" || forwarded === "http" ? forwarded : defaultProtocol(host);
|
||||
try {
|
||||
return new URL(`${protocol}://${host}`).origin;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { headers } from "next/headers";
|
||||
import { db } from "@/db";
|
||||
import { siteSettings, socialAccounts, type SocialAccount, type SocialNetwork } from "@/db/schema";
|
||||
import { decryptSecret, encryptSecret } from "./crypto";
|
||||
import { originFromHeaders } from "./site-url";
|
||||
|
||||
/**
|
||||
* Configuration des comptes Facebook / LinkedIn.
|
||||
@@ -43,8 +45,29 @@ export async function siteUrl(): Promise<string> {
|
||||
return (stored || env("NEXT_PUBLIC_SITE_URL") || env("AUTH_URL")).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* URL publique effective : le réglage s'il existe, sinon l'adresse par laquelle
|
||||
* la requête en cours est arrivée (en-têtes du reverse proxy compris). Chaîne
|
||||
* vide hors requête si rien n'est configuré.
|
||||
*/
|
||||
export async function publicBaseUrl(): Promise<string> {
|
||||
const configured = await siteUrl().catch(() => "");
|
||||
if (configured) return configured;
|
||||
try {
|
||||
return originFromHeaders(await headers());
|
||||
} catch {
|
||||
// Hors requête (génération de robots/sitemap au build par exemple).
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adresse de retour OAuth. Elle doit être identique au clic sur « Connecter »
|
||||
* et au retour du réseau : les deux passent par le même hôte, la déduction
|
||||
* depuis la requête est donc stable.
|
||||
*/
|
||||
export async function redirectUri(network: SocialNetwork): Promise<string> {
|
||||
return `${await siteUrl()}/api/social/${network}/callback`;
|
||||
return `${await publicBaseUrl()}/api/social/${network}/callback`;
|
||||
}
|
||||
|
||||
// ---- Lecture ----
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
import {
|
||||
refreshLinkedInIfNeeded,
|
||||
resolveCredentials,
|
||||
siteUrl,
|
||||
publicBaseUrl,
|
||||
type SocialNetwork,
|
||||
} from "./social-accounts";
|
||||
import { memberPath } from "./seo";
|
||||
@@ -42,7 +42,7 @@ export type PromoForSharing = {
|
||||
};
|
||||
|
||||
export async function promoLink(promo: PromoForSharing): Promise<string | null> {
|
||||
const base = await siteUrl();
|
||||
const base = await publicBaseUrl();
|
||||
if (!base) return null;
|
||||
return promo.memberId
|
||||
? `${base}${memberPath({ id: promo.memberId, name: promo.memberName ?? "", city: promo.memberCity })}`
|
||||
|
||||
Reference in new issue
Block a user