diff --git a/Dockerfile b/Dockerfile index 293b85ddcd004a17bc78e61712f0a01ad1259508..bee682a15c8297a99baf8b87a0fb52c7f50a759f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ COPY /src ./src ARG conf # Building the Angular app /dist i18n -RUN npm run build:dev +RUN npm run build:prod # Stage 1, based on Nginx, to have only the compiled app FROM nginx @@ -29,7 +29,6 @@ FROM nginx RUN apt-get update COPY --from=build /app/dist/fr /usr/share/nginx/html -COPY --from=build /app/dist/fr /usr/share/nginx/html/fr RUN touch /var/run/nginx.pid RUN ls -l /usr/share/nginx/html diff --git a/angular.json b/angular.json index 6e95360f6a90b5417e30d67c101ad0e1abe2b9be..962c5cd18d48fe6f6aa52040ec860e7d1878885b 100644 --- a/angular.json +++ b/angular.json @@ -14,9 +14,15 @@ "sourceRoot": "src", "prefix": "app", "i18n": { - "sourceLocale": "fr", + "sourceLocale": { + "code": "fr", + "baseHref": "" + }, "locales": { - "en": "src/locale/messages.en.xlf" + "en": { + "baseHref": "/en/", + "translation": "src/locale/messages.en.xlf" + } } }, "architect": { @@ -54,6 +60,7 @@ "with": "src/environments/environment.prod.ts" } ], + "aot": true, "optimization": true, "outputHashing": "all", "sourceMap": false, diff --git a/nginx/dev.conf b/nginx/dev.conf index 5998c857466cf8d5284d3aceea9e96b15f7e689d..4d727b257fe18fcdd966ab101a930d9da0abc7b9 100644 --- a/nginx/dev.conf +++ b/nginx/dev.conf @@ -1,25 +1,38 @@ - server { listen 8080 default_server; - root /usr/share/nginx/html; + root /usr/share/nginx/html/; + set $matomo_script + "<script type='text/javascript'> + var _paq = window._paq = window._paq || []; + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + (function() { + var u='<URL_GDLYON>'; + _paq.push(['setTrackerUrl', u+'piwik.php']); + _paq.push(['setSiteId', '<SITE_ID>']); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); + })(); + </script>"; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ /index.html; + sub_filter '</head>' '$matomo_script</head>'; } - location /api { - rewrite ^/api/(.*) /$1 break; - proxy_pass http://json-server:3000/api; + location /api { + rewrite ^/api/(.*) /$1 break; + proxy_pass http://service-ram:3000/api; } - # Public api redirect - location /base-adresse/base-adresse-nationale/streets { + location /base-adresse/base-adresse-nationale/streets { proxy_pass https://passerelle.formulaireextranet.grandlyon.com/base-adresse/base-adresse-nationale/streets; } + location /geocoding/photon/api { proxy_pass https://download.data.grandlyon.com/geocoding/photon/api; } @@ -32,9 +45,11 @@ server { proxy_pass https://download.data.grandlyon.com/wfs/grandlyon; } + # REALLY important for JavaScript modules (type="module") to work as expected!!! location ~ \.js { add_header Content-Type text/javascript; } } + diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index ca2b510f11ad37605306738bf353917834d1f60e..efacd3c848b600e4743d252025c6d9bee9c3fe48 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ import { HomeComponent } from './home/home.component'; import { LegalNoticeComponent } from './legal-notice/legal-notice.component'; import { ProfileComponent } from './profile/profile.component'; import { ResetEmailComponent } from './reset-email/reset-email.component'; +import { ResetPasswordComponent } from './reset-password/reset-password.component'; import { StructureDetailsComponent } from './structure-list/components/structure-details/structure-details.component'; import { StructureListComponent } from './structure-list/structure-list.component'; import { UserVerificationComponent } from './user-verification/user-verification.component'; @@ -53,6 +54,10 @@ const routes: Routes = [ canActivate: [AuthGuard], component: ProfileComponent, }, + { + path: 'reset-password', + component: ResetPasswordComponent, + }, { path: '**', redirectTo: 'home', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f46c10874aaab363c3b0a907122e882197bc567a..1a02174b10116efa3ecd4d721e85175b3d4acd57 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -26,6 +26,7 @@ import { AuthGuard } from './guards/auth.guard'; import { CustomHttpInterceptor } from './config/http-interceptor'; import { ProfileModule } from './profile/profile.module'; import { ResetEmailComponent } from './reset-email/reset-email.component'; +import { ResetPasswordComponent } from './reset-password/reset-password.component'; @NgModule({ declarations: [ @@ -45,6 +46,7 @@ import { ResetEmailComponent } from './reset-email/reset-email.component'; FormComponent, UserVerificationComponent, ResetEmailComponent, + ResetPasswordComponent, ], imports: [BrowserModule, HttpClientModule, AppRoutingModule, SharedModule, MapModule, ProfileModule], providers: [ diff --git a/src/app/reset-password/reset-password.component.html b/src/app/reset-password/reset-password.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f9c3810a248f7332f14c2dc7e4049afd51d14537 --- /dev/null +++ b/src/app/reset-password/reset-password.component.html @@ -0,0 +1,66 @@ +<div fxLayout="column" class="content-container"> + <div class="section-container" fxLayout="column" fxLayoutAlign="center center"> + <h1>Réinitialisation du mot de passe</h1> + <form *ngIf="!token" [formGroup]="resetForm" (ngSubmit)="onSubmit()"> + <div class="form-group"> + <label for="email">Email</label> + <input + type="email" + autocomplete="on" + formControlName="email" + class="form-control" + [ngClass]="{ 'is-invalid': submitted && f.email.errors }" + /> + <div *ngIf="submitted && f.email.errors" class="invalid-feedback"> + <div *ngIf="f.email.errors.required">Email is required</div> + </div> + </div> + <div class="form-group"> + <button [disabled]="loading" class="btn btn-primary"> + <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span> + Envoyer + </button> + <a routerLink="../login" class="btn btn-link">Cancel</a> + </div> + </form> + <form *ngIf="token" [formGroup]="resetFormChangePassword" (ngSubmit)="onSubmitPassword()"> + <div class="form-group"> + <label for="password">Mot de passe</label> + <input + type="password" + autocomplete="on" + formControlName="password" + class="form-control" + [ngClass]="{ 'is-invalid': submitted && fPassword.password.errors }" + /> + <div *ngIf="submitted && fPassword.password.errors" class="invalid-feedback"> + <div *ngIf="fPassword.password.errors.required">Le mot de passe est obligatoire</div> + <div *ngIf="fPassword.password.errors.pattern"> + Le mot de passe doit avoir au minimun 8 caractères, une majuscule, une minuscule, un chiffre et un caractère + spécial. + </div> + </div> + </div> + <div class="form-group"> + <label for="confirmPassword">Confirmation du mot de passe</label> + <input + type="password" + autocomplete="on" + formControlName="confirmPassword" + class="form-control" + [ngClass]="{ 'is-invalid': submitted && fPassword.confirmPassword.errors }" + /> + <div *ngIf="submitted && fPassword.confirmPassword.errors" class="invalid-feedback"> + <div *ngIf="fPassword.confirmPassword.errors.required">La confirmation du mot de passe est obligatoire</div> + <div *ngIf="fPassword.confirmPassword.errors.mustMatch">Les mot de passe ne sont pas les mêmes</div> + </div> + </div> + <div class="form-group"> + <button [disabled]="loading" class="btn btn-primary"> + <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span> + Enregistrer + </button> + </div> + </form> + </div> +</div> diff --git a/src/app/reset-password/reset-password.component.spec.ts b/src/app/reset-password/reset-password.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..92e44ad4534e2126ee299a3dcfce553d9a95ea0f --- /dev/null +++ b/src/app/reset-password/reset-password.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResetPasswordComponent } from './reset-password.component'; + +describe('ResetPasswordComponent', () => { + let component: ResetPasswordComponent; + let fixture: ComponentFixture<ResetPasswordComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ResetPasswordComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/reset-password/reset-password.component.ts b/src/app/reset-password/reset-password.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed5a77541a12372197e202763e6216c12fb822eb --- /dev/null +++ b/src/app/reset-password/reset-password.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { MustMatch } from '../shared/validator/form'; + +@Component({ + selector: 'app-reset-password', + templateUrl: './reset-password.component.html', +}) +export class ResetPasswordComponent implements OnInit { + public resetForm: FormGroup; + public resetFormChangePassword: FormGroup; + public token: string; + public loading = false; + public submitted = false; + + constructor( + private formBuilder: FormBuilder, + private authService: AuthService, + private router: Router, + private activatedRoute: ActivatedRoute + ) {} + + ngOnInit(): void { + this.activatedRoute.queryParams.subscribe((params) => { + this.token = params['token']; + }); + + this.initPasswordForm(); + this.resetForm = this.formBuilder.group({ + email: ['', Validators.required], + }); + } + + private initPasswordForm(): void { + this.resetFormChangePassword = this.formBuilder.group( + { + password: [ + '', + [Validators.required, Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/)], + ], + confirmPassword: [''], + }, + { validator: MustMatch('password', 'confirmPassword') } + ); + } + + // getter for form fields + get f(): { [key: string]: AbstractControl } { + return this.resetForm.controls; + } + + get fPassword(): { [key: string]: AbstractControl } { + return this.resetFormChangePassword.controls; + } + + public onSubmit(): void { + this.submitted = true; + + // stop here if form is invalid + if (this.resetForm.invalid) { + return; + } + + this.loading = true; + this.authService.resetPassword(this.f.email.value).subscribe( + () => { + this.router.navigate(['']); + }, + () => { + this.loading = false; + } + ); + } + + public onSubmitPassword(): void { + this.submitted = true; + + // stop here if form is invalid + if (this.resetFormChangePassword.invalid) { + return; + } + + this.loading = true; + this.authService.resetPasswordApply(this.token, this.fPassword.password.value).subscribe( + () => { + this.router.navigate(['']); + }, + () => { + this.loading = false; + } + ); + } +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 4f40f2c3de4bac22ba01f1723f73e3f9d68ce71b..4e98779cf926d18cb540833cd4a6341ed0cbff09 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -66,4 +66,15 @@ export class AuthService { params: { token }, }); } + + public resetPassword(email: string): Observable<any> { + return this.http.post(`api/users/reset-password`, { email }); + } + + public resetPasswordApply(token: string, password: string): Observable<any> { + return this.http.post(`api/users/reset-password/apply`, { + token, + password, + }); + } } diff --git a/src/app/shared/components/signup-modal/signup-modal.component.html b/src/app/shared/components/signup-modal/signup-modal.component.html index e12cffd1bb4d9ddf98e5be88099be372786c06c3..cf0a3fa0ada7f83d43ac102f4e11f41d0d98cdc0 100644 --- a/src/app/shared/components/signup-modal/signup-modal.component.html +++ b/src/app/shared/components/signup-modal/signup-modal.component.html @@ -40,6 +40,7 @@ </button> <button (click)="sendSwitchToSignIn()">Inscription</button> </div> + <a (click)="swithToResetPassword()">Mot de passe oublié</a> </form> </div> </div> diff --git a/src/app/shared/components/signup-modal/signup-modal.component.ts b/src/app/shared/components/signup-modal/signup-modal.component.ts index 1ef87d4d1f9ba8924e4642ec8a60bd5b3a834f56..3d70b98aeb373a6b83cd9679c00cec138305f346 100644 --- a/src/app/shared/components/signup-modal/signup-modal.component.ts +++ b/src/app/shared/components/signup-modal/signup-modal.component.ts @@ -48,6 +48,11 @@ export class SignUpModalComponent implements OnInit { this.closed.emit(false); } + public swithToResetPassword(): void { + this.closed.emit(true); + this.router.navigate(['/reset-password']); + } + public onSubmit(): void { this.submitted = true; diff --git a/src/index.html b/src/index.html index 9e350807caa211c6211153b2bd75853378be4ac2..034e83f362a671532cf29573af2a2b3ebd73ad4a 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="fr"> <head> <meta charset="utf-8" /> <title>Réseau des Acteurs de la Médiation</title>