up login page
This commit is contained in:
parent
92cbbb1595
commit
3b95c7871c
49 changed files with 12320 additions and 43 deletions
17
blueprint/.editorconfig
Normal file
17
blueprint/.editorconfig
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
42
blueprint/.gitignore
vendored
Normal file
42
blueprint/.gitignore
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
|
@ -0,0 +1 @@
|
||||||
|
# blueprint pour créer un projet angular
|
98
blueprint/angular.json
Normal file
98
blueprint/angular.json
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"eqlair": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "eqlair:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "eqlair:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular/build:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10031
blueprint/package-lock.json
generated
Normal file
10031
blueprint/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
49
blueprint/package.json
Normal file
49
blueprint/package.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "eqlair",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"link-sae-lib": "bash ./scripts/link-sae-lib.sh",
|
||||||
|
"postinstall": "npm run link-sae-lib",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^20.1.0",
|
||||||
|
"@angular/compiler": "^20.1.0",
|
||||||
|
"@angular/core": "^20.1.0",
|
||||||
|
"@angular/forms": "^20.1.0",
|
||||||
|
"@angular/platform-browser": "^20.1.0",
|
||||||
|
"@angular/router": "^20.1.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^20.1.1",
|
||||||
|
"@angular/cli": "^20.1.1",
|
||||||
|
"@angular/compiler-cli": "^20.1.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"sae-lib": "file:../my-workspace/projects/sae-lib",
|
||||||
|
"jasmine-core": "~5.8.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.8.2"
|
||||||
|
}
|
||||||
|
}
|
BIN
blueprint/public/favicon.ico
Normal file
BIN
blueprint/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
37
blueprint/scripts/link-sae-lib.sh
Executable file
37
blueprint/scripts/link-sae-lib.sh
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Script pour lier sae-lib comme un module npm local
|
||||||
|
|
||||||
|
# Vérifier si npm est installé
|
||||||
|
if ! [ -x "$(command -v npm)" ]; then
|
||||||
|
echo 'Erreur: npm n est pas installé.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configurer npm pour utiliser un répertoire dans l'espace utilisateur
|
||||||
|
NPM_PREFIX="$HOME/.npm-global"
|
||||||
|
mkdir -p "$NPM_PREFIX"
|
||||||
|
npm config set prefix "$NPM_PREFIX"
|
||||||
|
|
||||||
|
# Ajouter temporairement au PATH
|
||||||
|
export PATH="$NPM_PREFIX/bin:$PATH"
|
||||||
|
|
||||||
|
# Aller dans le dossier de la bibliothèque
|
||||||
|
cd ../my-workspace/projects/sae-lib
|
||||||
|
|
||||||
|
# Vérifier si package.json existe
|
||||||
|
if [ ! -f "package.json" ]; then
|
||||||
|
echo "Erreur: package.json n\'existe pas dans le dossier sae-lib." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Création d'un lien npm pour sae-lib..."
|
||||||
|
npm link
|
||||||
|
|
||||||
|
cd ../../../blueprint
|
||||||
|
|
||||||
|
# Utiliser le lien dans l'application
|
||||||
|
echo "Utilisation du lien dans l'application sae-csc..."
|
||||||
|
npm link sae-lib
|
||||||
|
|
||||||
|
|
||||||
|
echo "Lien créé avec succès. sae-lib est maintenant disponible comme un module npm."
|
12
blueprint/src/app/app.config.ts
Normal file
12
blueprint/src/app/app.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
|
provideRouter(routes)
|
||||||
|
]
|
||||||
|
};
|
3
blueprint/src/app/app.html
Normal file
3
blueprint/src/app/app.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<app-main-nav></app-main-nav>
|
||||||
|
|
||||||
|
<router-outlet/>
|
20
blueprint/src/app/app.routes.ts
Normal file
20
blueprint/src/app/app.routes.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {Routes} from '@angular/router';
|
||||||
|
import {Home} from './pages/home/home';
|
||||||
|
import {Results} from './pages/results/results';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'answer',
|
||||||
|
component: Results
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
15
blueprint/src/app/app.scss
Normal file
15
blueprint/src/app/app.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Styles globaux supplémentaires
|
||||||
|
html, body {
|
||||||
|
height: 80vw;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
23
blueprint/src/app/app.spec.ts
Normal file
23
blueprint/src/app/app.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { App } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [App],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, eqlair');
|
||||||
|
});
|
||||||
|
});
|
13
blueprint/src/app/app.ts
Normal file
13
blueprint/src/app/app.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {Component, signal} from '@angular/core';
|
||||||
|
import {RouterOutlet} from '@angular/router';
|
||||||
|
import {MainNav} from './nav/main-nav/main-nav';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet, MainNav],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrl: './app.scss'
|
||||||
|
})
|
||||||
|
export class App {
|
||||||
|
protected readonly title = signal('eqlair');
|
||||||
|
}
|
40
blueprint/src/app/nav/main-nav/main-nav.html
Normal file
40
blueprint/src/app/nav/main-nav/main-nav.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<header class="container">
|
||||||
|
<nav aria-label="main navigation" class="navbar" role="navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" routerLink="home" routerLinkActive="active-link">
|
||||||
|
<!-- <app-logo></app-logo>-->
|
||||||
|
Eqlair
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
(click)="toggleMenu()"
|
||||||
|
[class.is-active]="isMenuActive"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="menu"
|
||||||
|
class="navbar-burger"
|
||||||
|
role="button">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [class.is-active]="isMenuActive" class="navbar-menu" id="navbarBasicExample">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="navbar-item">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
|
||||||
|
<a class="navbar-item">
|
||||||
|
Eqlair
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item" routerLink="answer" routerLinkActive="active-link">answer </a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
0
blueprint/src/app/nav/main-nav/main-nav.scss
Normal file
0
blueprint/src/app/nav/main-nav/main-nav.scss
Normal file
23
blueprint/src/app/nav/main-nav/main-nav.spec.ts
Normal file
23
blueprint/src/app/nav/main-nav/main-nav.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MainNav } from './main-nav';
|
||||||
|
|
||||||
|
describe('MainNav', () => {
|
||||||
|
let component: MainNav;
|
||||||
|
let fixture: ComponentFixture<MainNav>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MainNav]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MainNav);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
21
blueprint/src/app/nav/main-nav/main-nav.ts
Normal file
21
blueprint/src/app/nav/main-nav/main-nav.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {RouterLink, RouterLinkActive} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-nav',
|
||||||
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
RouterLink,
|
||||||
|
RouterLinkActive,
|
||||||
|
// Logo
|
||||||
|
],
|
||||||
|
templateUrl: './main-nav.html',
|
||||||
|
styleUrl: './main-nav.scss'
|
||||||
|
})
|
||||||
|
export class MainNav {
|
||||||
|
isMenuActive: boolean = false;
|
||||||
|
|
||||||
|
toggleMenu() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
5
blueprint/src/app/pages/home/home.html
Normal file
5
blueprint/src/app/pages/home/home.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div id="home">
|
||||||
|
|
||||||
|
page d'accueil
|
||||||
|
|
||||||
|
</div>
|
0
blueprint/src/app/pages/home/home.scss
Normal file
0
blueprint/src/app/pages/home/home.scss
Normal file
23
blueprint/src/app/pages/home/home.spec.ts
Normal file
23
blueprint/src/app/pages/home/home.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Home } from './home';
|
||||||
|
|
||||||
|
describe('Home', () => {
|
||||||
|
let component: Home;
|
||||||
|
let fixture: ComponentFixture<Home>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Home]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Home);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
16
blueprint/src/app/pages/home/home.ts
Normal file
16
blueprint/src/app/pages/home/home.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MainButton} from 'sae-lib/buttons/main-button/main-button';
|
||||||
|
import {FeedbackButton} from 'sae-lib/buttons/feedback-button/feedback-button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
imports: [
|
||||||
|
MainButton,
|
||||||
|
FeedbackButton
|
||||||
|
],
|
||||||
|
templateUrl: './home.html',
|
||||||
|
styleUrl: './home.scss'
|
||||||
|
})
|
||||||
|
export class Home {
|
||||||
|
|
||||||
|
}
|
3
blueprint/src/app/pages/results/results.html
Normal file
3
blueprint/src/app/pages/results/results.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div id="results">
|
||||||
|
résultats
|
||||||
|
</div>
|
0
blueprint/src/app/pages/results/results.scss
Normal file
0
blueprint/src/app/pages/results/results.scss
Normal file
23
blueprint/src/app/pages/results/results.spec.ts
Normal file
23
blueprint/src/app/pages/results/results.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Results } from './results';
|
||||||
|
|
||||||
|
describe('Results', () => {
|
||||||
|
let component: Results;
|
||||||
|
let fixture: ComponentFixture<Results>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Results]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Results);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
11
blueprint/src/app/pages/results/results.ts
Normal file
11
blueprint/src/app/pages/results/results.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-results',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './results.html',
|
||||||
|
styleUrl: './results.scss'
|
||||||
|
})
|
||||||
|
export class Results {
|
||||||
|
|
||||||
|
}
|
13
blueprint/src/index.html
Normal file
13
blueprint/src/index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>My webapp</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
|
<link href="favicon.ico" rel="icon" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
blueprint/src/main.ts
Normal file
6
blueprint/src/main.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { App } from './app/app';
|
||||||
|
|
||||||
|
bootstrapApplication(App, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
13
blueprint/src/styles.scss
Normal file
13
blueprint/src/styles.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// from global to more precise
|
||||||
|
// sass lang utils
|
||||||
|
@use "sass:color";
|
||||||
|
// lib SAE Aero styles
|
||||||
|
@use 'sae-lib/src/styles/index.scss';
|
||||||
|
@use 'sae-lib/buttons/feedback-button/feedback-button.scss';
|
||||||
|
//@use 'sae-lib/src/styles/feedback.scss';
|
||||||
|
/* Fichier de styles global pour l'application */
|
||||||
|
|
||||||
|
// Importer les styles principaux
|
||||||
|
@use "app/app.scss";
|
||||||
|
//@use 'styles/main.scss';
|
||||||
|
|
15
blueprint/tsconfig.app.json
Normal file
15
blueprint/tsconfig.app.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
40
blueprint/tsconfig.json
Normal file
40
blueprint/tsconfig.json
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "preserve",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@sae-lib/*": ["../my-workspace/projects/sae-lib/*"],
|
||||||
|
"sae-lib": ["../my-workspace/projects/sae-lib"],
|
||||||
|
"sae-lib/*": ["../my-workspace/projects/sae-lib/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"typeCheckHostBindings": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
14
blueprint/tsconfig.spec.json
Normal file
14
blueprint/tsconfig.spec.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
89
devops/README.md
Normal file
89
devops/README.md
Normal 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 l’application Angular et ses `package.json`
|
||||||
|
- `./streamlit/` contient l’application 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 l’application (`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 l’appli 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 d’environnement 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
418
devops/auth.md
Normal file
|
@ -0,0 +1,418 @@
|
||||||
|
# Authentification Cognito pour webapps Angular
|
||||||
|
|
||||||
|
Ce guide décrit comment intégrer l’authentification 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 d’autres 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 d’entré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 d’auth):
|
||||||
|
|
||||||
|
```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 l’utilisateur vers l’app.
|
||||||
|
|
||||||
|
## 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 n’expose 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 l’heure système (skew de plus de 5 min peut invalider les tokens)
|
||||||
|
- Évitez d’exposer 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 l’app
|
||||||
|
- 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 d’environnement + Guard Angular
|
||||||
|
|
||||||
|
Nous supportons un mode où l’authentification Cognito est optionnelle selon une variable d’environnement, et une Guard Angular assure la redirection vers une page de login configurable.
|
||||||
|
|
||||||
|
### Variables d’environnement
|
||||||
|
|
||||||
|
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 d’Angular
|
||||||
|
|
||||||
|
Nous utilisons les fichiers `.env` d’Angular 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 l’outillage Angular, l’accès peut être `import.meta.env.NG_APP_...` au lieu de l’indexation par chaîne. Gardez le préfixe `NG_APP_` pour garantir l’exposition 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 l’app.
|
||||||
|
- 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 l’app.
|
||||||
|
|
||||||
|
### Guard Angular (générique)
|
||||||
|
|
||||||
|
Exemple de Guard compatible avec Amplify ou angular-oauth2-oidc: elle vérifie `requireAuth`. Si l’auth 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 l’utiliser comme ci‑dessus.
|
||||||
|
|
||||||
|
### 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 l’implé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 l’espace protégé.
|
||||||
|
|
31
devops/design-system-dockerfile.yml
Normal file
31
devops/design-system-dockerfile.yml
Normal 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
418
devops/design-system.md
Normal file
|
@ -0,0 +1,418 @@
|
||||||
|
# Authentification Cognito pour webapps Angular
|
||||||
|
|
||||||
|
Ce guide décrit comment intégrer l’authentification 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 d’autres 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 d’entré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 d’auth):
|
||||||
|
|
||||||
|
```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 l’utilisateur vers l’app.
|
||||||
|
|
||||||
|
## 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 n’expose 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 l’heure système (skew de plus de 5 min peut invalider les tokens)
|
||||||
|
- Évitez d’exposer 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 l’app
|
||||||
|
- 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 d’environnement + Guard Angular
|
||||||
|
|
||||||
|
Nous supportons un mode où l’authentification Cognito est optionnelle selon une variable d’environnement, et une Guard Angular assure la redirection vers une page de login configurable.
|
||||||
|
|
||||||
|
### Variables d’environnement
|
||||||
|
|
||||||
|
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 d’Angular
|
||||||
|
|
||||||
|
Nous utilisons les fichiers `.env` d’Angular 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 l’outillage Angular, l’accès peut être `import.meta.env.NG_APP_...` au lieu de l’indexation par chaîne. Gardez le préfixe `NG_APP_` pour garantir l’exposition 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 l’app.
|
||||||
|
- 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 l’app.
|
||||||
|
|
||||||
|
### Guard Angular (générique)
|
||||||
|
|
||||||
|
Exemple de Guard compatible avec Amplify ou angular-oauth2-oidc: elle vérifie `requireAuth`. Si l’auth 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 l’utiliser comme ci‑dessus.
|
||||||
|
|
||||||
|
### 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 l’implé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 l’espace protégé.
|
||||||
|
|
21
devops/docker-compose.yml
Normal file
21
devops/docker-compose.yml
Normal 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
|
||||||
|
|
|
@ -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
140
devops/gitlab-ci.yml
Normal 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
|
||||||
|
|
20
devops/streamlit-dockerfile.yml
Normal file
20
devops/streamlit-dockerfile.yml
Normal 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"]
|
238
devops/streamlit-to-swagger.py
Normal file
238
devops/streamlit-to-swagger.py
Normal 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())
|
||||||
|
|
|
@ -27,13 +27,11 @@ fi
|
||||||
echo "Création d'un lien npm pour sae-lib..."
|
echo "Création d'un lien npm pour sae-lib..."
|
||||||
npm link
|
npm link
|
||||||
|
|
||||||
cd ../../../sae-csc
|
cd ../../../eqlair
|
||||||
|
|
||||||
# Utiliser le lien dans l'application
|
# Utiliser le lien dans l'application
|
||||||
echo "Utilisation du lien dans l'application sae-csc..."
|
echo "Utilisation du lien dans l'application sae-csc..."
|
||||||
npm link sae-lib
|
npm link sae-lib
|
||||||
|
|
||||||
cd ../sae-airwatch
|
|
||||||
npm link sae-lib
|
|
||||||
|
|
||||||
echo "Lien créé avec succès. sae-lib est maintenant disponible comme un module npm."
|
echo "Lien créé avec succès. sae-lib est maintenant disponible comme un module npm."
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {CognitoAuthService} from './cognito-auth.service';
|
||||||
|
|
||||||
|
describe('CognitoAuthService', () => {
|
||||||
|
let service: CognitoAuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(CognitoAuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
220
my-workspace/projects/sae-lib/services/cognito-auth.service.ts
Normal file
220
my-workspace/projects/sae-lib/services/cognito-auth.service.ts
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
export type CognitoAuthProvider = 'amplify' | 'oidc';
|
||||||
|
|
||||||
|
export interface CognitoAuthConfig {
|
||||||
|
provider: CognitoAuthProvider;
|
||||||
|
region: string;
|
||||||
|
userPoolId: string;
|
||||||
|
userPoolWebClientId: string;
|
||||||
|
domain: string;
|
||||||
|
redirectSignIn: string;
|
||||||
|
redirectSignOut: string;
|
||||||
|
scopes: string[];
|
||||||
|
requireAuth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthProviderApi {
|
||||||
|
initialize(configuration: CognitoAuthConfig): Promise<void>;
|
||||||
|
signIn(): Promise<void>;
|
||||||
|
signOut(): Promise<void>;
|
||||||
|
isAuthenticated(): Promise<boolean>;
|
||||||
|
getIdToken(): Promise<string | null>;
|
||||||
|
getAccessToken(): Promise<string | null>;
|
||||||
|
handleCallback(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CognitoAuthService implements AuthProviderApi {
|
||||||
|
private configuration: CognitoAuthConfig | null = null;
|
||||||
|
private activeProvider: CognitoAuthProvider | null = null;
|
||||||
|
|
||||||
|
async initialize(configuration: CognitoAuthConfig): Promise<void> {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.activeProvider = configuration.provider;
|
||||||
|
|
||||||
|
if (configuration.provider === 'amplify') {
|
||||||
|
const { Amplify } = await import('aws-amplify');
|
||||||
|
Amplify.configure({
|
||||||
|
Auth: {
|
||||||
|
region: configuration.region,
|
||||||
|
userPoolId: configuration.userPoolId,
|
||||||
|
userPoolWebClientId: configuration.userPoolWebClientId,
|
||||||
|
oauth: {
|
||||||
|
domain: configuration.domain,
|
||||||
|
scope: configuration.scopes,
|
||||||
|
redirectSignIn: configuration.redirectSignIn,
|
||||||
|
redirectSignOut: configuration.redirectSignOut,
|
||||||
|
responseType: 'code'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.provider === 'oidc') {
|
||||||
|
// Configuration du provider OIDC via angular-oauth2-oidc.
|
||||||
|
// On s'attend à ce que l'app configure OAuthService au bootstrap
|
||||||
|
// (loadDiscoveryDocumentAndLogin, etc.). Ici, rien à faire si ce n'est valider la présence.
|
||||||
|
const { OAuthService } = await import('angular-oauth2-oidc');
|
||||||
|
const injector = (window as any).ngInjector;
|
||||||
|
if (!injector) {
|
||||||
|
throw new Error('Injector Angular global introuvable. Exposez window.ngInjector au bootstrap.');
|
||||||
|
}
|
||||||
|
const oauthService = injector.get(OAuthService);
|
||||||
|
if (!oauthService) {
|
||||||
|
throw new Error('OAuthService non disponible. Vérifiez l\'installation et la configuration angular-oauth2-oidc.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Provider non pris en charge. Utilisez "amplify" ou "oidc".');
|
||||||
|
}
|
||||||
|
|
||||||
|
async signIn(): Promise<void> {
|
||||||
|
this.assertInitialized();
|
||||||
|
if (this.activeProvider === 'amplify') {
|
||||||
|
const { Auth } = await import('aws-amplify');
|
||||||
|
await Auth.federatedSignIn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.activeProvider === 'oidc') {
|
||||||
|
const { OAuthService } = await import('angular-oauth2-oidc');
|
||||||
|
const injector = (window as any).ngInjector;
|
||||||
|
const oauthService = injector.get(OAuthService);
|
||||||
|
oauthService.initLoginFlow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signOut(): Promise<void> {
|
||||||
|
this.assertInitialized();
|
||||||
|
if (this.activeProvider === 'amplify') {
|
||||||
|
const { Auth } = await import('aws-amplify');
|
||||||
|
await Auth.signOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.activeProvider === 'oidc') {
|
||||||
|
const { OAuthService } = await import('angular-oauth2-oidc');
|
||||||
|
const injector = (window as any).ngInjector;
|
||||||
|
const oauthService = injector.get(OAuthService);
|
||||||
|
oauthService.logOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAuthenticated(): Promise<boolean> {
|
||||||
|
this.assertInitialized();
|
||||||
|
if (this.activeProvider === 'amplify') {
|
||||||
|
try {
|
||||||
|
const { Auth } = await import('aws-amplify');
|
||||||
|
const session = await Auth.currentSession();
|
||||||
|
return !!session?.getIdToken()?.getJwtToken();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeProvider === 'oidc') {
|
||||||
|
try {
|
||||||
|
const { OAuthService } = await import('angular-oauth2-oidc');
|
||||||
|
const injector = (window as any).ngInjector;
|
||||||
|
const oauthService = injector.get(OAuthService);
|
||||||
|
return oauthService.hasValidAccessToken() || oauthService.hasValidIdToken();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIdToken(): Promise<string | null> {
|
||||||
|
this.assertInitialized();
|
||||||
|
if (this.activeProvider === 'amplify') {
|
||||||
|
try {
|
||||||
|
const { Auth } = await import('aws-amplify');
|
||||||
|
const session = await Auth.currentSession();
|
||||||
|
return session.getIdToken().getJwtToken() ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeProvider === 'oidc') {
|
||||||
|
try {
|
||||||
|
const { OAuthService } = await import('angular-oauth2-oidc');
|
||||||
|
const injector = (window as any).ngInjector;
|
||||||
|
const oauthService = injector.get(OAuthService);
|
||||||
|
return oauthService.getIdToken() ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessToken(): Promise<string | null> {
|
||||||
|
this.assertInitialized();
|
||||||
|
if (this.activeProvider === 'amplify') {
|
||||||
|
try {
|
||||||
|
const { Auth } = await import('aws-amplify');
|
||||||
|
const session = await Auth.currentSession();
|
||||||
|
return session.getAccessToken().getJwtToken() ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeProvider === 'oidc') {
|
||||||
|
try {
|
||||||
|
const { OAuthService } = await import('angular-oauth2-oidc');
|
||||||
|
const injector = (window as any).ngInjector;
|
||||||
|
const oauthService = injector.get(OAuthService);
|
||||||
|
return oauthService.getAccessToken() ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(): Promise<void> {
|
||||||
|
this.assertInitialized();
|
||||||
|
if (this.activeProvider === 'amplify') {
|
||||||
|
// Amplify gère l'échange code->tokens automatiquement lors du retour.
|
||||||
|
// Appeler currentSession() force la résolution de la session.
|
||||||
|
const { Auth } = await import('aws-amplify');
|
||||||
|
await Auth.currentSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.activeProvider === 'oidc') {
|
||||||
|
// Pour angular-oauth2-oidc, la prise en charge est faite au bootstrap avec
|
||||||
|
// loadDiscoveryDocumentAndLogin(). Rien à faire ici si c'est déjà configuré.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureAuthenticated(loginRoute: string): Promise<boolean> {
|
||||||
|
const isAuthed = await this.isAuthenticated();
|
||||||
|
if (isAuthed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.configuration?.requireAuth) {
|
||||||
|
// Redirection douce vers la page de login Angular
|
||||||
|
window.location.assign(loginRoute);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertInitialized(): void {
|
||||||
|
if (!this.configuration || !this.activeProvider) {
|
||||||
|
throw new Error('CognitoAuthService non initialisé. Appelez initialize(config).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,5 @@ cd ../../../sae-csc
|
||||||
echo "Utilisation du lien dans l'application sae-csc..."
|
echo "Utilisation du lien dans l'application sae-csc..."
|
||||||
npm link sae-lib
|
npm link sae-lib
|
||||||
|
|
||||||
cd ../sae-airwatch
|
|
||||||
npm link sae-lib
|
|
||||||
|
|
||||||
echo "Lien créé avec succès. sae-lib est maintenant disponible comme un module npm."
|
echo "Lien créé avec succès. sae-lib est maintenant disponible comme un module npm."
|
||||||
|
|
|
@ -19,17 +19,16 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
<!-- <sae-m-button (click)="login()" [icon]="'ri-arrow-right-line'" [inconPosition]="'right'" [label]="'Login to start'"-->
|
<sae-m-button
|
||||||
<!-- [style]="'primary'"></sae-m-button>-->
|
[divider]="true"
|
||||||
<a class="button has-gradient" routerLink="/home">
|
[icon]="'arrow-right-line'"
|
||||||
<span class="label">
|
[inconPosition]="'right'"
|
||||||
Login to start
|
[kind]="'primary'"
|
||||||
</span>
|
[label]="'Login to start'"
|
||||||
<span class="pipe">
|
routerLink="/home"
|
||||||
|
|
size="medium"
|
||||||
</span>
|
|
||||||
<i class="ri-arrow-right-line"></i>
|
></sae-m-button>
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
<p class="request_access">
|
<p class="request_access">
|
||||||
First time to use CSC Solution Matcher ?
|
First time to use CSC Solution Matcher ?
|
||||||
|
|
|
@ -53,11 +53,19 @@
|
||||||
color: variables.$main-color-300;
|
color: variables.$main-color-300;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
.label{
|
|
||||||
font-size: variables.$spacing-4;
|
.label {
|
||||||
text-decoration: underline;
|
font-size: variables.$spacing-4;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
&.bottom{
|
|
||||||
|
.icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +73,8 @@
|
||||||
|
|
||||||
.magic-text {
|
.magic-text {
|
||||||
color: variables.$csc-magic-color;
|
color: variables.$csc-magic-color;
|
||||||
i{
|
|
||||||
|
i {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +92,6 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Body/Body 3/Sb */
|
/* Body/Body 3/Sb */
|
||||||
|
|
||||||
font-size: variables.$spacing-4;
|
font-size: variables.$spacing-4;
|
||||||
|
@ -99,6 +107,11 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.technical-contact {
|
.technical-contact {
|
||||||
|
@ -111,7 +124,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i{
|
i {
|
||||||
font-size: variables.$spacing-4;
|
font-size: variables.$spacing-4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {Router, RouterLink} from '@angular/router';
|
import {Router, RouterLink} from '@angular/router';
|
||||||
|
import {MainButton} from 'sae-lib/buttons/main-button/main-button';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
imports: [
|
imports: [
|
||||||
RouterLink
|
RouterLink,
|
||||||
|
MainButton
|
||||||
],
|
],
|
||||||
templateUrl: './login.html',
|
templateUrl: './login.html',
|
||||||
styleUrl: './login.scss'
|
styleUrl: './login.scss'
|
||||||
|
|
|
@ -45,33 +45,33 @@
|
||||||
<hr>
|
<hr>
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
Feedback
|
Feedback
|
||||||
</h2>
|
</h2>
|
||||||
conversationID:
|
conversationID:
|
||||||
<input [(ngModel)]="conversationID" type="text">
|
<input [(ngModel)]="conversationID" type="text">
|
||||||
<sae-m-button
|
<sae-m-button
|
||||||
(click)="sendFeedback()"
|
(click)="sendFeedback()"
|
||||||
[kind]="'secondary'" class="button"
|
[kind]="'secondary'" class="button"
|
||||||
label='envoi de feedback'>
|
label='envoi de feedback'>
|
||||||
|
|
||||||
</sae-m-button>
|
</sae-m-button>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
<h2>Envoi de fichier</h2>
|
<h2>Envoi de fichier</h2>
|
||||||
<!-- <br>-->
|
<!-- <br>-->
|
||||||
<sae-m-button
|
<sae-m-button
|
||||||
(click)="sendFile()"
|
(click)="sendFile()"
|
||||||
class="button" kind="secondary"
|
class="button" kind="secondary"
|
||||||
label='envoi de fichier'>
|
label='envoi de fichier'>
|
||||||
|
|
||||||
</sae-m-button>
|
</sae-m-button>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
<h2>Suppression</h2>
|
<h2>Suppression</h2>
|
||||||
Delete /api/v1/conversations/{conversationID}
|
Delete /api/v1/conversations/$conversationID
|
||||||
<sae-m-button
|
<sae-m-button
|
||||||
(click)="deleteConversation()"
|
(click)="deleteConversation()"
|
||||||
class="button" kind="secondary"
|
class="button" kind="secondary"
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
<h2>Get last answer </h2>
|
<h2>Get last answer </h2>
|
||||||
Get /api/v1/conversations/{conversationID}/last-answer
|
Get /api/v1/conversations/$conversationID/last-answer
|
||||||
|
|
||||||
<sae-m-button
|
<sae-m-button
|
||||||
(click)="getLastAnswer()"
|
(click)="getLastAnswer()"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue