up login page

This commit is contained in:
Tykayn 2025-10-03 11:08:34 +02:00 committed by tykayn
parent 92cbbb1595
commit 3b95c7871c
49 changed files with 12320 additions and 43 deletions

89
devops/README.md Normal file
View file

@ -0,0 +1,89 @@
# Documentation des fichiers Docker pour lancer la webapp et l'API Streamlit
Ce dossier contient les fichiers nécessaires pour construire et lancer deux services via Docker :
- **Frontend Angular** (service `frontend`)
- **Backend Streamlit** (service `streamlit`)
Les Dockerfiles attendent une arborescence suivante relativement à ce dossier `devops/` :
- `./frontend/` contient lapplication Angular et ses `package.json`
- `./streamlit/` contient lapplication Streamlit, `requirements.txt` et `app.py`
## 1) docker-compose.yml
Orchestre le lancement des services :
- Service `frontend` (port hôte 4200 → conteneur 4200)
- Service `streamlit` (port hôte 8501 → conteneur 8501)
Commandes de base :
```bash
# Construire les images et démarrer en arrière-plan
docker compose up -d --build
# Voir les logs des deux services
docker compose logs -f
# Arrêter les services
docker compose down
```
## 2) frontend-dockerfile.yml (Angular)
Image basée sur `node:24` qui :
- installe les dépendances `npm` à partir de `package*.json`
- construit lapplication (`npm run build -- --configuration=production`)
- sert le build avec `serve` sur le port 4200
Attendus côté projet :
- dossier `frontend/` avec `package.json`, code Angular, et un build générant `dist/`
Accès :
- Application disponible sur `http://localhost:4200`
Notes :
- Ce Dockerfile sert la version buildée (statique). Pour du hot-reload en dev, prévoir un autre service (ex. `ng serve`) ou un mapping de volumes.
## 3) streamlit-dockerfile.yml (Streamlit)
Image basée sur `python:3.11` qui :
- installe les dépendances via `requirements.txt`
- lance lappli avec `streamlit run app.py --server.port=8501 --server.address=0.0.0.0`
Attendus côté projet :
- dossier `streamlit/` avec `requirements.txt` et `app.py`
Accès :
- Application disponible sur `http://localhost:8501`
## 4) Variables denvironnement et volumes (optionnel)
Si vous avez besoin de configurer des variables ou des volumes, ajoutez-les dans `docker-compose.yml`, par exemple :
```yaml
services:
frontend:
environment:
- NODE_ENV=production
volumes:
- ./frontend/dist:/app/dist:ro
streamlit:
environment:
- STREAMLIT_ENV=production
volumes:
- ./streamlit:/app:ro
```
Adaptez selon vos besoins (mode lecture/écriture, fichiers de config, secrets via `.env`).
## 5) Dépannage
- Vérifiez que les dossiers `./frontend` et `./streamlit` existent bien au même niveau que `docker-compose.yml`.
- Assurez-vous que `frontend/package.json` contient les scripts et dépendances nécessaires au build.
- Assurez-vous que `streamlit/requirements.txt` liste toutes les dépendances Python requises et que `app.py` existe.
- Si un port est déjà utilisé, changez les ports exposés dans `docker-compose.yml`.
- Pour reconstruire proprement après des changements majeurs : `docker compose build --no-cache`.
## 6) Résumé des ports et conteneurs
- `frontend` → conteneur `frontend-app``http://localhost:4200`
- `streamlit` → conteneur `streamlit-app``http://localhost:8501`

418
devops/auth.md Normal file
View file

@ -0,0 +1,418 @@
# Authentification Cognito pour webapps Angular
Ce guide décrit comment intégrer lauthentification AWS Cognito (OIDC Code Flow avec PKCE) dans nos applications Angular.
## Prérequis côté Cognito
- Un User Pool Cognito créé
- Un App client « Public client » (sans secret) avec PKCE activé
- Un domaine Cognito configuré (ex: `my-tenant.auth.eu-west-1.amazoncognito.com`)
- Scopes OIDC: `openid`, `email`, `profile` (ajoutez dautres scopes si besoin)
- URLs de redirection et de logout autorisées
- Dev (Angular local):
- Callback: `http://localhost:4200/callback`
- Logout: `http://localhost:4200`
- Prod: adaptez avec votre domaine (ex: `https://app.example.com/callback` et `https://app.example.com`)
## Variables de configuration (Angular)
Exemple dentrée `environment.ts`:
```typescript
export const environment = {
production: false,
cognito: {
userPoolId: 'eu-west-1_XXXXXXXXX',
userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
domain: 'my-tenant.auth.eu-west-1.amazoncognito.com',
redirectSignIn: 'http://localhost:4200/callback',
redirectSignOut: 'http://localhost:4200',
region: 'eu-west-1',
scopes: ['openid', 'email', 'profile']
}
};
```
Mettez à jour `environment.prod.ts` avec les URLs de production.
## Option A — Hosted UI via AWS Amplify
Installation:
```bash
npm i aws-amplify @aws-amplify/ui-angular
```
Configuration (ex: `main.ts` ou `app.module.ts`):
```typescript
import { Amplify } from 'aws-amplify';
import { environment } from './environments/environment';
Amplify.configure({
Auth: {
region: environment.cognito.region,
userPoolId: environment.cognito.userPoolId,
userPoolWebClientId: environment.cognito.userPoolWebClientId,
oauth: {
domain: environment.cognito.domain,
scope: environment.cognito.scopes,
redirectSignIn: environment.cognito.redirectSignIn,
redirectSignOut: environment.cognito.redirectSignOut,
responseType: 'code'
}
}
});
```
Initiation du login (Hosted UI) et gestion du callback (ex: dans un service dauth):
```typescript
import { Auth } from 'aws-amplify';
export class AuthService {
signIn(): Promise<void> {
return Auth.federatedSignIn(); // redirige vers Hosted UI
}
async handleCallback(): Promise<void> {
// Amplify gère automatiquement léchange du code contre les tokens
const session = await Auth.currentSession();
const idToken = session.getIdToken().getJwtToken();
const accessToken = session.getAccessToken().getJwtToken();
// Stockez/consommez les tokens selon vos besoins
}
signOut(): Promise<void> {
return Auth.signOut(); // redirige vers redirectSignOut
}
}
```
Routing du callback (ex: `app-routing.module.ts`):
```typescript
{ path: 'callback', component: CallbackComponent }
```
Dans `CallbackComponent`, appelez `handleCallback()` puis redirigez lutilisateur vers lapp.
## Option B — angular-oauth2-oidc (OIDC Code Flow + PKCE)
Installation:
```bash
npm i angular-oauth2-oidc
```
Configuration (ex: `app.module.ts` et un `auth.config.ts`):
```typescript
// auth.config.ts
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'https://my-tenant.auth.eu-west-1.amazoncognito.com',
clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
responseType: 'code',
redirectUri: 'http://localhost:4200/callback',
postLogoutRedirectUri: 'http://localhost:4200',
scope: 'openid email profile',
showDebugInformation: false,
strictDiscoveryDocumentValidation: false, // Cognito nexpose pas toujours toutes les métadonnées standard
};
```
Initialisation au démarrage (ex: `app.module.ts`):
```typescript
import { OAuthService, OAuthModule } from 'angular-oauth2-oidc';
import { authConfig } from './auth.config';
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [OAuthService],
useFactory: (oauthService: OAuthService) => () => {
oauthService.configure(authConfig);
oauthService.setupAutomaticSilentRefresh();
return oauthService.loadDiscoveryDocumentAndLogin();
}
}
]
```
Déclenchement du login/logout:
```typescript
login(): void { this.oauthService.initLoginFlow(); }
logout(): void { this.oauthService.logOut(); }
getIdToken(): string | null { return this.oauthService.getIdToken(); }
```
Protection de routes (ex: guard minimal):
```typescript
canActivate(): boolean {
return this.oauthService.hasValidAccessToken() || this.oauthService.hasValidIdToken();
}
```
## Bonnes pratiques
- Utilisez le flow « Authorization Code + PKCE » pour les SPAs (sécurité accrue)
- Déclarez toutes les URLs de callback/logout (local, staging, prod) dans Cognito
- Synchronisez lheure système (skew de plus de 5 min peut invalider les tokens)
- Évitez dexposer des secrets côté frontend (client public uniquement)
- Renouvelez silencieusement les tokens lorsque possible (silent refresh)
## Dépannage
- `invalid_redirect_uri` / `redirect_mismatch`: vérifiez les URLs exactes dans Cognito
- Erreur « Not authorized » après login: facteurs possibles — scopes manquants, app client erroné
- Boucle de redirection: callback non routé ou handler non exécuté
- CORS/API: frontend authentifié ≠ API autorisée; configurez vos authorizers/API Gateway
- Cookies SameSite: si vous utilisez des cookies, ajustez SameSite/Lax/None selon le contexte
## Checklist déploiement
- Mettre à jour les URLs de callback/logout en prod
- Activer HTTPS sur le domaine de lapp
- Vérifier que le domaine Cognito est public et résout correctement
- Tester login/logout et rafraîchissement de token en environnement prod
## Activation conditionnelle via variable denvironnement + Guard Angular
Nous supportons un mode où lauthentification Cognito est optionnelle selon une variable denvironnement, et une Guard Angular assure la redirection vers une page de login configurable.
### Variables denvironnement
Dans `environment.ts` et `environment.prod.ts`:
```typescript
export const environment = {
production: false,
requireAuth: true, // si false, la Guard laisse passer sans authentification
loginRoute: '/login', // route Angular interne servant le bouton/flux de login
cognito: {
userPoolId: 'eu-west-1_XXXXXXXXX',
userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
domain: 'my-tenant.auth.eu-west-1.amazoncognito.com',
redirectSignIn: 'http://localhost:4200/callback',
redirectSignOut: 'http://localhost:4200',
region: 'eu-west-1',
scopes: ['openid', 'email', 'profile']
}
};
```
Mettez `requireAuth: false` sur des environnements de démonstration internes si nécessaire. En production, laissez `true`.
### Utilisation des fichiers .env dAngular
Nous utilisons les fichiers `.env` dAngular pour fournir ces variables au build. Créez les fichiers à la racine du projet Angular (même niveau que `package.json`) :
- `.env` (valeurs par défaut communes)
- `.env.development` (lues en dev)
- `.env.production` (lues en prod)
Exemple `.env.development` :
```bash
NG_APP_REQUIRE_AUTH=true
NG_APP_LOGIN_ROUTE=/login
NG_APP_COGNITO_USER_POOL_ID=eu-west-1_XXXXXXXXX
NG_APP_COGNITO_WEB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
NG_APP_COGNITO_DOMAIN=my-tenant.auth.eu-west-1.amazoncognito.com
NG_APP_COGNITO_REDIRECT_SIGNIN=http://localhost:4200/callback
NG_APP_COGNITO_REDIRECT_SIGNOUT=http://localhost:4200
NG_APP_COGNITO_REGION=eu-west-1
NG_APP_COGNITO_SCOPES=openid,email,profile
```
Exemple `.env.production` :
```bash
NG_APP_REQUIRE_AUTH=true
NG_APP_LOGIN_ROUTE=/login
NG_APP_COGNITO_USER_POOL_ID=eu-west-1_YYYYYYYYY
NG_APP_COGNITO_WEB_CLIENT_ID=yyyyyyyyyyyyyyyyyyyyyyyyyy
NG_APP_COGNITO_DOMAIN=prod-tenant.auth.eu-west-1.amazoncognito.com
NG_APP_COGNITO_REDIRECT_SIGNIN=https://app.example.com/callback
NG_APP_COGNITO_REDIRECT_SIGNOUT=https://app.example.com
NG_APP_COGNITO_REGION=eu-west-1
NG_APP_COGNITO_SCOPES=openid,email,profile
```
Conventions :
- Préfixe recommandé: `NG_APP_` pour exposer la variable au code frontend
- Les `.env.*` sont sélectionnés selon la configuration Angular (ex: `ng serve` utilise `development`, `ng build --configuration=production` utilise `production`)
Lecture dans le code Angular (Angular 17+/Vite) :
```typescript
const env = (import.meta as any).env as Record<string, string>;
export const environment = {
production: env['MODE'] === 'production',
requireAuth: String(env['NG_APP_REQUIRE_AUTH']).toLowerCase() === 'true',
loginRoute: env['NG_APP_LOGIN_ROUTE'] || '/login',
cognito: {
userPoolId: env['NG_APP_COGNITO_USER_POOL_ID'],
userPoolWebClientId: env['NG_APP_COGNITO_WEB_CLIENT_ID'],
domain: env['NG_APP_COGNITO_DOMAIN'],
redirectSignIn: env['NG_APP_COGNITO_REDIRECT_SIGNIN'],
redirectSignOut: env['NG_APP_COGNITO_REDIRECT_SIGNOUT'],
region: env['NG_APP_COGNITO_REGION'],
scopes: (env['NG_APP_COGNITO_SCOPES'] || 'openid,email,profile').split(',')
}
};
```
Note : selon la version de loutillage Angular, laccès peut être `import.meta.env.NG_APP_...` au lieu de lindexation par chaîne. Gardez le préfixe `NG_APP_` pour garantir lexposition au build.
### Injection des `.env` via Docker / docker-compose
Nos builds frontend sont statiques (servis par `serve`). Les variables doivent donc être connues au BUILD. Utilisez des `build.args` dans `docker-compose.yml` et, côté Dockerfile, écrivez un fichier `.env.production` avant le build Angular.
Exemple `docker-compose.yml` (extrait service `frontend`) :
```yaml
services:
frontend:
build:
context: ./frontend
dockerfile: ./frontend-dockerfile.yml
args:
NG_APP_REQUIRE_AUTH: "true"
NG_APP_LOGIN_ROUTE: "/login"
NG_APP_COGNITO_USER_POOL_ID: "eu-west-1_XXXXXXXXX"
NG_APP_COGNITO_WEB_CLIENT_ID: "xxxxxxxxxxxxxxxxxxxxxxxxxx"
NG_APP_COGNITO_DOMAIN: "my-tenant.auth.eu-west-1.amazoncognito.com"
NG_APP_COGNITO_REDIRECT_SIGNIN: "https://app.example.com/callback"
NG_APP_COGNITO_REDIRECT_SIGNOUT: "https://app.example.com"
NG_APP_COGNITO_REGION: "eu-west-1"
NG_APP_COGNITO_SCOPES: "openid,email,profile"
```
Adaptation optionnelle du `frontend-dockerfile.yml` (à ajouter avant `npm run build`) :
```dockerfile
# Arguments build-time pour générer .env.production
ARG NG_APP_REQUIRE_AUTH
ARG NG_APP_LOGIN_ROUTE
ARG NG_APP_COGNITO_USER_POOL_ID
ARG NG_APP_COGNITO_WEB_CLIENT_ID
ARG NG_APP_COGNITO_DOMAIN
ARG NG_APP_COGNITO_REDIRECT_SIGNIN
ARG NG_APP_COGNITO_REDIRECT_SIGNOUT
ARG NG_APP_COGNITO_REGION
ARG NG_APP_COGNITO_SCOPES
# Écrire le fichier .env.production à partir des args
RUN printf "NG_APP_REQUIRE_AUTH=%s\nNG_APP_LOGIN_ROUTE=%s\nNG_APP_COGNITO_USER_POOL_ID=%s\nNG_APP_COGNITO_WEB_CLIENT_ID=%s\nNG_APP_COGNITO_DOMAIN=%s\nNG_APP_COGNITO_REDIRECT_SIGNIN=%s\nNG_APP_COGNITO_REDIRECT_SIGNOUT=%s\nNG_APP_COGNITO_REGION=%s\nNG_APP_COGNITO_SCOPES=%s\n" \
"$NG_APP_REQUIRE_AUTH" \
"$NG_APP_LOGIN_ROUTE" \
"$NG_APP_COGNITO_USER_POOL_ID" \
"$NG_APP_COGNITO_WEB_CLIENT_ID" \
"$NG_APP_COGNITO_DOMAIN" \
"$NG_APP_COGNITO_REDIRECT_SIGNIN" \
"$NG_APP_COGNITO_REDIRECT_SIGNOUT" \
"$NG_APP_COGNITO_REGION" \
"$NG_APP_COGNITO_SCOPES" \
> .env.production
```
Ainsi, le build Angular lit automatiquement ces variables via `import.meta.env` et les injecte dans le bundle.
Remarques :
- Les `environment.*.ts` peuvent mapper `import.meta.env` pour exposer une API typed au reste de lapp.
- Pour des valeurs sensibles, préférez des mécanismes backend (ne jamais exposer de secrets côté SPA).
- Si vous avez besoin de « runtime config » (changer après build), utilisez une approche `config.json` servie par le serveur et chargée au démarrage de lapp.
### Guard Angular (générique)
Exemple de Guard compatible avec Amplify ou angular-oauth2-oidc: elle vérifie `requireAuth`. Si lauth est requise mais absente/expirée, elle redirige vers `loginRoute`.
```typescript
import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
async canActivate(): Promise<boolean | UrlTree> {
if (!environment.requireAuth) {
return true;
}
// 1) Vérification via Amplify (si utilisé)
try {
const { Auth } = await import('aws-amplify');
const session = await Auth.currentSession();
const valid = !!session?.getIdToken()?.getJwtToken();
if (valid) return true;
} catch (_) {
// ignore si Amplify non configuré
}
// 2) Vérification via angular-oauth2-oidc (si utilisé)
try {
const { OAuthService } = await import('angular-oauth2-oidc');
const oauth = (window as any).ngInjector?.get?.(OAuthService);
// Note: exposez éventuellement l'injector globalement au bootstrap
if (oauth && (oauth.hasValidAccessToken() || oauth.hasValidIdToken())) {
return true;
}
} catch (_) {
// ignore si non installé
}
return this.router.parseUrl(environment.loginRoute);
}
}
```
Astuce: pour récupérer des services dans la Guard sans injection directe (quand on veut la rendre agnostique), on peut exposer l`Injector` Angular globalement au bootstrap: `window.ngInjector = injector;` et lutiliser comme cidessus.
### Routage protégé
Protégez les routes nécessitant une session valide:
```typescript
{
path: 'app',
canActivate: [AuthGuard],
loadChildren: () => import('./features/app/app.module').then(m => m.AppModule)
}
```
### Page de login (route `loginRoute`)
La page associée à `environment.loginRoute` doit offrir un bouton « Se connecter ». Selon limplémentation:
- Avec Amplify:
```typescript
import { Auth } from 'aws-amplify';
login(): Promise<void> {
return Auth.federatedSignIn();
}
```
- Avec angular-oauth2-oidc:
```typescript
constructor(private oauthService: OAuthService) {}
login(): void {
this.oauthService.initLoginFlow();
}
```
Au retour sur `/callback`, suivez la section correspondante (Amplify: `currentSession()`, OIDC: `loadDiscoveryDocumentAndLogin()` déjà appelé au bootstrap) puis redirigez vers lespace protégé.

View file

@ -0,0 +1,31 @@
# dockerfile
FROM node:24
# Définir le répertoire de travail
WORKDIR /app
# Définir une variable d'environnement pour le chemin de la librairie Angular
ENV LIB_PATH=../my-workspace/projects/sae-lib
# Copier les fichiers package.json et package-lock.json de la librairie
COPY ${LIB_PATH}/package*.json ./
# Installer les dépendances npm
RUN npm install
# Copier le reste des sources (lib Angular + config Storybook)
COPY ${LIB_PATH}/ .
# Construire la librairie Angular (si un script build existe)
RUN npm run build --if-present
# Générer le build statique de Storybook
RUN npm run storybook:build
# Installer un serveur statique pour servir Storybook
RUN npm install -g serve
# Exposer le port standard de Storybook statique
EXPOSE 6006
# Servir le dossier généré par Storybook
CMD ["serve", "-s", "storybook-static", "-l", "6006"]

418
devops/design-system.md Normal file
View file

@ -0,0 +1,418 @@
# Authentification Cognito pour webapps Angular
Ce guide décrit comment intégrer lauthentification AWS Cognito (OIDC Code Flow avec PKCE) dans nos applications Angular.
## Prérequis côté Cognito
- Un User Pool Cognito créé
- Un App client « Public client » (sans secret) avec PKCE activé
- Un domaine Cognito configuré (ex: `my-tenant.auth.eu-west-1.amazoncognito.com`)
- Scopes OIDC: `openid`, `email`, `profile` (ajoutez dautres scopes si besoin)
- URLs de redirection et de logout autorisées
- Dev (Angular local):
- Callback: `http://localhost:4200/callback`
- Logout: `http://localhost:4200`
- Prod: adaptez avec votre domaine (ex: `https://app.example.com/callback` et `https://app.example.com`)
## Variables de configuration (Angular)
Exemple dentrée `environment.ts`:
```typescript
export const environment = {
production: false,
cognito: {
userPoolId: 'eu-west-1_XXXXXXXXX',
userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
domain: 'my-tenant.auth.eu-west-1.amazoncognito.com',
redirectSignIn: 'http://localhost:4200/callback',
redirectSignOut: 'http://localhost:4200',
region: 'eu-west-1',
scopes: ['openid', 'email', 'profile']
}
};
```
Mettez à jour `environment.prod.ts` avec les URLs de production.
## Option A — Hosted UI via AWS Amplify
Installation:
```bash
npm i aws-amplify @aws-amplify/ui-angular
```
Configuration (ex: `main.ts` ou `app.module.ts`):
```typescript
import { Amplify } from 'aws-amplify';
import { environment } from './environments/environment';
Amplify.configure({
Auth: {
region: environment.cognito.region,
userPoolId: environment.cognito.userPoolId,
userPoolWebClientId: environment.cognito.userPoolWebClientId,
oauth: {
domain: environment.cognito.domain,
scope: environment.cognito.scopes,
redirectSignIn: environment.cognito.redirectSignIn,
redirectSignOut: environment.cognito.redirectSignOut,
responseType: 'code'
}
}
});
```
Initiation du login (Hosted UI) et gestion du callback (ex: dans un service dauth):
```typescript
import { Auth } from 'aws-amplify';
export class AuthService {
signIn(): Promise<void> {
return Auth.federatedSignIn(); // redirige vers Hosted UI
}
async handleCallback(): Promise<void> {
// Amplify gère automatiquement léchange du code contre les tokens
const session = await Auth.currentSession();
const idToken = session.getIdToken().getJwtToken();
const accessToken = session.getAccessToken().getJwtToken();
// Stockez/consommez les tokens selon vos besoins
}
signOut(): Promise<void> {
return Auth.signOut(); // redirige vers redirectSignOut
}
}
```
Routing du callback (ex: `app-routing.module.ts`):
```typescript
{ path: 'callback', component: CallbackComponent }
```
Dans `CallbackComponent`, appelez `handleCallback()` puis redirigez lutilisateur vers lapp.
## Option B — angular-oauth2-oidc (OIDC Code Flow + PKCE)
Installation:
```bash
npm i angular-oauth2-oidc
```
Configuration (ex: `app.module.ts` et un `auth.config.ts`):
```typescript
// auth.config.ts
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'https://my-tenant.auth.eu-west-1.amazoncognito.com',
clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
responseType: 'code',
redirectUri: 'http://localhost:4200/callback',
postLogoutRedirectUri: 'http://localhost:4200',
scope: 'openid email profile',
showDebugInformation: false,
strictDiscoveryDocumentValidation: false, // Cognito nexpose pas toujours toutes les métadonnées standard
};
```
Initialisation au démarrage (ex: `app.module.ts`):
```typescript
import { OAuthService, OAuthModule } from 'angular-oauth2-oidc';
import { authConfig } from './auth.config';
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [OAuthService],
useFactory: (oauthService: OAuthService) => () => {
oauthService.configure(authConfig);
oauthService.setupAutomaticSilentRefresh();
return oauthService.loadDiscoveryDocumentAndLogin();
}
}
]
```
Déclenchement du login/logout:
```typescript
login(): void { this.oauthService.initLoginFlow(); }
logout(): void { this.oauthService.logOut(); }
getIdToken(): string | null { return this.oauthService.getIdToken(); }
```
Protection de routes (ex: guard minimal):
```typescript
canActivate(): boolean {
return this.oauthService.hasValidAccessToken() || this.oauthService.hasValidIdToken();
}
```
## Bonnes pratiques
- Utilisez le flow « Authorization Code + PKCE » pour les SPAs (sécurité accrue)
- Déclarez toutes les URLs de callback/logout (local, staging, prod) dans Cognito
- Synchronisez lheure système (skew de plus de 5 min peut invalider les tokens)
- Évitez dexposer des secrets côté frontend (client public uniquement)
- Renouvelez silencieusement les tokens lorsque possible (silent refresh)
## Dépannage
- `invalid_redirect_uri` / `redirect_mismatch`: vérifiez les URLs exactes dans Cognito
- Erreur « Not authorized » après login: facteurs possibles — scopes manquants, app client erroné
- Boucle de redirection: callback non routé ou handler non exécuté
- CORS/API: frontend authentifié ≠ API autorisée; configurez vos authorizers/API Gateway
- Cookies SameSite: si vous utilisez des cookies, ajustez SameSite/Lax/None selon le contexte
## Checklist déploiement
- Mettre à jour les URLs de callback/logout en prod
- Activer HTTPS sur le domaine de lapp
- Vérifier que le domaine Cognito est public et résout correctement
- Tester login/logout et rafraîchissement de token en environnement prod
## Activation conditionnelle via variable denvironnement + Guard Angular
Nous supportons un mode où lauthentification Cognito est optionnelle selon une variable denvironnement, et une Guard Angular assure la redirection vers une page de login configurable.
### Variables denvironnement
Dans `environment.ts` et `environment.prod.ts`:
```typescript
export const environment = {
production: false,
requireAuth: true, // si false, la Guard laisse passer sans authentification
loginRoute: '/login', // route Angular interne servant le bouton/flux de login
cognito: {
userPoolId: 'eu-west-1_XXXXXXXXX',
userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
domain: 'my-tenant.auth.eu-west-1.amazoncognito.com',
redirectSignIn: 'http://localhost:4200/callback',
redirectSignOut: 'http://localhost:4200',
region: 'eu-west-1',
scopes: ['openid', 'email', 'profile']
}
};
```
Mettez `requireAuth: false` sur des environnements de démonstration internes si nécessaire. En production, laissez `true`.
### Utilisation des fichiers .env dAngular
Nous utilisons les fichiers `.env` dAngular pour fournir ces variables au build. Créez les fichiers à la racine du projet Angular (même niveau que `package.json`) :
- `.env` (valeurs par défaut communes)
- `.env.development` (lues en dev)
- `.env.production` (lues en prod)
Exemple `.env.development` :
```bash
NG_APP_REQUIRE_AUTH=true
NG_APP_LOGIN_ROUTE=/login
NG_APP_COGNITO_USER_POOL_ID=eu-west-1_XXXXXXXXX
NG_APP_COGNITO_WEB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
NG_APP_COGNITO_DOMAIN=my-tenant.auth.eu-west-1.amazoncognito.com
NG_APP_COGNITO_REDIRECT_SIGNIN=http://localhost:4200/callback
NG_APP_COGNITO_REDIRECT_SIGNOUT=http://localhost:4200
NG_APP_COGNITO_REGION=eu-west-1
NG_APP_COGNITO_SCOPES=openid,email,profile
```
Exemple `.env.production` :
```bash
NG_APP_REQUIRE_AUTH=true
NG_APP_LOGIN_ROUTE=/login
NG_APP_COGNITO_USER_POOL_ID=eu-west-1_YYYYYYYYY
NG_APP_COGNITO_WEB_CLIENT_ID=yyyyyyyyyyyyyyyyyyyyyyyyyy
NG_APP_COGNITO_DOMAIN=prod-tenant.auth.eu-west-1.amazoncognito.com
NG_APP_COGNITO_REDIRECT_SIGNIN=https://app.example.com/callback
NG_APP_COGNITO_REDIRECT_SIGNOUT=https://app.example.com
NG_APP_COGNITO_REGION=eu-west-1
NG_APP_COGNITO_SCOPES=openid,email,profile
```
Conventions :
- Préfixe recommandé: `NG_APP_` pour exposer la variable au code frontend
- Les `.env.*` sont sélectionnés selon la configuration Angular (ex: `ng serve` utilise `development`, `ng build --configuration=production` utilise `production`)
Lecture dans le code Angular (Angular 17+/Vite) :
```typescript
const env = (import.meta as any).env as Record<string, string>;
export const environment = {
production: env['MODE'] === 'production',
requireAuth: String(env['NG_APP_REQUIRE_AUTH']).toLowerCase() === 'true',
loginRoute: env['NG_APP_LOGIN_ROUTE'] || '/login',
cognito: {
userPoolId: env['NG_APP_COGNITO_USER_POOL_ID'],
userPoolWebClientId: env['NG_APP_COGNITO_WEB_CLIENT_ID'],
domain: env['NG_APP_COGNITO_DOMAIN'],
redirectSignIn: env['NG_APP_COGNITO_REDIRECT_SIGNIN'],
redirectSignOut: env['NG_APP_COGNITO_REDIRECT_SIGNOUT'],
region: env['NG_APP_COGNITO_REGION'],
scopes: (env['NG_APP_COGNITO_SCOPES'] || 'openid,email,profile').split(',')
}
};
```
Note : selon la version de loutillage Angular, laccès peut être `import.meta.env.NG_APP_...` au lieu de lindexation par chaîne. Gardez le préfixe `NG_APP_` pour garantir lexposition au build.
### Injection des `.env` via Docker / docker-compose
Nos builds frontend sont statiques (servis par `serve`). Les variables doivent donc être connues au BUILD. Utilisez des `build.args` dans `docker-compose.yml` et, côté Dockerfile, écrivez un fichier `.env.production` avant le build Angular.
Exemple `docker-compose.yml` (extrait service `frontend`) :
```yaml
services:
frontend:
build:
context: ./frontend
dockerfile: ./frontend-dockerfile.yml
args:
NG_APP_REQUIRE_AUTH: "true"
NG_APP_LOGIN_ROUTE: "/login"
NG_APP_COGNITO_USER_POOL_ID: "eu-west-1_XXXXXXXXX"
NG_APP_COGNITO_WEB_CLIENT_ID: "xxxxxxxxxxxxxxxxxxxxxxxxxx"
NG_APP_COGNITO_DOMAIN: "my-tenant.auth.eu-west-1.amazoncognito.com"
NG_APP_COGNITO_REDIRECT_SIGNIN: "https://app.example.com/callback"
NG_APP_COGNITO_REDIRECT_SIGNOUT: "https://app.example.com"
NG_APP_COGNITO_REGION: "eu-west-1"
NG_APP_COGNITO_SCOPES: "openid,email,profile"
```
Adaptation optionnelle du `frontend-dockerfile.yml` (à ajouter avant `npm run build`) :
```dockerfile
# Arguments build-time pour générer .env.production
ARG NG_APP_REQUIRE_AUTH
ARG NG_APP_LOGIN_ROUTE
ARG NG_APP_COGNITO_USER_POOL_ID
ARG NG_APP_COGNITO_WEB_CLIENT_ID
ARG NG_APP_COGNITO_DOMAIN
ARG NG_APP_COGNITO_REDIRECT_SIGNIN
ARG NG_APP_COGNITO_REDIRECT_SIGNOUT
ARG NG_APP_COGNITO_REGION
ARG NG_APP_COGNITO_SCOPES
# Écrire le fichier .env.production à partir des args
RUN printf "NG_APP_REQUIRE_AUTH=%s\nNG_APP_LOGIN_ROUTE=%s\nNG_APP_COGNITO_USER_POOL_ID=%s\nNG_APP_COGNITO_WEB_CLIENT_ID=%s\nNG_APP_COGNITO_DOMAIN=%s\nNG_APP_COGNITO_REDIRECT_SIGNIN=%s\nNG_APP_COGNITO_REDIRECT_SIGNOUT=%s\nNG_APP_COGNITO_REGION=%s\nNG_APP_COGNITO_SCOPES=%s\n" \
"$NG_APP_REQUIRE_AUTH" \
"$NG_APP_LOGIN_ROUTE" \
"$NG_APP_COGNITO_USER_POOL_ID" \
"$NG_APP_COGNITO_WEB_CLIENT_ID" \
"$NG_APP_COGNITO_DOMAIN" \
"$NG_APP_COGNITO_REDIRECT_SIGNIN" \
"$NG_APP_COGNITO_REDIRECT_SIGNOUT" \
"$NG_APP_COGNITO_REGION" \
"$NG_APP_COGNITO_SCOPES" \
> .env.production
```
Ainsi, le build Angular lit automatiquement ces variables via `import.meta.env` et les injecte dans le bundle.
Remarques :
- Les `environment.*.ts` peuvent mapper `import.meta.env` pour exposer une API typed au reste de lapp.
- Pour des valeurs sensibles, préférez des mécanismes backend (ne jamais exposer de secrets côté SPA).
- Si vous avez besoin de « runtime config » (changer après build), utilisez une approche `config.json` servie par le serveur et chargée au démarrage de lapp.
### Guard Angular (générique)
Exemple de Guard compatible avec Amplify ou angular-oauth2-oidc: elle vérifie `requireAuth`. Si lauth est requise mais absente/expirée, elle redirige vers `loginRoute`.
```typescript
import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
async canActivate(): Promise<boolean | UrlTree> {
if (!environment.requireAuth) {
return true;
}
// 1) Vérification via Amplify (si utilisé)
try {
const { Auth } = await import('aws-amplify');
const session = await Auth.currentSession();
const valid = !!session?.getIdToken()?.getJwtToken();
if (valid) return true;
} catch (_) {
// ignore si Amplify non configuré
}
// 2) Vérification via angular-oauth2-oidc (si utilisé)
try {
const { OAuthService } = await import('angular-oauth2-oidc');
const oauth = (window as any).ngInjector?.get?.(OAuthService);
// Note: exposez éventuellement l'injector globalement au bootstrap
if (oauth && (oauth.hasValidAccessToken() || oauth.hasValidIdToken())) {
return true;
}
} catch (_) {
// ignore si non installé
}
return this.router.parseUrl(environment.loginRoute);
}
}
```
Astuce: pour récupérer des services dans la Guard sans injection directe (quand on veut la rendre agnostique), on peut exposer l`Injector` Angular globalement au bootstrap: `window.ngInjector = injector;` et lutiliser comme cidessus.
### Routage protégé
Protégez les routes nécessitant une session valide:
```typescript
{
path: 'app',
canActivate: [AuthGuard],
loadChildren: () => import('./features/app/app.module').then(m => m.AppModule)
}
```
### Page de login (route `loginRoute`)
La page associée à `environment.loginRoute` doit offrir un bouton « Se connecter ». Selon limplémentation:
- Avec Amplify:
```typescript
import { Auth } from 'aws-amplify';
login(): Promise<void> {
return Auth.federatedSignIn();
}
```
- Avec angular-oauth2-oidc:
```typescript
constructor(private oauthService: OAuthService) {}
login(): void {
this.oauthService.initLoginFlow();
}
```
Au retour sur `/callback`, suivez la section correspondante (Amplify: `currentSession()`, OIDC: `loadDiscoveryDocumentAndLogin()` déjà appelé au bootstrap) puis redirigez vers lespace protégé.

