Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb71b8fd8c | |||
| 81b23e0707 | |||
| 36224f30ee | |||
| dc208fa13a | |||
| ef31e2ab8c | |||
| 530bd52cb5 | |||
| 313bb1c6ef |
@@ -9,5 +9,9 @@ DB_NAME=eme_db
|
|||||||
DB_USER=sa
|
DB_USER=sa
|
||||||
DB_PASSWORD=YourStrongPassword123!
|
DB_PASSWORD=YourStrongPassword123!
|
||||||
|
|
||||||
|
AUTH_MODE=demo
|
||||||
AZURE_TENANT_ID=
|
AZURE_TENANT_ID=
|
||||||
AZURE_CLIENT_ID=
|
AZURE_API_CLIENT_ID=
|
||||||
|
AZURE_API_AUDIENCE=
|
||||||
|
AZURE_API_SCOPE=access_as_user
|
||||||
|
AZURE_ALLOWED_EMAIL_DOMAINS=ensup.eu,ensitech.eu
|
||||||
|
|||||||
Generated
+58
-3
@@ -14,7 +14,8 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3"
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"jwks-rsa": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -1068,7 +1069,6 @@
|
|||||||
"version": "9.0.10",
|
"version": "9.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ms": "*",
|
"@types/ms": "*",
|
||||||
@@ -1079,7 +1079,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
@@ -3074,6 +3073,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz",
|
||||||
|
"integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-md4": {
|
"node_modules/js-md4": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
|
||||||
@@ -3134,6 +3142,23 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwks-rsa": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.4",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"jose": "^6.1.3",
|
||||||
|
"limiter": "^1.1.5",
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"lru-memoizer": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >= 23.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jws": {
|
"node_modules/jws": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
@@ -3168,6 +3193,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/limiter": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -3184,6 +3214,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.clonedeep": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -3233,6 +3269,25 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "11.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||||
|
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-memoizer": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lru-cache": "^11.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru.min": {
|
"node_modules/lru.min": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3"
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"jwks-rsa": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
type AuthMode = 'demo' | 'azure';
|
||||||
|
|
||||||
function required(name: string): string {
|
function required(name: string): string {
|
||||||
const value = process.env[name];
|
const value = process.env[name];
|
||||||
|
|
||||||
@@ -28,9 +30,50 @@ function numberFromEnv(name: string, defaultValue: number): number {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authModeFromEnv(nodeEnv: string): AuthMode {
|
||||||
|
const value = process.env.AUTH_MODE || (nodeEnv === 'production' ? 'azure' : 'demo');
|
||||||
|
|
||||||
|
if (value !== 'demo' && value !== 'azure') {
|
||||||
|
throw new Error('Variable d’environnement invalide: AUTH_MODE');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeEnv === 'production' && value === 'demo') {
|
||||||
|
throw new Error('AUTH_MODE=demo est interdit en production');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function azureValue(name: string, authMode: AuthMode): string {
|
||||||
|
const value = process.env[name];
|
||||||
|
|
||||||
|
if (authMode === 'azure' && !value) {
|
||||||
|
throw new Error(`Variable d'environnement manquante en mode Azure: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function listFromEnv(name: string, defaultValue: string[]): string[] {
|
||||||
|
const value = process.env[name];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim().toLowerCase())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||||
|
const authMode = authModeFromEnv(nodeEnv);
|
||||||
|
const azureApiClientId = azureValue('AZURE_API_CLIENT_ID', authMode);
|
||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
port: numberFromEnv('PORT', 3000),
|
port: numberFromEnv('PORT', 3000),
|
||||||
nodeEnv: process.env.NODE_ENV || 'development',
|
nodeEnv,
|
||||||
frontendUrl: required('FRONTEND_URL'),
|
frontendUrl: required('FRONTEND_URL'),
|
||||||
databaseUrl: required('DATABASE_URL'),
|
databaseUrl: required('DATABASE_URL'),
|
||||||
db: {
|
db: {
|
||||||
@@ -40,8 +83,17 @@ export const env = {
|
|||||||
user: required('DB_USER'),
|
user: required('DB_USER'),
|
||||||
password: required('DB_PASSWORD'),
|
password: required('DB_PASSWORD'),
|
||||||
},
|
},
|
||||||
azure: {
|
auth: {
|
||||||
tenantId: required('AZURE_TENANT_ID'),
|
mode: authMode,
|
||||||
clientId: required('AZURE_CLIENT_ID'),
|
azure: {
|
||||||
|
tenantId: azureValue('AZURE_TENANT_ID', authMode),
|
||||||
|
apiClientId: azureApiClientId,
|
||||||
|
audience: process.env.AZURE_API_AUDIENCE || azureApiClientId,
|
||||||
|
scope: process.env.AZURE_API_SCOPE || 'access_as_user',
|
||||||
|
allowedEmailDomains: listFromEnv('AZURE_ALLOWED_EMAIL_DOMAINS', [
|
||||||
|
'ensup.eu',
|
||||||
|
'ensitech.eu',
|
||||||
|
]),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,10 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { AppError } from '../errors/app-error';
|
import { authenticateUser } from '../services/authentication.service';
|
||||||
import { findByEmail } from '../repositories/utilisateur.repository';
|
|
||||||
import { AuthenticatedUser } from '../types/authenticated-user';
|
|
||||||
|
|
||||||
/* Auth simulée (temporaire) : en attendant Azure AD, l'utilisateur courant est
|
|
||||||
résolu depuis l'en-tête `x-user-email`. À remplacer par la validation du JWT. */
|
|
||||||
export async function currentUser(req: Request, _res: Response, next: NextFunction): Promise<void> {
|
export async function currentUser(req: Request, _res: Response, next: NextFunction): Promise<void> {
|
||||||
const email = req.header('x-user-email');
|
req.user = await authenticateUser({
|
||||||
if (!email) {
|
authorization: req.header('authorization'),
|
||||||
throw new AppError(401, 'En-tete x-user-email requis (auth simulee)');
|
demoEmail: req.header('x-user-email'),
|
||||||
}
|
});
|
||||||
|
|
||||||
const utilisateur = await findByEmail(email);
|
|
||||||
if (!utilisateur || !utilisateur.actif) {
|
|
||||||
throw new AppError(401, 'Utilisateur non reconnu');
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticated: AuthenticatedUser = {
|
|
||||||
id: utilisateur.id,
|
|
||||||
email: utilisateur.email,
|
|
||||||
roleCode: utilisateur.role.code,
|
|
||||||
campusId: utilisateur.campusId,
|
|
||||||
};
|
|
||||||
|
|
||||||
req.user = authenticated;
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export function findByEmail(email: string): Promise<UtilisateurAvecRole | null>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findByMicrosoftId(microsoftId: string): Promise<UtilisateurAvecRole | null> {
|
||||||
|
return prisma.utilisateur.findUnique({
|
||||||
|
where: { microsoftId },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function findById(id: number): Promise<UtilisateurProfil | null> {
|
export function findById(id: number): Promise<UtilisateurProfil | null> {
|
||||||
return prisma.utilisateur.findUnique({
|
return prisma.utilisateur.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { env } from '../config/env';
|
||||||
|
import { AppError } from '../errors/app-error';
|
||||||
|
import {
|
||||||
|
findByEmail,
|
||||||
|
findByMicrosoftId,
|
||||||
|
UtilisateurAvecRole,
|
||||||
|
} from '../repositories/utilisateur.repository';
|
||||||
|
import { AuthenticatedUser } from '../types/authenticated-user';
|
||||||
|
import { verifyAzureAccessToken } from './azure-token.service';
|
||||||
|
|
||||||
|
export interface AuthenticationHeaders {
|
||||||
|
authorization?: string;
|
||||||
|
demoEmail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAuthenticatedUser(utilisateur: UtilisateurAvecRole): AuthenticatedUser {
|
||||||
|
return {
|
||||||
|
id: utilisateur.id,
|
||||||
|
email: utilisateur.email,
|
||||||
|
roleCode: utilisateur.role.code,
|
||||||
|
campusId: utilisateur.campusId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearerToken(authorization: string | undefined): string {
|
||||||
|
const match = authorization?.match(/^Bearer\s+(\S+)$/i);
|
||||||
|
if (!match) {
|
||||||
|
throw new AppError(401, 'Jeton Bearer requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticateDemo(email: string | undefined): Promise<UtilisateurAvecRole | null> {
|
||||||
|
if (!email) {
|
||||||
|
throw new AppError(401, 'En-tete x-user-email requis en mode demo');
|
||||||
|
}
|
||||||
|
|
||||||
|
return findByEmail(email.trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticateAzure(
|
||||||
|
authorization: string | undefined,
|
||||||
|
): Promise<UtilisateurAvecRole | null> {
|
||||||
|
const identity = await verifyAzureAccessToken(bearerToken(authorization));
|
||||||
|
const byMicrosoftId = await findByMicrosoftId(identity.microsoftId);
|
||||||
|
|
||||||
|
if (byMicrosoftId) {
|
||||||
|
return byMicrosoftId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findByEmail(identity.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authenticateUser(
|
||||||
|
headers: AuthenticationHeaders,
|
||||||
|
): Promise<AuthenticatedUser> {
|
||||||
|
const utilisateur =
|
||||||
|
env.auth.mode === 'azure'
|
||||||
|
? await authenticateAzure(headers.authorization)
|
||||||
|
: await authenticateDemo(headers.demoEmail);
|
||||||
|
|
||||||
|
if (!utilisateur || !utilisateur.actif) {
|
||||||
|
throw new AppError(401, 'Utilisateur non reconnu');
|
||||||
|
}
|
||||||
|
|
||||||
|
return toAuthenticatedUser(utilisateur);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import jwt, {
|
||||||
|
GetPublicKeyOrSecret,
|
||||||
|
JwtHeader,
|
||||||
|
JwtPayload,
|
||||||
|
SigningKeyCallback,
|
||||||
|
} from 'jsonwebtoken';
|
||||||
|
import jwksClient from 'jwks-rsa';
|
||||||
|
import { env } from '../config/env';
|
||||||
|
import { AppError } from '../errors/app-error';
|
||||||
|
|
||||||
|
export interface AzureIdentity {
|
||||||
|
microsoftId: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AzureClaims extends JwtPayload {
|
||||||
|
oid?: string;
|
||||||
|
tid?: string;
|
||||||
|
scp?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
upn?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuer = `https://login.microsoftonline.com/${env.auth.azure.tenantId}/v2.0`;
|
||||||
|
const client = jwksClient({
|
||||||
|
jwksUri: `${issuer}/discovery/v2.0/keys`,
|
||||||
|
cache: true,
|
||||||
|
cacheMaxEntries: 5,
|
||||||
|
cacheMaxAge: 10 * 60 * 1000,
|
||||||
|
rateLimit: true,
|
||||||
|
jwksRequestsPerMinute: 10,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSigningKey: GetPublicKeyOrSecret = (
|
||||||
|
header: JwtHeader,
|
||||||
|
callback: SigningKeyCallback,
|
||||||
|
): void => {
|
||||||
|
if (!header.kid) {
|
||||||
|
callback(new Error('Identifiant de cle de signature absent'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.getSigningKey(header.kid, (error, key) => {
|
||||||
|
if (error || !key) {
|
||||||
|
callback(error || new Error('Cle de signature Microsoft introuvable'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, key.getPublicKey());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function verifyJwt(token: string): Promise<AzureClaims> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
jwt.verify(
|
||||||
|
token,
|
||||||
|
getSigningKey,
|
||||||
|
{
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
audience: env.auth.azure.audience,
|
||||||
|
issuer,
|
||||||
|
},
|
||||||
|
(error, decoded) => {
|
||||||
|
if (error || !decoded || typeof decoded === 'string') {
|
||||||
|
reject(error || new Error('Contenu du jeton invalide'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(decoded);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailFromClaims(claims: AzureClaims): string | null {
|
||||||
|
const email = claims.preferred_username || claims.upn || claims.email;
|
||||||
|
return email?.trim().toLowerCase() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAllowedEmailDomain(email: string): boolean {
|
||||||
|
const separatorIndex = email.lastIndexOf('@');
|
||||||
|
if (separatorIndex < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = email.slice(separatorIndex + 1);
|
||||||
|
return env.auth.azure.allowedEmailDomains.includes(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyAzureAccessToken(token: string): Promise<AzureIdentity> {
|
||||||
|
if (env.auth.mode !== 'azure') {
|
||||||
|
throw new AppError(500, 'Validation Azure indisponible hors du mode Azure');
|
||||||
|
}
|
||||||
|
|
||||||
|
let claims: AzureClaims;
|
||||||
|
try {
|
||||||
|
claims = await verifyJwt(token);
|
||||||
|
} catch {
|
||||||
|
throw new AppError(401, 'Jeton Microsoft invalide ou expire');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claims.tid !== env.auth.azure.tenantId) {
|
||||||
|
throw new AppError(401, 'Tenant Microsoft non autorise');
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopes = claims.scp?.split(' ') || [];
|
||||||
|
if (!scopes.includes(env.auth.azure.scope)) {
|
||||||
|
throw new AppError(403, 'Permission Microsoft insuffisante');
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = emailFromClaims(claims);
|
||||||
|
if (!claims.oid || !email || !hasAllowedEmailDomain(email)) {
|
||||||
|
throw new AppError(401, 'Identite Microsoft non autorisee');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
microsoftId: claims.oid,
|
||||||
|
email,
|
||||||
|
};
|
||||||
|
}
|
||||||
+485
@@ -0,0 +1,485 @@
|
|||||||
|
-- Adminer 5.4.2 MS SQL dump
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[_prisma_migrations];
|
||||||
|
CREATE TABLE [dbo].[_prisma_migrations] (
|
||||||
|
[id] varchar(36) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[checksum] varchar(64) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[finished_at] datetimeoffset NULL,
|
||||||
|
[migration_name] nvarchar(250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[logs] nvarchar-05 COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[rolled_back_at] datetimeoffset NULL,
|
||||||
|
[started_at] datetimeoffset NOT NULL DEFAULT '(getdate())',
|
||||||
|
[applied_steps_count] int NOT NULL DEFAULT '((0))',
|
||||||
|
CONSTRAINT [PK___prisma___3213E83F83C32F3A] PRIMARY KEY ([id])
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[_prisma_migrations] ([id], [checksum], [finished_at], [migration_name], [logs], [rolled_back_at], [started_at], [applied_steps_count]) VALUES
|
||||||
|
('0f9bb926-166c-4082-8732-8a12e67b677d', 'c2faa7ef8d703d77e508f037850a7c2b07566b0eed00da21d85e1acfc5738a1e', 'Jun 4 2026 12:07:21:0885370PM', '20260604120720_add_materiel_models', NULL, NULL, 'Jun 4 2026 12:07:21:0042228PM', 1),
|
||||||
|
('247934d2-3c7e-4798-a551-d3bdc81eb86d', 'fb4d09b02739fd6bf7cea5bfd0aad1bb0fbfcce21a04f1bd91ce300a4c8bbfec', 'Jun 5 2026 08:03:28:7441715AM', '20260605080328_add_transaction_models', NULL, NULL, 'Jun 5 2026 08:03:28:6232902AM', 1),
|
||||||
|
('3266f9eb-7370-4b00-b749-f0ed39263d07', '2e7b96c74202f25fd3ab0a9438fcc8699059fd5501d66092148daa41252f146b', 'Jun 4 2026 11:52:51:4456229AM', '20260604115251_add_infrastructure_models', NULL, NULL, 'Jun 4 2026 11:52:51:3998600AM', 1),
|
||||||
|
('3cced054-eb44-41f8-89ac-51a72201310a', '5e82403dcc57c7ab48dea2fe70e688714145f7f46b770a5c79ea868e6ba60f7a', 'Jun 4 2026 10:30:16:9084629AM', '20260604103016_add_defaults_and_timestamps', NULL, NULL, 'Jun 4 2026 10:30:16:8610742AM', 1),
|
||||||
|
('3e9709da-458c-402d-8b32-bac51a84d241', 'c95d2b3e6c2454daa3fb9ba2ef7a99f9419e991174869612c81ab907e5a6d802', 'Jun 4 2026 12:02:27:3861796PM', '20260604120227_add_carte_etudiante', NULL, NULL, 'Jun 4 2026 12:02:27:3195110PM', 1),
|
||||||
|
('5b298865-5e96-4b54-ad96-1ec3acc7080a', '5e3e0d880e67c5ba91ccdb04f0dee6c056da6a325510c9dd9e4f9a80c5625fc4', 'Jun 4 2026 10:54:28:1304306AM', '20260604125356_add_unique_constraints_and_nullable_classe', NULL, NULL, 'Jun 4 2026 10:54:28:0949933AM', 1),
|
||||||
|
('66581e38-0e42-435a-9937-06479116e595', '4e632e5273ea555527c4726688865c7b21bc03d92df67e2f4679c6428d8a33e2', 'Jun 2 2026 02:19:19:8915005PM', '20260602141919_init', NULL, NULL, 'Jun 2 2026 02:19:19:8389748PM', 1),
|
||||||
|
('fbf572ea-075e-4077-9d58-39fdf613d737', '29faca22003505a071ea6b845d527566c51086c6b17980f386b70abff64d9148', 'Jun 15 2026 07:57:00:9671170AM', '20260615075700_add_suivi_models', NULL, NULL, 'Jun 15 2026 07:57:00:8308160AM', 1);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[accessoire];
|
||||||
|
CREATE TABLE [dbo].[accessoire] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[description] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[accessoire] ON;
|
||||||
|
INSERT INTO [dbo].[accessoire] ([id], [nom], [description], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'Chargeur', 'Chargeur secteur.', '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(2, 'Souris', 'Souris filaire.', '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(3, 'Housse', 'Housse de protection.', '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(1002, 'Chargeur USB-C', 'Accessoire Chargeur USB-C.', '1', '2026-07-07 11:39:38', '2026-07-07 11:39:38'),
|
||||||
|
(1003, 'Clavier Surface', 'Accessoire Clavier Surface.', '1', '2026-07-07 11:39:38', '2026-07-07 11:39:38'),
|
||||||
|
(1004, 'Stylet', 'Accessoire Stylet.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1005, 'Telecommande', 'Accessoire Telecommande.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1006, 'Cable HDMI', 'Accessoire Cable HDMI.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1007, 'Batterie', 'Accessoire Batterie.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1008, 'Carte SD', 'Accessoire Carte SD.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1009, 'Adaptateur USB-C', 'Accessoire Adaptateur USB-C.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1010, 'Cable USB-C', 'Accessoire Cable USB-C.', '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39');
|
||||||
|
SET IDENTITY_INSERT [dbo].[accessoire] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[anomalie];
|
||||||
|
CREATE TABLE [dbo].[anomalie] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[emprunt_id] int NOT NULL,
|
||||||
|
[materiel_id] int NOT NULL,
|
||||||
|
[utilisateur_id] int NOT NULL,
|
||||||
|
[traitee_par_id] int NULL,
|
||||||
|
[type] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[description] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[statut] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[detectee_automatiquement] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[observation] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[date_detection] datetime2 NOT NULL,
|
||||||
|
[date_resolution] datetime2 NULL,
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[anomalie] ON;
|
||||||
|
INSERT INTO [dbo].[anomalie] ([id], [emprunt_id], [materiel_id], [utilisateur_id], [traitee_par_id], [type], [description], [statut], [detectee_automatiquement], [observation], [date_detection], [date_resolution], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 4, 4, 3, NULL, 'ACCESSOIRE_MANQUANT', 'Housse declaree presente au depart, absente au retour.', 'DETECTEE', '1', NULL, '2026-06-14 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(2, 1002, 3, 1, NULL, 'RETOUR_NON_CONFORME', 'Retour non conforme : Etat general manquant au retour.', 'DETECTEE', '1', NULL, '2026-07-06 08:03:20', NULL, '2026-07-06 08:03:20', '2026-07-06 08:03:20'),
|
||||||
|
(1002, 4003, 1009, 2, NULL, 'RETOUR_NON_CONFORME', 'Retour non conforme : Cable USB-C manquant au retour.', 'DETECTEE', '1', NULL, '2026-07-08 09:19:47', NULL, '2026-07-08 09:19:47', '2026-07-08 09:19:47');
|
||||||
|
SET IDENTITY_INSERT [dbo].[anomalie] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[campus];
|
||||||
|
CREATE TABLE [dbo].[campus] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[adresse] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[ville] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[code_postal] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[campus] ON;
|
||||||
|
INSERT INTO [dbo].[campus] ([id], [nom], [adresse], [ville], [code_postal], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'Campus Saint-Christophe', 'À compléter — Cergy Saint-Christophe', 'Cergy', '95800', '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37');
|
||||||
|
SET IDENTITY_INSERT [dbo].[campus] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[carte_etudiante];
|
||||||
|
CREATE TABLE [dbo].[carte_etudiante] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[utilisateur_id] int NOT NULL,
|
||||||
|
[qr_code] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[date_activation] datetime2 NOT NULL,
|
||||||
|
[date_expiration] datetime2 NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[carte_etudiante] ON;
|
||||||
|
INSERT INTO [dbo].[carte_etudiante] ([id], [utilisateur_id], [qr_code], [date_activation], [date_expiration], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 1, 'QR-ETU-0001', '2025-08-19 14:48:51', '2027-07-20 14:48:51', '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(2, 2, 'QR-ETU-0002', '2025-08-19 14:48:51', '2027-07-20 14:48:51', '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(3, 3, 'QR-ETU-0003', '2025-08-19 14:48:51', '2027-07-20 14:48:51', '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52');
|
||||||
|
SET IDENTITY_INSERT [dbo].[carte_etudiante] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[categorie_materiel];
|
||||||
|
CREATE TABLE [dbo].[categorie_materiel] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[description] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[categorie_materiel] ON;
|
||||||
|
INSERT INTO [dbo].[categorie_materiel] ([id], [nom], [description], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'Ordinateur portable', 'Ordinateurs portables pédagogiques.', '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37'),
|
||||||
|
(2, 'Vidéoprojecteur', 'Vidéoprojecteurs et accessoires de projection.', '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37'),
|
||||||
|
(3, 'Tablette', 'Tablettes tactiles.', '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37'),
|
||||||
|
(4, 'Caméra', 'Caméras et matériel de captation vidéo.', '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37'),
|
||||||
|
(5, 'Périphérique audio', 'Micros, casques et enceintes.', '1', '2026-06-15 09:03:38', '2026-06-15 09:03:38'),
|
||||||
|
(6, 'Câble / Adaptateur', 'Câbles, adaptateurs et connectique.', '1', '2026-06-15 09:03:38', '2026-06-15 09:03:38');
|
||||||
|
SET IDENTITY_INSERT [dbo].[categorie_materiel] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[checklist];
|
||||||
|
CREATE TABLE [dbo].[checklist] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[emprunt_id] int NOT NULL,
|
||||||
|
[utilisateur_id] int NOT NULL,
|
||||||
|
[type] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[date_verification] datetime2 NOT NULL,
|
||||||
|
[commentaire] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[checklist] ON;
|
||||||
|
INSERT INTO [dbo].[checklist] ([id], [emprunt_id], [utilisateur_id], [type], [date_verification], [commentaire], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 1, 1, 'DEPART', '2026-06-13 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(2, 2, 2, 'DEPART', '2026-05-26 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(3, 3, 1, 'DEPART', '2026-06-05 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(4, 3, 1, 'RETOUR', '2026-06-12 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(5, 4, 3, 'DEPART', '2026-06-10 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(6, 4, 3, 'RETOUR', '2026-06-14 14:48:51', NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(1002, 1002, 1, 'DEPART', '2026-07-06 07:38:13', NULL, '2026-07-06 07:38:13', '2026-07-06 07:38:13'),
|
||||||
|
(1003, 1, 1, 'RETOUR', '2026-07-06 08:03:20', NULL, '2026-07-06 08:03:20', '2026-07-06 08:03:20'),
|
||||||
|
(1004, 1002, 1, 'RETOUR', '2026-07-06 08:03:20', NULL, '2026-07-06 08:03:20', '2026-07-06 08:03:20'),
|
||||||
|
(2002, 2002, 2, 'DEPART', '2026-07-07 11:51:13', NULL, '2026-07-07 11:51:13', '2026-07-07 11:51:13'),
|
||||||
|
(2003, 2003, 2, 'DEPART', '2026-07-07 11:58:06', NULL, '2026-07-07 11:58:06', '2026-07-07 11:58:06'),
|
||||||
|
(3002, 2, 2, 'RETOUR', '2026-07-07 12:33:18', NULL, '2026-07-07 12:33:18', '2026-07-07 12:33:18'),
|
||||||
|
(3003, 2002, 2, 'RETOUR', '2026-07-07 12:33:29', NULL, '2026-07-07 12:33:29', '2026-07-07 12:33:29'),
|
||||||
|
(3004, 2003, 2, 'RETOUR', '2026-07-07 12:33:43', NULL, '2026-07-07 12:33:43', '2026-07-07 12:33:43'),
|
||||||
|
(3005, 3002, 2, 'DEPART', '2026-07-07 12:34:25', NULL, '2026-07-07 12:34:25', '2026-07-07 12:34:25'),
|
||||||
|
(4002, 4002, 2, 'DEPART', '2026-07-08 09:19:17', NULL, '2026-07-08 09:19:17', '2026-07-08 09:19:17'),
|
||||||
|
(4003, 4002, 2, 'RETOUR', '2026-07-08 09:19:17', NULL, '2026-07-08 09:19:17', '2026-07-08 09:19:17'),
|
||||||
|
(4004, 4003, 2, 'DEPART', '2026-07-08 09:19:47', NULL, '2026-07-08 09:19:47', '2026-07-08 09:19:47'),
|
||||||
|
(4005, 4003, 2, 'RETOUR', '2026-07-08 09:19:47', NULL, '2026-07-08 09:19:47', '2026-07-08 09:19:47'),
|
||||||
|
(4006, 4004, 2, 'DEPART', '2026-07-08 13:19:04', NULL, '2026-07-08 13:19:04', '2026-07-08 13:19:04');
|
||||||
|
SET IDENTITY_INSERT [dbo].[checklist] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[checklist_element];
|
||||||
|
CREATE TABLE [dbo].[checklist_element] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[checklist_id] int NOT NULL,
|
||||||
|
[accessoire_id] int NULL,
|
||||||
|
[nom_element] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[etat] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[quantite_constatee] int NOT NULL,
|
||||||
|
[commentaire] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[checklist_element] ON;
|
||||||
|
INSERT INTO [dbo].[checklist_element] ([id], [checklist_id], [accessoire_id], [nom_element], [etat], [quantite_constatee], [commentaire], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 1, 1, 'Chargeur', 'PRESENT', 1, NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(2, 1, 2, 'Souris', 'PRESENT', 1, NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(3, 5, 3, 'Housse', 'PRESENT', 1, NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(4, 5, 1, 'Chargeur', 'PRESENT', 1, NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(5, 6, 3, 'Housse', 'ABSENT', 0, 'Manquante au retour', '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(6, 6, 1, 'Chargeur', 'PRESENT', 1, NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(1002, 1002, NULL, 'Etat general', 'PRESENT', 1, NULL, '2026-07-06 07:38:13', '2026-07-06 07:38:13'),
|
||||||
|
(1003, 1003, NULL, 'Chargeur', 'PRESENT', 1, NULL, '2026-07-06 08:03:20', '2026-07-06 08:03:20'),
|
||||||
|
(1004, 1003, NULL, 'Souris', 'PRESENT', 1, NULL, '2026-07-06 08:03:20', '2026-07-06 08:03:20'),
|
||||||
|
(1005, 1004, NULL, 'Etat general', 'ABSENT', 0, NULL, '2026-07-06 08:03:20', '2026-07-06 08:03:20'),
|
||||||
|
(2002, 2002, NULL, 'Batterie', 'ABSENT', 0, NULL, '2026-07-07 11:51:13', '2026-07-07 11:51:13'),
|
||||||
|
(2003, 2002, NULL, 'Carte SD', 'PRESENT', 1, NULL, '2026-07-07 11:51:13', '2026-07-07 11:51:13'),
|
||||||
|
(2004, 2002, NULL, 'Housse', 'PRESENT', 1, NULL, '2026-07-07 11:51:13', '2026-07-07 11:51:13'),
|
||||||
|
(2005, 2003, NULL, 'Chargeur USB-C', 'ABSENT', 0, NULL, '2026-07-07 11:58:06', '2026-07-07 11:58:06'),
|
||||||
|
(2006, 2003, NULL, 'Housse', 'PRESENT', 1, NULL, '2026-07-07 11:58:06', '2026-07-07 11:58:06'),
|
||||||
|
(3002, 3002, NULL, 'Vidéoprojecteur Epson', 'PRESENT', 1, NULL, '2026-07-07 12:33:18', '2026-07-07 12:33:18'),
|
||||||
|
(3003, 3003, NULL, 'Batterie', 'PRESENT', 1, NULL, '2026-07-07 12:33:29', '2026-07-07 12:33:29'),
|
||||||
|
(3004, 3003, NULL, 'Carte SD', 'PRESENT', 1, NULL, '2026-07-07 12:33:29', '2026-07-07 12:33:29'),
|
||||||
|
(3005, 3003, NULL, 'Housse', 'PRESENT', 1, NULL, '2026-07-07 12:33:29', '2026-07-07 12:33:29'),
|
||||||
|
(3006, 3004, NULL, 'Chargeur USB-C', 'ABSENT', 0, NULL, '2026-07-07 12:33:43', '2026-07-07 12:33:43'),
|
||||||
|
(3007, 3004, NULL, 'Housse', 'PRESENT', 1, NULL, '2026-07-07 12:33:43', '2026-07-07 12:33:43'),
|
||||||
|
(3008, 3005, NULL, 'Chargeur', 'PRESENT', 1, NULL, '2026-07-07 12:34:25', '2026-07-07 12:34:25'),
|
||||||
|
(3009, 3005, NULL, 'Housse', 'ABSENT', 0, NULL, '2026-07-07 12:34:25', '2026-07-07 12:34:25'),
|
||||||
|
(3010, 3005, NULL, 'Souris', 'PRESENT', 1, NULL, '2026-07-07 12:34:25', '2026-07-07 12:34:25'),
|
||||||
|
(4002, 4002, NULL, 'Casque Jabra Evolve', 'PRESENT', 1, NULL, '2026-07-08 09:19:17', '2026-07-08 09:19:17'),
|
||||||
|
(4003, 4003, NULL, 'Casque Jabra Evolve', 'PRESENT', 1, NULL, '2026-07-08 09:19:17', '2026-07-08 09:19:17'),
|
||||||
|
(4004, 4004, NULL, 'Cable USB-C', 'PRESENT', 1, NULL, '2026-07-08 09:19:47', '2026-07-08 09:19:47'),
|
||||||
|
(4005, 4005, NULL, 'Cable USB-C', 'ABSENT', 0, NULL, '2026-07-08 09:19:47', '2026-07-08 09:19:47'),
|
||||||
|
(4006, 4006, NULL, 'Chargeur USB-C', 'ABSENT', 0, NULL, '2026-07-08 13:19:04', '2026-07-08 13:19:04'),
|
||||||
|
(4007, 4006, NULL, 'Housse', 'PRESENT', 1, NULL, '2026-07-08 13:19:04', '2026-07-08 13:19:04');
|
||||||
|
SET IDENTITY_INSERT [dbo].[checklist_element] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[emprunt];
|
||||||
|
CREATE TABLE [dbo].[emprunt] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[utilisateur_id] int NOT NULL,
|
||||||
|
[materiel_id] int NOT NULL,
|
||||||
|
[campus_id] int NOT NULL,
|
||||||
|
[salle_pret_id] int NOT NULL,
|
||||||
|
[poste_emprunt_id] int NOT NULL,
|
||||||
|
[date_emprunt] datetime2 NOT NULL,
|
||||||
|
[date_retour_prevue] datetime2 NOT NULL,
|
||||||
|
[date_retour_reelle] datetime2 NULL,
|
||||||
|
[statut] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[mode_identification_emprunt] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[mode_identification_retour] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[commentaire_depart] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[commentaire_retour] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[emprunt] ON;
|
||||||
|
INSERT INTO [dbo].[emprunt] ([id], [utilisateur_id], [materiel_id], [campus_id], [salle_pret_id], [poste_emprunt_id], [date_emprunt], [date_retour_prevue], [date_retour_reelle], [statut], [mode_identification_emprunt], [mode_identification_retour], [commentaire_depart], [commentaire_retour], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 1, 1, 1, 1, 1, '2026-06-13 14:48:51', '2026-06-20 14:48:51', '2026-07-06 08:03:20', 'CLOTURE', 'QR_CODE', 'QR_CODE', NULL, NULL, '2026-06-15 14:48:53', '2026-07-06 08:03:20'),
|
||||||
|
(2, 2, 2, 1, 1, 1, '2026-05-26 14:48:51', '2026-06-09 14:48:51', '2026-07-07 12:33:18', 'CLOTURE', 'MAIL_ENSUP', 'MAIL_ENSUP', NULL, NULL, '2026-06-15 14:48:53', '2026-07-07 12:33:18'),
|
||||||
|
(3, 1, 3, 1, 1, 1, '2026-06-05 14:48:51', '2026-06-12 14:48:51', '2026-06-12 14:48:51', 'CLOTURE', 'QR_CODE', 'QR_CODE', NULL, NULL, '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(4, 3, 4, 1, 1, 1, '2026-06-10 14:48:51', '2026-06-14 14:48:51', '2026-06-14 14:48:51', 'RETOUR_NON_CONFORME', 'MAIL_ENSUP', 'MAIL_ENSUP', NULL, 'Housse non rapportee.', '2026-06-15 14:48:53', '2026-06-15 14:48:53'),
|
||||||
|
(1002, 1, 3, 1, 1, 1, '2026-07-06 07:38:13', '2026-07-20 07:38:13', '2026-07-06 08:03:20', 'RETOUR_NON_CONFORME', 'QR_CODE', 'QR_CODE', 'RAS', 'element manquant', '2026-07-06 07:38:13', '2026-07-06 08:03:20'),
|
||||||
|
(2002, 2, 1006, 1, 1, 1, '2026-07-07 11:51:13', '2026-07-21 11:51:13', '2026-07-07 12:33:29', 'CLOTURE', 'MAIL_ENSUP', 'MAIL_ENSUP', NULL, NULL, '2026-07-07 11:51:13', '2026-07-07 12:33:29'),
|
||||||
|
(2003, 2, 1003, 1, 1, 1, '2026-07-07 11:58:06', '2026-07-21 11:58:06', '2026-07-07 12:33:43', 'CLOTURE', 'MAIL_ENSUP', 'MAIL_ENSUP', NULL, NULL, '2026-07-07 11:58:06', '2026-07-07 12:33:44'),
|
||||||
|
(3002, 2, 1002, 1, 1, 1, '2026-07-07 12:34:25', '2026-07-21 12:34:25', NULL, 'EN_COURS', 'MAIL_ENSUP', NULL, NULL, NULL, '2026-07-07 12:34:25', '2026-07-07 12:34:25'),
|
||||||
|
(4002, 2, 1007, 1, 1, 1, '2026-07-08 09:19:17', '2026-07-22 09:19:17', '2026-07-08 09:19:17', 'CLOTURE', 'MAIL_ENSUP', 'MAIL_ENSUP', 'Test stabilisation API', 'Retour test conforme', '2026-07-08 09:19:17', '2026-07-08 09:19:17'),
|
||||||
|
(4003, 2, 1009, 1, 1, 1, '2026-07-08 09:19:47', '2026-07-22 09:19:46', '2026-07-08 09:19:47', 'RETOUR_NON_CONFORME', 'MAIL_ENSUP', 'MAIL_ENSUP', 'Test non conforme API', 'Retour test non conforme', '2026-07-08 09:19:47', '2026-07-08 09:19:47'),
|
||||||
|
(4004, 2, 1003, 1, 1, 1, '2026-07-08 13:19:04', '2026-07-22 13:19:04', NULL, 'EN_COURS', 'MAIL_ENSUP', NULL, NULL, NULL, '2026-07-08 13:19:04', '2026-07-08 13:19:04');
|
||||||
|
SET IDENTITY_INSERT [dbo].[emprunt] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[historique];
|
||||||
|
CREATE TABLE [dbo].[historique] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[utilisateur_id] int NOT NULL,
|
||||||
|
[emprunt_id] int NULL,
|
||||||
|
[materiel_id] int NULL,
|
||||||
|
[campus_id] int NULL,
|
||||||
|
[salle_pret_id] int NULL,
|
||||||
|
[poste_emprunt_id] int NULL,
|
||||||
|
[action] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[description] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[date_action] datetime2 NOT NULL,
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[historique] ON;
|
||||||
|
INSERT INTO [dbo].[historique] ([id], [utilisateur_id], [emprunt_id], [materiel_id], [campus_id], [salle_pret_id], [poste_emprunt_id], [action], [description], [date_action], [created_at]) VALUES
|
||||||
|
(1, 1, 1, 1, 1, 1, 1, 'CREATION_EMPRUNT', 'Emprunt du PC portable Dell.', '2026-06-13 14:48:51', '2026-06-15 14:48:53'),
|
||||||
|
(2, 3, 4, 4, 1, 1, 1, 'CREATION_ANOMALIE', 'Anomalie detectee au retour de la Caméra Sony.', '2026-06-14 14:48:51', '2026-06-15 14:48:53');
|
||||||
|
SET IDENTITY_INSERT [dbo].[historique] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[materiel];
|
||||||
|
CREATE TABLE [dbo].[materiel] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[marque] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[modele] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[reference] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[numero_inventaire] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[numero_serie] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[statut] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[etat_general] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[categorie_id] int NOT NULL,
|
||||||
|
[campus_id] int NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[materiel] ON;
|
||||||
|
INSERT INTO [dbo].[materiel] ([id], [nom], [marque], [modele], [reference], [numero_inventaire], [numero_serie], [statut], [etat_general], [categorie_id], [campus_id], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'PC portable Dell Latitude', 'Dell', 'Latitude 5440', 'REF-PC-001', 'INV-PC-001', 'SN-DELL-001', 'DISPONIBLE', 'Bon etat', 1, 1, '1', '2026-06-15 14:48:52', '2026-07-06 08:03:20'),
|
||||||
|
(2, 'Vidéoprojecteur Epson', 'Epson', 'EB-W06', 'REF-VP-001', 'INV-VP-001', 'SN-EPSON-001', 'DISPONIBLE', 'Bon etat', 2, 1, '1', '2026-06-15 14:48:52', '2026-07-07 12:33:18'),
|
||||||
|
(3, 'Tablette iPad', 'Apple', 'iPad 10', 'REF-TAB-001', 'INV-TAB-001', 'SN-IPAD-001', 'NON_CONFORME', 'Bon etat', 3, 1, '1', '2026-06-15 14:48:52', '2026-07-06 08:03:20'),
|
||||||
|
(4, 'Caméra Sony', 'Sony', 'Alpha 6400', 'REF-CAM-001', 'INV-CAM-001', 'SN-SONY-001', 'DETERIORE', 'Housse manquante au retour', 4, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(5, 'Micro Rode', 'Rode', 'NT-USB', 'REF-MIC-001', 'INV-MIC-001', 'SN-RODE-001', 'DISPONIBLE', 'Bon etat', 5, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(6, 'PC portable HP', 'HP', 'ProBook 450', 'REF-PC-002', 'INV-PC-002', 'SN-HP-002', 'MAINTENANCE', 'En revision', 1, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(1002, 'PC Lenovo ThinkPad E14', 'Lenovo', 'ThinkPad E14', 'REF-PC-003', 'INV-PC-003', 'SN-LENOVO-003', 'EMPRUNTE', 'Bon etat', 1, 1, '1', '2026-07-07 11:39:38', '2026-07-07 12:34:25'),
|
||||||
|
(1003, 'MacBook Air M2', 'Apple', 'MacBook Air 13', 'REF-PC-004', 'INV-PC-004', 'SN-MBA-004', 'EMPRUNTE', 'Tres bon etat', 1, 1, '1', '2026-07-07 11:39:38', '2026-07-08 13:19:04'),
|
||||||
|
(1004, 'Microsoft Surface Pro', 'Microsoft', 'Surface Pro 9', 'REF-TAB-002', 'INV-TAB-002', 'SN-SURFACE-002', 'DISPONIBLE', 'Bon etat', 3, 1, '1', '2026-07-07 11:39:38', '2026-07-07 11:39:38'),
|
||||||
|
(1005, 'Vidéoprojecteur Epson EB-X49', 'Epson', 'EB-X49', 'REF-VP-002', 'INV-VP-002', 'SN-EPSON-002', 'DISPONIBLE', 'Bon etat', 2, 1, '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1006, 'Caméra Canon EOS M50', 'Canon', 'EOS M50', 'REF-CAM-002', 'INV-CAM-002', 'SN-CANON-002', 'DISPONIBLE', 'Bon etat', 4, 1, '1', '2026-07-07 11:39:39', '2026-07-07 12:33:29'),
|
||||||
|
(1007, 'Casque Jabra Evolve', 'Jabra', 'Evolve 40', 'REF-AUD-002', 'INV-AUD-002', 'SN-JABRA-002', 'DISPONIBLE', 'Bon etat', 5, 1, '1', '2026-07-07 11:39:39', '2026-07-08 09:19:17'),
|
||||||
|
(1008, 'Kit câbles HDMI / USB-C', 'ENSUP', 'Kit connectique', 'REF-CAB-001', 'INV-CAB-001', 'SN-CABLE-001', 'DISPONIBLE', 'Bon etat', 6, 1, '1', '2026-07-07 11:39:39', '2026-07-07 11:39:39'),
|
||||||
|
(1009, 'Enceinte Bluetooth JBL', 'JBL', 'Flip 6', 'REF-AUD-003', 'INV-AUD-003', 'SN-JBL-003', 'NON_CONFORME', 'Bon etat', 5, 1, '1', '2026-07-07 11:39:39', '2026-07-08 09:19:47');
|
||||||
|
SET IDENTITY_INSERT [dbo].[materiel] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[materiel_accessoire];
|
||||||
|
CREATE TABLE [dbo].[materiel_accessoire] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[materiel_id] int NOT NULL,
|
||||||
|
[accessoire_id] int NOT NULL,
|
||||||
|
[quantite_attendue] int NOT NULL,
|
||||||
|
[obligatoire] bit NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[materiel_accessoire] ON;
|
||||||
|
INSERT INTO [dbo].[materiel_accessoire] ([id], [materiel_id], [accessoire_id], [quantite_attendue], [obligatoire]) VALUES
|
||||||
|
(1, 1, 1, 1, '1'),
|
||||||
|
(2, 1, 2, 1, '0'),
|
||||||
|
(3, 4, 3, 1, '1'),
|
||||||
|
(4, 4, 1, 1, '1'),
|
||||||
|
(1002, 1002, 1, 1, '1'),
|
||||||
|
(1003, 1002, 2, 1, '1'),
|
||||||
|
(1004, 1002, 3, 1, '1'),
|
||||||
|
(1005, 1003, 1002, 1, '1'),
|
||||||
|
(1006, 1003, 3, 1, '1'),
|
||||||
|
(1007, 1004, 1003, 1, '1'),
|
||||||
|
(1008, 1004, 1004, 1, '1'),
|
||||||
|
(1009, 1005, 1005, 1, '1'),
|
||||||
|
(1010, 1005, 1006, 1, '1'),
|
||||||
|
(1011, 1006, 1007, 1, '1'),
|
||||||
|
(1012, 1006, 1008, 1, '1'),
|
||||||
|
(1013, 1006, 3, 1, '1'),
|
||||||
|
(1014, 1008, 1006, 1, '1'),
|
||||||
|
(1015, 1008, 1009, 1, '1'),
|
||||||
|
(1016, 1009, 1010, 1, '1');
|
||||||
|
SET IDENTITY_INSERT [dbo].[materiel_accessoire] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[notification];
|
||||||
|
CREATE TABLE [dbo].[notification] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[utilisateur_id] int NOT NULL,
|
||||||
|
[anomalie_id] int NULL,
|
||||||
|
[titre] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[message] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[type] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[lu] bit NOT NULL DEFAULT '((0))',
|
||||||
|
[date_creation] datetime2 NOT NULL DEFAULT '(getdate())'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[notification] ON;
|
||||||
|
INSERT INTO [dbo].[notification] ([id], [utilisateur_id], [anomalie_id], [titre], [message], [type], [lu], [date_creation]) VALUES
|
||||||
|
(1, 4, 1, 'Nouvelle anomalie detectee', 'Retour non conforme sur la Caméra Sony (housse manquante).', 'ANOMALIE', '0', '2026-06-15 14:48:53'),
|
||||||
|
(2, 4, 2, 'Nouvelle anomalie detectee', 'Retour non conforme : Etat general manquant au retour.', 'ANOMALIE', '0', '2026-07-06 08:03:20'),
|
||||||
|
(1002, 4, 1002, 'Nouvelle anomalie detectee', 'Retour non conforme : Cable USB-C manquant au retour.', 'ANOMALIE', '0', '2026-07-08 09:19:47');
|
||||||
|
SET IDENTITY_INSERT [dbo].[notification] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[poste_emprunt];
|
||||||
|
CREATE TABLE [dbo].[poste_emprunt] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[identifiant_poste] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[salle_pret_id] int NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[poste_emprunt] ON;
|
||||||
|
INSERT INTO [dbo].[poste_emprunt] ([id], [nom], [identifiant_poste], [salle_pret_id], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'Poste 1', 'POSTE-01', 1, '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37');
|
||||||
|
SET IDENTITY_INSERT [dbo].[poste_emprunt] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[role];
|
||||||
|
CREATE TABLE [dbo].[role] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[code] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[libelle] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[description] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
CONSTRAINT [role_code_key] UNIQUE ([code])
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[role] ON;
|
||||||
|
INSERT INTO [dbo].[role] ([id], [code], [libelle], [description]) VALUES
|
||||||
|
(1, 'ETUDIANT', 'Étudiant', 'Emprunte et restitue du matériel via le poste dédié.'),
|
||||||
|
(2, 'RESPONSABLE', 'Responsable matériel', 'Supervise, gère le stock et traite les anomalies.');
|
||||||
|
SET IDENTITY_INSERT [dbo].[role] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[salle_pret];
|
||||||
|
CREATE TABLE [dbo].[salle_pret] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[localisation] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[campus_id] int NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[salle_pret] ON;
|
||||||
|
INSERT INTO [dbo].[salle_pret] ([id], [nom], [localisation], [campus_id], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'Salle de prêt principale', 'Campus Saint-Christophe', 1, '1', '2026-06-15 09:03:37', '2026-06-15 09:03:37');
|
||||||
|
SET IDENTITY_INSERT [dbo].[salle_pret] OFF;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS [dbo].[utilisateur];
|
||||||
|
CREATE TABLE [dbo].[utilisateur] (
|
||||||
|
[id] int NOT NULL IDENTITY PRIMARY KEY,
|
||||||
|
[microsoft_id] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[nom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[prenom] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[email] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
|
||||||
|
[classe] nvarchar(1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
|
||||||
|
[role_id] int NOT NULL,
|
||||||
|
[campus_id] int NOT NULL,
|
||||||
|
[actif] bit NOT NULL DEFAULT '((1))',
|
||||||
|
[created_at] datetime2 NOT NULL DEFAULT '(getdate())',
|
||||||
|
[updated_at] datetime2 NOT NULL,
|
||||||
|
CONSTRAINT [utilisateur_microsoft_id_key] UNIQUE ([microsoft_id]),
|
||||||
|
CONSTRAINT [utilisateur_email_key] UNIQUE ([email])
|
||||||
|
);
|
||||||
|
|
||||||
|
SET IDENTITY_INSERT [dbo].[utilisateur] ON;
|
||||||
|
INSERT INTO [dbo].[utilisateur] ([id], [microsoft_id], [nom], [prenom], [email], [classe], [role_id], [campus_id], [actif], [created_at], [updated_at]) VALUES
|
||||||
|
(1, 'ms-marie-dupont', 'Dupont', 'Marie', 'marie.dupont@ensitech.eu', 'B3 Informatique', 1, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(2, 'ms-lucas-martin', 'Martin', 'Lucas', 'lucas.martin@ensitech.eu', 'B2 Informatique', 1, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(3, 'ms-sofia-bernard', 'Bernard', 'Sofia', 'sofia.bernard@ensup.eu', 'B1 Marketing', 1, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52'),
|
||||||
|
(4, 'ms-karim-benali', 'Benali', 'Karim', 'karim.benali@ensup.eu', NULL, 2, 1, '1', '2026-06-15 14:48:52', '2026-06-15 14:48:52');
|
||||||
|
SET IDENTITY_INSERT [dbo].[utilisateur] OFF;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[anomalie] ADD
|
||||||
|
FOREIGN KEY ([emprunt_id]) REFERENCES [emprunt] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([materiel_id]) REFERENCES [materiel] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([utilisateur_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([traitee_par_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[carte_etudiante] ADD
|
||||||
|
FOREIGN KEY ([utilisateur_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[checklist] ADD
|
||||||
|
FOREIGN KEY ([emprunt_id]) REFERENCES [emprunt] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([utilisateur_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[checklist_element] ADD
|
||||||
|
FOREIGN KEY ([accessoire_id]) REFERENCES [accessoire] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([checklist_id]) REFERENCES [checklist] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[emprunt] ADD
|
||||||
|
FOREIGN KEY ([campus_id]) REFERENCES [campus] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([materiel_id]) REFERENCES [materiel] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([poste_emprunt_id]) REFERENCES [poste_emprunt] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([salle_pret_id]) REFERENCES [salle_pret] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([utilisateur_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[historique] ADD
|
||||||
|
FOREIGN KEY ([campus_id]) REFERENCES [campus] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([emprunt_id]) REFERENCES [emprunt] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([materiel_id]) REFERENCES [materiel] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([poste_emprunt_id]) REFERENCES [poste_emprunt] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([salle_pret_id]) REFERENCES [salle_pret] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([utilisateur_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[materiel] ADD
|
||||||
|
FOREIGN KEY ([campus_id]) REFERENCES [campus] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY ([categorie_id]) REFERENCES [categorie_materiel] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[materiel_accessoire] ADD
|
||||||
|
FOREIGN KEY ([accessoire_id]) REFERENCES [accessoire] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY ([materiel_id]) REFERENCES [materiel] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[notification] ADD
|
||||||
|
FOREIGN KEY ([anomalie_id]) REFERENCES [anomalie] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||||
|
FOREIGN KEY ([utilisateur_id]) REFERENCES [utilisateur] ([id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[poste_emprunt] ADD
|
||||||
|
FOREIGN KEY ([salle_pret_id]) REFERENCES [salle_pret] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[salle_pret] ADD
|
||||||
|
FOREIGN KEY ([campus_id]) REFERENCES [campus] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[utilisateur] ADD
|
||||||
|
FOREIGN KEY ([campus_id]) REFERENCES [campus] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY ([role_id]) REFERENCES [role] ([id]) ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- 2026-07-10 09:04:02 UTC
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
/// Identité simulée utilisée tant que l'authentification Azure AD n'est pas branchée.
|
import "services/auth_service.dart";
|
||||||
|
|
||||||
|
/// Identité de secours utilisée lorsque aucun profil de session n'est chargé.
|
||||||
class DemoIdentity {
|
class DemoIdentity {
|
||||||
DemoIdentity._();
|
DemoIdentity._();
|
||||||
|
|
||||||
static const userName = "Lucas Martin";
|
static String get userName =>
|
||||||
static const userInitials = "LM";
|
AuthService.currentProfile?.fullName ?? "Lucas Martin";
|
||||||
static const userSubtitle = "Étudiant · B2 Informatique · Ensitech Cergy";
|
static String get userInitials =>
|
||||||
static const campusTag = "Saint-Christophe · Cergy";
|
AuthService.currentProfile?.initials ?? "LM";
|
||||||
|
static String get userSubtitle =>
|
||||||
|
AuthService.currentProfile?.subtitle ??
|
||||||
|
"Étudiant · B2 Informatique · Ensitech Cergy";
|
||||||
|
static String get campusTag =>
|
||||||
|
AuthService.currentProfile?.campusTag ?? "Saint-Christophe · Cergy";
|
||||||
|
|
||||||
static const loginCampusTag = "Ensitech · Cergy";
|
static const loginCampusTag = "Ensitech · Cergy";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
class AuthenticatedProfile {
|
||||||
|
const AuthenticatedProfile({
|
||||||
|
required this.id,
|
||||||
|
required this.nom,
|
||||||
|
required this.prenom,
|
||||||
|
required this.email,
|
||||||
|
required this.classe,
|
||||||
|
required this.roleCode,
|
||||||
|
required this.roleLabel,
|
||||||
|
required this.campusId,
|
||||||
|
required this.campusNom,
|
||||||
|
required this.campusVille,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final String nom;
|
||||||
|
final String prenom;
|
||||||
|
final String email;
|
||||||
|
final String? classe;
|
||||||
|
final String roleCode;
|
||||||
|
final String roleLabel;
|
||||||
|
final int campusId;
|
||||||
|
final String campusNom;
|
||||||
|
final String campusVille;
|
||||||
|
|
||||||
|
factory AuthenticatedProfile.fromApi(dynamic reponse) {
|
||||||
|
if (reponse is! Map || reponse["data"] is! Map) {
|
||||||
|
throw const FormatException("Profil utilisateur invalide.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = reponse["data"] as Map;
|
||||||
|
final role = data["role"];
|
||||||
|
final campus = data["campus"];
|
||||||
|
if (role is! Map || campus is! Map) {
|
||||||
|
throw const FormatException("Role ou campus utilisateur invalide.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final id = data["id"];
|
||||||
|
final campusId = campus["id"];
|
||||||
|
if (id is! int || campusId is! int) {
|
||||||
|
throw const FormatException("Identifiant utilisateur invalide.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final classe = data["classe"];
|
||||||
|
if (classe != null && classe is! String) {
|
||||||
|
throw const FormatException("Classe utilisateur invalide.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthenticatedProfile(
|
||||||
|
id: id,
|
||||||
|
nom: _requiredString(data, "nom"),
|
||||||
|
prenom: _requiredString(data, "prenom"),
|
||||||
|
email: _requiredString(data, "email"),
|
||||||
|
classe: classe as String?,
|
||||||
|
roleCode: _requiredString(role, "code"),
|
||||||
|
roleLabel: _requiredString(role, "libelle"),
|
||||||
|
campusId: campusId,
|
||||||
|
campusNom: _requiredString(campus, "nom"),
|
||||||
|
campusVille: _requiredString(campus, "ville"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get fullName => "$prenom $nom";
|
||||||
|
|
||||||
|
String get initials {
|
||||||
|
final first = prenom.trim().isEmpty ? "" : prenom.trim()[0];
|
||||||
|
final last = nom.trim().isEmpty ? "" : nom.trim()[0];
|
||||||
|
return "$first$last".toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get campusTag => "$campusNom · $campusVille";
|
||||||
|
|
||||||
|
String get subtitle {
|
||||||
|
final details = <String>[roleLabel];
|
||||||
|
final studentClass = classe?.trim();
|
||||||
|
if (studentClass != null && studentClass.isNotEmpty) {
|
||||||
|
details.add(studentClass);
|
||||||
|
}
|
||||||
|
details.add(campusTag);
|
||||||
|
return details.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _requiredString(Map source, String key) {
|
||||||
|
final value = source[key];
|
||||||
|
if (value is! String || value.trim().isEmpty) {
|
||||||
|
throw FormatException("Champ utilisateur invalide : $key.");
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,8 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
final dateEmprunt = emprunt.dateEmprunt;
|
final dateEmprunt = emprunt.dateEmprunt;
|
||||||
final date =
|
final date =
|
||||||
"${_deuxChiffres(dateEmprunt.day)}/${_deuxChiffres(dateEmprunt.month)}/${dateEmprunt.year}";
|
"${_deuxChiffres(dateEmprunt.day)}/${_deuxChiffres(dateEmprunt.month)}/${dateEmprunt.year}";
|
||||||
final heure = "${_deuxChiffres(dateEmprunt.hour)}:${_deuxChiffres(dateEmprunt.minute)}";
|
final heure =
|
||||||
|
"${_deuxChiffres(dateEmprunt.hour)}:${_deuxChiffres(dateEmprunt.minute)}";
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: EnsupColors.soft,
|
backgroundColor: EnsupColors.soft,
|
||||||
@@ -48,7 +49,7 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const EnsupTopBar(
|
EnsupTopBar(
|
||||||
brandText: "EME · Confirmation",
|
brandText: "EME · Confirmation",
|
||||||
campusTag: DemoIdentity.campusTag,
|
campusTag: DemoIdentity.campusTag,
|
||||||
),
|
),
|
||||||
@@ -63,7 +64,10 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 28,
|
||||||
|
vertical: 32,
|
||||||
|
),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -75,10 +79,15 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: () => Navigator.of(context).popUntil(
|
onPressed: () =>
|
||||||
(route) => route.settings.name == "home",
|
Navigator.of(context).popUntil(
|
||||||
|
(route) =>
|
||||||
|
route.settings.name == "home",
|
||||||
|
),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.home_outlined,
|
||||||
|
size: 18,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.home_outlined, size: 18),
|
|
||||||
label: Text(
|
label: Text(
|
||||||
"Retour à l'accueil",
|
"Retour à l'accueil",
|
||||||
style: GoogleFonts.darkerGrotesque(
|
style: GoogleFonts.darkerGrotesque(
|
||||||
@@ -88,9 +97,13 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: EnsupColors.cyan,
|
backgroundColor: EnsupColors.cyan,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -118,7 +131,9 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(36),
|
padding: const EdgeInsets.all(36),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)]),
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)],
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
|
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
|
||||||
),
|
),
|
||||||
@@ -129,7 +144,9 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
height: 72,
|
height: 72,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: const LinearGradient(colors: [Color(0xFF16A34A), Color(0xFF22C55E)]),
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF16A34A), Color(0xFF22C55E)],
|
||||||
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFF16A34A).withValues(alpha: 0.3),
|
color: const Color(0xFF16A34A).withValues(alpha: 0.3),
|
||||||
@@ -153,7 +170,10 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
"Votre emprunt a bien été enregistré. Vous pouvez récupérer le matériel.",
|
"Votre emprunt a bien été enregistré. Vous pouvez récupérer le matériel.",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.titilliumWeb(fontSize: 14, color: const Color(0xFF166534)),
|
style: GoogleFonts.titilliumWeb(
|
||||||
|
fontSize: 14,
|
||||||
|
color: const Color(0xFF166534),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -191,7 +211,8 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
_rrow("Date d'emprunt", date),
|
_rrow("Date d'emprunt", date),
|
||||||
_rrow("Heure", heure),
|
_rrow("Heure", heure),
|
||||||
_rrow("Campus", item.campus),
|
_rrow("Campus", item.campus),
|
||||||
if (note != null) _rrow("Note", note, valueColor: const Color(0xFFD97706)),
|
if (note != null)
|
||||||
|
_rrow("Note", note, valueColor: const Color(0xFFD97706)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -203,7 +224,13 @@ class ConfirmationScreen extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)),
|
Text(
|
||||||
|
cle,
|
||||||
|
style: GoogleFonts.titilliumWeb(
|
||||||
|
fontSize: 13,
|
||||||
|
color: EnsupColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:google_fonts/google_fonts.dart";
|
import "package:google_fonts/google_fonts.dart";
|
||||||
import "../demo_identity.dart";
|
import "../demo_identity.dart";
|
||||||
|
import "../services/auth_service.dart";
|
||||||
import "../theme/ensup_colors.dart";
|
import "../theme/ensup_colors.dart";
|
||||||
import "../widgets/ensup_top_bar.dart";
|
import "../widgets/ensup_top_bar.dart";
|
||||||
import "../widgets/profile_card.dart";
|
import "../widgets/profile_card.dart";
|
||||||
@@ -13,6 +14,20 @@ import "restitution_select_screen.dart";
|
|||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
Future<void> _quitter(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
await AuthService.signOut();
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(error.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -42,7 +57,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
campusTag: DemoIdentity.campusTag,
|
campusTag: DemoIdentity.campusTag,
|
||||||
userName: DemoIdentity.userName,
|
userName: DemoIdentity.userName,
|
||||||
userInitials: DemoIdentity.userInitials,
|
userInitials: DemoIdentity.userInitials,
|
||||||
onBack: () => Navigator.of(context).pop(),
|
onBack: () async => _quitter(context),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -56,7 +71,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const ProfileCard(
|
ProfileCard(
|
||||||
name: DemoIdentity.userName,
|
name: DemoIdentity.userName,
|
||||||
subtitle: DemoIdentity.userSubtitle,
|
subtitle: DemoIdentity.userSubtitle,
|
||||||
initials: DemoIdentity.userInitials,
|
initials: DemoIdentity.userInitials,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:google_fonts/google_fonts.dart";
|
import "package:google_fonts/google_fonts.dart";
|
||||||
import "../demo_identity.dart";
|
import "../demo_identity.dart";
|
||||||
|
import "../services/api_client.dart";
|
||||||
|
import "../services/auth_service.dart";
|
||||||
import "../theme/ensup_colors.dart";
|
import "../theme/ensup_colors.dart";
|
||||||
import "../widgets/ensup_top_bar.dart";
|
import "../widgets/ensup_top_bar.dart";
|
||||||
import "../widgets/identification_option.dart";
|
import "../widgets/identification_option.dart";
|
||||||
@@ -9,10 +11,25 @@ import "home_screen.dart";
|
|||||||
import "responsable_screen.dart";
|
import "responsable_screen.dart";
|
||||||
|
|
||||||
/// Écran de démarrage : identification de l'étudiant (RG01/RG02).
|
/// Écran de démarrage : identification de l'étudiant (RG01/RG02).
|
||||||
class LoginScreen extends StatelessWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
void _identifier(BuildContext context) {
|
@override
|
||||||
|
State<LoginScreen> createState() => _LoginScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginScreenState extends State<LoginScreen> {
|
||||||
|
bool _connexionEnCours = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (AuthService.mode == AuthenticationMode.azure) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _restaurerSession());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ouvrirEtudiant() {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => const HomeScreen(),
|
builder: (_) => const HomeScreen(),
|
||||||
@@ -21,6 +38,99 @@ class LoginScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _ouvrirResponsable() {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const ResponsableScreen(),
|
||||||
|
settings: const RouteSettings(name: "responsable"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ouvrirPourRole(String roleCode) {
|
||||||
|
switch (roleCode) {
|
||||||
|
case "ETUDIANT":
|
||||||
|
_ouvrirEtudiant();
|
||||||
|
case "RESPONSABLE":
|
||||||
|
_ouvrirResponsable();
|
||||||
|
default:
|
||||||
|
throw StateError("Role utilisateur non autorise.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restaurerSession() async {
|
||||||
|
setState(() => _connexionEnCours = true);
|
||||||
|
try {
|
||||||
|
if (!await AuthService.hasMicrosoftAccount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final reponse = await ApiClient.get("/auth/me");
|
||||||
|
final profile = AuthService.setProfileFromApi(reponse);
|
||||||
|
if (mounted) {
|
||||||
|
_ouvrirPourRole(profile.roleCode);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("La session Microsoft doit etre renouvelee."),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _connexionEnCours = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _identifierParQr() async {
|
||||||
|
if (_connexionEnCours) return;
|
||||||
|
if (AuthService.mode == AuthenticationMode.azure) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"L'identification QR securisee n'est pas encore configuree.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _connecterMicrosoft(responsableDemo: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connecterMicrosoft({required bool responsableDemo}) async {
|
||||||
|
if (_connexionEnCours) return;
|
||||||
|
|
||||||
|
setState(() => _connexionEnCours = true);
|
||||||
|
try {
|
||||||
|
if (AuthService.mode == AuthenticationMode.demo) {
|
||||||
|
if (responsableDemo) {
|
||||||
|
AuthService.selectDemoResponsable();
|
||||||
|
} else {
|
||||||
|
AuthService.selectDemoStudent();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await AuthService.signInWithMicrosoft();
|
||||||
|
}
|
||||||
|
|
||||||
|
final reponse = await ApiClient.get("/auth/me");
|
||||||
|
final profile = AuthService.setProfileFromApi(reponse);
|
||||||
|
if (!mounted) return;
|
||||||
|
_ouvrirPourRole(profile.roleCode);
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(error.toString())));
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _connexionEnCours = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -122,7 +232,9 @@ class LoginScreen extends StatelessWidget {
|
|||||||
label: "Scanner ma carte étudiante",
|
label: "Scanner ma carte étudiante",
|
||||||
description:
|
description:
|
||||||
"Présentez votre carte étudiante dématérialisée à la douchette ou à la tablette",
|
"Présentez votre carte étudiante dématérialisée à la douchette ou à la tablette",
|
||||||
onTap: () => _identifier(context),
|
onTap: _connexionEnCours
|
||||||
|
? null
|
||||||
|
: _identifierParQr,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
IdentificationOption(
|
IdentificationOption(
|
||||||
@@ -132,7 +244,11 @@ class LoginScreen extends StatelessWidget {
|
|||||||
"Connexion compte professionnel ENSUP",
|
"Connexion compte professionnel ENSUP",
|
||||||
description:
|
description:
|
||||||
"Utiliser mon adresse e-mail Microsoft 365",
|
"Utiliser mon adresse e-mail Microsoft 365",
|
||||||
onTap: () => _identifier(context),
|
onTap: _connexionEnCours
|
||||||
|
? null
|
||||||
|
: () => _connecterMicrosoft(
|
||||||
|
responsableDemo: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
@@ -161,16 +277,11 @@ class LoginScreen extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () =>
|
onPressed: _connexionEnCours
|
||||||
Navigator.of(context).push(
|
? null
|
||||||
MaterialPageRoute(
|
: () => _connecterMicrosoft(
|
||||||
builder: (_) =>
|
responsableDemo: true,
|
||||||
const ResponsableScreen(),
|
|
||||||
settings: const RouteSettings(
|
|
||||||
name: "responsable",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.person_outline,
|
Icons.person_outline,
|
||||||
size: 16,
|
size: 16,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:google_fonts/google_fonts.dart";
|
import "package:google_fonts/google_fonts.dart";
|
||||||
|
|
||||||
import "../demo_identity.dart";
|
import "../services/auth_service.dart";
|
||||||
import "../services/responsable_service.dart";
|
import "../services/responsable_service.dart";
|
||||||
import "../theme/ensup_colors.dart";
|
import "../theme/ensup_colors.dart";
|
||||||
import "../utils/file_download.dart";
|
import "../utils/file_download.dart";
|
||||||
@@ -44,6 +44,20 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
|||||||
setState(() => _future = _charger());
|
setState(() => _future = _charger());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _quitter() async {
|
||||||
|
try {
|
||||||
|
await AuthService.signOut();
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(error.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _changerStatut(Map<String, dynamic> anomalie) async {
|
Future<void> _changerStatut(Map<String, dynamic> anomalie) async {
|
||||||
final prochain = _prochainStatut(anomalie["statut"] as String?);
|
final prochain = _prochainStatut(anomalie["statut"] as String?);
|
||||||
if (prochain == null || _transitionEnCours) {
|
if (prochain == null || _transitionEnCours) {
|
||||||
@@ -124,10 +138,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_ResponsableTopBar(
|
_ResponsableTopBar(onExit: _quitter, onRefresh: _rafraichir),
|
||||||
onExit: () => Navigator.of(context).pop(),
|
|
||||||
onRefresh: _rafraichir,
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FutureBuilder<_ResponsableData>(
|
child: FutureBuilder<_ResponsableData>(
|
||||||
future: _future,
|
future: _future,
|
||||||
@@ -249,7 +260,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
|||||||
anomaliesActives: anomaliesActives,
|
anomaliesActives: anomaliesActives,
|
||||||
notificationsNonLues: notificationsNonLues,
|
notificationsNonLues: notificationsNonLues,
|
||||||
onChanged: (index) => setState(() => _onglet = index),
|
onChanged: (index) => setState(() => _onglet = index),
|
||||||
onExit: () => Navigator.of(context).pop(),
|
onExit: _quitter,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -451,6 +462,12 @@ class _ResponsableTopBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final profile = AuthService.currentProfile;
|
||||||
|
final campusTag = profile?.campusTag ?? "Ensitech · Cergy";
|
||||||
|
final initials = profile?.initials ?? "KB";
|
||||||
|
final userName = profile?.fullName ?? "Karim Benali";
|
||||||
|
final roleLabel = profile?.roleLabel ?? "Responsable matériel";
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 68,
|
height: 68,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
@@ -521,7 +538,7 @@ class _ResponsableTopBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
DemoIdentity.loginCampusTag,
|
campusTag,
|
||||||
style: GoogleFonts.titilliumWeb(
|
style: GoogleFonts.titilliumWeb(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -553,7 +570,7 @@ class _ResponsableTopBar extends StatelessWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"KB",
|
initials,
|
||||||
style: GoogleFonts.darkerGrotesque(
|
style: GoogleFonts.darkerGrotesque(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -568,7 +585,7 @@ class _ResponsableTopBar extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Karim Benali",
|
userName,
|
||||||
style: GoogleFonts.titilliumWeb(
|
style: GoogleFonts.titilliumWeb(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
@@ -576,7 +593,7 @@ class _ResponsableTopBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Responsable matériel",
|
roleLabel,
|
||||||
style: GoogleFonts.titilliumWeb(
|
style: GoogleFonts.titilliumWeb(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: EnsupColors.muted,
|
color: EnsupColors.muted,
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import "../widgets/brand_corners.dart";
|
|||||||
/// Résultat de la restitution (RG17-RG20) : conforme (succès) ou non conforme
|
/// Résultat de la restitution (RG17-RG20) : conforme (succès) ou non conforme
|
||||||
/// (anomalie détectée + notification au responsable).
|
/// (anomalie détectée + notification au responsable).
|
||||||
class ResultatRetourScreen extends StatelessWidget {
|
class ResultatRetourScreen extends StatelessWidget {
|
||||||
const ResultatRetourScreen({super.key, required this.emprunt, required this.anomalies});
|
const ResultatRetourScreen({
|
||||||
|
super.key,
|
||||||
|
required this.emprunt,
|
||||||
|
required this.anomalies,
|
||||||
|
});
|
||||||
|
|
||||||
final EmpruntItem emprunt;
|
final EmpruntItem emprunt;
|
||||||
final List<AnomalieRetour> anomalies;
|
final List<AnomalieRetour> anomalies;
|
||||||
@@ -48,7 +52,7 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const EnsupTopBar(
|
EnsupTopBar(
|
||||||
brandText: "EME · Résultat du contrôle",
|
brandText: "EME · Résultat du contrôle",
|
||||||
campusTag: DemoIdentity.campusTag,
|
campusTag: DemoIdentity.campusTag,
|
||||||
),
|
),
|
||||||
@@ -63,7 +67,10 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 28,
|
||||||
|
vertical: 32,
|
||||||
|
),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -73,10 +80,15 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: () => Navigator.of(context).popUntil(
|
onPressed: () =>
|
||||||
(route) => route.settings.name == "home",
|
Navigator.of(context).popUntil(
|
||||||
|
(route) =>
|
||||||
|
route.settings.name == "home",
|
||||||
|
),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.home_outlined,
|
||||||
|
size: 18,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.home_outlined, size: 18),
|
|
||||||
label: Text(
|
label: Text(
|
||||||
"Retour à l'accueil",
|
"Retour à l'accueil",
|
||||||
style: GoogleFonts.darkerGrotesque(
|
style: GoogleFonts.darkerGrotesque(
|
||||||
@@ -86,9 +98,13 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: EnsupColors.cyan,
|
backgroundColor: EnsupColors.cyan,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -116,7 +132,9 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(36),
|
padding: const EdgeInsets.all(36),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)]),
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)],
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
|
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
|
||||||
),
|
),
|
||||||
@@ -127,7 +145,9 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
height: 72,
|
height: 72,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: LinearGradient(colors: [Color(0xFF16A34A), Color(0xFF22C55E)]),
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF16A34A), Color(0xFF22C55E)],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.check, color: Colors.white, size: 36),
|
child: const Icon(Icons.check, color: Colors.white, size: 36),
|
||||||
),
|
),
|
||||||
@@ -144,7 +164,10 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
"L'emprunt a été clôturé et le matériel est de nouveau disponible.",
|
"L'emprunt a été clôturé et le matériel est de nouveau disponible.",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.titilliumWeb(fontSize: 14, color: const Color(0xFF166534)),
|
style: GoogleFonts.titilliumWeb(
|
||||||
|
fontSize: 14,
|
||||||
|
color: const Color(0xFF166534),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -164,12 +187,19 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: EnsupColors.red.withValues(alpha: 0.06),
|
color: EnsupColors.red.withValues(alpha: 0.06),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: EnsupColors.red.withValues(alpha: 0.2), width: 1.5),
|
border: Border.all(
|
||||||
|
color: EnsupColors.red.withValues(alpha: 0.2),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.warning_amber_rounded, color: EnsupColors.red, size: 24),
|
const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: EnsupColors.red,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -227,7 +257,10 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
anomalie.etatRetour,
|
anomalie.etatRetour,
|
||||||
valeurCouleur: EnsupColors.red,
|
valeurCouleur: EnsupColors.red,
|
||||||
),
|
),
|
||||||
_drow("Matériel", "${emprunt.materiel.nom} · ${emprunt.materiel.reference}"),
|
_drow(
|
||||||
|
"Matériel",
|
||||||
|
"${emprunt.materiel.nom} · ${emprunt.materiel.reference}",
|
||||||
|
),
|
||||||
_drow("Emprunteur", DemoIdentity.userName),
|
_drow("Emprunteur", DemoIdentity.userName),
|
||||||
_drow("Date détection", date),
|
_drow("Date détection", date),
|
||||||
],
|
],
|
||||||
@@ -237,7 +270,11 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
"Le responsable matériel traitera cette anomalie. Vous pouvez retourner à l'accueil.",
|
"Le responsable matériel traitera cette anomalie. Vous pouvez retourner à l'accueil.",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted, height: 1.6),
|
style: GoogleFonts.titilliumWeb(
|
||||||
|
fontSize: 13,
|
||||||
|
color: EnsupColors.muted,
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -249,7 +286,13 @@ class ResultatRetourScreen extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)),
|
Text(
|
||||||
|
cle,
|
||||||
|
style: GoogleFonts.titilliumWeb(
|
||||||
|
fontSize: 13,
|
||||||
|
color: EnsupColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
import "dart:typed_data";
|
import "dart:typed_data";
|
||||||
import "package:http/http.dart" as http;
|
import "package:http/http.dart" as http;
|
||||||
|
import "auth_service.dart";
|
||||||
|
|
||||||
/// Erreur levée par le client API (serveur injoignable ou réponse en échec).
|
/// Erreur levée par le client API (serveur injoignable ou réponse en échec).
|
||||||
class ApiException implements Exception {
|
class ApiException implements Exception {
|
||||||
@@ -24,27 +25,20 @@ class ApiFile {
|
|||||||
final String contentType;
|
final String contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Point d'accès unique au backend : centralise l'URL de base, l'en-tête
|
/// Point d'accès unique au backend : centralise l'URL, l'authentification et
|
||||||
/// d'authentification (simulée) et la gestion des erreurs.
|
/// la gestion des erreurs.
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
ApiClient._();
|
ApiClient._();
|
||||||
|
|
||||||
static const String _baseUrl = "http://localhost:3000/api";
|
static const String _baseUrl = "http://localhost:3000/api";
|
||||||
static const String _utilisateurEmail = "lucas.martin@ensitech.eu";
|
|
||||||
static const String responsableEmail = "karim.benali@ensup.eu";
|
|
||||||
|
|
||||||
static Future<dynamic> get(
|
static Future<dynamic> get(String chemin) async {
|
||||||
String chemin, {
|
|
||||||
String utilisateurEmail = _utilisateurEmail,
|
|
||||||
}) async {
|
|
||||||
final uri = Uri.parse("$_baseUrl$chemin");
|
final uri = Uri.parse("$_baseUrl$chemin");
|
||||||
|
|
||||||
late final http.Response reponse;
|
late final http.Response reponse;
|
||||||
try {
|
try {
|
||||||
reponse = await http.get(
|
final authHeaders = await AuthService.requestHeaders();
|
||||||
uri,
|
reponse = await http.get(uri, headers: authHeaders);
|
||||||
headers: {"x-user-email": utilisateurEmail},
|
|
||||||
);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw const ApiException(
|
throw const ApiException(
|
||||||
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
|
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
|
||||||
@@ -60,16 +54,13 @@ class ApiClient {
|
|||||||
static Future<ApiFile> getFile(
|
static Future<ApiFile> getFile(
|
||||||
String chemin, {
|
String chemin, {
|
||||||
required String fallbackFileName,
|
required String fallbackFileName,
|
||||||
String utilisateurEmail = _utilisateurEmail,
|
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse("$_baseUrl$chemin");
|
final uri = Uri.parse("$_baseUrl$chemin");
|
||||||
|
|
||||||
late final http.Response reponse;
|
late final http.Response reponse;
|
||||||
try {
|
try {
|
||||||
reponse = await http.get(
|
final authHeaders = await AuthService.requestHeaders();
|
||||||
uri,
|
reponse = await http.get(uri, headers: authHeaders);
|
||||||
headers: {"x-user-email": utilisateurEmail},
|
|
||||||
);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw const ApiException(
|
throw const ApiException(
|
||||||
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
|
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
|
||||||
@@ -88,21 +79,15 @@ class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<dynamic> post(
|
static Future<dynamic> post(String chemin, Map<String, dynamic> body) async {
|
||||||
String chemin,
|
|
||||||
Map<String, dynamic> body, {
|
|
||||||
String utilisateurEmail = _utilisateurEmail,
|
|
||||||
}) async {
|
|
||||||
final uri = Uri.parse("$_baseUrl$chemin");
|
final uri = Uri.parse("$_baseUrl$chemin");
|
||||||
|
|
||||||
late final http.Response reponse;
|
late final http.Response reponse;
|
||||||
try {
|
try {
|
||||||
|
final authHeaders = await AuthService.requestHeaders();
|
||||||
reponse = await http.post(
|
reponse = await http.post(
|
||||||
uri,
|
uri,
|
||||||
headers: {
|
headers: {...authHeaders, "content-type": "application/json"},
|
||||||
"content-type": "application/json",
|
|
||||||
"x-user-email": utilisateurEmail,
|
|
||||||
},
|
|
||||||
body: jsonEncode(body),
|
body: jsonEncode(body),
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -117,21 +102,15 @@ class ApiClient {
|
|||||||
throw ApiException(_messageErreur(reponse));
|
throw ApiException(_messageErreur(reponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<dynamic> patch(
|
static Future<dynamic> patch(String chemin, Map<String, dynamic> body) async {
|
||||||
String chemin,
|
|
||||||
Map<String, dynamic> body, {
|
|
||||||
String utilisateurEmail = _utilisateurEmail,
|
|
||||||
}) async {
|
|
||||||
final uri = Uri.parse("$_baseUrl$chemin");
|
final uri = Uri.parse("$_baseUrl$chemin");
|
||||||
|
|
||||||
late final http.Response reponse;
|
late final http.Response reponse;
|
||||||
try {
|
try {
|
||||||
|
final authHeaders = await AuthService.requestHeaders();
|
||||||
reponse = await http.patch(
|
reponse = await http.patch(
|
||||||
uri,
|
uri,
|
||||||
headers: {
|
headers: {...authHeaders, "content-type": "application/json"},
|
||||||
"content-type": "application/json",
|
|
||||||
"x-user-email": utilisateurEmail,
|
|
||||||
},
|
|
||||||
body: jsonEncode(body),
|
body: jsonEncode(body),
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import "../models/authenticated_profile.dart";
|
||||||
|
import "msal_bridge.dart";
|
||||||
|
|
||||||
|
enum AuthenticationMode { demo, azure }
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
AuthService._();
|
||||||
|
|
||||||
|
static const String _configuredMode = String.fromEnvironment(
|
||||||
|
"AUTH_MODE",
|
||||||
|
defaultValue: "demo",
|
||||||
|
);
|
||||||
|
static const String azureTenantId = String.fromEnvironment("AZURE_TENANT_ID");
|
||||||
|
static const String azureSpaClientId = String.fromEnvironment(
|
||||||
|
"AZURE_SPA_CLIENT_ID",
|
||||||
|
);
|
||||||
|
static const String azureApiScope = String.fromEnvironment("AZURE_API_SCOPE");
|
||||||
|
|
||||||
|
static const String demoStudentEmail = "lucas.martin@ensitech.eu";
|
||||||
|
static const String demoResponsableEmail = "karim.benali@ensup.eu";
|
||||||
|
|
||||||
|
static String? _demoEmail;
|
||||||
|
static AuthenticatedProfile? _currentProfile;
|
||||||
|
static bool _azureInitialized = false;
|
||||||
|
|
||||||
|
static AuthenticatedProfile? get currentProfile => _currentProfile;
|
||||||
|
|
||||||
|
static AuthenticationMode get mode {
|
||||||
|
return switch (_configuredMode) {
|
||||||
|
"demo" => AuthenticationMode.demo,
|
||||||
|
"azure" => AuthenticationMode.azure,
|
||||||
|
_ => throw StateError("AUTH_MODE doit valoir demo ou azure."),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void selectDemoStudent() {
|
||||||
|
_ensureDemoMode();
|
||||||
|
_demoEmail = demoStudentEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void selectDemoResponsable() {
|
||||||
|
_ensureDemoMode();
|
||||||
|
_demoEmail = demoResponsableEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearSession() {
|
||||||
|
_demoEmail = null;
|
||||||
|
_currentProfile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AuthenticatedProfile setProfileFromApi(dynamic reponse) {
|
||||||
|
final profile = AuthenticatedProfile.fromApi(reponse);
|
||||||
|
_currentProfile = profile;
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> signInWithMicrosoft() async {
|
||||||
|
await _initializeAzure();
|
||||||
|
await MsalBridge.signIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> hasMicrosoftAccount() async {
|
||||||
|
await _initializeAzure();
|
||||||
|
return MsalBridge.hasAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> signOut() async {
|
||||||
|
if (mode == AuthenticationMode.azure) {
|
||||||
|
await _initializeAzure();
|
||||||
|
await MsalBridge.signOut();
|
||||||
|
_currentProfile = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, String>> requestHeaders() async {
|
||||||
|
if (mode == AuthenticationMode.azure) {
|
||||||
|
await _initializeAzure();
|
||||||
|
final token = await MsalBridge.getAccessToken();
|
||||||
|
return {"authorization": "Bearer $token"};
|
||||||
|
}
|
||||||
|
|
||||||
|
final email = _demoEmail;
|
||||||
|
if (email == null) {
|
||||||
|
throw StateError("Aucun utilisateur de demonstration selectionne.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"x-user-email": email};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _initializeAzure() async {
|
||||||
|
if (mode != AuthenticationMode.azure) {
|
||||||
|
throw StateError("MSAL est disponible uniquement en mode Azure.");
|
||||||
|
}
|
||||||
|
if (_azureInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (azureTenantId.isEmpty ||
|
||||||
|
azureSpaClientId.isEmpty ||
|
||||||
|
azureApiScope.isEmpty) {
|
||||||
|
throw StateError("La configuration Azure du frontend est incomplete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await MsalBridge.initialize(
|
||||||
|
tenantId: azureTenantId,
|
||||||
|
clientId: azureSpaClientId,
|
||||||
|
scope: azureApiScope,
|
||||||
|
redirectUri: Uri.base.origin,
|
||||||
|
);
|
||||||
|
_azureInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _ensureDemoMode() {
|
||||||
|
if (mode != AuthenticationMode.demo) {
|
||||||
|
throw StateError(
|
||||||
|
"Une identite de demonstration est interdite en mode Azure.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import "dart:js_interop";
|
||||||
|
|
||||||
|
@JS("emeMsal.initialize")
|
||||||
|
external JSPromise<JSAny?> _initialize(
|
||||||
|
JSString tenantId,
|
||||||
|
JSString clientId,
|
||||||
|
JSString scope,
|
||||||
|
JSString redirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
@JS("emeMsal.signIn")
|
||||||
|
external JSPromise<JSAny?> _signIn();
|
||||||
|
|
||||||
|
@JS("emeMsal.getAccessToken")
|
||||||
|
external JSPromise<JSString> _getAccessToken();
|
||||||
|
|
||||||
|
@JS("emeMsal.hasAccount")
|
||||||
|
external JSBoolean _hasAccount();
|
||||||
|
|
||||||
|
@JS("emeMsal.signOut")
|
||||||
|
external JSPromise<JSAny?> _signOut();
|
||||||
|
|
||||||
|
class MsalBridge {
|
||||||
|
MsalBridge._();
|
||||||
|
|
||||||
|
static Future<void> initialize({
|
||||||
|
required String tenantId,
|
||||||
|
required String clientId,
|
||||||
|
required String scope,
|
||||||
|
required String redirectUri,
|
||||||
|
}) async {
|
||||||
|
await _initialize(
|
||||||
|
tenantId.toJS,
|
||||||
|
clientId.toJS,
|
||||||
|
scope.toJS,
|
||||||
|
redirectUri.toJS,
|
||||||
|
).toDart;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> signIn() async {
|
||||||
|
await _signIn().toDart;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> getAccessToken() async {
|
||||||
|
final token = await _getAccessToken().toDart;
|
||||||
|
return token.toDart;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasAccount() {
|
||||||
|
return _hasAccount().toDart;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> signOut() async {
|
||||||
|
await _signOut().toDart;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,8 @@ import "api_client.dart";
|
|||||||
class ResponsableService {
|
class ResponsableService {
|
||||||
ResponsableService._();
|
ResponsableService._();
|
||||||
|
|
||||||
static const String _email = ApiClient.responsableEmail;
|
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> dashboard() async {
|
static Future<Map<String, dynamic>> dashboard() async {
|
||||||
final reponse = await ApiClient.get(
|
final reponse = await ApiClient.get("/responsable/dashboard");
|
||||||
"/responsable/dashboard",
|
|
||||||
utilisateurEmail: _email,
|
|
||||||
);
|
|
||||||
return Map<String, dynamic>.from(reponse["data"] as Map);
|
return Map<String, dynamic>.from(reponse["data"] as Map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +27,12 @@ class ResponsableService {
|
|||||||
static Future<ApiFile> exporterHistorique() => ApiClient.getFile(
|
static Future<ApiFile> exporterHistorique() => ApiClient.getFile(
|
||||||
"/responsable/historique/export.csv",
|
"/responsable/historique/export.csv",
|
||||||
fallbackFileName: "historique-responsable.csv",
|
fallbackFileName: "historique-responsable.csv",
|
||||||
utilisateurEmail: _email,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<int> marquerNotificationsLues() async {
|
static Future<int> marquerNotificationsLues() async {
|
||||||
final reponse = await ApiClient.patch(
|
final reponse = await ApiClient.patch(
|
||||||
"/responsable/notifications/lu-toutes",
|
"/responsable/notifications/lu-toutes",
|
||||||
{},
|
{},
|
||||||
utilisateurEmail: _email,
|
|
||||||
);
|
);
|
||||||
final data = Map<String, dynamic>.from(reponse["data"] as Map);
|
final data = Map<String, dynamic>.from(reponse["data"] as Map);
|
||||||
return data["count"] as int;
|
return data["count"] as int;
|
||||||
@@ -52,12 +45,12 @@ class ResponsableService {
|
|||||||
final reponse = await ApiClient.patch("/responsable/anomalies/$id/statut", {
|
final reponse = await ApiClient.patch("/responsable/anomalies/$id/statut", {
|
||||||
"statut": statut,
|
"statut": statut,
|
||||||
"observation": "Traitement depuis l'interface responsable",
|
"observation": "Traitement depuis l'interface responsable",
|
||||||
}, utilisateurEmail: _email);
|
});
|
||||||
return Map<String, dynamic>.from(reponse["data"] as Map);
|
return Map<String, dynamic>.from(reponse["data"] as Map);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Map<String, dynamic>>> _liste(String chemin) async {
|
static Future<List<Map<String, dynamic>>> _liste(String chemin) async {
|
||||||
final reponse = await ApiClient.get(chemin, utilisateurEmail: _email);
|
final reponse = await ApiClient.get(chemin);
|
||||||
final data = reponse["data"] as List;
|
final data = reponse["data"] as List;
|
||||||
return data.map((item) => Map<String, dynamic>.from(item as Map)).toList();
|
return data.map((item) => Map<String, dynamic>.from(item as Map)).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+520
@@ -0,0 +1,520 @@
|
|||||||
|
{
|
||||||
|
"name": "eme-frontend",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^5.17.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.28.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@azure/msal-browser": {
|
||||||
|
"version": "5.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.1.tgz",
|
||||||
|
"integrity": "sha512-zBhRGzABKSI7hfWh5EaZmril5ybZ7imBN1qEZl5sDTaelr+l8SnPjZO50Q4dnKnm347YPIlBMSnXKZyh3Yu5DQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-common": "16.11.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@azure/msal-common": {
|
||||||
|
"version": "16.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.2.tgz",
|
||||||
|
"integrity": "sha512-yDhtBOGDCdK9ipQ9g3+wmlMEPnZx2pXaDicDd9jYyR1L+7lEbvEohTDmF5qejZDutZY3m9pWPxeYxzNC701A2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.28.1",
|
||||||
|
"@esbuild/android-arm": "0.28.1",
|
||||||
|
"@esbuild/android-arm64": "0.28.1",
|
||||||
|
"@esbuild/android-x64": "0.28.1",
|
||||||
|
"@esbuild/darwin-arm64": "0.28.1",
|
||||||
|
"@esbuild/darwin-x64": "0.28.1",
|
||||||
|
"@esbuild/freebsd-arm64": "0.28.1",
|
||||||
|
"@esbuild/freebsd-x64": "0.28.1",
|
||||||
|
"@esbuild/linux-arm": "0.28.1",
|
||||||
|
"@esbuild/linux-arm64": "0.28.1",
|
||||||
|
"@esbuild/linux-ia32": "0.28.1",
|
||||||
|
"@esbuild/linux-loong64": "0.28.1",
|
||||||
|
"@esbuild/linux-mips64el": "0.28.1",
|
||||||
|
"@esbuild/linux-ppc64": "0.28.1",
|
||||||
|
"@esbuild/linux-riscv64": "0.28.1",
|
||||||
|
"@esbuild/linux-s390x": "0.28.1",
|
||||||
|
"@esbuild/linux-x64": "0.28.1",
|
||||||
|
"@esbuild/netbsd-arm64": "0.28.1",
|
||||||
|
"@esbuild/netbsd-x64": "0.28.1",
|
||||||
|
"@esbuild/openbsd-arm64": "0.28.1",
|
||||||
|
"@esbuild/openbsd-x64": "0.28.1",
|
||||||
|
"@esbuild/openharmony-arm64": "0.28.1",
|
||||||
|
"@esbuild/sunos-x64": "0.28.1",
|
||||||
|
"@esbuild/win32-arm64": "0.28.1",
|
||||||
|
"@esbuild/win32-ia32": "0.28.1",
|
||||||
|
"@esbuild/win32-x64": "0.28.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "eme-frontend-auth",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build:auth": "esbuild ./web/auth/msal_bridge.js --bundle --minify --format=iife --outfile=./web/msal_bridge.bundle.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^5.17.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.28.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
InteractionRequiredAuthError,
|
||||||
|
PublicClientApplication,
|
||||||
|
} from "@azure/msal-browser";
|
||||||
|
|
||||||
|
let msalInstance = null;
|
||||||
|
let apiScope = "";
|
||||||
|
|
||||||
|
function requireInstance() {
|
||||||
|
if (!msalInstance) {
|
||||||
|
throw new Error("MSAL n'est pas initialise.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return msalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeAccount() {
|
||||||
|
const instance = requireInstance();
|
||||||
|
return instance.getActiveAccount() ?? instance.getAllAccounts()[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initialize(tenantId, clientId, scope, redirectUri) {
|
||||||
|
if (msalInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tenantId || !clientId || !scope || !redirectUri) {
|
||||||
|
throw new Error("La configuration Azure du frontend est incomplete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
apiScope = scope;
|
||||||
|
msalInstance = await PublicClientApplication.createPublicClientApplication({
|
||||||
|
auth: {
|
||||||
|
clientId,
|
||||||
|
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||||
|
redirectUri,
|
||||||
|
postLogoutRedirectUri: redirectUri,
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
cacheLocation: "sessionStorage",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const account = activeAccount();
|
||||||
|
if (account) {
|
||||||
|
msalInstance.setActiveAccount(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signIn() {
|
||||||
|
const instance = requireInstance();
|
||||||
|
const result = await instance.loginPopup({
|
||||||
|
scopes: ["openid", "profile", "email", apiScope],
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.setActiveAccount(result.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccessToken() {
|
||||||
|
const instance = requireInstance();
|
||||||
|
const account = activeAccount();
|
||||||
|
if (!account) {
|
||||||
|
throw new Error("Aucun compte Microsoft connecte.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await instance.acquireTokenSilent({
|
||||||
|
account,
|
||||||
|
scopes: [apiScope],
|
||||||
|
});
|
||||||
|
return result.accessToken;
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof InteractionRequiredAuthError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await instance.acquireTokenPopup({
|
||||||
|
account,
|
||||||
|
scopes: [apiScope],
|
||||||
|
});
|
||||||
|
return result.accessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAccount() {
|
||||||
|
return activeAccount() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signOut() {
|
||||||
|
const instance = requireInstance();
|
||||||
|
const account = activeAccount();
|
||||||
|
if (!account) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await instance.logoutPopup({ account });
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.emeMsal = {
|
||||||
|
getAccessToken,
|
||||||
|
hasAccount,
|
||||||
|
initialize,
|
||||||
|
signIn,
|
||||||
|
signOut,
|
||||||
|
};
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script src="msal_bridge.bundle.js"></script>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@ l'historique complet, y compris des étapes devenues obsolètes après brancheme
|
|||||||
| Base de données | Terminé pour la V1 | Schéma Prisma 16 entités, migrations, seeds et fixtures de démonstration. |
|
| Base de données | Terminé pour la V1 | Schéma Prisma 16 entités, migrations, seeds et fixtures de démonstration. |
|
||||||
| API étudiant | Terminé pour la V1 | Catalogue, détail matériel, création d'emprunt, mes emprunts, restitution, anomalie automatique. |
|
| API étudiant | Terminé pour la V1 | Catalogue, détail matériel, création d'emprunt, mes emprunts, restitution, anomalie automatique. |
|
||||||
| Frontend étudiant | Fonctionnel pour la V1 | Parcours API complet, ordre identification puis choix validé, interface premium et responsive fidèle à la maquette. |
|
| Frontend étudiant | Fonctionnel pour la V1 | Parcours API complet, ordre identification puis choix validé, interface premium et responsive fidèle à la maquette. |
|
||||||
| Authentification | Simulée | `x-user-email` côté backend et identité démo côté frontend ; Azure AD reste à faire. |
|
| Authentification | Prête pour Azure, démo active | Double mode sécurisé : `x-user-email` seulement en développement, MSAL/OIDC + Bearer JWT en mode Azure. Validation réelle en attente des identifiants Entra ID. |
|
||||||
| API responsable | Terminé pour la V1 | Dashboard, emprunts, stock, anomalies, notifications, historique et export CSV, avec contrôle du rôle et du campus. |
|
| API responsable | Terminé pour la V1 | Dashboard, emprunts, stock, anomalies, notifications, historique et export CSV, avec contrôle du rôle et du campus. |
|
||||||
| Frontend responsable | Fonctionnel pour la V1 | Espace de supervision responsive avec navigation dédiée, dashboard métier, vues API et export CSV opérationnel. |
|
| Frontend responsable | Fonctionnel pour la V1 | Espace de supervision responsive avec navigation dédiée, dashboard métier, vues API et export CSV opérationnel. |
|
||||||
| Tests automatisés | Non démarré | Tests backend/frontend/E2E à ajouter ; tests runtime manuels effectués. |
|
| Tests automatisés | Non démarré | Tests backend/frontend/E2E à ajouter ; tests runtime manuels effectués. |
|
||||||
@@ -56,6 +56,7 @@ npm run lint
|
|||||||
npm run seed
|
npm run seed
|
||||||
npm run fixtures
|
npm run fixtures
|
||||||
cd ../eme-frontend
|
cd ../eme-frontend
|
||||||
|
npm run build:auth
|
||||||
flutter run -d web-server --web-port 5000
|
flutter run -d web-server --web-port 5000
|
||||||
C:\flutter\flutter\bin\cache\dart-sdk\bin\dart.exe analyze
|
C:\flutter\flutter\bin\cache\dart-sdk\bin\dart.exe analyze
|
||||||
```
|
```
|
||||||
@@ -64,7 +65,8 @@ Notes :
|
|||||||
- le backend écoute sur `http://localhost:3000` ;
|
- le backend écoute sur `http://localhost:3000` ;
|
||||||
- le frontend web de démo écoute sur `http://localhost:5000` ;
|
- le frontend web de démo écoute sur `http://localhost:5000` ;
|
||||||
- Adminer est disponible sur `http://localhost:8081` ;
|
- Adminer est disponible sur `http://localhost:8081` ;
|
||||||
- le wrapper `flutter` peut rester bloqué dans l'environnement Codex ; l'analyse directe avec l'exécutable Dart fonctionne et ne signale aucune erreur.
|
- le wrapper `flutter` peut rester bloqué dans l'environnement Codex ; l'analyse directe avec l'exécutable Dart fonctionne et ne signale aucune erreur ;
|
||||||
|
- la compilation Web complète a été validée avec l'exécutable Dart du SDK Flutter et produit `build/web`.
|
||||||
|
|
||||||
## Scénario de démo validé
|
## Scénario de démo validé
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ Scénario responsable validé avec SQL Server Docker et backend compilé :
|
|||||||
2. Lucas (`ETUDIANT`) reçoit une réponse `403` sur les routes responsable.
|
2. Lucas (`ETUDIANT`) reçoit une réponse `403` sur les routes responsable.
|
||||||
3. Une anomalie peut avancer dans le cycle `DETECTEE -> EN_COURS_TRAITEMENT -> RESOLUE -> CLOTUREE`.
|
3. Une anomalie peut avancer dans le cycle `DETECTEE -> EN_COURS_TRAITEMENT -> RESOLUE -> CLOTUREE`.
|
||||||
4. Chaque transition enregistre le responsable et crée une entrée d'historique.
|
4. Chaque transition enregistre le responsable et crée une entrée d'historique.
|
||||||
5. L'API génère l'export CSV de l'historique ; son téléchargement depuis l'interface Flutter reste à ajouter.
|
5. L'API génère l'export CSV de l'historique et l'interface Flutter déclenche son téléchargement.
|
||||||
|
|
||||||
### Note outillage — migrations en environnement non-interactif
|
### Note outillage — migrations en environnement non-interactif
|
||||||
|
|
||||||
@@ -269,7 +271,7 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
| `Materiel.statut` -> `@default("DISPONIBLE")` | À faire avec migration + ERD | Faible : le code fournit déjà le statut explicitement. |
|
| `Materiel.statut` -> `@default("DISPONIBLE")` | À faire avec migration + ERD | Faible : le code fournit déjà le statut explicitement. |
|
||||||
| Tailles de colonnes `@db.NVarChar(n)` | À cadrer | Faible V1, important pour qualité BDD. |
|
| Tailles de colonnes `@db.NVarChar(n)` | À cadrer | Faible V1, important pour qualité BDD. |
|
||||||
| Swagger/OpenAPI | À faire | Moyen : utile pour tester/documenter l'API. |
|
| Swagger/OpenAPI | À faire | Moyen : utile pour tester/documenter l'API. |
|
||||||
| Auth réelle Azure AD | À faire | Fort : requis pour sortir de la démo avec auth simulée. |
|
| Validation réelle Azure AD | En attente des identifiants Entra ID | Fort : le code OIDC/MSAL/JWT est prêt, mais une connexion Microsoft réelle doit encore être testée sur le tenant ENSUP. |
|
||||||
| Limite d'emprunts actifs par étudiant | À valider métier | Non bloquant V1 ; décision métier non présente dans les règles actuelles. |
|
| Limite d'emprunts actifs par étudiant | À valider métier | Non bloquant V1 ; décision métier non présente dans les règles actuelles. |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -442,6 +444,20 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Écrans de confirmation et de résultat conservés fonctionnellement et harmonisés via les composants partagés.
|
- Écrans de confirmation et de résultat conservés fonctionnellement et harmonisés via les composants partagés.
|
||||||
- Vérification statique : `dart analyze` sans erreur.
|
- Vérification statique : `dart analyze` sans erreur.
|
||||||
|
|
||||||
|
### Étape 47 — Sécurisation SSO Microsoft Entra ID
|
||||||
|
- Branche dédiée `feat/block-3-azure-sso` créée depuis `develop`.
|
||||||
|
- Backend configuré avec deux modes : `demo` en développement et `azure` pour une authentification réelle ; le mode démo est explicitement interdit en production.
|
||||||
|
- Middleware d'authentification refactoré vers un service dédié. En mode Azure, `x-user-email` est ignoré et un Bearer Token Microsoft est obligatoire.
|
||||||
|
- Validation JWT ajoutée : signature RSA via les clés JWKS Microsoft, algorithme `RS256`, issuer, audience, tenant, scope `access_as_user`, identifiant `oid` et domaines `ensup.eu` / `ensitech.eu`.
|
||||||
|
- L'utilisateur, son rôle et son campus restent résolus depuis `eme_db`. Un étudiant obtient `403` sur la supervision, tandis que le responsable de démonstration obtient `200`.
|
||||||
|
- Frontend doté d'un double mode de session. La simulation sélectionne Lucas ou Karim sans pouvoir être utilisée en mode Azure.
|
||||||
|
- Bibliothèque officielle `@azure/msal-browser` intégrée via un bundle local construit avec esbuild et un pont typé `dart:js_interop`.
|
||||||
|
- Flux OpenID Connect préparé avec connexion Microsoft, acquisition silencieuse de l'Access Token, appel de `/api/auth/me`, navigation selon le rôle local, restauration après actualisation et déconnexion MSAL.
|
||||||
|
- Profil authentifié typé et affiché dans les espaces étudiant et responsable : nom, initiales, classe, rôle et campus ne sont plus figés sur les identités de démonstration.
|
||||||
|
- Vérifications réussies : build et lint backend, analyse Dart, compilation Flutter Web complète et contrôles runtime des rôles en mode démo.
|
||||||
|
- Limite actuelle : faute d'identifiants Entra ID, le flux Microsoft réel n'a pas encore pu être exécuté. Le QR sécurisé reste un sous-bloc séparé à développer après le SSO.
|
||||||
|
- Commits : `313bb1c`, `530bd52`, `ef31e2a`, `dc208fa`, `36224f3`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dernière mise à jour : 2026-07-23 — Espace étudiant premium et responsive, fidèle à la maquette ENSUP.*
|
*Dernière mise à jour : 2026-07-23 — SSO Microsoft Entra ID prêt à configurer, mode démo sécurisé actif.*
|
||||||
|
|||||||
Reference in New Issue
Block a user