add auth login page and related functionality
1. Created a login page to create a JWT with the backend 2. Store the token locally so it can be reused between runs 3. Redirect to login on no auth 4. Redirect from login when already authenticated 5. Add a login / logout link 6. Clean up empty .css files from the tree TODO: the JWT needs to generate a refresh key yet Change-Id: I97b6f92e4ca897768c91d7816e2ef44dcd9d3acf
This commit is contained in:
parent
f59f2bc6eb
commit
b7260326eb
1
client/package.json
Normal file → Executable file
1
client/package.json
Normal file → Executable file
@ -21,6 +21,7 @@
|
||||
"@angular/platform-browser": "~10.0.3",
|
||||
"@angular/platform-browser-dynamic": "~10.0.3",
|
||||
"@angular/router": "~10.0.3",
|
||||
"@auth0/angular-jwt": "^5.0.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"ngx-monaco-editor": "^9.0.0",
|
||||
"ngx-toastr": "^13.0.0",
|
||||
|
@ -2,21 +2,27 @@ import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {HomeComponent} from './home/home.component';
|
||||
import {CtlComponent} from './ctl/ctl.component';
|
||||
|
||||
import {LoginComponent} from './login/login.component';
|
||||
import {AuthGuard} from 'src/services/auth-guard/auth-guard.service';
|
||||
|
||||
const routes: Routes = [{
|
||||
path: 'ctl',
|
||||
component: CtlComponent,
|
||||
canActivate: [AuthGuard],
|
||||
loadChildren: './ctl/ctl.module#CtlModule',
|
||||
}, {
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
component: HomeComponent
|
||||
}, {
|
||||
path: 'login',
|
||||
canActivate: [AuthGuard],
|
||||
component: LoginComponent
|
||||
}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
|
||||
}
|
||||
export class AppRoutingModule {}
|
||||
|
@ -57,7 +57,7 @@
|
||||
<mat-toolbar color="primary" class="toolbar-header">
|
||||
<button mat-icon-button (click)="sidenav.toggle()"><mat-icon svgIcon="list"></mat-icon></button>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button><mat-icon svgIcon="account"></mat-icon></button>
|
||||
<button mat-icon-button (click)="authToggle()" id="loginButton">Login</button>
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
<span class="page-body"></span>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { environment } from '../environments/environment';
|
||||
import { IconService } from '../services/icon/icon.service';
|
||||
import { WebsocketService } from '../services/websocket/websocket.service';
|
||||
import { Log } from '../services/log/log.service';
|
||||
import { LogMessage } from '../services/log/log-message';
|
||||
import { Dashboard, WSReceiver, WebsocketMessage } from '../services/websocket/websocket.models';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { IconService } from 'src/services/icon/icon.service';
|
||||
import { WebsocketService } from 'src/services/websocket/websocket.service';
|
||||
import { Log } from 'src/services/log/log.service';
|
||||
import { LogMessage } from 'src/services/log/log-message';
|
||||
import { Dashboard, WSReceiver, WebsocketMessage } from 'src/services/websocket/websocket.models';
|
||||
import { Nav } from './app.models';
|
||||
import { AuthGuard } from 'src/services/auth-guard/auth-guard.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -60,6 +61,15 @@ export class AppComponent implements OnInit, WSReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
public authToggle(): void {
|
||||
const button = document.getElementById('loginButton');
|
||||
|
||||
if (button.innerText === 'Logout') {
|
||||
AuthGuard.logout();
|
||||
button.innerText = 'Login';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.iconService.registerIcons();
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import { LogMessage } from '../../../services/log/log-message';
|
||||
@Component({
|
||||
selector: 'app-bare-metal',
|
||||
templateUrl: './baremetal.component.html',
|
||||
styleUrls: ['./baremetal.component.css']
|
||||
})
|
||||
|
||||
export class BaremetalComponent implements WSReceiver {
|
||||
|
@ -2,12 +2,15 @@ import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {DocumentComponent} from './document/document.component';
|
||||
import {BaremetalComponent} from './baremetal/baremetal.component';
|
||||
import {AuthGuard} from 'src/services/auth-guard/auth-guard.service';
|
||||
|
||||
const routes: Routes = [{
|
||||
path: 'documents',
|
||||
canActivate: [AuthGuard],
|
||||
component: DocumentComponent,
|
||||
}, {
|
||||
path: 'baremetal',
|
||||
canActivate: [AuthGuard],
|
||||
component: BaremetalComponent
|
||||
}];
|
||||
|
||||
|
@ -3,7 +3,6 @@ import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-ctl',
|
||||
templateUrl: './ctl.component.html',
|
||||
styleUrls: ['./ctl.component.css']
|
||||
})
|
||||
export class CtlComponent {
|
||||
}
|
||||
|
@ -3,6 +3,5 @@ import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.css']
|
||||
})
|
||||
export class HomeComponent {}
|
||||
|
69
client/src/app/login/login.component.css
Executable file
69
client/src/app/login/login.component.css
Executable file
@ -0,0 +1,69 @@
|
||||
/* Bordered form */
|
||||
form {
|
||||
border: 3px solid #f1f1f1;
|
||||
}
|
||||
|
||||
/* Full-width inputs */
|
||||
input[type=text], input[type=password] {
|
||||
width: 300px;
|
||||
padding: 12px 20px;
|
||||
margin: 8px 0;
|
||||
display: inline-block;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Set a style for all buttons */
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 14px 20px;
|
||||
margin: 8px 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
/* Add a hover effect for buttons */
|
||||
button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Extra style for the cancel button (red) */
|
||||
.cancelbtn {
|
||||
width: auto;
|
||||
padding: 10px 18px;
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
/* Add padding to containers */
|
||||
.container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* add border & center the table */
|
||||
.table {
|
||||
border-spacing: 10px;
|
||||
border:1px gray solid;
|
||||
border-radius: 5px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* The "Forgot password" text */
|
||||
span.psw {
|
||||
float: right;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
/* Change styles for span and cancel button on extra small screens */
|
||||
@media screen and (max-width: 300px) {
|
||||
span.psw {
|
||||
display: block;
|
||||
float: none;
|
||||
}
|
||||
.cancelbtn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
29
client/src/app/login/login.component.html
Executable file
29
client/src/app/login/login.component.html
Executable file
@ -0,0 +1,29 @@
|
||||
<div class="container">
|
||||
<p></p>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="uname"><b>Username</b></label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="userName" placeholder="Enter Username" name="uname" #id required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="psw"><b>Password</b></label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" id="passwd" placeholder="Enter Password" name="psw" #passwd required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td style="text-align:right">
|
||||
<button type="submit" id="loginSubmit" (click)="formSubmit(id.value,passwd.value)">Login</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
30
client/src/app/login/login.component.spec.ts
Executable file
30
client/src/app/login/login.component.spec.ts
Executable file
@ -0,0 +1,30 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {LoginComponent} from './login.component';
|
||||
import {ToastrModule} from 'ngx-toastr';
|
||||
|
||||
describe('CtlComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ToastrModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([]),
|
||||
],
|
||||
declarations: [LoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
40
client/src/app/login/login.component.ts
Executable file
40
client/src/app/login/login.component.ts
Executable file
@ -0,0 +1,40 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {WebsocketService} from 'src/services/websocket/websocket.service';
|
||||
import { WSReceiver, WebsocketMessage, Authentication } from 'src/services/websocket/websocket.models';
|
||||
|
||||
@Component({
|
||||
styleUrls: ['login.component.css'],
|
||||
templateUrl: 'login.component.html',
|
||||
})
|
||||
|
||||
export class LoginComponent implements WSReceiver, OnInit {
|
||||
className = this.constructor.name;
|
||||
type = 'ui'; // needed to have the websocket service in the constructor
|
||||
component = 'auth'; // needed to have the websocket service in the constructor
|
||||
|
||||
constructor(private websocketService: WebsocketService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// bind the enter key to the submit button on the page
|
||||
document.getElementById('passwd')
|
||||
.addEventListener('keyup', (event) => {
|
||||
event.preventDefault();
|
||||
if (event.key === 'Enter') {
|
||||
document.getElementById('loginSubmit').click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This will always throw an error but should never be called because we did not register a receiver
|
||||
// The auth guard will take care of the auth messages since it's dealing with the tokens
|
||||
receiver(message: WebsocketMessage): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// formSubmit sends the auth request to the backend
|
||||
public formSubmit(id, passwd): void {
|
||||
const message = new WebsocketMessage(this.type, this.component, 'authenticate');
|
||||
message.authentication = new Authentication(id, passwd);
|
||||
this.websocketService.sendMessage(message);
|
||||
}
|
||||
}
|
14
client/src/app/login/login.module.ts
Executable file
14
client/src/app/login/login.module.ts
Executable file
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { LoginComponent } from './login.component';
|
||||
import {ToastrModule} from 'ngx-toastr';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ToastrModule
|
||||
],
|
||||
declarations: [
|
||||
LoginComponent,
|
||||
]
|
||||
})
|
||||
|
||||
export class LoginModule { }
|
23
client/src/services/auth-guard/auth-guard.service.spec.ts
Executable file
23
client/src/services/auth-guard/auth-guard.service.spec.ts
Executable file
@ -0,0 +1,23 @@
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { AuthGuard } from './auth-guard.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import {ToastrModule} from 'ngx-toastr';
|
||||
|
||||
describe('AuthGuardService', () => {
|
||||
let service: AuthGuard;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([]),
|
||||
ToastrModule.forRoot(),
|
||||
],
|
||||
declarations: []
|
||||
});
|
||||
service = TestBed.inject(AuthGuard);
|
||||
}));
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
196
client/src/services/auth-guard/auth-guard.service.ts
Executable file
196
client/src/services/auth-guard/auth-guard.service.ts
Executable file
@ -0,0 +1,196 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Router, CanActivate, Event as RouterEvent, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
|
||||
import { Log } from 'src/services/log/log.service';
|
||||
import { LogMessage } from 'src/services/log/log-message';
|
||||
import { WebsocketService } from 'src/services/websocket/websocket.service';
|
||||
import { WSReceiver, WebsocketMessage, Authentication } from 'src/services/websocket/websocket.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
|
||||
export class AuthGuard implements WSReceiver, CanActivate {
|
||||
// static router for those who may need it, I'm looking at your app components
|
||||
public static router: Router;
|
||||
|
||||
private className = this.constructor.name;
|
||||
private loading = false;
|
||||
private sendToLogin = false;
|
||||
type = 'ui';
|
||||
component = 'auth';
|
||||
|
||||
// Called by the logout link at the top right of the page
|
||||
public static logout(): void {
|
||||
// blank out the object storage so we can't get re authenticate
|
||||
WebsocketService.token = undefined;
|
||||
WebsocketService.tokenExpiration = 0;
|
||||
|
||||
// blank out the local storage so we can't get re authenticate
|
||||
localStorage.removeItem('airshipUI-token');
|
||||
|
||||
// best to begin at the beginning so send the user back to /login
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
constructor(private websocketService: WebsocketService, private router: Router) {
|
||||
// create a static router so other components can access it if needs be
|
||||
AuthGuard.router = router;
|
||||
|
||||
this.websocketService.registerFunctions(this);
|
||||
// listen to the evens that are sent out from the angular router so we don't wind up in an endless loop
|
||||
this.router.events.subscribe((e: RouterEvent) => {
|
||||
this.navigationInterceptor(e);
|
||||
});
|
||||
}
|
||||
|
||||
async receiver(message: WebsocketMessage): Promise<void> {
|
||||
if (message.hasOwnProperty('error')) {
|
||||
Log.Error(new LogMessage('Error received in AuthGuard', this.className, message));
|
||||
this.websocketService.printIfToast(message);
|
||||
AuthGuard.logout();
|
||||
} else {
|
||||
switch (message.subComponent) {
|
||||
case 'approved':
|
||||
Log.Debug(new LogMessage('Auth approved received', this.className, message));
|
||||
this.setToken(message.token);
|
||||
this.router.navigate(['/']);
|
||||
break;
|
||||
case 'denied':
|
||||
Log.Debug(new LogMessage('Auth denied received', this.className, message));
|
||||
AuthGuard.logout();
|
||||
break;
|
||||
default:
|
||||
Log.Debug(new LogMessage('Unknown auth message received', this.className, message));
|
||||
AuthGuard.logout();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this decides if you can show a page
|
||||
// TODO: maybe RBAC type of stuff may need to go here
|
||||
canActivate(): boolean {
|
||||
const location = window.location.pathname;
|
||||
const authenticated = this.isAuthenticated();
|
||||
|
||||
// redirect everything to /login if not authenticated
|
||||
if (!authenticated && location !== '/login/') {
|
||||
// TODO: store the reference url and redirect after login
|
||||
// let the loading function complete before sending to login otherwise the redirect fails
|
||||
if (this.loading) {
|
||||
this.sendToLogin = true;
|
||||
} else {
|
||||
// loading is complete just send to login
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// login page specific details
|
||||
// redirect /login to / if authenticated and landing on /login
|
||||
// TODO (aschiefe): not super happy about this setup, may need to simplify
|
||||
if (location === '/login/') {
|
||||
if (authenticated) {
|
||||
this.router.navigate(['/']);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// flip the link if we're in or out of the fold
|
||||
this.toggleAuthButton(authenticated);
|
||||
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
// flip the text of the login / logout button according to where we are in the world
|
||||
private toggleAuthButton(authenticated): void {
|
||||
const button = document.getElementById('loginButton');
|
||||
const text = button.innerText;
|
||||
if (authenticated && text === 'Login') {
|
||||
button.innerText = 'Logout';
|
||||
} else if (!authenticated && text === 'Logout') {
|
||||
button.innerText = 'Login';
|
||||
}
|
||||
}
|
||||
|
||||
// test the auth token to see if we can let the user see the page
|
||||
// TODO: maybe RBAC type of stuff may need to go here
|
||||
private isAuthenticated(): boolean {
|
||||
if (WebsocketService.token === undefined) { this.getStoredToken(); }
|
||||
try {
|
||||
let authenticated = false;
|
||||
// test for token expiration
|
||||
// if the token is null the date test will always return true
|
||||
if (WebsocketService.token !== undefined && WebsocketService.tokenExpiration > 0) {
|
||||
authenticated = WebsocketService.tokenExpiration >= new Date().getTime();
|
||||
}
|
||||
return authenticated;
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve the stored token & send it to the go backend for validation
|
||||
private getStoredToken(): void {
|
||||
const tokenString = localStorage.getItem('airshipUI-token');
|
||||
const token = JSON.parse(tokenString);
|
||||
if (token !== null) {
|
||||
if (token.hasOwnProperty('token')) {
|
||||
WebsocketService.token = token.token;
|
||||
}
|
||||
if (token.hasOwnProperty('date')) {
|
||||
WebsocketService.tokenExpiration = token.date;
|
||||
}
|
||||
|
||||
// even after all this it's possible to have nothing. I started with nothing and still have most of it left
|
||||
if (WebsocketService.token !== undefined) {
|
||||
this.validateToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the UI frontend is not the decider, the back end is. If this token is good we continue, if it's not we stop
|
||||
private validateToken(): void {
|
||||
const message = new WebsocketMessage(this.type, this.component, 'validate');
|
||||
message.token = WebsocketService.token;
|
||||
this.websocketService.sendMessage(message);
|
||||
}
|
||||
|
||||
// store the token locally so we can be authenticated between runs
|
||||
private setToken(token): void {
|
||||
// calculate 1 hour expiration
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (1 * 60 * 60 * 1000));
|
||||
|
||||
// set the token for auth check going forward
|
||||
WebsocketService.token = token;
|
||||
WebsocketService.tokenExpiration = date.getTime();
|
||||
|
||||
// set the token locally to have a login till browser exits
|
||||
const json = { date: WebsocketService.tokenExpiration, token: WebsocketService.token };
|
||||
localStorage.setItem('airshipUI-token', JSON.stringify(json));
|
||||
}
|
||||
|
||||
// detect navigation events in case we redirect from authguard which would happen too fast to protect /login and cause an endless loop
|
||||
// Random Shack Data Processing Dictionary: Endless Loop: n., see Loop, Endless. Loop, Endless: n., see Endless Loop
|
||||
private navigationInterceptor(event: RouterEvent): void {
|
||||
if (event instanceof NavigationStart) {
|
||||
this.loading = true;
|
||||
}
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.loading = false;
|
||||
if (this.sendToLogin) {
|
||||
this.router.navigate(['/login']);
|
||||
this.sendToLogin = false;
|
||||
}
|
||||
}
|
||||
if (event instanceof NavigationCancel) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (event instanceof NavigationError) {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,11 @@ export class LogMessage {
|
||||
// the holy trinity of the websocket messages, a triumvirate if you will, which is how all are routed
|
||||
message: string;
|
||||
className: string;
|
||||
wsMessage: WebsocketMessage;
|
||||
logMessage: string | WebsocketMessage;
|
||||
|
||||
constructor(message?: string | undefined, className?: string | undefined, wsMessage?: WebsocketMessage | undefined) {
|
||||
constructor(message?: string | undefined, className?: string | undefined, logMessage?: string | WebsocketMessage | undefined) {
|
||||
this.message = message;
|
||||
this.className = className;
|
||||
this.wsMessage = wsMessage;
|
||||
this.logMessage = logMessage;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Log } from './log.service';
|
||||
|
||||
describe('LogService', () => {
|
||||
|
@ -34,7 +34,7 @@ export class Log {
|
||||
if (level <= this.Level) {
|
||||
console.log(
|
||||
'[airshipui][' + LogLevel[level] + '] ' + new Date().toLocaleString() + ' - ' +
|
||||
message.className + ' - ' + message.message + ': ', message.wsMessage);
|
||||
message.className + ' - ' + message.message + ': ', message.logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export interface WSReceiver {
|
||||
receiver(message: WebsocketMessage): Promise<void>;
|
||||
}
|
||||
|
||||
// WebsocketMessage is the structure for the json that is used to talk to the backend
|
||||
export class WebsocketMessage {
|
||||
sessionID: string;
|
||||
type: string;
|
||||
@ -20,8 +21,10 @@ export class WebsocketMessage {
|
||||
id: string;
|
||||
isAuthenticated: boolean;
|
||||
message: string;
|
||||
token: string;
|
||||
data: JSON;
|
||||
yaml: string;
|
||||
authentication: Authentication;
|
||||
|
||||
// this constructor looks like this in case anyone decides they want just a raw message with no data predefined
|
||||
// or an easy way to specify the defaults
|
||||
@ -32,9 +35,21 @@ export class WebsocketMessage {
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard has the urls of the links that will pop out new dashboard tabs on the left hand side
|
||||
export class Dashboard {
|
||||
name: string;
|
||||
baseURL: string;
|
||||
path: string;
|
||||
isProxied: boolean;
|
||||
}
|
||||
|
||||
// AuthMessage is used to send and auth request and hold the token if it's authenticated
|
||||
export class Authentication {
|
||||
id: string;
|
||||
password: string;
|
||||
|
||||
constructor(id?: string | undefined, password?: string | undefined) {
|
||||
this.id = id;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {WebsocketMessage, WSReceiver} from './websocket.models';
|
||||
import {WebsocketMessage, WSReceiver, Authentication} from './websocket.models';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import 'reflect-metadata';
|
||||
|
||||
@ -8,6 +8,10 @@ import 'reflect-metadata';
|
||||
})
|
||||
|
||||
export class WebsocketService implements OnDestroy {
|
||||
// to avoid circular includes this has to go here
|
||||
public static token: string;
|
||||
public static tokenExpiration: number;
|
||||
|
||||
private ws: WebSocket;
|
||||
private timeout: any;
|
||||
private sessionID: string;
|
||||
@ -39,11 +43,14 @@ export class WebsocketService implements OnDestroy {
|
||||
try {
|
||||
message.sessionID = this.sessionID;
|
||||
message.timestamp = new Date().getTime();
|
||||
if (WebsocketService.token !== undefined) { message.token = WebsocketService.token; }
|
||||
// TODO (aschiefe): determine if this debug statement is a good thing (tm)
|
||||
// Log.Debug(new LogMessage('Sending WebSocket Message', this.className, message));
|
||||
this.ws.send(JSON.stringify(message));
|
||||
} catch (err) {
|
||||
// on a refresh it may fire a request before the backend is ready so give it ye'ol retry
|
||||
// TODO (aschiefe): determine if there's a limit on retries
|
||||
return new Promise( resolve => setTimeout(() => { this.sendMessage(message); }, 100));
|
||||
return new Promise(() => setTimeout(() => { this.sendMessage(message); }, 100));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,6 +262,13 @@
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@auth0/angular-jwt@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@auth0/angular-jwt/-/angular-jwt-5.0.1.tgz#37851d3ca2a0e88b3e673afd7dd2891f0c61bdf5"
|
||||
integrity sha512-djllMh6rthPscEj5n5T9zF223q8t+sDqnUuAYTJjdKoHvMAzYwwi2yP67HbojqjODG4ZLFAcPtRuzGgp+r7nDQ==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.8.3":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
|
||||
|
3
go.mod
3
go.mod
@ -3,13 +3,12 @@ module opendev.org/airship/airshipui
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect
|
||||
opendev.org/airship/airshipctl v0.0.0-20200812155702-f61953bcf558
|
||||
sigs.k8s.io/kustomize/api v0.5.1
|
||||
)
|
||||
|
10
go.sum
10
go.sum
@ -1094,9 +1094,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1164,9 +1163,8 @@ golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1230,10 +1228,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -16,6 +16,8 @@ package configs
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -36,9 +38,10 @@ var (
|
||||
|
||||
// Config basic structure to hold configuration params for Airship UI
|
||||
type Config struct {
|
||||
WebService *WebService `json:"webservice,omitempty"`
|
||||
AuthMethod *AuthMethod `json:"authMethod,omitempty"`
|
||||
Dashboards []Dashboard `json:"dashboards,omitempty"`
|
||||
WebService *WebService `json:"webservice,omitempty"`
|
||||
AuthMethod *AuthMethod `json:"authMethod,omitempty"`
|
||||
Dashboards []Dashboard `json:"dashboards,omitempty"`
|
||||
Users map[string]string `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
// AuthMethod structure to hold authentication parameters
|
||||
@ -56,6 +59,12 @@ type WebService struct {
|
||||
PrivateKey string `json:"privateKey,omitempty"`
|
||||
}
|
||||
|
||||
// Authentication structure to hold authentication parameters
|
||||
type Authentication struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// Dashboard structure
|
||||
type Dashboard struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
@ -86,15 +95,24 @@ const (
|
||||
CTLConfig WsComponentType = "config"
|
||||
Baremetal WsComponentType = "baremetal"
|
||||
Document WsComponentType = "document"
|
||||
Auth WsComponentType = "auth"
|
||||
|
||||
SetContext WsSubComponentType = "context"
|
||||
SetCluster WsSubComponentType = "cluster"
|
||||
SetCredential WsSubComponentType = "credential"
|
||||
// auth sub components
|
||||
Approved WsSubComponentType = "approved"
|
||||
Authenticate WsSubComponentType = "authenticate"
|
||||
Denied WsSubComponentType = "denied"
|
||||
Refresh WsSubComponentType = "refresh"
|
||||
Validate WsSubComponentType = "validate"
|
||||
|
||||
// ctl components
|
||||
GetDefaults WsSubComponentType = "getDefaults"
|
||||
GenerateISO WsSubComponentType = "generateISO"
|
||||
DocPull WsSubComponentType = "docPull"
|
||||
Yaml WsSubComponentType = "yaml"
|
||||
YamlWrite WsSubComponentType = "yamlWrite"
|
||||
GetYaml WsSubComponentType = "getYaml"
|
||||
GetSource WsSubComponentType = "getSource"
|
||||
GetRendered WsSubComponentType = "getRendered"
|
||||
GetPhaseTree WsSubComponentType = "getPhaseTree"
|
||||
GetPhaseSourceFiles WsSubComponentType = "getPhaseSource"
|
||||
GetPhaseDocuments WsSubComponentType = "getPhaseDocs"
|
||||
@ -118,10 +136,14 @@ type WsMessage struct {
|
||||
YAML string `json:"yaml,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Token *string `json:"token,omitempty"`
|
||||
|
||||
// used for auth
|
||||
Authentication *Authentication `json:"authentication,omitempty"`
|
||||
|
||||
// information related to the init of the UI
|
||||
Dashboards []Dashboard `json:"dashboards,omitempty"`
|
||||
Authentication *AuthMethod `json:"authentication,omitempty"`
|
||||
AuthMethod *AuthMethod `json:"authMethod,omitempty"`
|
||||
AuthInfoOptions *config.AuthInfoOptions `json:"authInfoOptions,omitempty"`
|
||||
ContextOptions *config.ContextOptions `json:"contextOptions,omitempty"`
|
||||
ClusterOptions *config.ClusterOptions `json:"clusterOptions,omitempty"`
|
||||
@ -151,7 +173,9 @@ func SetUIConfig() error {
|
||||
}
|
||||
|
||||
func checkConfigs() error {
|
||||
writeFile := false
|
||||
if UIConfig.WebService == nil {
|
||||
writeFile = true
|
||||
log.Debug("No UI config found, generating ssl keys & host & port info")
|
||||
err := setEtcDir()
|
||||
if err != nil {
|
||||
@ -176,16 +200,32 @@ func checkConfigs() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(UIConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(UIConfigFile, bytes, 0440)
|
||||
}
|
||||
if UIConfig.Users == nil {
|
||||
writeFile = true
|
||||
err := createDefaultUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if writeFile {
|
||||
bytes, err := json.Marshal(UIConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(UIConfigFile, bytes, 0600)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDefaultUser() error {
|
||||
hash := sha512.New()
|
||||
_, err := hash.Write([]byte("admin"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
UIConfig.Users = map[string]string{"admin": hex.EncodeToString(hash.Sum(nil))}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
124
pkg/webservice/auth.go
Executable file
124
pkg/webservice/auth.go
Executable file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
Licensed 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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package webservice
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"opendev.org/airship/airshipui/pkg/configs"
|
||||
"opendev.org/airship/airshipui/pkg/log"
|
||||
)
|
||||
|
||||
// Create the JWT key used to create the signature
|
||||
// TODO: use a private key for this instead of a phrase
|
||||
var jwtKey = []byte("airshipUI_JWT_key")
|
||||
|
||||
// The UI will either request authentication or validation, handle those situations here
|
||||
func handleAuth(request configs.WsMessage) configs.WsMessage {
|
||||
response := configs.WsMessage{
|
||||
Type: configs.UI,
|
||||
Component: configs.Auth,
|
||||
}
|
||||
|
||||
var err error
|
||||
switch request.SubComponent {
|
||||
case configs.Authenticate:
|
||||
if request.Authentication != nil {
|
||||
var token *string
|
||||
authRequest := request.Authentication
|
||||
token, err = createToken(authRequest.ID, authRequest.Password)
|
||||
sessions[request.SessionID].jwt = *token
|
||||
response.SubComponent = configs.Approved
|
||||
response.Token = token
|
||||
} else {
|
||||
err = errors.New("No AuthRequest found in the request")
|
||||
}
|
||||
case configs.Validate:
|
||||
if request.Token != nil {
|
||||
err = validateToken(*request.Token)
|
||||
response.SubComponent = configs.Approved
|
||||
response.Token = request.Token
|
||||
} else {
|
||||
err = errors.New("No token found in the request")
|
||||
}
|
||||
default:
|
||||
err = errors.New("Invalid authentication request")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
response.Error = err.Error()
|
||||
response.SubComponent = configs.Denied
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// validate JWT (JSON Web Token)
|
||||
func validateToken(tokenString string) error {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return nil
|
||||
}
|
||||
return errors.New("Invalid JWT Token")
|
||||
}
|
||||
|
||||
// create a JWT (JSON Web Token)
|
||||
// TODO (aschiefe): for demo purposes, this is not to be used in production
|
||||
func createToken(id string, passwd string) (*string, error) {
|
||||
origPasswdHash, ok := configs.UIConfig.Users[id]
|
||||
if !ok {
|
||||
return nil, errors.New("Not authenticated")
|
||||
}
|
||||
|
||||
// test the password to make sure it's valid
|
||||
hash := sha512.New()
|
||||
_, err := hash.Write([]byte(passwd))
|
||||
if err != nil {
|
||||
return nil, errors.New("Error authenticating")
|
||||
}
|
||||
if origPasswdHash != hex.EncodeToString(hash.Sum(nil)) {
|
||||
return nil, errors.New("Not authenticated")
|
||||
}
|
||||
|
||||
// set some claims
|
||||
claims := make(jwt.MapClaims)
|
||||
claims["username"] = id
|
||||
claims["password"] = passwd
|
||||
claims["exp"] = time.Now().Add(time.Hour * 1).Unix()
|
||||
|
||||
// create the token
|
||||
jwtClaim := jwt.New(jwt.SigningMethodHS256)
|
||||
jwtClaim.Claims = claims
|
||||
|
||||
// Sign and get the complete encoded token as string
|
||||
token, err := jwtClaim.SignedString(jwtKey)
|
||||
return &token, err
|
||||
}
|
@ -54,27 +54,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// handle an auth complete attempt
|
||||
func handleAuth(http.ResponseWriter, *http.Request) {
|
||||
// TODO: handle the response body to capture the credentials
|
||||
err := WebSocketSend(configs.WsMessage{
|
||||
Type: configs.UI,
|
||||
Component: configs.Authcomplete,
|
||||
})
|
||||
|
||||
// error sending the websocket request
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// WebServer will run the handler functions for WebSockets
|
||||
func WebServer() {
|
||||
webServerMux := http.NewServeMux()
|
||||
|
||||
// some things may need a redirect so we'll give them a url to do that with
|
||||
webServerMux.HandleFunc("/auth", handleAuth)
|
||||
|
||||
// hand off the websocket upgrade over http
|
||||
webServerMux.HandleFunc("/ws", onOpen)
|
||||
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
// session is a struct to hold information about a given session
|
||||
type session struct {
|
||||
id string
|
||||
jwt string
|
||||
writeMutex sync.Mutex
|
||||
ws *websocket.Conn
|
||||
}
|
||||
@ -49,6 +50,7 @@ var upgrader = websocket.Upgrader{
|
||||
var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
|
||||
configs.UI: {
|
||||
configs.Keepalive: keepaliveReply,
|
||||
configs.Auth: handleAuth,
|
||||
},
|
||||
configs.CTL: ctl.CTLFunctionMap,
|
||||
}
|
||||
@ -86,27 +88,49 @@ func (session *session) onMessage() {
|
||||
|
||||
// this has to be a go routine otherwise it will block any incoming messages waiting for a command return
|
||||
go func() {
|
||||
// look through the function map to find the type to handle the request
|
||||
if reqType, ok := functionMap[request.Type]; ok {
|
||||
// the function map may have a component (function) to process the request
|
||||
if component, ok := reqType[request.Component]; ok {
|
||||
response := component(request)
|
||||
if err = session.webSocketSend(response); err != nil {
|
||||
session.onError(err)
|
||||
}
|
||||
// test the auth token for request validity on non auth requests
|
||||
// TODO (aschiefe): this will need to be amended when refresh tokens are implemented
|
||||
if request.Type != configs.UI && request.Component != configs.Auth && request.SubComponent != configs.Authenticate {
|
||||
if request.Token != nil {
|
||||
err = validateToken(*request.Token)
|
||||
} else {
|
||||
if err = session.webSocketSend(requestErrorHelper(fmt.Sprintf("Requested component: %s, not found",
|
||||
request.Component), request)); err != nil {
|
||||
session.onError(err)
|
||||
}
|
||||
log.Errorf("Requested component: %s, not found\n", request.Component)
|
||||
err = errors.New("No authentication token found")
|
||||
}
|
||||
} else {
|
||||
if err = session.webSocketSend(requestErrorHelper(fmt.Sprintf("Requested type: %s, not found",
|
||||
request.Type), request)); err != nil {
|
||||
}
|
||||
if err != nil {
|
||||
// deny the request if we get a bad token, this will force the UI to a login screen
|
||||
response := configs.WsMessage{
|
||||
Type: configs.UI,
|
||||
Component: configs.Auth,
|
||||
SubComponent: configs.Denied,
|
||||
Error: "Invalid token, authentication denied",
|
||||
}
|
||||
if err = session.webSocketSend(response); err != nil {
|
||||
session.onError(err)
|
||||
}
|
||||
log.Errorf("Requested type: %s, not found\n", request.Type)
|
||||
} else {
|
||||
// look through the function map to find the type to handle the request
|
||||
if reqType, ok := functionMap[request.Type]; ok {
|
||||
// the function map may have a component (function) to process the request
|
||||
if component, ok := reqType[request.Component]; ok {
|
||||
response := component(request)
|
||||
if err = session.webSocketSend(response); err != nil {
|
||||
session.onError(err)
|
||||
}
|
||||
} else {
|
||||
if err = session.webSocketSend(requestErrorHelper(fmt.Sprintf("Requested component: %s, not found",
|
||||
request.Component), request)); err != nil {
|
||||
session.onError(err)
|
||||
}
|
||||
log.Errorf("Requested component: %s, not found\n", request.Component)
|
||||
}
|
||||
} else {
|
||||
if err = session.webSocketSend(requestErrorHelper(fmt.Sprintf("Requested type: %s, not found",
|
||||
request.Type), request)); err != nil {
|
||||
session.onError(err)
|
||||
}
|
||||
log.Errorf("Requested type: %s, not found\n", request.Type)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -181,11 +205,10 @@ func WebSocketSend(response configs.WsMessage) error {
|
||||
// sendInit is generated on the onOpen event and sends the information the UI needs to startup
|
||||
func (session *session) sendInit() {
|
||||
if err := session.webSocketSend(configs.WsMessage{
|
||||
Type: configs.UI,
|
||||
Component: configs.Initialize,
|
||||
IsAuthenticated: true,
|
||||
Dashboards: configs.UIConfig.Dashboards,
|
||||
Authentication: configs.UIConfig.AuthMethod,
|
||||
Type: configs.UI,
|
||||
Component: configs.Initialize,
|
||||
Dashboards: configs.UIConfig.Dashboards,
|
||||
AuthMethod: configs.UIConfig.AuthMethod,
|
||||
}); err != nil {
|
||||
log.Errorf("Error receiving / sending init to session %s: %s\n", session.id, err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user