Checkpoint: v9c : Correction déploiement VPS - migration DB (colonnes login/firstName/lastName/isActive), correction JWT name vide, protection OAuth Invalid URL, seed-admin avec openId automatique

This commit is contained in:
Manus
2026-04-21 13:00:55 -04:00
parent 971449de41
commit 8c0c75fb17
7 changed files with 126 additions and 31 deletions

68
Dockerfile Normal file
View File

@@ -0,0 +1,68 @@
FROM node:22-alpine AS builder
WORKDIR /app
# Installer pnpm
RUN npm install -g pnpm
# Copier les fichiers de dépendances
COPY package.json pnpm-lock.yaml ./
COPY patches/ ./patches/
# Installer toutes les dépendances (y compris devDependencies pour le build)
RUN pnpm install --frozen-lockfile
# Copier les sources
COPY . .
# Variables d'environnement pour le build Vite (frontend)
# Ces valeurs sont overridées par les ARG du docker-compose au moment du build
ARG VITE_APP_ID=""
ARG VITE_OAUTH_PORTAL_URL=""
ARG VITE_APP_TITLE="SONUM"
ARG VITE_APP_LOGO=""
ARG VITE_ANALYTICS_ENDPOINT=""
ARG VITE_ANALYTICS_WEBSITE_ID=""
ARG VITE_FRONTEND_FORGE_API_KEY=""
ARG VITE_FRONTEND_FORGE_API_URL=""
ENV VITE_APP_ID=$VITE_APP_ID
ENV VITE_OAUTH_PORTAL_URL=$VITE_OAUTH_PORTAL_URL
ENV VITE_APP_TITLE=$VITE_APP_TITLE
ENV VITE_APP_LOGO=$VITE_APP_LOGO
ENV VITE_ANALYTICS_ENDPOINT=$VITE_ANALYTICS_ENDPOINT
ENV VITE_ANALYTICS_WEBSITE_ID=$VITE_ANALYTICS_WEBSITE_ID
ENV VITE_FRONTEND_FORGE_API_KEY=$VITE_FRONTEND_FORGE_API_KEY
ENV VITE_FRONTEND_FORGE_API_URL=$VITE_FRONTEND_FORGE_API_URL
# Build : Vite (frontend → dist/public) + esbuild (serveur → dist/index.js)
RUN pnpm build
# ── Stage production ──────────────────────────────────────────────────────────
FROM node:22-alpine AS production
WORKDIR /app
RUN npm install -g pnpm
# Copier les fichiers nécessaires
COPY package.json pnpm-lock.yaml ./
COPY patches/ ./patches/
# Installer toutes les dépendances (vite est requis au runtime par le serveur Express)
RUN pnpm install --frozen-lockfile
# Copier les artefacts de build
COPY --from=builder /app/dist ./dist
# Copier les fichiers de configuration et le seed
COPY drizzle.config.ts ./
COPY drizzle/ ./drizzle/
COPY seed-admin.mjs ./
COPY shared/ ./shared/
COPY server/ ./server/
EXPOSE 3000
# Démarrer le serveur et exécuter le seed admin
CMD node seed-admin.mjs && node dist/index.js

BIN
client/public/logoFEHAP.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -4,6 +4,10 @@ export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
export const getLoginUrl = () => { export const getLoginUrl = () => {
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL; const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
const appId = import.meta.env.VITE_APP_ID; const appId = import.meta.env.VITE_APP_ID;
// Si OAUTH_PORTAL_URL n'est pas configuré (déploiement sans OAuth Manus), retourner chaîne vide
if (!oauthPortalUrl) return "";
const redirectUri = `${window.location.origin}/api/oauth/callback`; const redirectUri = `${window.location.origin}/api/oauth/callback`;
const state = btoa(redirectUri); const state = btoa(redirectUri);

View File

@@ -2,11 +2,13 @@ import { getLoginUrl } from "@/const";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { KeyRound, ExternalLink } from "lucide-react"; import { KeyRound, ExternalLink } from "lucide-react";
const FEHAP_LOGO = "/manus-storage/logoFEHAP_69ddd0ee.PNG"; const FEHAP_LOGO = "/logoFEHAP.PNG";
const SANTINOVA_LOGO_TEXT = "Santinova Soft"; const SANTINOVA_LOGO_TEXT = "Santinova Soft";
export default function Login() { export default function Login() {
const [, navigate] = useLocation(); const [, navigate] = useLocation();
const loginUrl = getLoginUrl();
const oauthEnabled = !!loginUrl;
return ( return (
<div className="min-h-screen flex bg-background"> <div className="min-h-screen flex bg-background">
@@ -78,29 +80,33 @@ export default function Login() {
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
{/* Connexion via espace adhérent FEHAP */} {/* Connexion via espace adhérent FEHAP — masqué si OAuth non configuré */}
<a {oauthEnabled && (
href={getLoginUrl()} <a
className="group flex items-center gap-4 p-5 bg-primary text-white rounded-2xl shadow-md hover:bg-primary/90 transition-all hover:shadow-lg hover:-translate-y-0.5" href={loginUrl}
> className="group flex items-center gap-4 p-5 bg-primary text-white rounded-2xl shadow-md hover:bg-primary/90 transition-all hover:shadow-lg hover:-translate-y-0.5"
<div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center flex-shrink-0"> >
<ExternalLink size={20} className="text-white" /> <div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center flex-shrink-0">
</div> <ExternalLink size={20} className="text-white" />
<div className="flex-1 text-left">
<div className="font-semibold text-base">Espace adhérent FEHAP</div>
<div className="text-sm text-white/75 mt-0.5">
Connexion via votre compte FEHAP existant
</div> </div>
</div> <div className="flex-1 text-left">
<span className="text-white/50 group-hover:text-white/80 transition-colors text-lg">→</span> <div className="font-semibold text-base">Espace adhérent FEHAP</div>
</a> <div className="text-sm text-white/75 mt-0.5">
Connexion via votre compte FEHAP existant
</div>
</div>
<span className="text-white/50 group-hover:text-white/80 transition-colors text-lg">→</span>
</a>
)}
{/* Séparateur */} {/* Séparateur — affiché seulement si OAuth activé */}
<div className="flex items-center gap-3 py-1"> {oauthEnabled && (
<div className="flex-1 h-px bg-border" /> <div className="flex items-center gap-3 py-1">
<span className="text-xs text-muted-foreground font-medium">ou</span> <div className="flex-1 h-px bg-border" />
<div className="flex-1 h-px bg-border" /> <span className="text-xs text-muted-foreground font-medium">ou</span>
</div> <div className="flex-1 h-px bg-border" />
</div>
)}
{/* Connexion locale */} {/* Connexion locale */}
<button <button

View File

@@ -5,7 +5,7 @@ import { useLocation } from "wouter";
import { toast } from "sonner"; import { toast } from "sonner";
import { Eye, EyeOff, Lock, User, ArrowLeft, ExternalLink } from "lucide-react"; import { Eye, EyeOff, Lock, User, ArrowLeft, ExternalLink } from "lucide-react";
const FEHAP_LOGO = "/manus-storage/logoFEHAP_69ddd0ee.PNG"; const FEHAP_LOGO = "/logoFEHAP.PNG";
const SANTINOVA_LOGO_TEXT = "Santinova Soft"; const SANTINOVA_LOGO_TEXT = "Santinova Soft";
export default function LoginLocal() { export default function LoginLocal() {

View File

@@ -4,22 +4,33 @@
*/ */
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
const conn = await mysql.createConnection(process.env.DATABASE_URL); const conn = await mysql.createConnection(process.env.DATABASE_URL);
// Vérifier si adminItinova existe déjà // Vérifier si adminItinova existe déjà
const [rows] = await conn.execute("SELECT id FROM users WHERE login = 'adminItinova' LIMIT 1"); const [rows] = await conn.execute("SELECT id, openId FROM users WHERE login = 'adminItinova' LIMIT 1");
if (rows.length > 0) { if (rows.length > 0) {
console.log('Compte adminItinova déjà existant, seed ignoré.'); const existing = rows[0];
// Si openId est null, le corriger pour permettre la connexion JWT
if (!existing.openId) {
const openId = `local:adminItinova:${randomUUID()}`;
await conn.execute("UPDATE users SET openId = ? WHERE id = ?", [openId, existing.id]);
console.log(`Compte adminItinova : openId corrigé (id=${existing.id})`);
} else {
console.log('Compte adminItinova déjà existant, seed ignoré.');
}
await conn.end(); await conn.end();
process.exit(0); process.exit(0);
} }
// Créer l'utilisateur adminItinova // Créer l'utilisateur adminItinova avec un openId local unique
const openId = `local:adminItinova:${randomUUID()}`;
const [result] = await conn.execute( const [result] = await conn.execute(
`INSERT INTO users (login, email, firstName, lastName, name, role, sonumRole, isActive, loginMethod, cguAccepted, lastSignedIn, createdAt, updatedAt) `INSERT INTO users (login, email, firstName, lastName, name, openId, role, sonumRole, isActive, loginMethod, cguAccepted, lastSignedIn, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, 'admin', 'gestionnaire', 1, 'local', 1, NOW(), NOW(), NOW())`, VALUES (?, ?, ?, ?, ?, ?, 'admin', 'gestionnaire', 1, 'local', 1, NOW(), NOW(), NOW())`,
['adminItinova', 'adminItinova@santinova-soft.org', 'Admin', 'SONUM', 'Admin SONUM'] ['adminItinova', 'adminItinova@santinova-soft.org', 'Admin', 'SONUM', 'Admin SONUM', openId]
); );
const userId = result.insertId; const userId = result.insertId;
@@ -31,5 +42,5 @@ await conn.execute(
[userId, hash] [userId, hash]
); );
console.log(`Compte adminItinova créé avec succès (id=${userId})`); console.log(`Compte adminItinova créé avec succès (id=${userId}, openId=${openId})`);
await conn.end(); await conn.end();

View File

@@ -105,8 +105,14 @@ export const appRouter = router({
} }
// Créer un token de session avec l'openId de l'utilisateur local // Créer un token de session avec l'openId de l'utilisateur local
// Le champ name doit être non vide pour passer la vérification JWT
const displayName = user.name
|| [user.firstName, user.lastName].filter(Boolean).join(' ')
|| user.login
|| user.email
|| 'Utilisateur';
const sessionToken = await sdk.createSessionToken(user.openId!, { const sessionToken = await sdk.createSessionToken(user.openId!, {
name: user.name ?? "", name: displayName,
}); });
const cookieOptions = getSessionCookieOptions(ctx.req); const cookieOptions = getSessionCookieOptions(ctx.req);