Commit 0d2f7c19 authored by FORESTIER Fabien's avatar FORESTIER Fabien
Browse files

Merge branch 'poc_auth-oidc-glc'

parents f8e8468d 6453f08a
Pipeline #1370 failed with stages
in 8 minutes and 32 seconds
......@@ -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
......
......@@ -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
......
......@@ -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",
......
......@@ -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",
......
......@@ -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",
......
......@@ -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({
......
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 { }
......@@ -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>
......
......@@ -20,4 +20,9 @@
}
.navbar-menu {
padding: 0.5rem 2rem;
}
.username {
margin-right: 1rem;
font-weight: bold;
}
\ No newline at end of file
......@@ -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()', () => {
......
......@@ -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}`;
}
}
......@@ -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,
];
<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
@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
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();
});
});
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';