21
devops/docker-compose.yml Normal file
View file

@ -0,0 +1,21 @@
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: ./frontend-dockerfile.yml
ports:
- "4200:4200"
restart: unless-stopped
container_name: frontend-app
streamlit:
build:
context: ./streamlit
dockerfile: ./streamlit-dockerfile.yml
ports:
- "8501:8501"
restart: unless-stopped
container_name: streamlit-app

View file

@ -0,0 +1,30 @@
# dockerfile
FROM node:24
# Définir le répertoire de travail
WORKDIR /app
# Définir une variable d'environnement pour le chemin de la librairie Angular
ENV LIB_PATH=../ma-webapp-angular
# Copier les fichiers package.json et package-lock.json de la librairie
COPY ${LIB_PATH}/package*.json ./
# Installer les dépendances npm
RUN npm install
# Copier le reste des sources (lib Angular + config Storybook)
COPY ${LIB_PATH}/ .
# Construire l'application Angular
RUN npm run build -- --configuration=production
# Installer un serveur HTTP pour servir l'application (par exemple, serve)
RUN npm install -g serve
# Exposer le port 4200 (ou 80 si vous préférez)
EXPOSE 4200
# Commande pour servir l'application Angular buildée
CMD ["serve", "-s", "dist", "-l", "4200"]

140
devops/gitlab-ci.yml Normal file
View file

@ -0,0 +1,140 @@
stages:
- setup
- build
- test
- package
- deploy
variables:
NODE_OPTIONS: --max_old_space_size=4096
# Répertoires (à adapter à votre mono-repo)
FRONTEND_DIR: "frontend"
DESIGN_SYSTEM_DIR: "design-system"
STREAMLIT_DIR: "streamlit"
default:
before_script:
- echo "Node $(node -v) / NPM $(npm -v)"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- ${FRONTEND_DIR}/.npm/
- ${DESIGN_SYSTEM_DIR}/.npm/
setup:node:
image: node:24
stage: setup
script:
- node -v && npm -v
rules:
- when: always
build:angular:
image: node:24
stage: build
needs: ["setup:node"]
variables:
# Variables .env pour le build Angular (optionnelles)
NG_APP_REQUIRE_AUTH: "true"
NG_APP_LOGIN_ROUTE: "/login"
before_script:
- cd ${FRONTEND_DIR}
- npm ci
script:
- echo "NG_APP_REQUIRE_AUTH=${NG_APP_REQUIRE_AUTH}" >> .env.production
- echo "NG_APP_LOGIN_ROUTE=${NG_APP_LOGIN_ROUTE}" >> .env.production
- npm run build -- --configuration=production
artifacts:
when: always
expire_in: 1 week
paths:
- ${FRONTEND_DIR}/dist/
rules:
- if: $CI_COMMIT_BRANCH
build:storybook:
image: node:24
stage: build
needs: ["setup:node"]
before_script:
- cd ${DESIGN_SYSTEM_DIR}
- npm ci
script:
- npm run build --if-present
- npm run storybook:build
artifacts:
when: always
expire_in: 1 week
paths:
- ${DESIGN_SYSTEM_DIR}/storybook-static/
rules:
- if: $CI_COMMIT_BRANCH
setup:streamlit:
image: python:3.11
stage: setup
before_script:
- cd ${STREAMLIT_DIR}
script:
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
artifacts:
when: on_failure
rules:
- when: always
test:streamlit-imports:
image: python:3.11
stage: test
needs: ["setup:streamlit"]
before_script:
- cd ${STREAMLIT_DIR}
script:
- python -c "import importlib,sys; importlib.import_module('streamlit'); print('streamlit ok')"
- if [ -f app.py ]; then python -m py_compile app.py; fi
rules:
- if: $CI_COMMIT_BRANCH
build:openapi-from-streamlit:
image: python:3.11
stage: build
needs: ["setup:streamlit"]
before_script:
- pip install --no-cache-dir astunparse || true
script:
- python devops/streamlit-to-swagger.py --src ${STREAMLIT_DIR} --out openapi.json --title "Streamlit API" --version ${CI_COMMIT_SHORT_SHA}
artifacts:
when: always
expire_in: 1 week
paths:
- openapi.json
rules:
- if: $CI_COMMIT_BRANCH
package:docker-images:
stage: package
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_DRIVER: overlay2
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
- docker build -f devops/frontend-dockerfile.yml -t $CI_REGISTRY_IMAGE/frontend:${CI_COMMIT_SHORT_SHA} ${FRONTEND_DIR}
- docker build -f devops/design-system-dockerfile.yml -t $CI_REGISTRY_IMAGE/design-system:${CI_COMMIT_SHORT_SHA} ${DESIGN_SYSTEM_DIR}
- docker build -f devops/streamlit-dockerfile.yml -t $CI_REGISTRY_IMAGE/streamlit:${CI_COMMIT_SHORT_SHA} ${STREAMLIT_DIR}
- docker push $CI_REGISTRY_IMAGE/frontend:${CI_COMMIT_SHORT_SHA}
- docker push $CI_REGISTRY_IMAGE/design-system:${CI_COMMIT_SHORT_SHA}
- docker push $CI_REGISTRY_IMAGE/streamlit:${CI_COMMIT_SHORT_SHA}
rules:
- if: $CI_COMMIT_BRANCH
deploy:preview:
stage: deploy
image: alpine:3.19
script:
- echo "Déploiement de preview (placeholder)"
rules:
- if: $CI_COMMIT_BRANCH

