From fb2a7fd6439aa0813af20167af9f001cf1af588d Mon Sep 17 00:00:00 2001 From: Martin Stockhammer Date: Tue, 3 Nov 2020 00:31:44 +0100 Subject: [PATCH] Additional angular code --- .../archiva-web/src/app/app-routing.module.ts | 1 + .../archiva-web/src/app/app.component.html | 134 +++++++++-------- .../main/archiva-web/src/app/app.component.ts | 34 ++++- .../src/app/model/user-info.spec.ts | 25 ++++ .../archiva-web/src/app/model/user-info.ts | 36 +++++ .../modules/general/login/login.component.ts | 3 + .../app/services/archiva-request.service.ts | 92 ++++++++---- .../app/services/authentication.service.ts | 140 ++++++++++++------ .../src/app/services/user.service.spec.ts | 34 +++++ .../src/app/services/user.service.ts | 109 ++++++++++++++ .../main/archiva-web/src/assets/i18n/de.json | 1 + .../main/archiva-web/src/assets/i18n/en.json | 1 + 12 files changed, 474 insertions(+), 136 deletions(-) create mode 100644 archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.spec.ts create mode 100644 archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts create mode 100644 archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.spec.ts create mode 100644 archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app-routing.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app-routing.module.ts index 19a64efbd..f8c9eb033 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app-routing.module.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app-routing.module.ts @@ -30,6 +30,7 @@ const routes: Routes = [ { path: 'contact', component: ContactComponent }, { path: 'about', component: AboutComponent }, { path: 'login', component: LoginComponent }, + { path: 'logout', component: HomeComponent }, { path: '**', component: NotFoundComponent } ]; diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.html index 7e15657d2..f223be39d 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.html +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.html @@ -16,70 +16,84 @@ ~ specific language governing permissions and limitations ~ under the License. --> -
-
- -
+ + -
-
- -
-
+
+
+ +
+
-
-
-
-
-
-

© 2020 - archiva.apache.org

+
+
+
+
+
+

© 2020 - archiva.apache.org

