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 = () => {
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
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 state = btoa(redirectUri);

View File

@@ -2,11 +2,13 @@ import { getLoginUrl } from "@/const";
import { useLocation } from "wouter";
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";
export default function Login() {
const [, navigate] = useLocation();
const loginUrl = getLoginUrl();
const oauthEnabled = !!loginUrl;
return (
<div className="min-h-screen flex bg-background">
@@ -78,29 +80,33 @@ export default function Login() {
</p>
<div className="space-y-4">
{/* Connexion via espace adhérent FEHAP */}
<a
href={getLoginUrl()}
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>
<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
{/* Connexion via espace adhérent FEHAP — masqué si OAuth non configuré */}
{oauthEnabled && (
<a
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>
</div>
<span className="text-white/50 group-hover:text-white/80 transition-colors text-lg">→</span>
</a>
<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>
<span className="text-white/50 group-hover:text-white/80 transition-colors text-lg">→</span>
</a>
)}
{/* Séparateur */}
<div className="flex items-center gap-3 py-1">
<div className="flex-1 h-px bg-border" />
<span className="text-xs text-muted-foreground font-medium">ou</span>
<div className="flex-1 h-px bg-border" />
</div>
{/* Séparateur — affiché seulement si OAuth activé */}
{oauthEnabled && (
<div className="flex items-center gap-3 py-1">
<div className="flex-1 h-px bg-border" />
<span className="text-xs text-muted-foreground font-medium">ou</span>
<div className="flex-1 h-px bg-border" />
</div>
)}
{/* Connexion locale */}
<button

View File

@@ -5,7 +5,7 @@ import { useLocation } from "wouter";
import { toast } from "sonner";
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";
export default function LoginLocal() {

View File

@@ -4,22 +4,33 @@
*/
import mysql from 'mysql2/promise';
import bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
const conn = await mysql.createConnection(process.env.DATABASE_URL);
// 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) {
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();
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(
`INSERT INTO users (login, email, firstName, lastName, name, role, sonumRole, isActive, loginMethod, cguAccepted, lastSignedIn, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, 'admin', 'gestionnaire', 1, 'local', 1, NOW(), NOW(), NOW())`,
['adminItinova', 'adminItinova@santinova-soft.org', 'Admin', 'SONUM', 'Admin SONUM']
`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())`,
['adminItinova', 'adminItinova@santinova-soft.org', 'Admin', 'SONUM', 'Admin SONUM', openId]
);
const userId = result.insertId;
@@ -31,5 +42,5 @@ await conn.execute(
[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();

View File

@@ -105,8 +105,14 @@ export const appRouter = router({
}
// 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!, {
name: user.name ?? "",
name: displayName,
});
const cookieOptions = getSessionCookieOptions(ctx.req);