diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0fe791257cfb35a418e04070ab75e91fe9b53057..148473e1d27cde613f5fc75a912e77c2ac193261 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ build_development: - sandbox script: - export NGINX_PORT=8081 - - docker-compose --project-name data-reloaded-dev build --no-cache + - docker-compose --project-name data-reloaded-dev build --no-cache --build-arg env=dev nginx-app deploy_development: stage: deploy @@ -52,7 +52,7 @@ build_staging: - staging script: - export NGINX_PORT=8080 - - docker-compose --project-name data-reloaded-rec build + - docker-compose --project-name data-reloaded-rec build --build-arg env=rec nginx-app deploy_staging: stage: deploy diff --git a/Dockerfile b/Dockerfile index 353920e33e13f27767a497e48526a9edca66b827..17ac052a3574efa7786328f838cc3641039ecbea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,10 @@ RUN npm install # Copy the project COPY . /app +ARG env + # Building the Angular app /dist i18n -RUN npm run build-i18n +RUN npm run build-i18n:${env} # Stage 1, based on Nginx, to have only the compiled app FROM nginx diff --git a/angular.json b/angular.json index 5483fa1cd096e150f7febcaae44cb71c0e944afb..23496065d7436f9e30f918d4f8c684034f4f200b 100644 --- a/angular.json +++ b/angular.json @@ -81,11 +81,11 @@ "fileReplacements": [ { "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" + "with": "src/environments/environment.rec.ts" } ] }, - "production-fr": { + "development-fr": { "optimization": true, "outputHashing": "all", "sourceMap": false, @@ -98,12 +98,72 @@ "fileReplacements": [ { "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" + "with": "src/environments/environment.dev.ts" }, { "replace": "src/i18n/geosource/geosource.ts", "with": "src/i18n/geosource/geosource.fr.ts" }, + { + "replace": "src/i18n/error-messages/error-messages.ts", + "with": "src/i18n/error-messages/error-messages.fr.ts" + }, + { + "replace": "src/i18n/contact/contact.ts", + "with": "src/i18n/contact/contact.fr.ts" + } + ], + "outputPath": "dist/fr", + "i18nFile": "src/i18n/messages.fr.xlf", + "i18nFormat": "xlf", + "i18nLocale": "fr", + "baseHref": "/fr/" + }, + "development-en": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.dev.ts" + } + ], + "outputPath": "dist/en", + "i18nFile": "src/i18n/messages.en.xlf", + "i18nFormat": "xlf", + "i18nLocale": "en", + "baseHref": "/en/" + }, + "recette-fr": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.rec.ts" + }, + { + "replace": "src/i18n/geosource/geosource.ts", + "with": "src/i18n/geosource/geosource.fr.ts" + }, + { + "replace": "src/i18n/error-messages/error-messages.ts", + "with": "src/i18n/error-messages/error-messages.fr.ts" + }, { "replace": "src/i18n/contact/contact.ts", "with": "src/i18n/contact/contact.fr.ts" @@ -115,7 +175,7 @@ "i18nLocale": "fr", "baseHref": "/fr/" }, - "production-en": { + "recette-en": { "optimization": true, "outputHashing": "all", "sourceMap": false, @@ -128,7 +188,7 @@ "fileReplacements": [ { "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" + "with": "src/environments/environment.rec.ts" } ], "outputPath": "dist/en", diff --git a/package-lock.json b/package-lock.json index f32b209fa64af723d99a0d70f31e726f0671234c..ace6f12e1331636d83be35cf5194a5ce3a7d4e6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2196,7 +2196,8 @@ "@types/geojson": { "version": "7946.0.4", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz", - "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==" + "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==", + "dev": true }, "@types/jasmine": { "version": "2.8.8", @@ -2213,15 +2214,23 @@ "@types/jasmine": "*" } }, + "@types/jwt-decode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz", + "integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==", + "dev": true + }, "@types/lodash": { "version": "4.14.116", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", - "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==" + "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==", + "dev": true }, "@types/lodash.clonedeep": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.4.tgz", "integrity": "sha512-+rCVPIZOJaub++wU/lmyp/SxiKlqXQaXI5LryzjuHBKFj51ApVt38Xxk9psLWNGMuR/obEQNTH0l/yDfG4ANNQ==", + "dev": true, "requires": { "@types/lodash": "*" } @@ -2230,6 +2239,7 @@ "version": "0.47.2", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-0.47.2.tgz", "integrity": "sha512-2RYTLUCPWkyh2RtzA8g7J5zybA12WbnKQnGLa+4Em2E6Sb0F/aQPl03nVxNX9URXu0+4fk+7ReGB8Pj07SPoEw==", + "dev": true, "requires": { "@types/geojson": "*" } @@ -8322,6 +8332,11 @@ } } }, + "jwt-decode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", + "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" + }, "karma": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/karma/-/karma-2.0.2.tgz", diff --git a/package.json b/package.json index fffab5b22c3d0f061e7c1f2ae1a81e02273a5b97..3746d72100328894a2ea8598a56b2d28056c2732 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "ng": "ng", "start-aot-fr": "ng serve --configuration=aot-fr", "start-aot-en": "ng serve --configuration=aot-en", - "build-i18n": "for lang in en fr; do ng build --prod --build-optimizer --configuration=production-$lang; done", - "win-build-i18n:fr": "ng build --prod --build-optimizer --configuration=production-fr", - "win-build-i18n:en": "ng build --prod --build-optimizer --configuration=production-en", + "build-i18n:dev": "for lang in en fr; do ng build --prod --build-optimizer --configuration=develoment-$lang; done", + "build-i18n:rec": "for lang in en fr; do ng build --prod --build-optimizer --configuration=recette-$lang; done", + "win-build-i18n:fr": "ng build --prod --build-optimizer --configuration=recette-fr", + "win-build-i18n:en": "ng build --prod --build-optimizer --configuration=recette-en", "win-build-i18n": "npm run win-build-i18n:en && npm run win-build-i18n:fr", "test": "ng test --browsers=Chrome --code-coverage=true", "test:ci": "ng test --browsers=ChromeHeadlessCI --code-coverage=true --watch=false", @@ -29,8 +30,6 @@ "@angular/router": "6.1.0", "@turf/centroid": "^5.1.5", "@turf/helpers": "^6.1.4", - "@types/lodash.clonedeep": "^4.5.4", - "@types/mapbox-gl": "^0.47.0", "angulartics2": "^6.2.0", "bulma": "^0.7.1", "bulma-checkradio": "^2.1.0", @@ -38,6 +37,7 @@ "bulma-tooltip": "^2.0.1", "core-js": "^2.5.7", "font-awesome": "^4.7.0", + "jwt-decode": "^2.2.0", "lodash": "^4.17.10", "lodash.clonedeep": "^4.5.0", "mapbox-gl": "^0.47.0", @@ -54,6 +54,9 @@ "@angular/language-service": "6.1.0", "@types/jasmine": "^2.8.8", "@types/jasminewd2": "~2.0.2", + "@types/jwt-decode": "^2.2.1", + "@types/lodash.clonedeep": "^4.5.4", + "@types/mapbox-gl": "^0.47.0", "@types/node": "^6.0.112", "codelyzer": "^4.3.0", "jasmine-core": "~2.8.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index dfc8cab300199ec2c928b265d14d0dec34d89c33..0a2dfd5b6e429fd7f61cf88f71a31f4cedd7848e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,7 +3,7 @@ import { Router, NavigationEnd, ActivatedRoute } from '../../node_modules/@angul import { pairwise, filter, map, mergeMap } from 'rxjs/operators'; import { AppRoutes } from './routes'; import { Angulartics2Piwik } from 'angulartics2/piwik'; -import { NavigationHistoryService } from './core/services'; +import { NavigationHistoryService, AuthService } from './core/services'; import { Title } from '@angular/platform-browser'; @Component({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ab6462bb1396f14a1ad27622eee484b5948c2c3..9b0b3b731198568cdc3c4d0d4ebe93a2d739e059 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgModule } from '@angular/core'; +import { NgModule, APP_INITIALIZER } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; @@ -9,6 +9,17 @@ import { CoreModule } from './core/core.module'; import { EditorialisationModule } from './editorialisation/editorialisation.module'; import { Angulartics2Module } from 'angulartics2'; import { Angulartics2Piwik } from 'angulartics2/piwik'; +import { AuthService } from './core/services'; + +// Function used by APP_INITIALIZER before the app start: init user info / statut (expect a promise) +export function loadUser(authService: AuthService) { + return (): Promise<any> => { + return new Promise((resolve, reject) => { + authService.setUserInfo(); + resolve(); + }); + }; +} @NgModule({ declarations: [ @@ -23,7 +34,14 @@ import { Angulartics2Piwik } from 'angulartics2/piwik'; AppRoutingModule, Angulartics2Module.forRoot([Angulartics2Piwik]), ], - providers: [], + providers: [ + { + provide: APP_INITIALIZER, + useFactory: loadUser, + deps: [AuthService], + multi: true, + }, + ], bootstrap: [AppComponent], }) export class AppModule { } diff --git a/src/app/core/components/header/header.component.html b/src/app/core/components/header/header.component.html index 895bb17b2543d0a88ad9f3ef8ddc567469c635e5..24fc57c9d0b840dea7b6688e9df63563c589c4c1 100644 --- a/src/app/core/components/header/header.component.html +++ b/src/app/core/components/header/header.component.html @@ -15,14 +15,15 @@ </div> <div class="navbar-menu" [ngClass]="{'is-active': burgerActive === true}"> <div class="navbar-start"> - <a class="navbar-item home-link" [routerLink]="['/', AppRoutes.home.uri]" routerLinkActive="active-link" i18n="@@header.home"> Home </a> - <a class="navbar-item research-link" [routerLink]="['/', AppRoutes.research.uri]" routerLinkActive="active-link" i18n="@@header.data"> + <a class="navbar-item research-link" [routerLink]="['/', AppRoutes.research.uri]" routerLinkActive="active-link" + i18n="@@header.data"> Data </a> - <a class="navbar-item approach-link" [routerLink]="['/', AppRoutes.approach.uri]" routerLinkActive="active-link" i18n="@@header.approach"> + <a class="navbar-item approach-link" [routerLink]="['/', AppRoutes.approach.uri]" routerLinkActive="active-link" + i18n="@@header.approach"> Approach </a> <a class="navbar-item approach-link" [routerLink]="['/', AppRoutes.actors.uri]" routerLinkActive="active-link" i18n="@@header.actors"> @@ -31,12 +32,19 @@ </div> <div class="navbar-end"> <div class="navbar-item"> - <a href preventDefault class="flag-logo" (click)="changeLanguage('en')"><img src="./assets/img/uk-flag.png" title="English" - i18n-title="@@header.logoEnglish" alt="Drapeau français"></a> + <a href preventDefault class="flag-logo" (click)="changeLanguage('en')"><img src="./assets/img/uk-flag.png" + title="English" i18n-title="@@header.logoEnglish" alt="Drapeau français"></a> <a href preventDefault class="flag-logo" (click)="changeLanguage('fr')"><img src="./assets/img/france-flag.png" title="French" i18n-title="@@header.logoFrench" alt="Drapeau du Royaume-Uni"></a> </div> - + <div class="navbar-item"> + <a [routerLink]="['/', AppRoutes.signin.uri]" routerLinkActive="active-link" *ngIf="!userIsSignedIn" i18n="@@header.signIn"> + Sign In + </a> + <span class="username" *ngIf="userIsSignedIn">{{ username }}</span> + <!-- <a [href]="env.logoutEndpoint" *ngIf="userIsSignedIn" i18n="@@header.signOut">Sign Out</a> --> + <a href preventDefault (click)="signOut()" *ngIf="userIsSignedIn" i18n="@@header.signOut">Sign Out</a> + </div> </div> </div> </nav> diff --git a/src/app/core/components/header/header.component.scss b/src/app/core/components/header/header.component.scss index ca9cecf685ef9bbf14d2433112ddc4351b0babf7..cc5e799e64a17423dab0f59921f2a6465e917678 100644 --- a/src/app/core/components/header/header.component.scss +++ b/src/app/core/components/header/header.component.scss @@ -20,4 +20,9 @@ } .navbar-menu { padding: 0.5rem 2rem; +} + +.username { + margin-right: 1rem; + font-weight: bold; } \ No newline at end of file diff --git a/src/app/core/components/header/header.component.spec.ts b/src/app/core/components/header/header.component.spec.ts index 9ba41e9df8f8b68192dc23a952e137c0a2e4a576..c39130af302155729b2f1232cfe9658ff2bb879b 100644 --- a/src/app/core/components/header/header.component.spec.ts +++ b/src/app/core/components/header/header.component.spec.ts @@ -6,7 +6,18 @@ import { FormsModule } from '@angular/forms'; import { HeaderComponent } from './header.component'; import { Router } from '../../../../../node_modules/@angular/router'; -import { AppRoutes } from '../../../routes'; +import { AuthService } from '../../services'; +import { User } from '../../models'; + +export class AuthServiceMock { + + constructor() { } + + userIsSignedIn() { + return false; + } + +} describe('HeaderComponent', () => { @@ -24,7 +35,12 @@ describe('HeaderComponent', () => { FormsModule, RouterTestingModule, ], - providers: [], + providers: [ + { + provide: AuthService, + useClass: AuthServiceMock, + }, + ], }) .compileComponents(); })); @@ -48,8 +64,14 @@ describe('HeaderComponent', () => { url: '', } as Router; + const authServiceMock = { + userIsSignedIn: () => { + return false; + }, + } as AuthService; + beforeEach(() => { - component = new HeaderComponent(routerMock); + component = new HeaderComponent(routerMock, authServiceMock); }); it('ngOnInit()', () => { diff --git a/src/app/core/components/header/header.component.ts b/src/app/core/components/header/header.component.ts index cadcc70b84dd836cecbece833feea9e3001a9009..ab8f8a535fb41b8da36e828c6d0e941845df706c 100644 --- a/src/app/core/components/header/header.component.ts +++ b/src/app/core/components/header/header.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { environment } from '../../../../environments/environment'; import { AppRoutes } from '../../../routes'; import { ActivatedRoute, Router } from '../../../../../node_modules/@angular/router'; +import { AuthService } from '../../services'; @Component({ selector: 'app-header', @@ -12,9 +13,11 @@ export class HeaderComponent implements OnInit { burgerActive: boolean; AppRoutes = AppRoutes; + env = environment; constructor( private _router: Router, + private _authService: AuthService, ) { } ngOnInit() { @@ -25,4 +28,16 @@ export class HeaderComponent implements OnInit { window.location.href = environment.angularAppHost[lang] + this._router.url; } + signOut() { + this._authService.signOut(); + } + + get userIsSignedIn() { + return this._authService.userIsSignedIn(); + } + + get username() { + return `${this._authService.user.firstname} ${this._authService.user.lastname}`; + } + } diff --git a/src/app/core/components/index.ts b/src/app/core/components/index.ts index 84499c9c375311044cafe141314ed1f4dfd6555e..ae18b2382f245aa69fc3ecbac7ed2ddd7aff5cda 100644 --- a/src/app/core/components/index.ts +++ b/src/app/core/components/index.ts @@ -3,9 +3,12 @@ import { MainComponent } from './main/main.component'; import { FooterComponent } from './footer/footer.component'; import { NotificationsComponent } from './notifications/notifications.component'; import { ErrorComponent } from './error/error.component'; +import { LoginComponent } from './login/login.component'; +import { LogoutComponent } from './logout/logout.component'; import { ContactComponent } from './contact/contact.component'; -export { HeaderComponent, MainComponent, FooterComponent, NotificationsComponent, ErrorComponent, ContactComponent }; +export { HeaderComponent, MainComponent, FooterComponent, NotificationsComponent, + ErrorComponent, LoginComponent, LogoutComponent, ContactComponent }; // tslint:disable-next-line:variable-name export const CoreComponents = [ @@ -15,4 +18,6 @@ export const CoreComponents = [ NotificationsComponent, ErrorComponent, ContactComponent, + LoginComponent, + LogoutComponent, ]; diff --git a/src/app/core/components/login/login.component.html b/src/app/core/components/login/login.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5a9971cb3be3809ac1eec7ab7ad7275fb5842c80 --- /dev/null +++ b/src/app/core/components/login/login.component.html @@ -0,0 +1,63 @@ +<div class="columns"> + <form [formGroup]="form" class="column is-8-touch is-offset-2-touch is-6 is-offset-3"> + <h1 class="has-text-centered" i18n="@@login.signIn">Sign In</h1> + + <div class="notification is-danger" [ngStyle]="{'visibility': (errorLogin === true ? 'visible' : 'hidden')}"> + <button class="delete" (click)="closeErrorMessage()"></button> + <span i18n="@@login.incorrectCredentials">Your credentials are not correct.</span> + </div> + + <div class="has-text-centered"> + <div class="inline"> + <a class="button" [href]="env.oidcLoginEndpoint"> + <img src="./assets/img/oidc.png" alt="Logo OIDC"> + </a> + </div> + <div class="inline"> + <a class="button" [href]="env.glcLoginEndpoint"> + <img src="./assets/img/glc.png" alt="Logo Grand Lyon Connect"> + </a> + </div> + </div> + + <div class="field"> + <label class="label">Email:</label> + <div class="field"> + <p class="control has-icons-left"> + <input class="input" type="email" formControlName="email"> + <span class="icon is-small is-left"> + <i class="fas fa-envelope"></i> + </span> + </p> + <div *ngIf="email.invalid && email.touched" class="field-error"> + <div *ngIf="email.errors['required']" i18n="@@login.emailRequired"> + Email is required. + </div> + <div *ngIf="email.errors['email']" i18n="@@login.incorrectEmail"> + You need to provide a valid email. + </div> + </div> + </div> + </div> + + <div class="field"> + <label class="label" i18n="@@login.password">Password:</label> + <div class="field"> + <p class="control has-icons-left"> + <input class="input" type="password" formControlName="password"> + <span class="icon is-small is-left"> + <i class="fas fa-lock"></i> + </span> + </p> + <div *ngIf="password.invalid && password.touched" class="field-error"> + <div *ngIf="password.errors['required']" i18n="@@login.passwordRequired"> + Password is required. + </div> + </div> + </div> + </div> + <div class="has-text-centered button-wrapper"> + <button class="button is-success" (click)="login()" [disabled]="form.invalid" i18n="@@login.signInBtn">Sign In</button> + </div> + </form> +</div> \ No newline at end of file diff --git a/src/app/core/components/login/login.component.scss b/src/app/core/components/login/login.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..9e3f9eacf29a14cb2e0b59faa3d79d936e81b5f4 --- /dev/null +++ b/src/app/core/components/login/login.component.scss @@ -0,0 +1,35 @@ +@import '../../../../scss/variables.scss'; + +.notification { + padding: 0.5rem 1.75rem 0.5rem 0.75rem; +} + +.field-error { + color: $red; +} + +.ng-touched { + &.ng-valid:not(form) { + border-right: 5px solid $green; /* green */ + } + + &.ng-invalid:not(form) { + border-right: 5px solid $red; /* red */ + } +} + +.button-wrapper { + margin-top: 1rem; +} + +.inline { + display: inline-block; + padding: 0 0.5rem; + + a { + img { + max-height: 100%; + margin: 0.5rem 0.5rem; + } + } +} \ No newline at end of file diff --git a/src/app/core/components/login/login.component.spec.ts b/src/app/core/components/login/login.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..94ec03e78030be80e51d1e43fd819af65fb6a2ae --- /dev/null +++ b/src/app/core/components/login/login.component.spec.ts @@ -0,0 +1,48 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { AuthService } from '../../services'; +import { RouterTestingModule } from '@angular/router/testing'; + +export class AuthServiceMock { + + constructor() { } + + userIsSignedIn() { + return false; + } + +} + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture<LoginComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + ], + declarations: [LoginComponent], + providers: [ + { + provide: AuthService, + useClass: AuthServiceMock, + }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/components/login/login.component.ts b/src/app/core/components/login/login.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..8da07cd7b182cbe5dc963b8f08f971fbf827f021 --- /dev/null +++ b/src/app/core/components/login/login.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../../services'; +import { Router, ActivatedRoute } from '@angular/router'; +import { environment } from '../../../../environments/environment'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent implements OnInit { + + form: FormGroup; + errorLogin = false; + env = environment; + + constructor( + private _fb: FormBuilder, + private _authService: AuthService, + private _router: Router, + private _activatedRoute: ActivatedRoute, + ) { + + this.form = this._fb.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + } + + ngOnInit() { + this._activatedRoute + .queryParams + .subscribe((params) => { + // Defaults to 0 if no query param provided. + if (params['code'] !== undefined && params['state'] !== undefined) { + this._authService.exchangeCode(params['code'], params['state']).subscribe((res) => { + this._router.navigate(['']); + }); + } + }); + } + + login() { + const val = this.form.value; + + if (val.email && val.password) { + this._authService.login(val.email, val.password).subscribe( + (res) => { + if (res) { + this.errorLogin = false; + this._router.navigateByUrl('/'); + } else { + this.errorLogin = true; + } + }, + (err) => { + this.errorLogin = true; + }, + ); + } + } + + closeErrorMessage() { + this.errorLogin = false; + } + + get email() { return this.form.get('email'); } + get password() { return this.form.get('password'); } + +} diff --git a/src/app/core/components/logout/logout.component.html b/src/app/core/components/logout/logout.component.html new file mode 100644 index 0000000000000000000000000000000000000000..bd95f0be2c79321916eaad3f633690b325d8e530 --- /dev/null +++ b/src/app/core/components/logout/logout.component.html @@ -0,0 +1,3 @@ +<p> + logout works! +</p> diff --git a/src/app/core/components/logout/logout.component.scss b/src/app/core/components/logout/logout.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/core/components/logout/logout.component.spec.ts b/src/app/core/components/logout/logout.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..89e4870cc2ee1c63690da2c848f56db57781ccc0 --- /dev/null +++ b/src/app/core/components/logout/logout.component.spec.ts @@ -0,0 +1,43 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogoutComponent } from './logout.component'; +import { AuthService } from '../../services'; +import { RouterTestingModule } from '@angular/router/testing'; + +export class AuthServiceMock { + + constructor() { } + + resetAuth() { + } + +} + +describe('LogoutComponent', () => { + let component: LogoutComponent; + let fixture: ComponentFixture<LogoutComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [LogoutComponent], + imports: [RouterTestingModule], + providers: [ + { + provide: AuthService, + useClass: AuthServiceMock, + }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LogoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/components/logout/logout.component.ts b/src/app/core/components/logout/logout.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c5b572c1c2004e293d537e2b7db7cd784cecff5 --- /dev/null +++ b/src/app/core/components/logout/logout.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from '@angular/core'; +import { AuthService } from '../../services'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-logout', + templateUrl: './logout.component.html', + styleUrls: ['./logout.component.scss'], +}) +export class LogoutComponent implements OnInit { + + constructor( + private _authService: AuthService, + private _router: Router, + ) { } + + ngOnInit() { + this._authService.resetAuth(); + this._router.navigate(['/']); + } + +} diff --git a/src/app/core/core-routing.module.ts b/src/app/core/core-routing.module.ts index 8b73098a4c181c67f2d96e2304ea3bda276835c7..ad0beae68e55c7b2db1dc49c341488adb099b080 100644 --- a/src/app/core/core-routing.module.ts +++ b/src/app/core/core-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AppRoutes } from '../routes'; -import { ErrorComponent, ContactComponent } from './components'; +import { ErrorComponent, ContactComponent, LoginComponent, LogoutComponent } from './components'; export const routes: Routes = [ { @@ -9,6 +9,20 @@ export const routes: Routes = [ redirectTo: AppRoutes.home.uri, pathMatch: 'full', }, + { + path: AppRoutes.logout.uri, + component: LogoutComponent, + data: { + title: 'Déconnexion', + }, + }, + { + path: AppRoutes.signin.uri, + component: LoginComponent, + data: { + title: AppRoutes.signin.title, + }, + }, { path: AppRoutes.error.uri, component: ErrorComponent, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 058450e9b4b9cf934c204c381d8c6a216fae33e7..c1b7432608801fb79c3e3e5a9de84e3650b1c268 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -8,8 +8,8 @@ import { ErrorsHandler } from './handlers/errors-handler'; import { HttpErrorResponseInterceptor } from './interceptors/http-error-response-interceptor'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { CoreServices } from './services'; -import { RouterModule } from '../../../node_modules/@angular/router'; import { ReactiveFormsModule } from '@angular/forms'; +import { AuthInterceptor } from './interceptors/auth-interceptor'; @NgModule({ imports: [ @@ -30,6 +30,11 @@ import { ReactiveFormsModule } from '@angular/forms'; useClass: HttpErrorResponseInterceptor, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + }, ], exports: [MainComponent, NotificationsComponent], }) diff --git a/src/app/core/handlers/errors-handler.ts b/src/app/core/handlers/errors-handler.ts index 3816dd42ab2a2068e544523646b50761079ded14..e44ea1796ce2bd9b8bc7436525f521259eac433b 100644 --- a/src/app/core/handlers/errors-handler.ts +++ b/src/app/core/handlers/errors-handler.ts @@ -4,6 +4,7 @@ import { ErrorHandler, Injectable } from '@angular/core'; export class ErrorsHandler implements ErrorHandler { handleError(error: Error) { + console.log(error); return; } diff --git a/src/app/core/interceptors/auth-interceptor.ts b/src/app/core/interceptors/auth-interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..8815868614d70bbf451f9e7c2ac4ddc6d14f2b09 --- /dev/null +++ b/src/app/core/interceptors/auth-interceptor.ts @@ -0,0 +1,23 @@ +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + + intercept( + req: HttpRequest<any>, + next: HttpHandler, + ): Observable<HttpEvent<any>> { + const token = localStorage.getItem('token'); + let request = req; + + // && req.url.includes('https://data-intothesky.alpha.grandlyon.com/authentication/api/logout' + if (token) { + request = req.clone({ + headers: req.headers.set('Authorization', 'Bearer ' + token), + }); + } + return next.handle(request); + } +} diff --git a/src/app/core/interceptors/http-error-response-interceptor.ts b/src/app/core/interceptors/http-error-response-interceptor.ts index a54205a989c1bc62bc6b0ed5342259bb4d5a93a3..71197df7fa2611192b436d63adb052330eb49f0f 100644 --- a/src/app/core/interceptors/http-error-response-interceptor.ts +++ b/src/app/core/interceptors/http-error-response-interceptor.ts @@ -6,10 +6,15 @@ import { HttpEvent, HttpResponse, HttpErrorResponse, } from '@angular/common/http'; import { tap } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; +import { Router } from '@angular/router'; +import { AppRoutes } from '../../routes'; @Injectable() export class HttpErrorResponseInterceptor implements HttpInterceptor { - constructor() { } + constructor( + private router: Router, + ) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req).pipe(tap( (event: HttpEvent<any>) => { @@ -22,6 +27,8 @@ export class HttpErrorResponseInterceptor implements HttpInterceptor { switch (err.status) { case 401: console.log('HTTP ERROR: Unauthorized'); + window.location.href = environment.oidcLoginEndpoint; + // this.router.navigate(['/', AppRoutes.signin.uri]) break; case 403: console.log('HTTP ERROR: Forbidden'); diff --git a/src/app/core/models/auth.model.ts b/src/app/core/models/auth.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..6de44da6faa4fc16e828b328272204e6b26a167a --- /dev/null +++ b/src/app/core/models/auth.model.ts @@ -0,0 +1,18 @@ +export class User { + id: number; + firstname: string; + lastname: string; + + // payload is the decrypted payload of the JWT token + constructor(payload) { + if (payload) { + this.id = payload.id; + this.firstname = payload.firstname; + this.lastname = payload.lastname; + } + } +} + +export interface ILoginResponse { + token: string; +} diff --git a/src/app/core/models/index.ts b/src/app/core/models/index.ts index 0d3c38d322c55b79a68bf7e0e53594f3ac8a21c1..63bb6924eaffca2ba8654ae4396cbca8c8738ce6 100644 --- a/src/app/core/models/index.ts +++ b/src/app/core/models/index.ts @@ -1,7 +1,6 @@ import { Notification, INotification } from './notification.model'; import { IMatomoResponse } from './matomo.model'; import { IContactForm, Email } from './email.model'; +import { User, ILoginResponse } from './auth.model'; -export { Notification, INotification }; -export { IMatomoResponse }; -export { IContactForm, Email }; +export { Notification, INotification, User, IMatomoResponse, ILoginResponse, IContactForm, Email }; diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5f34ecb09d19c0a8790d09ea5295fe209ec002e --- /dev/null +++ b/src/app/core/services/auth.service.ts @@ -0,0 +1,102 @@ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import * as JwtDecode from 'jwt-decode'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { ILoginResponse, User } from '../models'; +import { environment as env } from '../../../environments/environment'; + +@Injectable() +export class AuthService { + + private _user: User = null; + + constructor( + private _http: HttpClient, + ) { } + + login(email: string, password: string): Observable<boolean> { + return this._http.post<ILoginResponse>('http://localhost:3000/api/login', { email, password }).pipe( + map( + (res) => { + return this.setSession(res); + }, + (err) => { + return false; + }, + ), + ); + } + + exchangeCode(code: string, state: string): Observable<boolean> { + const url = `${env.kongUrl}/authentication/token?code=${code}&state=${state}`; + return this._http.get<ILoginResponse>(url).pipe( + map( + (res) => { + return this.setSession(res); + }, + (err) => { + return false; + }, + ), + ); + } + + logout() { + localStorage.removeItem('token'); + } + + private setSession(authResult): boolean { + let success = false; + if (authResult && authResult.token) { + localStorage.setItem('token', authResult.token); + this.setUserInfo(); + success = true; + } else { + this.signOut(); + } + + return success; + } + + setUserInfo() { + const token = localStorage.getItem('token'); + try { + const payload = JwtDecode(token); + this._user = new User(payload); + } catch (error) { + } + } + + signOut() { + const token = localStorage.getItem('token'); + let idToken = null; + let identityProvider = null; + + try { + const payload = JwtDecode(token); + idToken = payload['id_token']; + identityProvider = payload['identity_provider']; + } catch (error) { + } + + let url = `${env.kongUrl}/authentication/logout?`; + url += `id_token=${idToken}&identity_provider=${identityProvider}`; + + window.location.href = url; + } + + resetAuth() { + localStorage.removeItem('token'); + this._user = null; + } + + get user() { + return this._user; + } + + userIsSignedIn() { + return this._user != null; + } +} diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index 15ae6b2712fa5254fde435de2ef7bf1e5028a5ac..1fb610a30b5250e1964a5ad8aa8ba75b15078560 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -4,8 +4,8 @@ import { MatomoService } from './matomo.service'; import { NavigationHistoryService } from './navigation-history.service'; import { StorageService } from './storage.service'; import { EmailService } from './email.service'; - -export { ErrorService, NotificationService, MatomoService, NavigationHistoryService, EmailService }; +import { AuthService } from './auth.service'; +export { ErrorService, NotificationService, MatomoService, NavigationHistoryService, EmailService, AuthService }; // tslint:disable-next-line:variable-name export const CoreServices = [ @@ -15,4 +15,5 @@ export const CoreServices = [ NavigationHistoryService, StorageService, EmailService, + AuthService, ]; diff --git a/src/app/editorialisation/editorialisation.module.ts b/src/app/editorialisation/editorialisation.module.ts index 080de402b2a42529009f69dfc0b59ae301b0b7eb..24092e52d9ce8d4d18b35288afb92e4a6a6a03a5 100644 --- a/src/app/editorialisation/editorialisation.module.ts +++ b/src/app/editorialisation/editorialisation.module.ts @@ -6,7 +6,6 @@ import { EditorialisationServices } from './services'; import { EditorialisationResolvers } from './resolvers'; import { SharedModule } from '../shared/shared.module'; import { GeosourceModule } from '../geosource/geosource.module'; -import { DatasetResearchService } from '../geosource/services'; @NgModule({ imports: [ diff --git a/src/app/geosource/services/elasticsearch.service.ts b/src/app/geosource/services/elasticsearch.service.ts index cab37974091ddcc1ae1eb47787111178990cc0b3..abf216186a015760dee2931a6b9a74a4d1e1d04c 100644 --- a/src/app/geosource/services/elasticsearch.service.ts +++ b/src/app/geosource/services/elasticsearch.service.ts @@ -28,7 +28,7 @@ export class ElasticsearchService { getFullDataList(options: ElasticsearchOptions): Observable<IElasticsearchResponse> { const requestOptions = this.constructElasticsearchRequest(options); const request = this._storageService.get('requestES'); - if (request === JSON.stringify(requestOptions)) { + if (environment.enableEsFrontCache === true && request === JSON.stringify(requestOptions)) { const jsonESResponse = JSON.parse(this._storageService.get('resultES')) as IElasticsearchResponse; return of(jsonESResponse); } diff --git a/src/app/routes.ts b/src/app/routes.ts index 27db92a085df95479d39970f1394e216f8f7ed9b..17c92327fa03bd23d7dd6007cd34df68538db088 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -1,5 +1,19 @@ // tslint:disable-next-line:variable-name export const AppRoutes = { + logout: { + uri: 'logout', + title: { + fr: 'Déconnexion', + en: 'Log out', + }, + }, + signin: { + uri: 'login', + title: { + fr: 'Connexion', + en: 'Log In', + }, + }, home: { uri: 'accueil', title: { diff --git a/src/assets/img/glc.png b/src/assets/img/glc.png new file mode 100644 index 0000000000000000000000000000000000000000..cabd3fa3f765cff13cdc33e091795743a284defa Binary files /dev/null and b/src/assets/img/glc.png differ diff --git a/src/assets/img/oidc.png b/src/assets/img/oidc.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba87cee06f44c997988d1adb2bdb965c1f230ae Binary files /dev/null and b/src/assets/img/oidc.png differ diff --git a/src/environments/environment.prod.ts b/src/environments/environment.dev.ts similarity index 83% rename from src/environments/environment.prod.ts rename to src/environments/environment.dev.ts index 40f7139cbc7993534c5baa6c2402fe61de090034..773028e6ce3f95ac248d1bce8c8b3003626de84a 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.dev.ts @@ -5,22 +5,29 @@ import { AppRoutes } from '../app/routes'; // `ng build --env=prod` then `environment.prod.ts` will be used instead. // The list of which env maps to which file can be found in `.angular-cli.json`. const servicesProxyUrl = 'https://data-intothesky.alpha.grandlyon.com'; +const kongBaseUrl = 'https://kong.alpha.grandlyon.com'; export const environment = { production: true, + kongUrl: kongBaseUrl, + oidcLoginEndpoint: kongBaseUrl + '/authentication/dev/login/oidc', + glcLoginEndpoint: kongBaseUrl + '/authentication/dev/login/glc', + logoutEndpoint: kongBaseUrl + '/authentication/dev/logout', // ElasticSearch elasticsearchUrl: { - full: servicesProxyUrl + '/elasticsearch2/test-all-in-one-index.full.v8.quadtree', - meta: servicesProxyUrl + '/elasticsearch/*.meta', + full: kongBaseUrl + '/es-consumer-aware', + meta: kongBaseUrl + '/elasticsearch/*.meta', }, + enableEsFrontCache: false, + matomo: { url: 'https://matomo-intothesky.alpha.grandlyon.com', }, backend: { - url: servicesProxyUrl + '/backend', + url: kongBaseUrl + '/organizations', }, emailService: { diff --git a/src/environments/environment.rec.ts b/src/environments/environment.rec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6a82f0d79b50630d140306d27e9b60430fc4c6e --- /dev/null +++ b/src/environments/environment.rec.ts @@ -0,0 +1,96 @@ +import { AppRoutes } from '../app/routes'; + +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. +const servicesProxyUrl = 'https://data-intothesky.alpha.grandlyon.com'; +const kongBaseUrl = 'https://kong.alpha.grandlyon.com'; + +export const environment = { + production: true, + kongUrl: kongBaseUrl, + oidcLoginEndpoint: kongBaseUrl + '/authentication/rec/login/oidc', + glcLoginEndpoint: kongBaseUrl + '/authentication/rec/login/glc', + logoutEndpoint: kongBaseUrl + '/authentication/rec/logout', + + // ElasticSearch + elasticsearchUrl: { + full: kongBaseUrl + '/es-consumer-aware', + meta: kongBaseUrl + '/elasticsearch/*.meta', + }, + + enableEsFrontCache: false, + + matomo: { + url: 'https://matomo-intothesky.alpha.grandlyon.com', + }, + + backend: { + url: kongBaseUrl + '/organizations', + }, + + // Path to the built app in a particular language + angularAppHost: { + fr: '/fr', + en: '/en', + }, + + // Wordpress + wordpressUrl: servicesProxyUrl + '/wordpress/wp-json/wp/v2/', + wordpressStaticPages: {}, + wordpressContent: { + categoryIdUniqueContent: 5, + slugPostVideo: 'video-dgl', + }, + + // Map + defaultBaseLayer: 0, + baseLayers: [ + { + id: 1, + maxzoom: 21, + fileName: 'etalab.json', + labels: { + fr: 'Etalab', + en: 'Etalab', + }, + }, + { + id: 2, + maxzoom: 21, + fileName: 'vector.json', + labels: { + fr: 'Vecteur', + en: 'Vector', + }, + }, + { + id: 3, + maxzoom: 17, + fileName: 'plan.json', + labels: { + fr: 'Plan', + en: 'Plan', + }, + }, + { + id: 4, + maxzoom: 21, + fileName: 'satellite.json', + labels: { + fr: 'Satellite', + en: 'Satellite', + }, + }, + ], + +}; + +// Use variable route names (AppRoutes.approach...) for each static page to attribute the corresponding +// wordpress page in order to make sure that if the name of the route change in the config file (routes.ts), +// the wordpress page will still be served. +environment.wordpressStaticPages[AppRoutes.approach.uri] = 'la-demarche'; +environment.wordpressStaticPages[AppRoutes.accessibility.uri] = 'accessibilite'; +environment.wordpressStaticPages[AppRoutes.siteMap.uri] = 'plan-du-site'; +environment.wordpressStaticPages[AppRoutes.legalNotices.uri] = 'mentions-legales'; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index a5259414d400596e255d2fb252b08dd87513f173..b860dc1b9c6f01641baac30ab7e331a8ca31d590 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -5,9 +5,14 @@ import { AppRoutes } from '../app/routes'; // `ng build --env=prod` then `environment.prod.ts` will be used instead. // The list of which env maps to which file can be found in `.angular-cli.json`. const servicesProxyUrl = 'https://data-intothesky.alpha.grandlyon.com'; +const kongBaseUrl = 'https://kong.alpha.grandlyon.com'; export const environment = { production: false, + kongUrl: kongBaseUrl, + oidcLoginEndpoint: kongBaseUrl + '/authentication/dev/login/oidc', + glcLoginEndpoint: kongBaseUrl + '/authentication/dev/login/glc', + logoutEndpoint: kongBaseUrl + '/authentication/dev/logout', // ElasticSearch elasticsearchUrl: { @@ -15,12 +20,14 @@ export const environment = { meta: servicesProxyUrl + '/elasticsearch/*.meta', }, + enableEsFrontCache: true, + matomo: { url: 'https://matomo-intothesky.alpha.grandlyon.com', }, backend: { - url: 'http://localhost:3000', + url: kongBaseUrl + '/organizations', }, emailService: { diff --git a/src/i18n/messages.en.xlf b/src/i18n/messages.en.xlf index 69beaa12491b596941485abca3eb6a33f0072497..de3129be728f55667e3fc8c3ce67f1507333af24 100644 --- a/src/i18n/messages.en.xlf +++ b/src/i18n/messages.en.xlf @@ -18,6 +18,14 @@ <source>Actors</source> <target>Actors</target> </trans-unit> + <trans-unit id="header.signIn" datatype="html"> + <source>Sign In</source> + <target>Sign In</target> + </trans-unit> + <trans-unit id="header.signOut" datatype="html"> + <source>Sign Out</source> + <target>Sign Out</target> + </trans-unit> <trans-unit id="footer.rss" datatype="html"> <source>RSS Feed (new window)</source> <target>RSS Feed (new window)</target> @@ -379,6 +387,34 @@ <source>Cancel</source> <target>Cancel</target> </trans-unit> + <trans-unit id="login.signIn" datatype="html"> + <source>Sign In</source> + <target>Sign In</target> + </trans-unit> + <trans-unit id="login.incorrectCredentials" datatype="html"> + <source>Your credentials are not correct.</source> + <target>Your credentials are not correct.</target> + </trans-unit> + <trans-unit id="login.emailRequired" datatype="html"> + <source>Email is required.</source> + <target>Email is required.</target> + </trans-unit> + <trans-unit id="login.incorrectEmail" datatype="html"> + <source>You need to provide a valid email.</source> + <target>You need to provide a valid email.</target> + </trans-unit> + <trans-unit id="login.password" datatype="html"> + <source>Password:</source> + <target>Password:</target> + </trans-unit> + <trans-unit id="login.passwordRequired" datatype="html"> + <source>Password is required.</source> + <target>Password is required.</target> + </trans-unit> + <trans-unit id="login.signInBtn" datatype="html"> + <source>Sign In</source> + <target>Sign In</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/i18n/messages.fr.xlf b/src/i18n/messages.fr.xlf index 3aa38827300b53531b25d547d26f728350070947..2e716c421ba9a90eea8eea6636c8735c680e30a8 100644 --- a/src/i18n/messages.fr.xlf +++ b/src/i18n/messages.fr.xlf @@ -18,6 +18,14 @@ <source>Actors</source> <target>Les acteurs</target> </trans-unit> + <trans-unit id="header.signIn" datatype="html"> + <source>Sign In</source> + <target>Connexion</target> + </trans-unit> + <trans-unit id="header.signOut" datatype="html"> + <source>Sign Out</source> + <target>Déconnexion</target> + </trans-unit> <trans-unit id="footer.rss" datatype="html"> <source>RSS Feed (new window)</source> <target>Flux RSS (nouvelle fenêtre)</target> @@ -387,6 +395,34 @@ <source>Cancel</source> <target>Annuler</target> </trans-unit> + <trans-unit id="login.signIn" datatype="html"> + <source>Sign In</source> + <target>Connexion</target> + </trans-unit> + <trans-unit id="login.incorrectCredentials" datatype="html"> + <source>Your credentials are not correct.</source> + <target>Vos identifiants ne sont pas corrects.</target> + </trans-unit> + <trans-unit id="login.emailRequired" datatype="html"> + <source>Email is required.</source> + <target>Veuillez renseigner votre email.</target> + </trans-unit> + <trans-unit id="login.incorrectEmail" datatype="html"> + <source>You need to provide a valid email.</source> + <target>Veuillez renseigner un email valide.</target> + </trans-unit> + <trans-unit id="login.password" datatype="html"> + <source>Password:</source> + <target>Mot de passe:</target> + </trans-unit> + <trans-unit id="login.passwordRequired" datatype="html"> + <source>Password is required.</source> + <target>Veuillez renseigner votre mot de passe.</target> + </trans-unit> + <trans-unit id="login.signInBtn" datatype="html"> + <source>Sign In</source> + <target>Connexion</target> + </trans-unit> </body> </file> </xliff>