+
+
-
-
-
+
\ No newline at end of file diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.ts index 30b0a2933..3dae41bb2 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.component.ts @@ -16,28 +16,33 @@ * specific language governing permissions and limitations * under the License. */ -import { Component } from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { AuthenticationService } from "./services/authentication.service"; +import {UserService} from "./services/user.service"; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) -export class AppComponent { +export class AppComponent implements OnInit, OnDestroy{ title = 'archiva-web'; version = 'Angular version 10.0.2'; constructor( - public translate: TranslateService + public translate: TranslateService, + public auth: AuthenticationService, + public user: UserService ) { translate.addLangs(['en', 'de']); translate.setDefaultLang('en'); - translate.use('en'); } switchLang(lang: string) { this.translate.use(lang); + this.user.userInfo.language = lang; + this.user.persistUserInfo(); } langIcon() : string { @@ -50,4 +55,25 @@ export class AppComponent { return "flag-icon-" + this.translate.currentLang; } } + + ngOnDestroy(): void { + this.auth.LoginEvent.unsubscribe(); + } + + + ngOnInit(): void { + let lang = this.user.userInfo.language; + if (lang==null) { + this.translate.use('en'); + } else { + this.translate.use(lang); + } + // Subscribe to login event in authenticator to switch the language + this.auth.LoginEvent.subscribe(userInfo => { + if (userInfo.language != null) { + this.switchLang(userInfo.language); + } + }) + + } } diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.spec.ts new file mode 100644 index 000000000..79fadf144 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.spec.ts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UserInfo } from './user-info'; + +describe('UserInfo', () => { + it('should create an instance', () => { + expect(new UserInfo()).toBeTruthy(); + }); +}); diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts new file mode 100644 index 000000000..6356fe2cf --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class UserInfo { + user_id:string; + id:string; + fullName:string; + email:string; + validated:boolean; + locked:boolean; + passwordChangeRequired:boolean; + permanent:boolean; + timestampAccountCreation:Date; + timestampLastLogin:Date; + timestampLastPasswordChange:Date; + assignedRoles:string[]; + readOnly:boolean; + userManagerId:string; + validationToken:string; + language:string; +} diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/login/login.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/login/login.component.ts index 82c797bbd..5cdc7f5a6 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/login/login.component.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/login/login.component.ts @@ -74,4 +74,7 @@ export class LoginComponent implements OnInit { this.loginForm.reset(); this.authenticationService.login(customerData.userid, customerData.password, resultHandler); } + + + } diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts index 2060469b2..9d79733ba 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts @@ -16,49 +16,81 @@ * specific language governing permissions and limitations * under the License. */ -import { Injectable } from '@angular/core'; -import {HttpClient, HttpEvent, HttpResponse} from "@angular/common/http"; -import { environment } from "../../environments/environment"; +import {Injectable} from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {environment} from "../../environments/environment"; import {Observable} from "rxjs"; import {ErrorMessage} from "../model/error-message"; import {TranslateService} from "@ngx-translate/core"; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class ArchivaRequestService { - constructor(private http : HttpClient, private translator : TranslateService) { } + // Stores the access token locally + accessToken: string; - executeRestCall(type: string, module: string, service: string, input: object ) : Observable { - let modulePath = environment.application.servicePaths[module]; - let url = environment.application.baseUrl + environment.application.restPath + "/" + modulePath + "/" + service; - let token = localStorage.getItem("access_token") - let headers = null; - if (token != null) { - headers = { - "Authorization": "Bearer " + localStorage.getItem("access_token") - } - } else { - headers = {}; + constructor(private http: HttpClient, private translator: TranslateService) { } - if (type == "get") { - return this.http.get(url, {"headers":headers}); - } else if ( type == "post") { - return this.http.post(url, input, {"headers":headers}); + + /** + * Executes a rest call to the archiva / redback REST services. + * @param type the type of the call (get, post, update) + * @param module the module (archiva, redback) + * @param service the REST service to call + * @param input the input data, if this is a POST or UPDATE request + */ + executeRestCall(type: string, module: string, service: string, input: object): Observable { + let modulePath = environment.application.servicePaths[module]; + let url = environment.application.baseUrl + environment.application.restPath + "/" + modulePath + "/" + service; + let token = this.getToken(); + let headers = null; + if (token != null) { + headers = { + "Authorization": "Bearer " + token + } + } else { + headers = {}; + } + if (type == "get") { + return this.http.get(url, {"headers": headers}); + } else if (type == "post") { + return this.http.post(url, input, {"headers": headers}); + } } - } + public resetToken() { + this.accessToken = null; + } + + private getToken(): string { + if (this.accessToken != null) { + return this.accessToken; + } else { + let token = localStorage.getItem("access_token"); + if (token != null && token != "") { + this.accessToken = token; + return token; + } else { + return null; + } + } + } - translateError(errorMsg : ErrorMessage) : string { - if (errorMsg.errorKey!=null && errorMsg.errorKey!='') { - let parms = {}; - if (errorMsg.args!=null && errorMsg.args.length>0) { - for ( let i=0; i 0) { + for (let i = 0; i < errorMsg.args.length; i++) { + parms['arg' + i] = errorMsg.args[i]; + } + } + return this.translator.instant('api.' + errorMsg.errorKey, parms); } - } - return this.translator.instant('api.'+errorMsg.errorKey, parms); } - } } diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/authentication.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/authentication.service.ts index c062b953e..ba4cdb156 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/authentication.service.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/authentication.service.ts @@ -16,61 +16,117 @@ * specific language governing permissions and limitations * under the License. */ -import { Injectable } from '@angular/core'; +import {EventEmitter, Injectable} from '@angular/core'; import {ArchivaRequestService} from "./archiva-request.service"; import {AccessToken} from "../model/access-token"; -import { environment } from "../../environments/environment"; +import {environment} from "../../environments/environment"; import {ErrorMessage} from "../model/error-message"; import {ErrorResult} from "../model/error-result"; import {HttpErrorResponse} from "@angular/common/http"; +import {UserService} from "./user.service"; +import {UserInfo} from "../model/user-info"; +/** + * The AuthenticationService handles user authentication and stores user data after successful login + */ @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class AuthenticationService { + loggedIn: boolean; + + /** + * The LoginEvent is emitted, when a successful login happened. And the corresponding user info was retrieved. + */ + public LoginEvent: EventEmitter = new EventEmitter(); + + + constructor(private rest: ArchivaRequestService, + private userService: UserService) { + this.loggedIn = false; + this.restoreLoginData(); + } + + private restoreLoginData() { + let accessToken = localStorage.getItem("access_token"); + if (accessToken != null) { + let expirationDate = localStorage.getItem("token_expire"); + if (expirationDate != null) { + let expDate = new Date(expirationDate); + let currentDate = new Date(); + if (currentDate < expDate) { + this.loggedIn = true + let observer = this.userService.retrieveUserInfo(); + observer.subscribe(userInfo => + this.LoginEvent.emit(userInfo) + ); + } + } + } + + + } - constructor(private rest: ArchivaRequestService) { } - login(userid:string, password:string, resultHandler: (n: string, err?: ErrorMessage[]) => void) { + /** + * Tries to login by sending the login data to the REST service. If the login was successful the access + * and refresh token is stored locally. + * + * @param userid The user id for the login + * @param password The password + * @param resultHandler A result handler that is executed, after calling the login service + */ + login(userid: string, password: string, resultHandler: (n: string, err?: ErrorMessage[]) => void) { - const data = { 'grant_type':'authorization_code', - 'client_id':environment.application.client_id, - 'user_id':userid, 'password':password - }; - let authObserver = this.rest.executeRestCall('post','redback', 'auth/authenticate', data ); - let tokenObserver = { - next: (x: AccessToken) => { - localStorage.setItem("access_token", x.access_token); - localStorage.setItem("refresh_token", x.refresh_token); - if (x.expires_in!=null) { - let dt = new Date(); - dt.setSeconds(dt.getSeconds() + x.expires_in); - localStorage.setItem("token_expire", dt.toISOString()); - } - resultHandler("OK"); - }, - error: ( err: HttpErrorResponse) => { - console.log("Error " + (JSON.stringify(err))); - let result = err.error as ErrorResult - if (result.errorMessages!=null) { - for (let msg of result.errorMessages) { - console.error('Observer got an error: ' + msg.errorKey) - } - resultHandler("ERROR", result.errorMessages); - } else { - resultHandler("ERROR", null); - } + const data = { + 'grant_type': 'authorization_code', + 'client_id': environment.application.client_id, + 'user_id': userid, 'password': password + }; + let authObserver = this.rest.executeRestCall('post', 'redback', 'auth/authenticate', data); + let tokenObserver = { + next: (x: AccessToken) => { + localStorage.setItem("access_token", x.access_token); + localStorage.setItem("refresh_token", x.refresh_token); + if (x.expires_in != null) { + let dt = new Date(); + dt.setSeconds(dt.getSeconds() + x.expires_in); + localStorage.setItem("token_expire", dt.toISOString()); + } + let userObserver = this.userService.retrieveUserInfo(); + this.loggedIn = true; + userObserver.subscribe(userInfo => + this.LoginEvent.emit(userInfo)); + resultHandler("OK"); + }, + error: (err: HttpErrorResponse) => { + console.log("Error " + (JSON.stringify(err))); + let result = err.error as ErrorResult + if (result.errorMessages != null) { + for (let msg of result.errorMessages) { + console.error('Observer got an error: ' + msg.errorKey) + } + resultHandler("ERROR", result.errorMessages); + } else { + resultHandler("ERROR", null); + } - }, - // complete: () => console.log('Observer got a complete notification'), - }; - authObserver.subscribe(tokenObserver) + }, + // complete: () => console.log('Observer got a complete notification'), + }; + authObserver.subscribe(tokenObserver) - } + } - logout() { - localStorage.removeItem("access_token"); - localStorage.removeItem("refresh_token"); - localStorage.removeItem("token_expire"); - } + /** + * Resets the stored user data + */ + logout() { + localStorage.removeItem("access_token"); + localStorage.removeItem("refresh_token"); + localStorage.removeItem("token_expire"); + this.loggedIn = false; + this.userService.resetUser(); + this.rest.resetToken(); + } } diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.spec.ts new file mode 100644 index 000000000..e21a65193 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.spec.ts @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts new file mode 100644 index 000000000..3360e5524 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {Injectable} from '@angular/core'; +import {ArchivaRequestService} from "./archiva-request.service"; +import {UserInfo} from '../model/user-info'; +import {HttpErrorResponse} from "@angular/common/http"; +import {ErrorResult} from "../model/error-result"; +import {Observable} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + userInfo: UserInfo; + + constructor(private rest: ArchivaRequestService) { + this.userInfo = new UserInfo() + this.loadPersistedUserInfo(); + } + + /** + * Retrieves the user information from the REST service for the current logged in user. + * This works only, if a valid access token is present. + * It returns a observable that can be subscribed to catch the user information. + */ + public retrieveUserInfo(): Observable { + return new Observable((resultObserver) => { + let accessToken = localStorage.getItem("access_token"); + + if (accessToken != null) { + let infoObserver = this.rest.executeRestCall("get", "redback", "users/me", null); + let userInfoObserver = { + next: (x: UserInfo) => { + this.userInfo = x; + if (this.userInfo.language == null) { + this.loadPersistedUserInfo(); + } + this.persistUserInfo(); + resultObserver.next(this.userInfo); + }, + error: (err: HttpErrorResponse) => { + console.log("Error " + (JSON.stringify(err))); + let result = err.error as ErrorResult + if (result.errorMessages != null) { + for (let msg of result.errorMessages) { + console.error('Observer got an error: ' + msg.errorKey) + } + } + resultObserver.error(); + }, + complete: () => { + resultObserver.complete(); + } + }; + infoObserver.subscribe(userInfoObserver); + } + }); + } + + /** + * Stores user information persistent. Not the complete UserInfo object, only properties, that + * are needed. + */ + public persistUserInfo() { + if (this.userInfo != null && this.userInfo.user_id != null && this.userInfo.user_id != "") { + let prefix = "user." + this.userInfo.user_id; + localStorage.setItem(prefix + ".user_id", this.userInfo.user_id); + localStorage.setItem(prefix + ".id", this.userInfo.id); + if (this.userInfo.language != null && this.userInfo.language != "") { + localStorage.setItem(prefix + ".language", this.userInfo.language); + } + } + } + + /** + * Loads the persisted user info from the local storage + */ + public loadPersistedUserInfo() { + if (this.userInfo.user_id != null && this.userInfo.user_id != "") { + let prefix = "user." + this.userInfo.user_id; + this.userInfo.language = localStorage.getItem(prefix + ".language"); + } + } + + /** + * Resets the user info to default values. + */ + resetUser() { + this.userInfo = new UserInfo(); + } + +} diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/de.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/de.json index c98ece682..12eec0c37 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/de.json +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/de.json @@ -11,6 +11,7 @@ "menu": { "home": "Home", "login": "Anmelden", + "logout": "Abmelden", "about": "Über", "contact": "Kontakt" }, diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json index 418bc205e..68494c40c 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json @@ -11,6 +11,7 @@ "menu": { "home": "Home", "login": "Login", + "logout": "Logout", "about": "About", "contact": "Contact" }, -- 2.39.5