mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-22 06:38:05 +02:00
Connexion Facebook / LinkedIn en OAuth depuis le backoffice
Les jetons se posaient à la main dans docker-compose. Un écran Backend ›
Réseaux sociaux permet désormais de coller les identifiants de l'application,
cliquer Connecter, choisir la page, et ne plus y revenir.
- Table `social_accounts` : identifiants, jetons et cible par réseau. Tous les
secrets sont chiffrés en AES-256-GCM (`src/lib/crypto.ts`), clé
`SOCIAL_TOKEN_KEY` avec repli sur `AUTH_SECRET`.
- Routes `api/social/[network]/{connect,callback}` : `state` anti-CSRF en cookie
httpOnly, échange du code, récupération des pages administrées. Aucun jeton ne
transite par une URL — les cibles sont relistées côté serveur au moment de la
sélection.
- Facebook : jeton utilisateur longue durée puis jeton de PAGE, qui n'expire
pas. LinkedIn : jeton 60 jours, rafraîchi automatiquement si l'application a
obtenu les jetons de rafraîchissement programmatiques.
- Faute de quoi le backoffice affiche la date d'expiration, un bandeau sur le
tableau de bord à J-7 et un bouton Reconnecter.
- `social.ts` lit les identifiants via `social-accounts.ts` : base d'abord,
variables d'environnement ensuite. Les installations existantes continuent de
fonctionner sans modification.
- Le secret d'application n'est jamais renvoyé au navigateur : champ vide =
valeur conservée.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
19 files changed
+2599
-82
No files matched your search
@@ -31,6 +31,11 @@ import {
|
||||
SOCIAL_NETWORKS,
|
||||
type SocialNetwork,
|
||||
} from "@/lib/social";
|
||||
import {
|
||||
disconnectAccount,
|
||||
saveAppCredentials,
|
||||
selectTarget,
|
||||
} from "@/lib/social-accounts";
|
||||
import { normalizeWebsite } from "@/lib/member-profile";
|
||||
import { SITE_SETTING_DEFAULTS } from "@/lib/site-settings";
|
||||
import type { AppRole } from "@/types/next-auth";
|
||||
@@ -164,7 +169,7 @@ async function publishPromoShares(
|
||||
for (const network of networks) {
|
||||
if (alreadyPosted.has(network)) continue;
|
||||
|
||||
if (!isNetworkConfigured(network)) {
|
||||
if (!(await isNetworkConfigured(network))) {
|
||||
await db.insert(socialPosts).values({
|
||||
promotionId: promoId,
|
||||
network,
|
||||
@@ -1128,6 +1133,54 @@ export async function setContactStatus(formData: FormData) {
|
||||
revalidatePath("/backend");
|
||||
}
|
||||
|
||||
// ---- Réseaux sociaux : identifiants d'application et connexion ----
|
||||
export async function saveSocialApp(formData: FormData) {
|
||||
const { role } = await requireRole();
|
||||
if (!can(role, "manageSettings")) throw new Error("Accès refusé");
|
||||
|
||||
const network = String(formData.get("network")) as SocialNetwork;
|
||||
if (!SOCIAL_NETWORKS.includes(network)) return;
|
||||
|
||||
const appId = asString(formData, "appId");
|
||||
if (!appId) throw new Error("L'identifiant de l'application est requis.");
|
||||
// Champ secret laissé vide = on garde celui déjà enregistré.
|
||||
const appSecret = asString(formData, "appSecret") || null;
|
||||
|
||||
await saveAppCredentials(network, appId, appSecret);
|
||||
revalidatePath("/backend/reseaux");
|
||||
}
|
||||
|
||||
export async function selectSocialTarget(formData: FormData) {
|
||||
const { role } = await requireRole();
|
||||
if (!can(role, "manageSettings")) throw new Error("Accès refusé");
|
||||
|
||||
const network = String(formData.get("network")) as SocialNetwork;
|
||||
const targetId = asString(formData, "targetId");
|
||||
if (!SOCIAL_NETWORKS.includes(network) || !targetId) return;
|
||||
|
||||
await selectTarget(network, targetId);
|
||||
await logActivity(`Page ${SOCIAL_LABELS[network]} sélectionnée pour la publication`, "#2C6FB3");
|
||||
revalidatePath("/backend/reseaux");
|
||||
revalidatePath("/backend/promotions");
|
||||
}
|
||||
|
||||
export async function disconnectSocial(formData: FormData) {
|
||||
const { role, name } = await requireRole();
|
||||
if (!can(role, "manageSettings")) throw new Error("Accès refusé");
|
||||
|
||||
const network = String(formData.get("network")) as SocialNetwork;
|
||||
if (!SOCIAL_NETWORKS.includes(network)) return;
|
||||
|
||||
await disconnectAccount(network);
|
||||
await logActivity(
|
||||
`Compte ${SOCIAL_LABELS[network]} déconnecté par <strong>${name}</strong>`,
|
||||
"#d8472b"
|
||||
);
|
||||
revalidatePath("/backend/reseaux");
|
||||
revalidatePath("/backend/promotions");
|
||||
revalidatePath("/backend/espace");
|
||||
}
|
||||
|
||||
// ---- Sign out ----
|
||||
export async function doSignOut() {
|
||||
await signOut({ redirectTo: "/" });
|
||||
|
||||
Reference in new issue
Block a user