diff --git a/package-lock.json b/package-lock.json index f32b209fa64af723d99a0d70f31e726f0671234c..687d84c5bd7f2bd4051565b35a7428b5042c5a28 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 fffab5b22c3d0f061e7c1f2ae1a81e02273a5b97..321978f323c797cb3242f8aa818bf26e1829c79e 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 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..60141364346d350d9f2370d3da1cab0eb2fd4d4f 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 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.ts b/src/app/core/components/header/header.component.ts index cadcc70b84dd836cecbece833feea9e3001a9009..9f386be0c91542f8b79cf2806500e9b52661a76c 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 82c8d51d679ad7dbfaa2a3ab26af313913aafd4c..dea54885798b477bd3747c5f78cedaef47df81e0 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 0000000000000000000000000000000000000000..20c16afed2d2fe7488c01fc969699cdd7bd6ab07 --- /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 0000000000000000000000000000000000000000..9b3550ac7b3bdc2a6e2f1e2d609fe872b7bd147b --- /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 0000000000000000000000000000000000000000..d6d85a8465b79ee37cb75c371f6e6e936997c573 --- /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 0000000000000000000000000000000000000000..1e5ac0d969f67718b4e5bbe06ae50fb31fd47bfd --- /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 5f7c40380de53928ca3c06c5c803c518ac7fb50a..319b82251901952bc4eab670deafe8a28e07d753 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 4a5b5aa6c31e21248953c02735e6b3a45742a71b..57da56fbd3c2308499a5121dfad54534b4c6dd7d 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 0000000000000000000000000000000000000000..0551ba9d83af7c814cc1e536806a9840e56fb4c3 --- /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 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 804ec59ded0ef6ad842ec93fe8c958a85456e135..019c0524db0a584e4af3a961feb6fc4782403e22 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 0000000000000000000000000000000000000000..128004fef37151df81eb784f1798bfd0db657329 --- /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 ed90907ea1e9a8b600f61fe1ae841317c93fb80e..30fdaef52ec4915243cde5ff2a8012be38895b1f 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 18c900f31150a428a6b70fbec525cad9fd9ab9a8..c727cc8e05283f4ee47bae0ffbc113f4b0cd8c1d 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 5fd3e499a4fb037ec31cfd05374e0ceeb349b671..f40531a57ae4f568d2c49b80ef0e9fb55b6b5c08 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 1543096819c509f9b67683168486020a55b31be5..96473e96903f04213a032773bb22f89be7043c4e 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>