Commit 4e34996d authored by Tifenn Guillas's avatar Tifenn Guillas
Browse files

Merge branch 'develop' into 'master'

Develop

See merge request !36
parents a7247d13 dcd50093
......@@ -6,7 +6,7 @@ stages:
- deploy
variables:
VERSION: "3.3"
VERSION: "3.5"
SONARQUBE_URL: https://sonarqube.lam.fr
CONTAINER_IMAGE: portus.lam.fr/anis/anis-admin
......
......@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.5.0]
### Added
- #51: Add datasets rights depending on user group
- #49: Add authentication as instance option
- #48: Add Keycloak authentication
- #46: Add dataset public flag option
- #45: Add instance option for search multiple: by default, all datasets selected or none
## [3.4.0]
### Added
- #43: Add dataset result download options
......
3.4.0
\ No newline at end of file
3.5.0
\ No newline at end of file
FROM node:13-slim
FROM node:14-slim
ENV DEBIAN_FRONTEND=noninteractive
......
......@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
......
......@@ -11,46 +11,48 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~9.1.1",
"@angular/common": "~9.1.1",
"@angular/compiler": "~9.1.1",
"@angular/core": "~9.1.1",
"@angular/forms": "~9.1.1",
"@angular/platform-browser": "~9.1.1",
"@angular/platform-browser-dynamic": "~9.1.1",
"@angular/router": "~9.1.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@ngrx/effects": "^9.1.0",
"@ngrx/router-store": "^9.1.0",
"@ngrx/store": "^9.1.0",
"@ngrx/store-devtools": "^9.1.0",
"bootstrap": "^4.4.1",
"jwt-decode": "^2.2.0",
"ngx-bootstrap": "^5.6.1",
"ngx-toastr": "^12.0.1",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"@angular/animations": "~10.2.3",
"@angular/common": "~10.2.3",
"@angular/compiler": "~10.2.3",
"@angular/core": "~10.2.3",
"@angular/forms": "~10.2.3",
"@angular/platform-browser": "~10.2.3",
"@angular/platform-browser-dynamic": "~10.2.3",
"@angular/router": "~10.2.3",
"@fortawesome/fontawesome-free": "^5.15.1",
"@ngrx/effects": "^10.0.1",
"@ngrx/router-store": "^10.0.1",
"@ngrx/store": "^10.0.1",
"@ngrx/store-devtools": "^10.0.1",
"bootstrap": "^4.5.3",
"jwt-decode": "^3.1.2",
"keycloak-angular": "^8.0.1",
"keycloak-js": "^11.0.3",
"ngx-bootstrap": "^6.1.0",
"ngx-toastr": "^13.1.0",
"rxjs": "~6.6.3",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.1",
"@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~9.1.1",
"@angular/language-service": "~9.1.1",
"@types/jasmine": "~3.3.8",
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.3",
"@angular/language-service": "~10.2.3",
"@types/jasmine": "~3.6.1",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.7.5"
"codelyzer": "^6.0.1",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~9.0.0",
"tslint": "~6.1.0",
"typescript": "~4.0.5"
}
}
\ No newline at end of file
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpClientModule } from '@angular/common/http';
import { StoreModule } from '@ngrx/store';
import { StoreRouterConnectingModule, RouterState } from '@ngrx/router-store';
......@@ -11,14 +11,12 @@ import { EffectsModule } from '@ngrx/effects';
import { CustomRouterStateSerializer } from './shared/utils';
import { reducers, metaReducers } from './app.reducer';
import { CoreModule } from './core/core.module';
import { LoginModule } from './login/login.module';
import { AuthModule } from './auth/auth.module';
import { MetamodelModule} from './metamodel/metamodel.module';
import { SettingsModule } from './settings/settings.module';
import { AppRoutingModule } from './app.routing';
import { AppComponent } from './core/containers/app.component';
import { environment } from '../environments/environment';
import { TokenInterceptor } from './shared/token-interceptor';
@NgModule({
imports: [
......@@ -26,7 +24,6 @@ import { TokenInterceptor } from './shared/token-interceptor';
BrowserAnimationsModule,
HttpClientModule,
CoreModule,
LoginModule,
AuthModule,
MetamodelModule,
SettingsModule,
......@@ -48,9 +45,6 @@ import { TokenInterceptor } from './shared/token-interceptor';
!environment.production ? StoreDevtoolsModule.instrument() : [],
EffectsModule.forRoot([])
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './core/containers/login.component';
import { NotFoundPageComponent } from './core/containers/not-found-page.component';
import { UnauthorizedComponent } from './core/containers/unauthorized.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'unauthorized', component: UnauthorizedComponent },
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: '**', component: NotFoundPageComponent }
];
......
import { Action } from '@ngrx/store';
import { UserProfile } from './user-profile.model';
export const LOGIN = '[Auth] Login';
export const LOGOUT = '[Auth] Logout';
export const AUTH_SUCCESS = '[Auth] Auth Success';
export const LOAD_USER_PROFILE_SUCCESS = '[Auth] Load User Profile Success';
export const LOAD_USER_ROLES_SUCCESS = '[Auth] Load User Roles Success';
export const OPEN_EDIT_PROFILE = '[Auth] Edit Profile';
export class LoginAction implements Action {
readonly type = LOGIN;
constructor(public payload: {} = null) { }
}
export class LogoutAction implements Action {
readonly type = LOGOUT;
constructor(public payload: {} = null) { }
}
export class AuthSuccessAction implements Action {
readonly type = AUTH_SUCCESS;
constructor(public payload: {} = null) { }
}
export class LoadUserProfileSuccessAction implements Action {
readonly type = LOAD_USER_PROFILE_SUCCESS;
constructor(public payload: UserProfile) { }
}
export class LoadUserRolesSuccessAction implements Action {
readonly type = LOAD_USER_ROLES_SUCCESS;
constructor(public payload: string[]) { }
}
export class OpenEditProfileAction implements Action {
readonly type = OPEN_EDIT_PROFILE;
constructor(public payload: {} = null) { }
}
export type Actions
= LoginAction
| LogoutAction
| AuthSuccessAction
| LoadUserProfileSuccessAction
| LoadUserRolesSuccessAction
| OpenEditProfileAction;
import { Injectable } from '@angular/core';
import { from } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { KeycloakService } from 'keycloak-angular';
import * as authActions from './auth.action';
import { environment } from '../../environments/environment';
@Injectable()
export class AuthEffects {
constructor(
private actions$: Actions,
private keycloak: KeycloakService
) { }
@Effect({ dispatch: false })
loginAction$ = this.actions$.pipe(
ofType(authActions.LOGIN),
tap(_ => {
let redirectUri = window.location.origin;
if (environment.baseHref !== '/') {
redirectUri += environment.baseHref;
}
redirectUri += environment.ssoLoginRedirectUri;
this.keycloak.login({ redirectUri });
})
);
@Effect({ dispatch: false })
logoutAction$ = this.actions$.pipe(
ofType(authActions.LOGOUT),
tap(_ => {
let redirectUri = window.location.origin;
if (environment.baseHref !== '/') {
redirectUri += environment.baseHref;
}
redirectUri += environment.ssoLogoutRedirectUri;
this.keycloak.logout(redirectUri);
})
);
@Effect()
authSuccessAction$ = this.actions$.pipe(
ofType(authActions.AUTH_SUCCESS),
switchMap(_ =>
from(this.keycloak.loadUserProfile()).pipe(
switchMap(userProfile => [
new authActions.LoadUserProfileSuccessAction(userProfile),
new authActions.LoadUserRolesSuccessAction(this.keycloak.getUserRoles())
])
)
)
);
@Effect({ dispatch: false })
OpenEditProfileAction$ = this.actions$.pipe(
ofType(authActions.OPEN_EDIT_PROFILE),
tap(_ => window.open(environment.ssoAuthUrl + '/realms/' + environment.ssoRealm + '/account', '_blank'))
);
}
import { NgModule } from '@angular/core';
import { KeycloakAngularModule } from 'keycloak-angular';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { SharedModule } from '../shared/shared.module';
import { AuthRoutingModule, routedComponents } from './auth.routing';
import { dummiesComponents } from './components';
import { reducer } from './store/reducer';
import { UserEffects, AppEffects, TokenEffects } from './store/effects';
import { UserService, AppService, TokenService } from './store/service';
import { initializeKeycloakAnis } from './init.keycloak';
import { reducer } from './auth.reducer';
import { AuthEffects } from './auth.effects';
@NgModule({
imports: [
SharedModule,
AuthRoutingModule,
KeycloakAngularModule,
StoreModule.forFeature('auth', reducer),
EffectsModule.forFeature([ UserEffects, AppEffects, TokenEffects ])
],
declarations: [
routedComponents,
dummiesComponents
EffectsModule.forFeature([ AuthEffects ])
],
providers: [
UserService,
AppService,
TokenService
initializeKeycloakAnis
]
})
export class AuthModule { }
import * as actions from './login.action';
import * as actions from './auth.action';
import { LoginToken } from './model';
import { UserProfile } from './user-profile.model';
export interface State {
isAuthenticated: boolean;
loginToken: LoginToken;
userProfile: UserProfile;
userRoles: string[];
}
const initialState: State = {
export const initialState: State = {
isAuthenticated: false,
loginToken: null
userProfile: null,
userRoles: []
};
export function reducer(state: State = initialState, action: actions.Actions): State {
switch (action.type) {
case actions.LOGIN_LOCAL_STORAGE_SUCCESS:
case actions.LOGIN_SUCCESS:
const loginToken = action.payload as LoginToken;
case actions.AUTH_SUCCESS:
return {
...state,
isAuthenticated: true
};
case actions.LOAD_USER_PROFILE_SUCCESS:
const userProfile = action.payload;
return {
...state,
isAuthenticated: true,
loginToken
userProfile
};
case actions.LOGOUT_SUCCESS:
case actions.LOGOUT_FAIL:
case actions.LOGOUT_DENIED:
case actions.LOAD_USER_ROLES_SUCCESS:
const userRoles = action.payload;
return {
isAuthenticated: false,
loginToken: null
...state,
userRoles
};
default:
......@@ -38,4 +44,5 @@ export function reducer(state: State = initialState, action: actions.Actions): S
}
export const isAuthenticated = (state: State) => state.isAuthenticated;
export const getLoginToken = (state: State) => state.loginToken;
export const getUserProfile = (state: State) => state.userProfile;
export const getUserRoles = (state: State) => state.userRoles;
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserPageComponent } from './containers/user-page.component';
import { UserComponent } from './containers/user.component';
import { AppAvailableComponent } from './containers/app-available.component';
import { TokenComponent } from './containers/token.component';
import { NewAppComponent } from './containers/new-app.component';
import { AuthGuard } from '../core/auth.guard';
const routes: Routes = [
{
path: 'user', component: UserPageComponent, canActivate: [AuthGuard], children: [
{ path: '', redirectTo: 'user-list', pathMatch: 'full' },
{ path: 'user-list', component: UserComponent },
{ path: 'app-list', component: AppAvailableComponent },
{ path: 'token-list', component:  TokenComponent },
{ path: 'new-app', component: NewAppComponent }
]
}
];
@NgModule({
imports: [ RouterModule.forChild(routes) ],
exports: [ RouterModule ]
})
export class AuthRoutingModule { }
export const routedComponents = [
UserPageComponent,
UserComponent,
AppAvailableComponent,
TokenComponent,
NewAppComponent
];
import { createSelector, createFeatureSelector } from '@ngrx/store';
import * as auth from './auth.reducer';
export const getAuthState = createFeatureSelector<auth.State>('auth');
export const isAuthenticated = createSelector(
getAuthState,
auth.isAuthenticated
);
export const getUserProfile = createSelector(
getAuthState,
auth.getUserProfile
);
export const getUserRoles = createSelector(
getAuthState,
auth.getUserRoles
);
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let app of appList; let i = index">
<th scope="row">{{ i + 1 }}</th>
<td>{{ app.name }}</td>
<td>{{ app.description }}</td>
<td>
<button class="btn btn-outline-secondary btn-sm" (click)="deleteApp.emit(app)">
<span title="Delete the app" class="far fa-trash-alt"></span>
</button>
</td>
</tr>
</tbody>
</table>
\ No newline at end of file
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { App } from '../store/model';
@Component({
selector: 'app-app-list',
templateUrl: 'app-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppListComponent {
@Input() appList: App[];
@Output() deleteApp: EventEmitter<App> = new EventEmitter();
}
<form name="form" (ngSubmit)="f.form.valid && emit(f.form.value); f.reset()" #f="ngForm" novalidate>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" name="name" ngModel required>
</div>
<div class="form-group">
<label for="label">Description</label>
<input type="text" class="form-control" name="description" ngModel required>
</div>
<div class="form-group">
<ng-content></ng-content>
</div>
</form>
import { Component, Output, EventEmitter, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { App } from '../store/model';
@Component({
selector: 'app-form-app',
templateUrl: 'form-app.component.html'
})
export class FormAppComponent {
@ViewChild(NgForm, {static: true}) ngForm: NgForm;
@Output() changeProject: EventEmitter<number> = new EventEmitter();
@Output() submitted: EventEmitter<App> = new EventEmitter();
emit(app: App): void {
this.submitted.emit(app);
}
}
import { UserListComponent } from './user-list.component';
import { AppListComponent } from './app-list.component';
import { TokenListComponent } from './token-list.component';
import { FormAppComponent } from './form-app.component';
export const dummiesComponents = [
UserListComponent,
AppListComponent,
TokenListComponent,
FormAppComponent
];
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment