Compare commits

...
Author SHA1 Message Date
Johan LEROY 6c09beeb3c fix(etl): demander la mesure de l'heure pile à l'API Mock
Correctif de cbbfaf4, dont le message affirmait à tort une mesure à :00. Avec
--limit 1, l'API Mock renvoie le point de début d'intervalle : le DAG, déclenché
à :45 sur [:45 - 1 h, :45], aurait écrit ses mesures à :45, au pas horaire mais
hors de la grille du dataset historique.

L'intervalle part désormais de l'heure pile du déclenchement : un run à 13:45
demande [13:00, 13:45] et importe la mesure de 13:00, avant ml_score à 14:00.
Vérifié contre l'API Mock en recette et par le rendu du gabarit Jinja.
2026-09-23 15:39:04 +02:00
Johan LEROY cbbfaf4910 fix(etl): importer une seule mesure par heure depuis l'API Mock
L'API Mock ne renvoie pas les mesures d'une période : elle génère `limit` points
répartis sur l'intervalle demandé (1 000 par heure avec --limit 1000, un toutes
les 3,6 s). Le DAG aurait écrit 7 000 lignes par heure et par environnement,
alors que le dataset historique a une mesure horaire et que les features ML
décalent par ligne : `shift(168)`, le retard d'une semaine, serait devenu un
retard de dix minutes, sans erreur visible au scoring ni au réentraînement.

Avec --limit 1, l'API renvoie la mesure de :00 de chaque heure, au pas du CSV.
Constaté sur la recette le 23/09 avant la réactivation des DAGs.
2026-09-23 15:31:51 +02:00
Johan LEROY c2f360c591 fix(auth): ne plus redemander le mot de passe provisoire à la première connexion
L'écran de changement imposé redemandait le mot de passe provisoire qui venait
d'être vérifié, sans champ identifiant. Un gestionnaire de mots de passe y
collait un ancien mot de passe du site : /auth/password répondait 401
« Identifiants invalides », et le message unique accusait aussi la politique
de mot de passe. Constaté en rec et en dev sur les comptes nominatifs.

- AuthService garde en mémoire le mot de passe d'une connexion qui impose le
  changement, rendu une seule fois par takeProvisionalPassword() et effacé
  avec la session.
- Le champ « Mot de passe actuel » ne s'affiche que si ce mot de passe manque
  (page rechargée) ou vient d'être refusé.
- Champ identifiant masqué pour les gestionnaires de mots de passe.
- Messages distincts pour 401, 422 et le reste, liste des critères en direct.
2026-09-23 14:42:37 +02:00
Johan LEROYandGitHub 933f0a3360 Merge pull request #159 from ineszang/fix/prod-sous-domaine
fix(deploy): la prod passe sur prod.enervision-g3.dynv6.net
2026-09-23 12:40:44 +02:00
Johan LEROY dbcd5c4240 fix(deploy): passe la prod sur prod.enervision-g3.dynv6.net
dynv6 sert mal un TXT _acme-challenge à la racine de la zone : l'API ne
le liste ni ne le supprime, et un seul de ses trois serveurs le renvoie.
Le défi DNS-01 de la prod échouait donc à chaque essai, alors que rec. et
dev. passaient. La prod rejoint ses voisines en sous-domaine, ce qui aligne
aussi les trois noms sur les environnements.

- provision-host.sh : hôte prod.$DOMAINE, enregistrement A prod publié.
- Makefile : --dnssleep 90, le temps que les trois serveurs de dynv6
  servent le TXT avant la validation multi-réseaux de Let's Encrypt.
