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:
68
Dockerfile
Normal file
68
Dockerfile
Normal 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
BIN
client/public/logoFEHAP.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user