\d{3})\s*:\s*(?P.*)")
+
+LINE_ANNOTATION = re.compile(
+ r"#\s*api:\s*(?P.+)", 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())
+
diff --git a/eqlair/scripts/link-sae-lib.sh b/eqlair/scripts/link-sae-lib.sh
index 8ce7d34..d7c2371 100755
--- a/eqlair/scripts/link-sae-lib.sh
+++ b/eqlair/scripts/link-sae-lib.sh
@@ -27,13 +27,11 @@ fi
echo "Création d'un lien npm pour sae-lib..."
npm link
-cd ../../../sae-csc
+cd ../../../eqlair
# Utiliser le lien dans l'application
echo "Utilisation du lien dans l'application sae-csc..."
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."
diff --git a/my-workspace/projects/sae-lib/services/cognito-auth.service.spec.ts b/my-workspace/projects/sae-lib/services/cognito-auth.service.spec.ts
new file mode 100644
index 0000000..9ebb3ad
--- /dev/null
+++ b/my-workspace/projects/sae-lib/services/cognito-auth.service.spec.ts
@@ -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();
+ });
+});
diff --git a/my-workspace/projects/sae-lib/services/cognito-auth.service.ts b/my-workspace/projects/sae-lib/services/cognito-auth.service.ts
new file mode 100644
index 0000000..e9cb624
--- /dev/null
+++ b/my-workspace/projects/sae-lib/services/cognito-auth.service.ts
@@ -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;
+ signIn(): Promise;
+ signOut(): Promise;
+ isAuthenticated(): Promise;
+ getIdToken(): Promise;
+ getAccessToken(): Promise;
+ handleCallback(): Promise;
+}
+
+@Injectable({ providedIn: 'root' })
+export class CognitoAuthService implements AuthProviderApi {
+ private configuration: CognitoAuthConfig | null = null;
+ private activeProvider: CognitoAuthProvider | null = null;
+
+ async initialize(configuration: CognitoAuthConfig): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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).');
+ }
+ }
+}
+
+
diff --git a/sae-csc/scripts/link-sae-lib.sh b/sae-csc/scripts/link-sae-lib.sh
index 8ce7d34..60bf858 100755
--- a/sae-csc/scripts/link-sae-lib.sh
+++ b/sae-csc/scripts/link-sae-lib.sh
@@ -33,7 +33,5 @@ cd ../../../sae-csc
echo "Utilisation du lien dans l'application sae-csc..."
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."
diff --git a/sae-csc/src/app/pages/login/login.html b/sae-csc/src/app/pages/login/login.html
index 6ba4c31..5841db1 100644
--- a/sae-csc/src/app/pages/login/login.html
+++ b/sae-csc/src/app/pages/login/login.html
@@ -19,17 +19,16 @@
-
-
-
-
- Login to start
-
-
- |
-
-
-
+
First time to use CSC Solution Matcher ?
diff --git a/sae-csc/src/app/pages/login/login.scss b/sae-csc/src/app/pages/login/login.scss
index 1d24e7e..0b402b2 100644
--- a/sae-csc/src/app/pages/login/login.scss
+++ b/sae-csc/src/app/pages/login/login.scss
@@ -53,11 +53,19 @@
color: variables.$main-color-300;
width: 100%;
margin-left: 24px;
- .label{
- font-size: variables.$spacing-4;
- text-decoration: underline;
+
+ .label {
+ font-size: variables.$spacing-4;
+ text-decoration: underline;
+ margin-right: 6px;
+ font-size: 14px;
}
- &.bottom{
+
+ .icon {
+ font-size: 16px;
+ }
+
+ &.bottom {
margin-left: 10px;
font-size: 14px;
}
@@ -65,7 +73,8 @@
.magic-text {
color: variables.$csc-magic-color;
- i{
+
+ i {
font-size: 24px;
}
}
@@ -83,7 +92,6 @@
border: 0;
-
/* Body/Body 3/Sb */
font-size: variables.$spacing-4;
@@ -99,6 +107,11 @@
font-weight: 600;
line-height: 24px;
+ .label {
+
+ font-size: 18px;
+ }
+
}
.technical-contact {
@@ -111,7 +124,7 @@
}
}
- i{
+ i {
font-size: variables.$spacing-4;
}
}
diff --git a/sae-csc/src/app/pages/login/login.ts b/sae-csc/src/app/pages/login/login.ts
index db993b5..5aafdcd 100644
--- a/sae-csc/src/app/pages/login/login.ts
+++ b/sae-csc/src/app/pages/login/login.ts
@@ -1,10 +1,12 @@
import {Component} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
+import {MainButton} from 'sae-lib/buttons/main-button/main-button';
@Component({
selector: 'app-login',
imports: [
- RouterLink
+ RouterLink,
+ MainButton
],
templateUrl: './login.html',
styleUrl: './login.scss'
diff --git a/sae-csc/src/app/pages/testing-api/testing-api.html b/sae-csc/src/app/pages/testing-api/testing-api.html
index 3603b57..d279b33 100644
--- a/sae-csc/src/app/pages/testing-api/testing-api.html
+++ b/sae-csc/src/app/pages/testing-api/testing-api.html
@@ -45,33 +45,33 @@
-
- Feedback
-
- conversationID:
-
-
+
+ Feedback
+
+ conversationID:
+
+
-
+
- Envoi de fichier
-
-
+ Envoi de fichier
+
+
-
+
Suppression
- Delete /api/v1/conversations/{conversationID}
+ Delete /api/v1/conversations/$conversationID
Get last answer
- Get /api/v1/conversations/{conversationID}/last-answer
+ Get /api/v1/conversations/$conversationID/last-answer