- deploy.yml, ADR 0018, 10-infra.md, infra/README.md, Terraform.
2026-09-23 12:35:20 +02:00
Johan LEROY 46d10209f1 Merge branch 'main' into dev 2026-09-23 12:01:54 +02:00
Johan LEROYandGitHub db6ee6e56d Merge pull request #156 from ineszang/feat/domaine-duckdns-tls
feat(deploy): URL sans port et certificats Let's Encrypt sur la VM ENI
2026-09-23 12:01:41 +02:00
Johan LEROYandGitHub 84969d3375 Merge pull request #158 from ineszang/dependabot/npm_and_yarn/tests/e2e/e2e-dependencies-b7ceb5d816
chore(deps-dev): bump typescript from 6.0.3 to 7.0.2 in /tests/e2e in the e2e-dependencies group
2026-09-23 11:59:01 +02:00
Johan LEROYandGitHub 93d5cad5af Merge pull request #157 from ineszang/dependabot/github_actions/astral-sh/setup-uv-10.1.0
chore(deps): bump astral-sh/setup-uv from 7.6.0 to 10.1.0
2026-09-23 11:58:56 +02:00
dependabot[bot]andGitHub 288970df77 chore(deps-dev): bump typescript
Bumps the e2e-dependencies group in /tests/e2e with 1 update: [typescript](https://github.com/microsoft/TypeScript).


Updates `typescript` from 6.0.3 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: e2e-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-23 09:34:42 +00:00
dependabot[bot]andGitHub 985c188106 chore(deps): bump astral-sh/setup-uv from 7.6.0 to 10.1.0
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.6.0 to 10.1.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/37802adc94f370d6bfd71619e3f0bf239e1f3b78...bec219d24cd3e171d82865faccec33120bb574f4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-23 09:34:37 +00:00
PhyriosandGitHub b786f27a4d Merge pull request #152 from ineszang/dev
Remontée dev vers main : mise en production sur la VM ENI
2026-09-23 11:33:44 +02:00
22 changed files with 590 additions and 94 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: etl/airflow/uv.lock
+4 -4
View File
@@ -26,7 +26,7 @@ jobs:
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
@@ -95,7 +95,7 @@ jobs:
uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
@@ -133,7 +133,7 @@ jobs:
uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
@@ -161,7 +161,7 @@ jobs:
# Pourquoi : pas de cache ici. uvx n'installe pas le projet, le verrou n'alimente donc
# aucune clé de cache ; la seule roue téléchargée est celle de Bandit.
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: false
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
- name: Installe uv
# Épinglé sur le commit du tag v7 (règle Sonar githubactions:S7637 : dépendance tierce,
# contrairement à actions/checkout ou actions/upload-artifact, premières parties).
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
timeout-minutes: 30
environment:
name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
url: ${{ github.ref_name == 'main' && 'https://enervision-g3.dynv6.net' || github.ref_name == 'dev' && 'https://rec.enervision-g3.dynv6.net' || 'https://dev.enervision-g3.dynv6.net' }}
url: ${{ github.ref_name == 'main' && 'https://prod.enervision-g3.dynv6.net' || github.ref_name == 'dev' && 'https://rec.enervision-g3.dynv6.net' || 'https://dev.enervision-g3.dynv6.net' }}
env:
ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
PORT_HTTPS: ${{ github.ref_name == 'main' && '10443' || github.ref_name == 'dev' && '8443' || '9443' }}
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: ml/uv.lock
@@ -95,7 +95,7 @@ jobs:
uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
cache-dependency-glob: |
@@ -155,7 +155,7 @@ jobs:
# Pourquoi : pas de cache ici. uvx n'installe pas le projet, le verrou n'alimente donc
# aucune clé de cache ; la seule roue téléchargée est celle de Bandit.
- name: Installe uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: false
+3 -3
View File
@@ -244,14 +244,14 @@ DNS01_JETON_FICHIER ?= $(abspath $(CURDIR)/../dns.token)
acme-sh = docker run --rm --user "$$(id -u):$$(id -g)" -e $(DNS01_JETON_VAR) -e AUTO_UPGRADE=0 \
-v "$(CURDIR)/infra/proxy/acme:/acme.sh" -v "$(CURDIR)/infra/proxy/tls:/tls" $(ACME_SH)
# acme.sh sort en 2 quand le certificat n'est pas encore à renouveler, et recopie le jeton dans
# acme/account.conf : d'où le chmod, qui le soustrait aux autres comptes de la machine.
# acme.sh sort en 2 quand le certificat n'est pas à renouveler, et recopie le jeton dans
# acme/account.conf, d'où le chmod. `--dnssleep` : Let's Encrypt valide depuis plusieurs réseaux.
tls-dns01: ## Certificat Let's Encrypt par DNS-01, renouvelé seulement à échéance. Jeton : ../dns.token
@case "$(PUBLIC_HOST)" in *.local | localhost) echo "PUBLIC_HOST=$(PUBLIC_HOST) n'est pas un nom public"; exit 1 ;; esac
@test -r "$(DNS01_JETON_FICHIER)" || { echo "Jeton DNS illisible : $(DNS01_JETON_FICHIER)"; exit 1; }
@mkdir -p infra/proxy/acme && chmod 700 infra/proxy/acme
@$(DNS01_JETON_VAR)="$$(tr -d '[:space:]' < "$(DNS01_JETON_FICHIER)")"; export $(DNS01_JETON_VAR); \
$(acme-sh) --issue --server letsencrypt --dns $(DNS01_API) -d "$(PUBLIC_HOST)"; \
$(acme-sh) --issue --server letsencrypt --dns $(DNS01_API) --dnssleep 90 -d "$(PUBLIC_HOST)"; \
code=$$?; chmod -R go-rwx infra/proxy/acme; [ $$code -eq 0 ] || [ $$code -eq 2 ] || exit $$code
@$(acme-sh) --install-cert --ecc -d "$(PUBLIC_HOST)" \
--fullchain-file /tls/fullchain.pem --key-file /tls/privkey.pem
@@ -43,6 +43,31 @@ describe('AuthService', () => {
expect(service.isAuthenticated()).toBe(true);
});
it('garde le mot de passe provisoire pour un seul changement quand il doit être changé', () => {
service.login({ email: 'a@a.com', password: 'Provisoire' }).subscribe();
httpMock.expectOne(`${environment.apiUrl}/auth/login`).flush({
...tokenResponse,
principal: { ...tokenResponse.principal, must_change_password: true },
});
expect(service.takeProvisionalPassword()).toBe('Provisoire');
expect(service.takeProvisionalPassword()).toBeNull();
});
it('ne garde aucun mot de passe quand il est déjà définitif, ni après la fin de session', () => {
service.login({ email: 'a@a.com', password: 'Definitif' }).subscribe();
httpMock.expectOne(`${environment.apiUrl}/auth/login`).flush(tokenResponse);
expect(service.takeProvisionalPassword()).toBeNull();
service.login({ email: 'a@a.com', password: 'Provisoire' }).subscribe();
httpMock.expectOne(`${environment.apiUrl}/auth/login`).flush({
...tokenResponse,
principal: { ...tokenResponse.principal, must_change_password: true },
});
service.clearSession();
expect(service.takeProvisionalPassword()).toBeNull();
});
it('efface la session au logout', () => {
service.login({ email: 'a@a.com', password: 'secret' }).subscribe();
httpMock.expectOne(`${environment.apiUrl}/auth/login`).flush(tokenResponse);
@@ -19,6 +19,9 @@ export class AuthService {
// mémoire. Un rechargement de page le perd, c'est voulu par le contrat.
private accessTokenSignal = signal<string | null>(null);
private principalSignal = signal<Principal | null>(null);
// Pourquoi : redemander le mot de passe provisoire qu'on vient de vérifier laisse un gestionnaire
// de mots de passe y coller un ancien mot de passe du site, et `/auth/password` répond 401.
private provisionalPassword: string | null = null;
readonly principal = this.principalSignal.asReadonly();
readonly isAuthenticated = computed(() => this.principalSignal() !== null);
@@ -37,12 +40,26 @@ export class AuthService {
clearSession(): void {
this.accessTokenSignal.set(null);
this.principalSignal.set(null);
this.provisionalPassword = null;
}
login(credentials: LoginRequest): Observable<TokenResponse> {
return this.http
.post<TokenResponse>(`${environment.apiUrl}/auth/login`, credentials, { withCredentials: true })
.pipe(tap((response) => this.setSession(response)));
.pipe(
tap((response) => {
this.setSession(response);
this.provisionalPassword = response.principal.must_change_password
? credentials.password
: null;
})
);
}
takeProvisionalPassword(): string | null {
const password = this.provisionalPassword;
this.provisionalPassword = null;
return password;
}
// Un seul rafraîchissement en vol à la fois, partagé entre tous les
@@ -7,14 +7,18 @@
Votre mot de passe est provisoire, vous devez le modifier avant de continuer
</p>
<label class="form-label" for="current_password">Mot de passe actuel</label>
<input
id="current_password"
class="form-input"
type="password"
formControlName="current_password"
autocomplete="current-password"
/>
<input hidden type="email" autocomplete="username" [value]="email" readonly />
@if (asksCurrentPassword()) {
<label class="form-label" for="current_password">Mot de passe actuel</label>
<input
id="current_password"
class="form-input"
type="password"
formControlName="current_password"
autocomplete="current-password"
/>
}
<label class="form-label" for="new_password">Nouveau mot de passe</label>
<input
@@ -24,7 +28,7 @@
formControlName="new_password"
autocomplete="new-password"
/>
<span class="form-hint">{{ passwordHint }}</span>
<app-password-requirements [password]="newPassword()" />
@if (errorMessage()) {
<ev-alert severity="danger">{{ errorMessage() }}</ev-alert>
@@ -1,17 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
import { signal } from '@angular/core';
import { of, throwError } from 'rxjs';
import { vi } from 'vitest';
import { ChangePassword } from './change-password';
import { AuthService } from '../../../core/services/auth.service';
const NOUVEAU = 'Un-nouveau-mot-de-passe1!';
describe('ChangePassword', () => {
let authMock: { changePassword: ReturnType<typeof vi.fn> };
let authMock: {
changePassword: ReturnType<typeof vi.fn>;
takeProvisionalPassword: ReturnType<typeof vi.fn>;
principal: ReturnType<typeof signal>;
};
let routerMock: { navigate: ReturnType<typeof vi.fn> };
beforeEach(async () => {
authMock = { changePassword: vi.fn() };
authMock = {
changePassword: vi.fn(),
takeProvisionalPassword: vi.fn().mockReturnValue(null),
principal: signal({ email: 'johan@enervision.fr' }),
};
routerMock = { navigate: vi.fn() };
await TestBed.configureTestingModule({
@@ -23,6 +35,10 @@ describe('ChangePassword', () => {
}).compileComponents();
});
function champActuel(fixture: { nativeElement: HTMLElement }): HTMLInputElement | null {
return fixture.nativeElement.querySelector('#current_password');
}
it('ne soumet pas si le formulaire est invalide (mot de passe trop court)', () => {
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
@@ -44,7 +60,7 @@ describe('ChangePassword', () => {
it('redirige vers /dashboard après un changement réussi', () => {
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
component.form.setValue({ current_password: 'ancien-mot-de-passe', new_password: 'Un-nouveau-mot-de-passe1!' });
component.form.setValue({ current_password: 'ancien-mot-de-passe', new_password: NOUVEAU });
authMock.changePassword.mockReturnValue(of({ principal: { role: 'admin' } }));
@@ -52,46 +68,94 @@ describe('ChangePassword', () => {
expect(routerMock.navigate).toHaveBeenCalledWith(['/dashboard']);
});
it("affiche un message d'erreur si le mot de passe actuel est incorrect", () => {
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
component.form.setValue({ current_password: 'mauvais-mot-de-passe', new_password: 'Un-nouveau-mot-de-passe1!' });
it("demande le mot de passe actuel quand la connexion ne l'a pas transmis (page rechargée)", () => {
const fixture = TestBed.createComponent(ChangePassword);
fixture.detectChanges();
authMock.changePassword.mockReturnValue(throwError(() => new Error('401')));
expect(champActuel(fixture)).not.toBeNull();
});
component.onSubmit();
fixture.detectChanges(); // rend le bloc @if (errorMessage())
it('réutilise le mot de passe provisoire de la connexion sans le redemander', () => {
authMock.takeProvisionalPassword.mockReturnValue('Provisoire-24-caracteres');
authMock.changePassword.mockReturnValue(of({ principal: { role: 'admin' } }));
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
fixture.detectChanges();
expect(component.errorMessage()).toContain('incorrect');
const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('incorrect');
expect(champActuel(fixture)).toBeNull();
component.form.controls.new_password.setValue(NOUVEAU);
component.onSubmit();
expect(authMock.changePassword).toHaveBeenCalledWith({
current_password: 'Provisoire-24-caracteres',
new_password: NOUVEAU,
});
});
it('associe le formulaire au compte connecté pour les gestionnaires de mots de passe', () => {
const fixture = TestBed.createComponent(ChangePassword);
fixture.detectChanges();
const identifiant = fixture.nativeElement.querySelector('input[autocomplete="username"]');
expect(identifiant.value).toBe('johan@enervision.fr');
});
it('sur un 401, dit que le mot de passe actuel est faux et le redemande', () => {
authMock.takeProvisionalPassword.mockReturnValue('Provisoire-perime');
authMock.changePassword.mockReturnValue(
throwError(() => new HttpErrorResponse({ status: 401 })),
);
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
component.form.controls.new_password.setValue(NOUVEAU);
component.onSubmit();
fixture.detectChanges();
expect(component.errorMessage()).toContain('Mot de passe actuel incorrect');
expect(fixture.nativeElement.querySelector('.ev-alert')?.textContent).toContain('incorrect');
expect(champActuel(fixture)).not.toBeNull();
expect(component.form.controls.current_password.value).toBe('');
});
it('sur un 422, dit que le nouveau mot de passe ne respecte pas la politique', () => {
authMock.changePassword.mockReturnValue(
throwError(() => new HttpErrorResponse({ status: 422 })),
);
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
component.form.setValue({ current_password: 'ancien-mot-de-passe', new_password: NOUVEAU });
component.onSubmit();
expect(component.errorMessage()).toContain('Nouveau mot de passe refusé');
expect(component.form.controls.current_password.value).toBe('ancien-mot-de-passe');
});
it('désactive le bouton tant que le formulaire est invalide', () => {
const fixture = TestBed.createComponent(ChangePassword);
fixture.detectChanges();
const fixture = TestBed.createComponent(ChangePassword);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button[type="submit"]');
expect(button.disabled).toBe(true);
expect(fixture.nativeElement.querySelector('.ev-alert')).toBeNull();
const button = fixture.nativeElement.querySelector('button[type="submit"]');
expect(button.disabled).toBe(true);
expect(fixture.nativeElement.querySelector('.ev-alert')).toBeNull();
});
it('déclenche onSubmit via la soumission réelle du formulaire (ngSubmit)', () => {
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
component.form.setValue({ current_password: 'ancien-mot-de-passe', new_password: 'Un-nouveau-mot-de-passe1!' });
fixture.detectChanges();
const fixture = TestBed.createComponent(ChangePassword);
const component = fixture.componentInstance;
component.form.setValue({ current_password: 'ancien-mot-de-passe', new_password: NOUVEAU });
fixture.detectChanges();
authMock.changePassword.mockReturnValue(of({ principal: { role: 'admin' } }));
authMock.changePassword.mockReturnValue(of({ principal: { role: 'admin' } }));
const form = fixture.nativeElement.querySelector('form');
form.dispatchEvent(new Event('submit'));
fixture.detectChanges();
const form = fixture.nativeElement.querySelector('form');
form.dispatchEvent(new Event('submit'));
fixture.detectChanges();
expect(authMock.changePassword).toHaveBeenCalledWith({
current_password: 'ancien-mot-de-passe',
new_password: 'Un-nouveau-mot-de-passe1!',
expect(authMock.changePassword).toHaveBeenCalledWith({
current_password: 'ancien-mot-de-passe',
new_password: NOUVEAU,
});
});
});
});
@@ -1,17 +1,20 @@
import { Component, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../../../core/services/auth.service';
import { Button } from '../../../shared/components/ui/button/button';
import { Card } from '../../../shared/components/ui/card/card';
import { Alert } from '../../../shared/components/ui/alert/alert';
import { Brand } from '../../../shared/components/ui/brand/brand';
import { PasswordRequirementsChecklist } from '../../../shared/components/password-requirements/password-requirements';
import { passwordValidators, PASSWORD_HINT } from '../../../shared/validators/password.validator';
@Component({
selector: 'app-change-password',
standalone: true,
imports: [ReactiveFormsModule, Button, Card, Alert, Brand],
imports: [ReactiveFormsModule, Button, Card, Alert, Brand, PasswordRequirementsChecklist],
templateUrl: './change-password.html',
styleUrl: './change-password.scss',
})
@@ -20,30 +23,47 @@ export class ChangePassword {
private auth = inject(AuthService);
private router = inject(Router);
private provisionalPassword = this.auth.takeProvisionalPassword();
errorMessage = signal<string | null>(null);
isLoading = signal(false);
passwordHint = PASSWORD_HINT;
asksCurrentPassword = signal(this.provisionalPassword === null);
email = this.auth.principal()?.email ?? '';
form = this.fb.nonNullable.group({
current_password: ['', Validators.required],
current_password: [this.provisionalPassword ?? '', Validators.required],
new_password: ['', passwordValidators],
});
newPassword = toSignal(this.form.controls.new_password.valueChanges, { initialValue: '' });
onSubmit(): void {
if (this.form.invalid) return;
this.isLoading.set(true);
this.errorMessage.set(null);
this.auth.changePassword(this.form.getRawValue()).subscribe({
next: (response) => {
next: () => {
this.router.navigate(['/dashboard']);
},
error: () => {
error: (error: HttpErrorResponse) => {
this.isLoading.set(false);
this.errorMessage.set(
`Mot de passe actuel incorrect, ou nouveau mot de passe invalide (${this.passwordHint}).`,
);
this.errorMessage.set(this.explique(error));
if (error.status === 401) {
this.form.controls.current_password.reset('');
this.asksCurrentPassword.set(true);
}
},
});
}
private explique(error: HttpErrorResponse): string {
if (error.status === 401) {
return 'Mot de passe actuel incorrect : saisissez le mot de passe provisoire qui vous a été transmis.';
}
if (error.status === 422) {
return `Nouveau mot de passe refusé (${PASSWORD_HINT}).`;
}
return 'Le changement de mot de passe a échoué, réessayez dans un instant.';
}
}
@@ -20,16 +20,19 @@ et API, depuis les postes comme depuis la VM.
## Décision
**Des noms publics qui visent l'IP privée.** `enervision-g3.dynv6.net`, zone gratuite de dynv6,
porte la prod, et deux enregistrements A portent `rec.` et `dev.`. `provision-host.sh` les publie
par l'API dynv6 : le DNS est décrit par le code comme le reste. Tout
**Des noms publics qui visent l'IP privée.** Dans `enervision-g3.dynv6.net`, zone gratuite de
dynv6, trois enregistrements A portent `prod.`, `rec.` et `dev.`. `provision-host.sh` les publie
par l'API dynv6 : le DNS est décrit par le code comme le reste. La prod n'est pas à la racine de
la zone : dynv6 y sert mal un TXT `_acme-challenge`, que l'API ne liste ni ne supprime et qu'un
seul de ses trois serveurs renvoie (constaté le 23/09), si bien que son défi DNS-01 échoue. Tout
poste du réseau de l'école les résout sans configuration ; hors de ce réseau, l'IP ne mène
nulle part.
**Des certificats Let's Encrypt par défi DNS-01.** Le défi passe par l'API dynv6, qui pose
l'enregistrement TXT : Let's Encrypt n'a jamais à joindre la VM. `make tls-dns01` (acme.sh
épinglé) le joue dans chaque stack ; il ne renouvelle qu'à échéance, d'où son rejeu à chaque
déploiement et chaque nuit par cron. Un certificat par environnement plutôt qu'un joker : chaque
déploiement et chaque nuit par cron. `--dnssleep 90` laisse aux trois serveurs de dynv6 le temps
de servir le TXT avant que Let's Encrypt ne le cherche depuis plusieurs réseaux. Un certificat par environnement plutôt qu'un joker : chaque
stack garde le sien, et la clé de la prod n'est pas lisible depuis le clone de dev.
**Un frontal SNI sur 443, le seul composant exposé.** `infra/front`, un nginx sur le réseau de
+2 -2
View File
@@ -89,7 +89,7 @@ l'[ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md).
| `ml_score` | `0 * * * *` | `enervision_ml.score`, dans `/opt/ml/.venv` |
| `alertes` | `15 * * * *` | `app.detection.internal_alerts` puis `app.cli generate-recommendations`, dans `/opt/backend/.venv` |
| `historical_import` | manuelle | `app.etl.historical_import`, dans `/opt/backend/.venv` ; les fichiers de `data/raw` sont montés en lecture seule dans `/opt/data/raw` |
| `mock_api_import` | `45 * * * *` | `app.etl.mock_api_import`, dans `/opt/backend/.venv` ; importe l'heure précédant son déclenchement depuis l'API Mock |
| `mock_api_import` | `45 * * * *` | `app.etl.mock_api_import`, dans `/opt/backend/.venv` ; importe depuis l'API Mock la mesure de l'heure pile précédant son déclenchement |
| `derive` | `30 5 * * *` | `app.monitoring.drift`, dans `/opt/backend/.venv` ; quotidien parce que sa fenêtre couvre 168 h, et sans reprise parce qu'une dérive n'est pas une panne passagère |
Le DAG `historical_import` réutilise le pipeline historique existant sans dupliquer sa logique.
@@ -248,7 +248,7 @@ les secrets et les certificats, et ne démarre rien.
|---|---|---|---|
| Branche, environnement GitHub | toute branche lancée à la main, `dev` | `dev`, `rec` | `main`, `prod` |
| Dossier, projet Compose | `/srv/enervision/dev`, `enervision-dev` | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` |
| URL | `https://dev.enervision-g3.dynv6.net` | `https://rec.enervision-g3.dynv6.net` | `https://enervision-g3.dynv6.net` |
| URL | `https://dev.enervision-g3.dynv6.net` | `https://rec.enervision-g3.dynv6.net` | `https://prod.enervision-g3.dynv6.net` |
| Proxy HTTP, HTTPS, PROXY protocol, sur `127.0.0.1` | `8083`, `9443`, `9444` | `8081`, `8443`, `8444` | `10080`, `10443`, `10444` |
| PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5435`, `8027`, `8084` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` |
| Supervision (profil `monitoring`) | à la demande, `make monitoring-up` | à la demande, `make monitoring-up` | active, `COMPOSE_PROFILES=monitoring` |
+5 -3
View File
@@ -669,13 +669,15 @@ des recommandations (`alertes`, issue #116), l'import historique (`historical_im
issue #119) et l'import périodique de l'API Mock (`mock_api_import`, issue #15).
Le DAG `mock_api_import` s'exécute chaque heure, à la minute `:45`. Il appelle
`app.etl.mock_api_import` avec un intervalle explicite d'une heure et une limite de 1 000 lectures
par site. Les deux pipelines normalisent leurs données vers les tables communes `site` et
`app.etl.mock_api_import` sur l'intervalle qui va de l'heure pile à son déclenchement, avec une
limite d'une lecture par site. L'API Mock génère autant de points que la limite demandée,
répartis sur l'intervalle et le premier à son début : un seul donne la mesure de :00, au pas
horaire du dataset historique, que les features ML supposent en décalant par ligne. Les deux pipelines normalisent leurs données vers les tables communes `site` et
`reading`, tout en conservant leur source (`csv` ou `api_history`). La réconciliation globale
des deux sources reste à compléter dans l'issue #15.
Le DAG `mock_api_import` exécute `app.etl.mock_api_import` toutes les heures. Chaque exécution
traite l'intervalle Airflow précédent. Les deux pipelines normalisent leurs données vers les
importe la mesure de l'heure pile qui précède son déclenchement. Les deux pipelines normalisent leurs données vers les
tables communes `site` et `reading`, tout en conservant leur source (`csv` ou `api_history`).
Airflow permet de planifier les traitements, gérer leur ordre d'exécution, suivre leur état et remonter les erreurs. Il ne remplace pas la logique ETL Python existante : les scripts actuels restent responsables de l'extraction, de la validation, de la transformation et du chargement. `etl/airflow/dags/ml_train.py`, `ml_score.py`, `alertes.py`, `historical_import.py` et
+5 -5
View File
@@ -1,7 +1,7 @@
"""DAG d'import périodique des données de l'API Mock EnerVision (issue #15).
Orchestre le pipeline existant `app.etl.mock_api_import` sans dupliquer sa logique ETL.
Chaque exécution traite l'heure précédant son déclenchement.
Chaque exécution importe la mesure de l'heure pile qui précède son déclenchement.
Le pipeline backend reste responsable de la validation, de la normalisation, du suivi de la
qualité, de l'idempotence et du chargement dans PostgreSQL/TimescaleDB.
@@ -18,9 +18,9 @@ from airflow.timetables.trigger import CronTriggerTimetable
# Le backend possède son propre environnement uv dans l'image Airflow (ADR 0008).
COMMANDE_BACKEND = "cd /opt/backend && env -u VIRTUAL_ENV uv run --no-sync python -m"
# Le pipeline backend et l'API acceptent au maximum 1 000 lectures par site.
# Cette marge évite de perdre silencieusement une lecture si une heure en contient plus de 60.
LIMITE_LECTURES = 1000
# Contrainte : l'API Mock génère `limit` points répartis sur l'intervalle, le premier à son début.
# Un seul, depuis l'heure pile, donne la mesure de :00 au pas du CSV que suppose `shift(168)`.
LIMITE_LECTURES = 1
# Deux reprises donnent trois tentatives au total. Même dans le pire cas, l'exécution reste
# inférieure au pas horaire du DAG.
@@ -51,7 +51,7 @@ with DAG(
task_id="import_mock_api",
bash_command=(
f"{COMMANDE_BACKEND} app.etl.mock_api_import "
"--start-time \"{{ data_interval_start.strftime('%Y-%m-%dT%H:%M:%S') }}\" "
"--start-time \"{{ data_interval_end.strftime('%Y-%m-%dT%H:00:00') }}\" "
"--end-time \"{{ data_interval_end.strftime('%Y-%m-%dT%H:%M:%S') }}\" "
f"--limit {LIMITE_LECTURES}"
),
+3 -3
View File
@@ -113,12 +113,12 @@ def test_mock_api_import_calls_the_existing_backend_module(dagbag: DagBag) -> No
assert "app.etl.mock_api_import" in commande
def test_mock_api_import_uses_the_airflow_data_interval(dagbag: DagBag) -> None:
def test_mock_api_import_asks_for_the_on_the_hour_reading(dagbag: DagBag) -> None:
commande = dagbag.dags["mock_api_import"].get_task("import_mock_api").bash_command
assert "--start-time \"{{ data_interval_start.strftime('%Y-%m-%dT%H:%M:%S') }}\"" in commande
assert "--start-time \"{{ data_interval_end.strftime('%Y-%m-%dT%H:00:00') }}\"" in commande
assert "--end-time \"{{ data_interval_end.strftime('%Y-%m-%dT%H:%M:%S') }}\"" in commande
assert "--limit 1000" in commande
assert commande.endswith("--limit 1")
@pytest.mark.parametrize("task_id", ["detection", "recommandations"])
+1 -1
View File
@@ -40,7 +40,7 @@ sur `dev` et sur `main`, et a chaque lancement manuel d'une autre branche pour `
Noms et certificats (ADR 0018) : avant l'apply, la zone `domaine` doit exister chez dynv6 et son
jeton se trouver dans `<racine>/dns.token` (600, proprietaire). L'apply fait alors pointer la
zone, `rec` et `dev` vers la machine, obtient un certificat Let's Encrypt par environnement et planifie leur
zone, `prod`, `rec` et `dev` vers la machine, obtient un certificat Let's Encrypt par environnement et planifie leur
renouvellement ; sans jeton, chaque environnement garde un certificat auto-signe. Le frontal SNI (`infra/front`)
se demarre une fois depuis le dossier de la prod, `make front-up`.
@@ -45,7 +45,7 @@ variable "depot_url" {
variable "domaine" {
type = string
description = "Zone dynv6 de la prod, rec. et dev. en sous-domaines (ADR 0018). Son jeton doit se trouver dans <racine>/dns.token sur la machine : provision-host.sh y fait pointer la zone et ses deux sous-domaines vers la machine."
description = "Zone dynv6 des trois environnements, prod., rec. et dev. en sous-domaines (ADR 0018). Son jeton doit se trouver dans <racine>/dns.token sur la machine : provision-host.sh y fait pointer la zone et ses trois sous-domaines vers la machine."
default = "enervision-g3.dynv6.net"
}
+4 -4
View File
@@ -141,7 +141,7 @@ preparer() {
# jeton passe par l'environnement du seul processus Python, jamais par `argv`.
publier_dns() {
[[ -r "$JETON_DNS" ]] || { echo "pas de jeton $JETON_DNS : ni DNS ni Let's Encrypt"; return 0; }
DNS_TOKEN="$(tr -d '[:space:]' < "$JETON_DNS")" python3 - "$DOMAINE" "$ADRESSE" rec dev <<'PY' \
DNS_TOKEN="$(tr -d '[:space:]' < "$JETON_DNS")" python3 - "$DOMAINE" "$ADRESSE" prod rec dev <<'PY' \
|| echo "DNS : dynv6 refuse la mise à jour de $DOMAINE, enregistrements inchangés" >&2
import json, os, sys, time, urllib.request
@@ -198,10 +198,10 @@ verifier_outils
mkdir -p "$RACINE"
publier_dns
# Supervision active en prod seulement (ADR 0016). Le frontal (infra/front) publie 80 et 443 et
# relaie vers les ports `front` ; les stacks ne publient plus rien hors de la boucle locale.
# Supervision active en prod seulement (ADR 0016). La prod vit sur `prod.` et non à la racine :
# dynv6 ne sert pas de façon fiable un TXT `_acme-challenge` à la racine de la zone (ADR 0018).
# env branche hôte https http front pg mailpit airflow profils grafana prometheus alertmanager
preparer prod main "$DOMAINE" 127.0.0.1:10443 127.0.0.1:10080 127.0.0.1:10444 5433 8025 8080 monitoring 3001 9090 9093
preparer prod main "prod.$DOMAINE" 127.0.0.1:10443 127.0.0.1:10080 127.0.0.1:10444 5433 8025 8080 monitoring 3001 9090 9093
preparer rec dev "rec.$DOMAINE" 127.0.0.1:8443 127.0.0.1:8081 127.0.0.1:8444 5434 8026 8082 "" 3002 9091 9094
preparer dev dev "dev.$DOMAINE" 127.0.0.1:9443 127.0.0.1:8083 127.0.0.1:9444 5435 8027 8084 "" 3003 9092 9095
+368 -7
View File
@@ -10,7 +10,7 @@
"devDependencies": {
"@playwright/test": "^1.63.0",
"@types/node": "^26.6.2",
"typescript": "~6.0.2"
"typescript": "~7.0.2"
}
},
"node_modules/@playwright/test": {
@@ -39,6 +39,346 @@
"undici-types": "~8.9.0"
}
},
"node_modules/@typescript/typescript-aix-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-loong64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-mips64el": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-riscv64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-s390x": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-sunos-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/playwright": {
"version": "1.63.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
@@ -69,17 +409,38 @@
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
"tsc": "bin/tsc"
},
"engines": {
"node": ">=14.17"
"node": ">=16.20.0"
},
"optionalDependencies": {
"@typescript/typescript-aix-ppc64": "7.0.2",
"@typescript/typescript-darwin-arm64": "7.0.2",
"@typescript/typescript-darwin-x64": "7.0.2",
"@typescript/typescript-freebsd-arm64": "7.0.2",
"@typescript/typescript-freebsd-x64": "7.0.2",
"@typescript/typescript-linux-arm": "7.0.2",
"@typescript/typescript-linux-arm64": "7.0.2",
"@typescript/typescript-linux-loong64": "7.0.2",
"@typescript/typescript-linux-mips64el": "7.0.2",
"@typescript/typescript-linux-ppc64": "7.0.2",
"@typescript/typescript-linux-riscv64": "7.0.2",
"@typescript/typescript-linux-s390x": "7.0.2",
"@typescript/typescript-linux-x64": "7.0.2",
"@typescript/typescript-netbsd-arm64": "7.0.2",
"@typescript/typescript-netbsd-x64": "7.0.2",
"@typescript/typescript-openbsd-arm64": "7.0.2",
"@typescript/typescript-openbsd-x64": "7.0.2",
"@typescript/typescript-sunos-x64": "7.0.2",
"@typescript/typescript-win32-arm64": "7.0.2",
"@typescript/typescript-win32-x64": "7.0.2"
}
},
"node_modules/undici-types": {
+1 -1
View File
@@ -10,6 +10,6 @@
"devDependencies": {
"@playwright/test": "^1.63.0",
"@types/node": "^26.6.2",
"typescript": "~6.0.2"
"typescript": "~7.0.2"
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ test('impose le changement du mot de passe temporaire avant le tableau de bord',
await expect(page).toHaveURL(/\/change-password$/);
await expect(page.getByRole('heading', { name: 'Nouveau mot de passe' })).toBeVisible();
await page.getByLabel('Mot de passe actuel').fill(compte.password);
await expect(page.getByLabel('Mot de passe actuel')).toHaveCount(0);
await page.getByLabel('Nouveau mot de passe').fill(nouveauMotDePasse());
await page.getByRole('button', { name: 'Valider' }).click();