From 65d7d6cd738e85e18e9a3c401c4594786a879ea0 Mon Sep 17 00:00:00 2001 From: FORESTIER Fabien <fabien.forestier@soprasteria.com> Date: Fri, 21 Sep 2018 11:06:36 +0200 Subject: [PATCH] Add base for authentification with JWT --- package-lock.json | 11 +++ package.json | 6 +- src/app/app.module.ts | 22 +++++- .../components/header/header.component.html | 19 +++-- .../components/header/header.component.scss | 5 ++ .../components/header/header.component.ts | 14 ++++ src/app/core/components/index.ts | 4 +- .../components/login/login.component.html | 50 ++++++++++++++ .../components/login/login.component.scss | 23 +++++++ .../components/login/login.component.spec.ts | 25 +++++++ .../core/components/login/login.component.ts | 55 +++++++++++++++ src/app/core/core-routing.module.ts | 9 ++- src/app/core/core.module.ts | 8 +++ src/app/core/interceptors/auth-interceptor.ts | 25 +++++++ src/app/core/models/auth.model.ts | 18 +++++ src/app/core/models/index.ts | 4 +- src/app/core/services/auth.service.ts | 69 +++++++++++++++++++ src/app/core/services/index.ts | 4 +- src/app/routes.ts | 7 ++ src/i18n/messages.en.xlf | 36 ++++++++++ src/i18n/messages.fr.xlf | 36 ++++++++++ 21 files changed, 435 insertions(+), 15 deletions(-) create mode 100644 src/app/core/components/login/login.component.html create mode 100644 src/app/core/components/login/login.component.scss create mode 100644 src/app/core/components/login/login.component.spec.ts create mode 100644 src/app/core/components/login/login.component.ts create mode 100644 src/app/core/interceptors/auth-interceptor.ts create mode 100644 src/app/core/models/auth.model.ts create mode 100644 src/app/core/services/auth.service.ts diff --git a/package-lock.json b/package-lock.json index f32b209f..687d84c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2213,6 +2213,12 @@ "@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", @@ -8322,6 +8328,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 fffab5b2..321978f3 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,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 +36,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 +53,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.module.ts b/src/app/app.module.ts index 0ab6462b..9b0b3b73 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 895bb17b..60141364 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,18 @@ </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 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 ca9cecf6..cc5e799e 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.ts b/src/app/core/components/header/header.component.ts index cadcc70b..9f386be0 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', @@ -15,6 +16,7 @@ export class HeaderComponent implements OnInit { constructor( private _router: Router, + private _authService: AuthService, ) { } ngOnInit() { @@ -25,4 +27,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 82c8d51d..dea54885 100644 --- a/src/app/core/components/index.ts +++ b/src/app/core/components/index.ts @@ -3,8 +3,9 @@ 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'; -export { HeaderComponent, MainComponent, FooterComponent, NotificationsComponent, ErrorComponent }; +export { HeaderComponent, MainComponent, FooterComponent, NotificationsComponent, ErrorComponent, LoginComponent }; // tslint:disable-next-line:variable-name export const CoreComponents = [ @@ -13,4 +14,5 @@ export const CoreComponents = [ FooterComponent, NotificationsComponent, ErrorComponent, + LoginComponent, ]; 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 00000000..20c16afe --- /dev/null +++ b/src/app/core/components/login/login.component.html @@ -0,0 +1,50 @@ +<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="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 00000000..9b3550ac --- /dev/null +++ b/src/app/core/components/login/login.component.scss @@ -0,0 +1,23 @@ +@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; +} \ 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 00000000..d6d85a84 --- /dev/null +++ b/src/app/core/components/login/login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture<LoginComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .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 00000000..1e5ac0d9 --- /dev/null +++ b/src/app/core/components/login/login.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../../services'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent { + + form: FormGroup; + errorLogin = false; + + constructor( + private _fb: FormBuilder, + private _authService: AuthService, + private _router: Router, + ) { + + this.form = this._fb.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + } + + 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/core-routing.module.ts b/src/app/core/core-routing.module.ts index 5f7c4038..319b8225 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 } from './components'; +import { ErrorComponent, LoginComponent } from './components'; export const routes: Routes = [ { @@ -9,6 +9,13 @@ export const routes: Routes = [ redirectTo: AppRoutes.home.uri, pathMatch: 'full', }, + { + 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 4a5b5aa6..57da56fb 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -9,12 +9,15 @@ import { HttpErrorResponseInterceptor } from './interceptors/http-error-response 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: [ CommonModule, CoreRoutingModule, SharedModule, + ReactiveFormsModule, ], declarations: [CoreComponents], providers: [ @@ -28,6 +31,11 @@ import { RouterModule } from '../../../node_modules/@angular/router'; useClass: HttpErrorResponseInterceptor, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + }, ], exports: [MainComponent, NotificationsComponent], }) diff --git a/src/app/core/interceptors/auth-interceptor.ts b/src/app/core/interceptors/auth-interceptor.ts new file mode 100644 index 00000000..0551ba9d --- /dev/null +++ b/src/app/core/interceptors/auth-interceptor.ts @@ -0,0 +1,25 @@ +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from '../../../environments/environment.prod'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + + intercept( + req: HttpRequest<any>, + next: HttpHandler, + ): Observable<HttpEvent<any>> { + const token = localStorage.getItem('token'); + const request = req; + + // Can also filter by url + // && req.url.includes(environment.elasticsearchUrl.full) + // if (token) { + // request = req.clone({ + // headers: req.headers.set('Authorization', 'Bearer ' + token), + // }); + // } + return next.handle(request); + } +} diff --git a/src/app/core/models/auth.model.ts b/src/app/core/models/auth.model.ts new file mode 100644 index 00000000..6de44da6 --- /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 804ec59d..019c0524 100644 --- a/src/app/core/models/index.ts +++ b/src/app/core/models/index.ts @@ -1,5 +1,5 @@ import { Notification, INotification } from './notification.model'; import { IMatomoResponse } from './matomo.model'; +import { User, ILoginResponse } from './auth.model'; -export { Notification, INotification }; -export { IMatomoResponse }; +export { Notification, INotification, User, IMatomoResponse, ILoginResponse }; diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts new file mode 100644 index 00000000..128004fe --- /dev/null +++ b/src/app/core/services/auth.service.ts @@ -0,0 +1,69 @@ + +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'; + +@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; + }, + ), + ); + } + + 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() { + 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 ed90907e..30fdaef5 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -3,8 +3,9 @@ import { NotificationService } from './notification.service'; import { MatomoService } from './matomo.service'; import { NavigationHistoryService } from './navigation-history.service'; import { StorageService } from './storage.service'; +import { AuthService } from './auth.service'; -export { ErrorService, NotificationService, MatomoService, NavigationHistoryService }; +export { ErrorService, NotificationService, MatomoService, NavigationHistoryService, AuthService }; // tslint:disable-next-line:variable-name export const CoreServices = [ @@ -13,4 +14,5 @@ export const CoreServices = [ MatomoService, NavigationHistoryService, StorageService, + AuthService, ]; diff --git a/src/app/routes.ts b/src/app/routes.ts index 18c900f3..c727cc8e 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -1,5 +1,12 @@ // tslint:disable-next-line:variable-name export const AppRoutes = { + signin: { + uri: 'connexion', + title: { + fr: 'Connexion', + en: 'Sign In', + }, + }, home: { uri: 'accueil', title: { diff --git a/src/i18n/messages.en.xlf b/src/i18n/messages.en.xlf index 5fd3e499..f40531a5 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> @@ -275,6 +283,34 @@ <source>Go back to home page</source> <target>Go back to home page</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 15430968..96473e96 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> @@ -283,6 +291,34 @@ <source>Go back to home page</source> <target>Retour à la page d'accueil</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> -- GitLab