Additional angular code

This commit is contained in:
Martin Stockhammer 2020-11-03 00:31:44 +01:00
parent 108b60a4df
commit fb2a7fd643
12 changed files with 477 additions and 139 deletions

View File

@ -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 }
];

View File

@ -16,70 +16,84 @@
~ specific language governing permissions and limitations
~ under the License.
-->
<div class="app d-flex flex-column" >
<header>
<nav class="navbar navbar-expand-md fixed-top navbar-light " style="background-color: #c6cbd2;">
<a class="navbar-brand" routerLink="/">
<img src="../assets/params/images/archiva_logo_40h.png" alt="ARCHIVA">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsDefault"
aria-controls="navbarsDefault" aria-expanded="false" aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsDefault">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" routerLink="/">
<i class="fas fa-home mr-1"></i>{{ 'menu.home' |translate }}
<div class="app d-flex flex-column">
<header>
<nav class="navbar navbar-expand-md fixed-top navbar-light " style="background-color: #c6cbd2;">
<a class="navbar-brand" routerLink="/">
<img src="../assets/params/images/archiva_logo_40h.png" alt="ARCHIVA">
</a>
</li>
<li class="nav-item active">
<a class="nav-link" routerLink="/login" data-toggle="modal" data-target="#loginModal">
<i class="fas fa-user mr-1"></i>{{'menu.login' | translate}}
</a>
</li>
<li class="nav-item active dropdown">
<a class="nav-link dropdown-toggle" id="dropdown09" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="flag-icon {{langIcon()}}"></span></a>
<div class="dropdown-menu" aria-labelledby="dropdown09">
<a class="dropdown-item" href="#en" (click)="switchLang('en')"><span class="flag-icon flag-icon-gb"> </span> English</a>
<a class="dropdown-item" href="#de" (click)="switchLang('de')"><span class="flag-icon flag-icon-de"> </span> German</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsDefault"
aria-controls="navbarsDefault" aria-expanded="false" aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsDefault">
<div class="navbar-nav ml-auto">
<span *ngIf="auth.loggedIn" class="navbar-text border-right pr-2 mr-2">
{{user.userInfo.fullName}}
</span>
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" routerLink="/">
<i class="fas fa-home mr-1"></i>{{ 'menu.home' |translate }}
</a>
</li>
<li *ngIf="!auth.loggedIn" class="nav-item active">
<a class="nav-link" routerLink="/login" data-toggle="modal" data-target="#loginModal">
<i class="fas fa-user mr-1"></i>{{'menu.login' | translate}}
</a>
</li>
<li *ngIf="auth.loggedIn" class="nav-item active">
<a class="nav-link" routerLink="/logout" (click)="auth.logout()">
<i class="fas fa-user mr-1"></i>{{'menu.logout' | translate}}
</a>
</li>
<li class="nav-item active dropdown">
<a class="nav-link dropdown-toggle" id="dropdown09" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><span class="flag-icon {{langIcon()}}"></span></a>
<div class="dropdown-menu" aria-labelledby="dropdown09">
<a class="dropdown-item" href="#en" (click)="switchLang('en')"><span
class="flag-icon flag-icon-gb"> </span> English</a>
<a class="dropdown-item" href="#de" (click)="switchLang('de')"><span
class="flag-icon flag-icon-de"> </span> German</a>
</div>
</li>
<li class="nav-item active">
<a class="nav-link" routerLink="/about">
<i class="far fa-question-circle mr-1"></i>{{'menu.about' | translate}}
</a>
</li>
<li class="nav-item active">
<a class="nav-link" routerLink="/contact">
<i class="fas fa-envelope mr-1"></i>{{ 'menu.contact' | translate }}
</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="https://github.com/apache/archiva">
<i class="fab fa-github mr-1"></i>
</a>
</li>
</ul>
</div>
</div>
</li>
<li class="nav-item active">
<a class="nav-link" routerLink="/about">
<i class="far fa-question-circle mr-1"></i>{{'menu.about' | translate}}
</a>
</li>
<li class="nav-item active">
<a class="nav-link" routerLink="/contact">
<i class="fas fa-envelope mr-1"></i>{{ 'menu.contact' | translate }}
</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="https://github.com/apache/archiva">
<i class="fab fa-github mr-1"></i>
</a>
</li>
</ul>
</div>
</nav>
</header>
</nav>
</header>
<main class="container-fluid flex-fill">
<div >
<router-outlet></router-outlet>
</div>
</main>
<hr />
<footer class="container-fluid">
<div class="container">
<div class="row">
<div class="col-12">
<p class="text-center text-black-50">&copy; 2020 - archiva.apache.org</p>
<main class="container-fluid flex-fill">
<div>
<router-outlet></router-outlet>
</div>
</div>
</div>
</footer>
</main>
<hr/>
<footer class="container-fluid">
<div class="container">
<div class="row">
<div class="col-12">
<p class="text-center text-black-50">&copy; 2020 - archiva.apache.org</p>
</div>
</div>
</div>
</footer>
</div>

View File

@ -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);
}
})
}
}

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -74,4 +74,7 @@ export class LoginComponent implements OnInit {
this.loginForm.reset();
this.authenticationService.login(customerData.userid, customerData.password, resultHandler);
}
}

View File

@ -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<R>(type: string, module: string, service: string, input: object ) : Observable<R> {
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<R>(url, {"headers":headers});
} else if ( type == "post") {
return this.http.post<R>(url, input, {"headers":headers});
}
}
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<errorMsg.args.length; i++) {
parms['arg' + i] = errorMsg.args[i];
/**
* 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<R>(type: string, module: string, service: string, input: object): Observable<R> {
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<R>(url, {"headers": headers});
} else if (type == "post") {
return this.http.post<R>(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;
}
}
}
/**
* Translates a given error message to the current set language.
* @param errorMsg the errorMsg as returned by a REST call
*/
public 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 < 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);
}
}
}

View File

@ -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;
constructor(private rest: ArchivaRequestService) { }
/**
* The LoginEvent is emitted, when a successful login happened. And the corresponding user info was retrieved.
*/
public LoginEvent: EventEmitter<UserInfo> = new EventEmitter<UserInfo>();
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<AccessToken>('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);
}
constructor(private rest: ArchivaRequestService,
private userService: UserService) {
this.loggedIn = false;
this.restoreLoginData();
}
},
// complete: () => console.log('Observer got a complete notification'),
};
authObserver.subscribe(tokenObserver)
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)
);
}
}
}
}
logout() {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("token_expire");
}
}
/**
* 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<AccessToken>('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)
}
/**
* 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();
}
}

View File

@ -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();
});
});

View File

@ -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<UserInfo> {
return new Observable<UserInfo>((resultObserver) => {
let accessToken = localStorage.getItem("access_token");
if (accessToken != null) {
let infoObserver = this.rest.executeRestCall<UserInfo>("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();
}
}

View File

@ -11,6 +11,7 @@
"menu": {
"home": "Home",
"login": "Anmelden",
"logout": "Abmelden",
"about": "Über",
"contact": "Kontakt"
},

View File

@ -11,6 +11,7 @@
"menu": {
"home": "Home",
"login": "Login",
"logout": "Logout",
"about": "About",
"contact": "Contact"
},