Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 438d739b1a | |||
| c92519a324 | |||
| 69a1826f73 | |||
| bf6071f785 | |||
| f32702a5d0 | |||
| f5d551948b | |||
| 932e398a1c | |||
| 407f2b317a | |||
| ce68a6fbdc |
@@ -0,0 +1,61 @@
|
|||||||
|
name: Deploy Aegis Portal
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master # Branche principale confirmée
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# 1. On désactive la vérification SSL pour tout le conteneur du job (Node.js et Git)
|
||||||
|
env:
|
||||||
|
GIT_SSL_NO_VERIFY: "true"
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED: "0"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Patch DNS local pour Gitea Runner
|
||||||
|
run: sudo echo "10.10.40.31 git.bunker.lan" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
# 2. On force l'exécutable Git interne à ignorer la vérification de l'autorité locale
|
||||||
|
- name: Configuration de Git pour SSL
|
||||||
|
run: git config --global http.sslVerify false
|
||||||
|
|
||||||
|
- name: Récupération du code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configuration de Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Installation des dépendances (React/Vite)
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Compilation de la Build (Aegis)
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Installation de lftp
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y lftp
|
||||||
|
|
||||||
|
- name: Déploiement FTP (10.10.40.12)
|
||||||
|
env:
|
||||||
|
FTP_USER: ${{ secrets.FTP_USER }} # Le compte FTP enfermé dans aegis.gise.be
|
||||||
|
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||||
|
FTP_SERVER: ${{ secrets.FTP_SERVER }} # Doit contenir : 10.10.40.12
|
||||||
|
run: |
|
||||||
|
cat << EOF > script.lftp
|
||||||
|
set ftp:passive-mode true
|
||||||
|
set ftp:ssl-allow no
|
||||||
|
set ssl:verify-certificate no
|
||||||
|
set net:timeout 5
|
||||||
|
set net:max-retries 2
|
||||||
|
set dns:fatal-timeout 2
|
||||||
|
open -u "${FTP_USER}","${FTP_PASSWORD}" "${FTP_SERVER}"
|
||||||
|
cd home/${FTP_USER}
|
||||||
|
mirror -R --delete --verbose dist/ ./
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
|
||||||
|
lftp -f script.lftp
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
Generated
+47
-4
@@ -9,6 +9,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"@zitadel/react-auth": "^1.2.1",
|
||||||
|
"oidc-client-ts": "^3.5.0",
|
||||||
"otpauth": "^9.5.1",
|
"otpauth": "^9.5.1",
|
||||||
"pocketbase": "^0.27.0",
|
"pocketbase": "^0.27.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
@@ -1664,6 +1666,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@zitadel/react-auth": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zitadel/react-auth/-/react-auth-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-+aq52lxjH1sz9SImqfjfu/bZhY/yvMS9RfbvbjnFURTu6ZdMtysW+rrfuVQtu8YM4UYlX4KBvnVNzHb0eosNzw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"oidc-client-ts": ">=3.0.0",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"react-dom": ">=18.0.0",
|
||||||
|
"react-router-dom": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-router-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.17.0",
|
"version": "8.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||||
@@ -1728,16 +1750,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
@@ -2361,6 +2383,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -2727,6 +2758,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-client-ts": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jwt-decode": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"@zitadel/react-auth": "^1.2.1",
|
||||||
|
"oidc-client-ts": "^3.5.0",
|
||||||
"otpauth": "^9.5.1",
|
"otpauth": "^9.5.1",
|
||||||
"pocketbase": "^0.27.0",
|
"pocketbase": "^0.27.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function BackupModale(){
|
export default function BackupModale(closeModal: () => void) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-3xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-3xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||||
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function BackupWidget({ onClick }: { onClick?: () => void }) {
|
export default function BackupWidget({ onClick }: { onClick?: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function SecurityWidget() {
|
export default function SecurityWidget() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { pb } from '../../config/pocketbase';
|
import { pb } from '../../config/pocketbase';
|
||||||
|
|
||||||
export default function ServicesWidget() {
|
export default function ServicesWidget() {
|
||||||
const [contracts, setContracts] = useState([]);
|
const [contracts, setContracts] = useState<any[]>([]);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -69,7 +69,7 @@ export default function ServicesWidget() {
|
|||||||
) : contracts.length === 0 ? (
|
) : contracts.length === 0 ? (
|
||||||
<p className= "text-sm text-slate-500" > Aucun contrat actif détecté.</p>
|
<p className= "text-sm text-slate-500" > Aucun contrat actif détecté.</p>
|
||||||
) : (
|
) : (
|
||||||
contracts.map((contract) => (
|
contracts.map((contract?: any) => (
|
||||||
<div key= { contract.id } className = "p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center" >
|
<div key= { contract.id } className = "p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center" >
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-slate-900" > { contract.service_name } </h3>
|
<h3 className="text-sm font-medium text-slate-900" > { contract.service_name } </h3>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
const PB_URL = 'https://pocket.gise.be';
|
const PB_URL = 'https://pocket.bunker.lan';
|
||||||
|
|
||||||
export const pb = new PocketBase(PB_URL);
|
export const pb = new PocketBase(PB_URL);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default function AccountSettings() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 selection:bg-blue-900 selection:text-white">
|
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 selection:bg-blue-900 selection:text-white">
|
||||||
|
|||||||
+65
-25
@@ -15,28 +15,52 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [step, setStep] = useState<1 | 2>(1);
|
const [step, setStep] = useState<1 | 2>(1);
|
||||||
|
|
||||||
// Nouveaux états pour la cryptographie TOTP
|
// États pour la cryptographie TOTP (Flux Local)
|
||||||
const [userId, setUserId] = useState('');
|
const [userId, setUserId] = useState('');
|
||||||
const [totpSecret, setTotpSecret] = useState('');
|
const [totpSecret, setTotpSecret] = useState('');
|
||||||
const [qrUrl, setQrUrl] = useState('');
|
const [qrUrl, setQrUrl] = useState('');
|
||||||
const [isFirstSetup, setIsFirstSetup] = useState(false);
|
const [isFirstSetup, setIsFirstSetup] = useState(false);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
// --------------------------------------------------------
|
||||||
|
// 1 Connexion Zitadel (SSO)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
const handleZitadelLogin = async () => {
|
||||||
|
setError('');
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// PocketBase gère automatiquement la popup vers Zitadel et le retour du token
|
||||||
|
const authData = await pb.collection('aegis_users').authWithOAuth2({ provider: 'oidc' });
|
||||||
|
console.log("Données d'authentification SSO :", authData);
|
||||||
|
|
||||||
|
if (authData) {
|
||||||
|
// Zitadel a déjà géré la sécurité et le MFA de son côté.
|
||||||
|
// On ouvre directement le coffre-fort.
|
||||||
|
onLoginSuccess();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Erreur d'authentification SSO :", err);
|
||||||
|
setError("Échec de la connexion sécurisée via GISE Identity.");
|
||||||
|
pb.authStore.clear();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 2 Connexion Email - Mot de passe
|
||||||
|
// --------------------------------------------------------
|
||||||
|
const handleLocalLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Authentification PocketBase
|
|
||||||
const authData = await pb.collection('aegis_users').authWithPassword(email, password);
|
const authData = await pb.collection('aegis_users').authWithPassword(email, password);
|
||||||
|
|
||||||
// 2. Vérification du MFA
|
|
||||||
if (authData.record.mfa_enabled) {
|
if (authData.record.mfa_enabled) {
|
||||||
setUserId(authData.record.id);
|
setUserId(authData.record.id);
|
||||||
|
|
||||||
// Si le client n'a pas encore configuré son MFA
|
|
||||||
if (!authData.record.totp_secret) {
|
if (!authData.record.totp_secret) {
|
||||||
// Génération cryptographique native pour le navigateur
|
|
||||||
const totp = new OTPAuth.TOTP({
|
const totp = new OTPAuth.TOTP({
|
||||||
issuer: 'AEGIS by GISE',
|
issuer: 'AEGIS by GISE',
|
||||||
label: email,
|
label: email,
|
||||||
@@ -46,23 +70,18 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
secret: new OTPAuth.Secret({ size: 20 })
|
secret: new OTPAuth.Secret({ size: 20 })
|
||||||
});
|
});
|
||||||
|
|
||||||
const newSecret = totp.secret.base32;
|
setTotpSecret(totp.secret.base32);
|
||||||
const otpauth = totp.toString(); // Génère l'URL pour le QR Code
|
setQrUrl(totp.toString());
|
||||||
|
|
||||||
setTotpSecret(newSecret);
|
|
||||||
setQrUrl(otpauth);
|
|
||||||
setIsFirstSetup(true);
|
setIsFirstSetup(true);
|
||||||
} else {
|
} else {
|
||||||
// Le client l'a déjà configuré dans le passé
|
|
||||||
setTotpSecret(authData.record.totp_secret);
|
setTotpSecret(authData.record.totp_secret);
|
||||||
setIsFirstSetup(false);
|
setIsFirstSetup(false);
|
||||||
}
|
}
|
||||||
setStep(2);
|
setStep(2); // On passe à l'étape MFA locale
|
||||||
} else {
|
} else {
|
||||||
onLoginSuccess();
|
onLoginSuccess();
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Erreur d'authentification", err);
|
|
||||||
setError("Identifiants institutionnels incorrects ou accès révoqué.");
|
setError("Identifiants institutionnels incorrects ou accès révoqué.");
|
||||||
pb.authStore.clear();
|
pb.authStore.clear();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -76,7 +95,6 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// On recrée l'instance TOTP avec le secret pour vérifier le code saisi
|
|
||||||
const totp = new OTPAuth.TOTP({
|
const totp = new OTPAuth.TOTP({
|
||||||
issuer: 'AEGIS by GISE',
|
issuer: 'AEGIS by GISE',
|
||||||
label: email,
|
label: email,
|
||||||
@@ -86,23 +104,18 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
secret: OTPAuth.Secret.fromBase32(totpSecret)
|
secret: OTPAuth.Secret.fromBase32(totpSecret)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validation : Retourne un nombre si valide, null sinon (tolérance de 1 fenêtre de 30s)
|
|
||||||
const isValid = totp.validate({ token: mfaCode, window: 1 }) !== null;
|
const isValid = totp.validate({ token: mfaCode, window: 1 }) !== null;
|
||||||
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// Si c'était la première configuration, on sauvegarde le secret en base de données
|
|
||||||
if (isFirstSetup) {
|
if (isFirstSetup) {
|
||||||
await pb.collection('aegis_users').update(userId, {
|
await pb.collection('aegis_users').update(userId, { totp_secret: totpSecret });
|
||||||
totp_secret: totpSecret
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
onLoginSuccess(); // La porte du coffre s'ouvre !
|
onLoginSuccess();
|
||||||
} else {
|
} else {
|
||||||
setError("Code de sécurité invalide ou expiré.");
|
setError("Code de sécurité invalide ou expiré.");
|
||||||
setMfaCode('');
|
setMfaCode('');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
|
||||||
setError("Une erreur critique est survenue lors de la vérification.");
|
setError("Une erreur critique est survenue lors de la vérification.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -141,13 +154,40 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
<div className="bg-white py-8 px-4 shadow-sm border border-slate-200 rounded-xl sm:px-10">
|
<div className="bg-white py-8 px-4 shadow-sm border border-slate-200 rounded-xl sm:px-10">
|
||||||
|
|
||||||
{step === 1 ? (
|
{step === 1 ? (
|
||||||
<form className="space-y-6" onSubmit={handleLogin}>
|
<div className="space-y-6">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm font-medium animate-in fade-in slide-in-from-top-1">
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm font-medium animate-in fade-in slide-in-from-top-1">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* BOUTON SSO ZITADEL */}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleZitadelLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex w-full justify-center items-center rounded-md border border-slate-300 bg-white py-2.5 px-4 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none transition-colors disabled:opacity-70"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2 text-blue-900" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||||
|
</svg>
|
||||||
|
{isLoading ? 'Connexion en cours...' : 'Connexion via GISE Identity'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SÉPARATEUR VISUEL */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-slate-200" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="bg-white px-2 text-slate-400">Ou via vos identifiants locaux</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FORMULAIRE CLASSIQUE */}
|
||||||
|
<form className="space-y-6" onSubmit={handleLocalLogin}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-slate-700">Identifiant institutionnel</label>
|
<label htmlFor="email" className="block text-sm font-medium text-slate-700">Identifiant institutionnel</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -164,10 +204,11 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" disabled={isLoading} className="flex w-full justify-center rounded-md border border-transparent bg-blue-900 py-2.5 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-800 focus:outline-none transition-colors disabled:opacity-70">
|
<button type="submit" disabled={isLoading} className="flex w-full justify-center rounded-md border border-transparent bg-blue-900 py-2.5 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-800 focus:outline-none transition-colors disabled:opacity-70">
|
||||||
{isLoading ? 'Chiffrement en cours...' : 'Authentification'}
|
{isLoading ? 'Chiffrement en cours...' : 'Authentification classique'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300" onSubmit={handleFinalStep}>
|
<form className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300" onSubmit={handleFinalStep}>
|
||||||
{error && (
|
{error && (
|
||||||
@@ -213,7 +254,6 @@ export default function Login({ onLoginSuccess }: LoginProps) {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface Message {
|
|||||||
created: string;
|
created: string;
|
||||||
expand?: {
|
expand?: {
|
||||||
author?: {
|
author?: {
|
||||||
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
@@ -153,7 +154,7 @@ export default function TicketDetail({ ticketId, onBack }: TicketDetailProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
messages.map((msg) => {
|
messages.map((msg) => {
|
||||||
const isMyMessage = msg.author === currentUser?.id;
|
const isMyMessage = msg.expand?.author?.id === currentUser?.id;
|
||||||
const authorName = msg.expand?.author?.name || msg.expand?.author?.email || 'Expert GISE';
|
const authorName = msg.expand?.author?.name || msg.expand?.author?.email || 'Expert GISE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface TrustCenterProps {
|
|||||||
|
|
||||||
export default function TrustCenter({ onNavigate }: TrustCenterProps) {
|
export default function TrustCenter({ onNavigate }: TrustCenterProps) {
|
||||||
const [activeModal, setActiveModal] = useState<'none' | 'evolution' | 'analytics' | 'backups'>('none');
|
const [activeModal, setActiveModal] = useState<'none' | 'evolution' | 'analytics' | 'backups'>('none');
|
||||||
const [selectedService, setSelectedService] = useState<string>('');
|
//const [selectedService, setSelectedService] = useState<string>('');
|
||||||
const [evolutionChoice, setEvolutionChoice] = useState<string>('');
|
const [evolutionChoice, setEvolutionChoice] = useState<string>('');
|
||||||
|
|
||||||
const closeModal = () => setActiveModal('none');
|
const closeModal = () => setActiveModal('none');
|
||||||
@@ -47,13 +47,7 @@ export default function TrustCenter({ onNavigate }: TrustCenterProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ServicesWidget
|
<ServicesWidget />
|
||||||
onEvolutionClick={() => setActiveModal('evolution')}
|
|
||||||
onServiceClick={(service) => {
|
|
||||||
setSelectedService(service);
|
|
||||||
setActiveModal('analytics');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* CTA Support ITSM Actif */}
|
{/* CTA Support ITSM Actif */}
|
||||||
<div className="bg-blue-900 rounded-xl shadow-sm border border-blue-800 p-6 text-white">
|
<div className="bg-blue-900 rounded-xl shadow-sm border border-blue-800 p-6 text-white">
|
||||||
@@ -116,7 +110,7 @@ export default function TrustCenter({ onNavigate }: TrustCenterProps) {
|
|||||||
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-slate-900">Télémétrie & Analytics</h3>
|
<h3 className="text-lg font-bold text-slate-900">Télémétrie & Analytics</h3>
|
||||||
<p className="text-sm text-slate-500">{selectedService}</p>
|
<p className="text-sm text-slate-500">{'selectedService'}</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={closeModal} className="text-slate-400 hover:text-slate-900"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
<button onClick={closeModal} className="text-slate-400 hover:text-slate-900"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user