View file

@ -0,0 +1,20 @@
# dockerfile
FROM python:3.11
# Définir le répertoire de travail
WORKDIR /app
# Copier les fichiers requirements.txt (s'il existe)
COPY requirements.txt ./
# Installer les dépendances Python
RUN pip install --no-cache-dir -r requirements.txt
# Copier le reste des fichiers de l'application
COPY . .
# Exposer le port par défaut de Streamlit
EXPOSE 8501
# Commande pour lancer l'application Streamlit
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]

View file

@ -0,0 +1,238 @@
#!/usr/bin/env python3
# produit un fichier swagger à partir d'une application streamlit
import argparse
import ast
import json
import os
import re
import sys
from typing import Any, Dict, List, Optional, Tuple
DOC_TAG_ROUTE = re.compile(r"@route\s+(?P<path>\S+)")
DOC_TAG_METHOD = re.compile(r"@method\s+(?P<method>GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)", re.IGNORECASE)
DOC_TAG_SUMMARY = re.compile(r"@summary\s+(?P<summary>.+)")
DOC_TAG_DESC = re.compile(r"@desc(ription)?\s+(?P<desc>.+)")
DOC_TAG_PARAM = re.compile(r"@param\s+(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?P<type>[^\s:]+)(?:\s*:\s*(?P<where>query|path|header|cookie|body))?(?:\s*-\s*(?P<desc>.*))?", re.IGNORECASE)
DOC_TAG_RESP = re.compile(r"@response\s+(?P<code>\d{3})\s*:\s*(?P<desc>.*)")
LINE_ANNOTATION = re.compile(
r"#\s*api:\s*(?P<kvs>.+)", re.IGNORECASE
)
def parse_kv_line(line: str) -> Dict[str, str]:
kvs: Dict[str, str] = {}
m = LINE_ANNOTATION.search(line)
if not m:
return kvs
parts = re.split(r"\s+", m.group("kvs").strip())
for part in parts:
if "=" in part:
k, v = part.split("=", 1)
kvs[k.strip().lower()] = v.strip()
return kvs
def parse_docstring(doc: Optional[str]) -> Dict[str, Any]:
result: Dict[str, Any] = {"params": [], "responses": []}
if not doc:
return result
for line in doc.splitlines():
line = line.strip()
if not line:
continue
m = DOC_TAG_ROUTE.search(line)
if m:
result["route"] = m.group("path")
continue
m = DOC_TAG_METHOD.search(line)
if m:
result["method"] = m.group("method").upper()
continue
m = DOC_TAG_SUMMARY.search(line)
if m:
result["summary"] = m.group("summary").strip()
continue
m = DOC_TAG_DESC.search(line)
if m:
result["description"] = m.group("desc").strip()
continue
m = DOC_TAG_PARAM.search(line)
if m:
result["params"].append(
{
"name": m.group("name"),
"type": (m.group("type") or "string"),
"in": (m.group("where") or "query").lower(),
"description": (m.group("desc") or "").strip(),
}
)
continue
m = DOC_TAG_RESP.search(line)
if m:
result["responses"].append(
{
"code": m.group("code"),
"description": (m.group("desc") or "").strip(),
}
)
continue
return result
def guess_schema(t: str) -> Dict[str, Any]:
t = t.lower()
if t in ("int", "integer"): # basic mapping
return {"type": "integer"}
if t in ("float", "number", "double"):
return {"type": "number"}
if t in ("bool", "boolean"):
return {"type": "boolean"}
if t in ("array", "list"):
return {"type": "array", "items": {"type": "string"}}
if t in ("object", "dict", "map"):
return {"type": "object"}
return {"type": "string"}
def build_operation(meta: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
method = meta.get("method", "GET").lower()
summary = meta.get("summary")
description = meta.get("description")
params = meta.get("params", [])
responses = meta.get("responses", [])
op: Dict[str, Any] = {
"tags": ["streamlit"],
"parameters": [],
"responses": {"200": {"description": "OK"}},
}
if summary:
op["summary"] = summary
if description:
op["description"] = description
request_body_props: List[Dict[str, Any]] = []
for p in params:
where = p.get("in", "query")
name = p.get("name")
type_ = p.get("type", "string")
desc = p.get("description", "")
schema = guess_schema(type_)
if where == "body":
request_body_props.append({"name": name, "schema": schema, "description": desc})
elif where in ("query", "path", "header", "cookie"):
op["parameters"].append(
{
"name": name,
"in": where,
"required": where == "path",
"description": desc,
"schema": schema,
}
)
if request_body_props:
properties = {x["name"]: x["schema"] for x in request_body_props}
op["requestBody"] = {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": properties,
}
}
},
}
if responses:
op["responses"] = {r["code"]: {"description": r["description"] or ""} for r in responses}
return method, op
def scan_file(path: str) -> List[Dict[str, Any]]:
with open(path, "r", encoding="utf-8") as f:
src = f.read()
try:
tree = ast.parse(src, filename=path)
except SyntaxError:
return []
endpoints: List[Dict[str, Any]] = []
# 1) Docstrings de fonctions top-level
for node in tree.body:
if isinstance(node, ast.FunctionDef):
meta = parse_docstring(ast.get_docstring(node))
if "route" in meta:
endpoints.append(meta)
# 2) Annotations en ligne: # api: route=/x method=GET summary=...
for i, line in enumerate(src.splitlines(), start=1):
if "# api:" in line.lower():
kv = parse_kv_line(line)
if "route" in kv:
endpoints.append(
{
"route": kv.get("route"),
"method": (kv.get("method") or "GET").upper(),
"summary": kv.get("summary"),
"description": kv.get("description"),
"params": [],
"responses": [],
}
)
return endpoints
def generate_openapi(src_dir: str, title: str, version: str, server_url: Optional[str]) -> Dict[str, Any]:
spec: Dict[str, Any] = {
"openapi": "3.0.3",
"info": {"title": title, "version": version},
"paths": {},
}
if server_url:
spec["servers"] = [{"url": server_url}]
for root, _dirs, files in os.walk(src_dir):
for fname in files:
if not fname.endswith(".py"):
continue
fpath = os.path.join(root, fname)
endpoints = scan_file(fpath)
for meta in endpoints:
route = meta.get("route")
if not route:
continue
method, op = build_operation(meta)
if route not in spec["paths"]:
spec["paths"][route] = {}
spec["paths"][route][method] = op
return spec
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(description="Génère un fichier OpenAPI (Swagger) depuis une app Streamlit par conventions.")
parser.add_argument("--src", default=os.environ.get("STREAMLIT_SRC", "./streamlit"), help="Répertoire source de l'app Streamlit")
parser.add_argument("--out", default="openapi.json", help="Chemin du fichier de sortie OpenAPI JSON")
parser.add_argument("--title", default="Streamlit API", help="Titre de l'API")
parser.add_argument("--version", default="0.1.0", help="Version de l'API")
parser.add_argument("--server-url", default=os.environ.get("OPENAPI_SERVER_URL"), help="URL du serveur à inclure dans le spec")
args = parser.parse_args(argv)
spec = generate_openapi(args.src, args.title, args.version, args.server_url)
with open(args.out, "w", encoding="utf-8") as f:
json.dump(spec, f, ensure_ascii=False, indent=2)
print(f"OpenAPI généré: {args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())