From 54b00a99e9ed052a97f13e05778273cca1c2a839 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Thu, 25 Jan 2018 16:10:29 +0000 Subject: [PATCH] mgr/dashboard_v2: Add frontend login/logout Signed-off-by: Ricardo Marques --- src/pybind/mgr/.gitignore | 1 + .../dashboard_v2/frontend/.angular-cli.json | 2 + .../mgr/dashboard_v2/frontend/README.md | 6 +- .../mgr/dashboard_v2/frontend/package.json | 2 + .../frontend/proxy.conf.json.sample | 7 + .../frontend/src/app/app-routing.module.ts | 9 +- .../frontend/src/app/app.component.html | 6 +- .../frontend/src/app/app.component.spec.ts | 12 +- .../frontend/src/app/app.component.ts | 17 +- .../frontend/src/app/app.module.ts | 27 +++- .../frontend/src/app/core/auth/auth.module.ts | 16 ++ .../app/core/auth/login/login.component.html | 52 +++++++ .../app/core/auth/login/login.component.scss | 0 .../core/auth/login/login.component.spec.ts | 39 +++++ .../app/core/auth/login/login.component.ts | 36 +++++ .../core/auth/logout/logout.component.html | 4 + .../core/auth/logout/logout.component.scss | 0 .../core/auth/logout/logout.component.spec.ts | 35 +++++ .../app/core/auth/logout/logout.component.ts | 23 +++ .../src/app/core/core-routing.module.ts | 5 +- .../frontend/src/app/core/core.module.ts | 4 +- .../app/core/navigation/navigation.module.ts | 4 +- .../navigation/navigation.component.html | 3 + .../navigation/navigation.component.scss | 143 ----------------- .../navigation/navigation.component.spec.ts | 14 +- .../src/app/shared/empty/empty.component.html | 1 + .../src/app/shared/empty/empty.component.scss | 0 .../app/shared/empty/empty.component.spec.ts | 25 +++ .../src/app/shared/empty/empty.component.ts | 15 ++ .../app/shared/models/credentials.model.ts | 4 + .../app/shared/services/auth-guard.service.ts | 18 +++ .../services/auth-interceptor.service.ts | 35 +++++ .../shared/services/auth-storage.service.ts | 21 +++ .../src/app/shared/services/auth.service.ts | 24 +++ .../frontend/src/app/shared/shared.module.ts | 11 +- ...go_Stacked_RGB_White_120411_fa_256x256.png | Bin 0 -> 8330 bytes .../frontend/src/openattic-theme.scss | 145 ++++++++++++++++++ 37 files changed, 606 insertions(+), 160 deletions(-) create mode 100644 src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png diff --git a/src/pybind/mgr/.gitignore b/src/pybind/mgr/.gitignore index 485dee64bcf..59aab7cfe1b 100644 --- a/src/pybind/mgr/.gitignore +++ b/src/pybind/mgr/.gitignore @@ -1 +1,2 @@ .idea +proxy.conf.json diff --git a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json b/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json index 9d39081a536..d95ac3d6e5f 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json +++ b/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json @@ -20,6 +20,8 @@ "prefix": "oa", "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.css", + "../node_modules/ng2-toastr/bundles/ng2-toastr.min.css", + "../node_modules/font-awesome/css/font-awesome.css", "styles.scss" ], "scripts": [], diff --git a/src/pybind/mgr/dashboard_v2/frontend/README.md b/src/pybind/mgr/dashboard_v2/frontend/README.md index 518ea551bb5..5e39db51953 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/README.md +++ b/src/pybind/mgr/dashboard_v2/frontend/README.md @@ -12,7 +12,11 @@ If you do not have installed [Angular CLI](https://github.com/angular/angular-cl ## Development server -Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +Create the `proxy.conf.json` file based on `proxy.conf.json.sample`. + +Run `npm start -- --proxy-config proxy.conf.json` for a dev server. +Navigate to `http://localhost:4200/`. +The app will automatically reload if you change any of the source files. ## Code scaffolding diff --git a/src/pybind/mgr/dashboard_v2/frontend/package.json b/src/pybind/mgr/dashboard_v2/frontend/package.json index e7eb975aaf2..8a6ce84fd35 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/package.json +++ b/src/pybind/mgr/dashboard_v2/frontend/package.json @@ -23,6 +23,8 @@ "@angular/router": "^5.0.0", "bootstrap": "^3.3.7", "core-js": "^2.4.1", + "font-awesome": "4.7.0", + "ng2-toastr": "4.1.2", "ngx-bootstrap": "^2.0.1", "rxjs": "^5.5.2", "zone.js": "^0.8.14" diff --git a/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample b/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample new file mode 100644 index 00000000000..e654419c9cf --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample @@ -0,0 +1,7 @@ +{ + "/api/": { + "target": "http://localhost:8080", + "secure": false, + "logLevel": "debug" + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts index d425c6f56b5..6f320da0067 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts @@ -1,10 +1,15 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { AuthGuardService } from './shared/services/auth-guard.service'; +import { EmptyComponent } from './shared/empty/empty.component'; -const routes: Routes = []; +const routes: Routes = [ + // TODO configure an appropriate default route (maybe on ceph module?) + { path: '', canActivate: [AuthGuardService], component: EmptyComponent }, +]; @NgModule({ - imports: [RouterModule.forRoot(routes)], + imports: [RouterModule.forRoot(routes, {useHash: true})], exports: [RouterModule] }) export class AppRoutingModule { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html index 41d2dd69a62..22e31e98d53 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html @@ -1,5 +1,5 @@ - - -
+ +
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts index f99d866c498..37723daaf0f 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts @@ -1,17 +1,21 @@ import { TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; -import { NavigationComponent } from './core/navigation/navigation/navigation.component'; +import { CoreModule } from './core/core.module'; +import { SharedModule } from './shared/shared.module'; +import { ToastModule } from 'ng2-toastr'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - RouterTestingModule + RouterTestingModule, + CoreModule, + SharedModule, + ToastModule.forRoot() ], declarations: [ - AppComponent, - NavigationComponent + AppComponent ], }).compileComponents(); })); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts index 3d2afc2725e..d3c94b399ca 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { Component, ViewContainerRef } from '@angular/core'; +import { AuthStorageService } from './shared/services/auth-storage.service'; +import { ToastsManager } from 'ng2-toastr'; +import { Router } from '@angular/router'; @Component({ selector: 'oa-root', @@ -7,4 +10,16 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'oa'; + + constructor(private authStorageService: AuthStorageService, + private router: Router, + public toastr: ToastsManager, + private vcr: ViewContainerRef) { + this.toastr.setRootViewContainerRef(vcr); + } + + isLogginActive() { + return this.router.url === '/login' || !this.authStorageService.isLoggedIn(); + } + } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts index 4375175cc36..6bd89ae34b1 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts @@ -3,10 +3,22 @@ import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; +import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr'; + import { AppComponent } from './app.component'; import { CoreModule } from './core/core.module'; import { SharedModule } from './shared/shared.module'; import { CephModule } from './ceph/ceph.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +import { AuthInterceptorService } from './shared/services/auth-interceptor.service'; + +export class CustomOption extends ToastOptions { + animate = 'flyRight'; + newestOnTop = true; + showCloseButton = true; + enableHTML = true; +} @NgModule({ declarations: [ @@ -14,13 +26,26 @@ import { CephModule } from './ceph/ceph.module'; ], imports: [ BrowserModule, + BrowserAnimationsModule, + ToastModule.forRoot(), AppRoutingModule, + HttpClientModule, CoreModule, SharedModule, CephModule ], exports: [SharedModule], - providers: [], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptorService, + multi: true + }, + { + provide: ToastOptions, + useClass: CustomOption + }, + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts new file mode 100644 index 00000000000..51254ba767e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { LoginComponent } from './login/login.component'; +import { LogoutComponent } from './logout/logout.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule + ], + declarations: [LoginComponent, LogoutComponent], + exports: [LogoutComponent] +}) +export class AuthModule { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html new file mode 100644 index 00000000000..09241b8b26f --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html @@ -0,0 +1,52 @@ + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts new file mode 100644 index 00000000000..5ba4db168e3 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts @@ -0,0 +1,39 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; +import { FormsModule } from '@angular/forms'; +import { SharedModule } from '../../../shared/shared.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ToastModule } from 'ng2-toastr'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule, + SharedModule, + RouterTestingModule, + HttpClientTestingModule, + ToastModule.forRoot() + ], + declarations: [ + LoginComponent + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts new file mode 100644 index 00000000000..60f4605da3e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit, ViewContainerRef } from '@angular/core'; +import { Credentials } from '../../../shared/models/credentials.model'; +import { AuthService } from '../../../shared/services/auth.service'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { Router } from '@angular/router'; +import { ToastsManager } from 'ng2-toastr'; + +@Component({ + selector: 'oa-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit { + + model = new Credentials(); + + constructor(private authService: AuthService, + private authStorageService: AuthStorageService, + private router: Router, + public toastr: ToastsManager, + private vcr: ViewContainerRef) { + this.toastr.setRootViewContainerRef(vcr); + } + + ngOnInit() { + if (this.authStorageService.isLoggedIn()) { + this.router.navigate(['']); + } + } + + login() { + this.authService.login(this.model).then(() => { + this.router.navigate(['']); + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html new file mode 100644 index 00000000000..21b9881fd2d --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html @@ -0,0 +1,4 @@ + + Logout + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts new file mode 100644 index 00000000000..960f0ef065f --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogoutComponent } from './logout.component'; +import { SharedModule } from '../../../shared/shared.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('LogoutComponent', () => { + let component: LogoutComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule, + HttpClientTestingModule + ], + declarations: [ + LogoutComponent + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LogoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts new file mode 100644 index 00000000000..9c3d43704ef --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '../../../shared/services/auth.service'; + +@Component({ + selector: 'oa-logout', + templateUrl: './logout.component.html', + styleUrls: ['./logout.component.scss'] +}) +export class LogoutComponent implements OnInit { + + constructor(private authService: AuthService, + private router: Router) { } + + ngOnInit() { + } + + logout() { + this.authService.logout().then(() => { + this.router.navigate(['/login']); + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core-routing.module.ts index 405e5a0ff7d..fbd98d27d12 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core-routing.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core-routing.module.ts @@ -1,7 +1,10 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { LoginComponent } from './auth/login/login.component'; -const routes: Routes = []; +const routes: Routes = [ + { path: 'login', component: LoginComponent } +]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts index a738ccbe780..4f4a618ef40 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts @@ -2,12 +2,14 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreRoutingModule } from './core-routing.module'; import { NavigationModule } from './navigation/navigation.module'; +import { AuthModule } from './auth/auth.module'; @NgModule({ imports: [ CommonModule, CoreRoutingModule, - NavigationModule + NavigationModule, + AuthModule ], exports: [NavigationModule], declarations: [] diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts index bbac8779bab..42a0d5a6eed 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts @@ -1,10 +1,12 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NavigationComponent } from './navigation/navigation.component'; +import { AuthModule } from '../auth/auth.module'; @NgModule({ imports: [ - CommonModule + CommonModule, + AuthModule ], declarations: [NavigationComponent], exports: [NavigationComponent] diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html index a097b052672..d01067dfd37 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -85,6 +85,9 @@
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss index 0f5acfd9d32..e69de29bb2d 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss @@ -1,143 +0,0 @@ -.navbar-openattic { - margin-bottom: 0; - background: #474544; - border: 0; - border-radius: 0; - border-top: 4px solid #288cea; - font-size: 1.2em; -} -.navbar-openattic .navbar-header { - display: flex; - float: none; -} -.navbar-openattic .navbar-toggle { - margin-left: auto; - border: 0; -} -.navbar-openattic .navbar-toggle:focus, -.navbar-openattic .navbar-toggle:hover { - background-color: transparent; - outline: 0; -} -.navbar-openattic .navbar-toggle .icon-bar { - background-color: #ececec; -} -.navbar-openattic .navbar-toggle:focus .icon-bar, -.navbar-openattic .navbar-toggle:hover .icon-bar { - -webkit-box-shadow: 0 0 3px #fff; - box-shadow: 0 0 3px #fff; -} -.navbar-openattic .navbar-collapse { - padding: 0; -} -.navbar-openattic .navbar-nav > li > a, -.navbar-openattic .navbar-nav > li > .oa-navbar > a { - color: #ececec; - line-height: 1; - padding: 10px 20px; - position: relative; - display: block; - text-decoration: none; -} -.navbar-openattic .navbar-nav > li > a:focus, -.navbar-openattic .navbar-nav > li > a:hover, -.navbar-openattic .navbar-nav > li > .oa-navbar > a:focus, -.navbar-openattic .navbar-nav > li > .oa-navbar > a:hover { - color: #ececec; -} -.navbar-openattic .navbar-nav > li > a:hover, -.navbar-openattic .navbar-nav > li > .oa-navbar > a:hover { - background-color: #505050; -} -.navbar-openattic .navbar-nav > .open > a, -.navbar-openattic .navbar-nav > .open > a:hover, -.navbar-openattic .navbar-nav > .open > a:focus, -.navbar-openattic .navbar-nav > .open > .oa-navbar > a, -.navbar-openattic .navbar-nav > .open > .oa-navbar > a:hover, -.navbar-openattic .navbar-nav > .open > .oa-navbar > a:focus { - color: #ececec; - border-color: transparent; - background-color: transparent; -} -.navbar-openattic .navbar-primary > li > a { - border: 0; -} -.navbar-openattic .navbar-primary > .active > a, -.navbar-openattic .navbar-primary > .active > a:hover, -.navbar-openattic .navbar-primary > .active > a:focus { - color: #ececec; - background-color: #288cea; - border: 0; -} -.navbar-openattic .navbar-utility a, -.navbar-openattic .navbar-utility .fa { - font-size: 1em; -} -.navbar-openattic .navbar-utility > .active > a { - color: #ececec; - background-color: #505050; -} -.navbar-openattic .navbar-utility > li > .open > a, -.navbar-openattic .navbar-utility > li > .open > a:hover, -.navbar-openattic .navbar-utility > li > .open > a:focus { - color: #ececec; - border-color: transparent; - background-color: transparent; -} -@media (min-width: 768px) { - .navbar-openattic .navbar-primary > li > a { - border-bottom: 4px solid transparent; - } - .navbar-openattic .navbar-primary > .active > a, - .navbar-openattic .navbar-primary > .active > a:hover, - .navbar-openattic .navbar-primary > .active > a:focus { - background-color: transparent; - border-bottom: 4px solid #288cea; - } - .navbar-openattic .navbar-utility { - border-bottom: 0; - font-size: 11px; - position: absolute; - right: 0; - top: 0; - } -} -@media (max-width: 767px) { - .navbar-openattic .navbar-nav { - margin: 0; - } - .navbar-openattic .navbar-collapse, - .navbar-openattic .navbar-form { - border-color: #ececec; - } - .navbar-openattic .navbar-collapse { - padding: 0; - } - .navbar-nav .open .dropdown-menu { - padding-top: 0; - padding-bottom: 0; - background-color: #505050; - } - .navbar-nav .open .dropdown-menu .dropdown-header, - .navbar-nav .open .dropdown-menu > li > a { - padding: 5px 15px 5px 35px; - } - .navbar-openattic .navbar-nav .open .dropdown-menu > li > a { - color: #ececec; - } - .navbar-openattic .navbar-nav .open .dropdown-menu > .active > a { - color: #ececec; - background-color: #288cea; - } - .navbar-openattic .navbar-nav > li > a:hover { - background-color: #288cea; - } - .navbar-openattic .navbar-utility { - border-top: 1px solid #ececec; - } - .navbar-openattic .navbar-primary > .active > a, - .navbar-openattic .navbar-primary > .active > a:hover, - .navbar-openattic .navbar-primary > .active > a:focus { - background-color: #288cea; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts index 38577181602..aca8268568b 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts @@ -1,6 +1,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NavigationComponent } from './navigation.component'; +import { LogoutComponent } from '../../auth/logout/logout.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SharedModule } from '../../../shared/shared.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('NavigationComponent', () => { let component: NavigationComponent; @@ -8,7 +12,15 @@ describe('NavigationComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ NavigationComponent ] + imports: [ + SharedModule, + RouterTestingModule, + HttpClientTestingModule + ], + declarations: [ + NavigationComponent, + LogoutComponent + ] }) .compileComponents(); })); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html new file mode 100644 index 00000000000..15798c69479 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html @@ -0,0 +1 @@ + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts new file mode 100644 index 00000000000..2bbad565772 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmptyComponent } from './empty.component'; + +describe('EmptyComponent', () => { + let component: EmptyComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EmptyComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EmptyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts new file mode 100644 index 00000000000..3c35d4b4082 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'oa-empty', + templateUrl: './empty.component.html', + styleUrls: ['./empty.component.scss'] +}) +export class EmptyComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts new file mode 100644 index 00000000000..2c2b7d76e39 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts @@ -0,0 +1,4 @@ +export class Credentials { + username: string; + password: string; +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts new file mode 100644 index 00000000000..3d2cffb9f44 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { AuthStorageService } from './auth-storage.service'; + +@Injectable() +export class AuthGuardService implements CanActivate { + + constructor(private router: Router, private authStorageService: AuthStorageService) { + } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + if (this.authStorageService.isLoggedIn()) { + return true; + } + this.router.navigate(['/login']); + return false; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts new file mode 100644 index 00000000000..a0640acc4c8 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { AuthStorageService } from './auth-storage.service'; +import { + HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, + HttpResponse +} from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; +import { ToastsManager } from 'ng2-toastr'; +import { Router } from '@angular/router'; + +@Injectable() +export class AuthInterceptorService implements HttpInterceptor { + + constructor(private router: Router, + private authStorageService: AuthStorageService, + public toastr: ToastsManager) { + } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).do((event: HttpEvent) => { + if (event instanceof HttpResponse) { + // do nothing + } + }, (err: any) => { + if (err instanceof HttpErrorResponse) { + this.toastr.error(err.error.detail || '', `${err.status} - ${err.statusText}`); + if (err.status === 401) { + this.authStorageService.remove(); + this.router.navigate(['/login']); + } + } + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts new file mode 100644 index 00000000000..cd6dbbe7a0b --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class AuthStorageService { + + constructor() { + } + + set(username: string) { + localStorage.setItem('dashboard_username', username); + } + + remove() { + localStorage.removeItem('dashboard_username'); + } + + isLoggedIn() { + return localStorage.getItem('dashboard_username') !== null; + } + +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts new file mode 100644 index 00000000000..30706abea4e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts @@ -0,0 +1,24 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Credentials } from '../models/credentials.model'; +import { AuthStorageService } from './auth-storage.service'; + +@Injectable() +export class AuthService { + + constructor(private authStorageService: AuthStorageService, + private http: HttpClient) { + } + + login(credentials: Credentials) { + return this.http.post('/api/auth', credentials).toPromise().then((resp: Credentials) => { + this.authStorageService.set(resp.username); + }); + } + + logout() { + return this.http.delete('/api/auth').toPromise().then(() => { + this.authStorageService.remove(); + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts index fffbe5b239e..ef3e58a57db 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts @@ -1,10 +1,19 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { AuthService } from './services/auth.service'; +import { AuthStorageService } from './services/auth-storage.service'; +import { AuthGuardService } from './services/auth-guard.service'; +import { EmptyComponent } from './empty/empty.component'; @NgModule({ imports: [ CommonModule ], - declarations: [] + declarations: [EmptyComponent], + providers: [ + AuthService, + AuthStorageService, + AuthGuardService + ] }) export class SharedModule { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..26d602be3bc36430e2ddbc181df7d8bc701048e5 GIT binary patch literal 8330 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, +.navbar-openattic .navbar-nav>li>.oa-navbar>a { + color: #ececec; + line-height: 1; + padding: 10px 20px; + position: relative; + display: block; + text-decoration: none; +} +.navbar-openattic .navbar-nav>li>a:focus, +.navbar-openattic .navbar-nav>li>a:hover, +.navbar-openattic .navbar-nav>li>.oa-navbar>a:focus, +.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover { + color: #ececec; +} +.navbar-openattic .navbar-nav>li>a:hover, +.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover { + background-color: #505050; +} +.navbar-openattic .navbar-nav>.open>a, +.navbar-openattic .navbar-nav>.open>a:hover, +.navbar-openattic .navbar-nav>.open>a:focus, +.navbar-openattic .navbar-nav>.open>.oa-navbar>a, +.navbar-openattic .navbar-nav>.open>.oa-navbar>a:hover, +.navbar-openattic .navbar-nav>.open>.oa-navbar>a:focus { + color: #ececec; + border-color: transparent; + background-color: transparent; +} +.navbar-openattic .navbar-primary>li>a { + border: 0; +} +.navbar-openattic .navbar-primary>.active>a, +.navbar-openattic .navbar-primary>.active>a:hover, +.navbar-openattic .navbar-primary>.active>a:focus { + color: #ececec; + background-color: #288cea; + border: 0; +} +.navbar-openattic .navbar-utility a, +.navbar-openattic .navbar-utility .fa{ + font-size: 1.0em; +} +.navbar-openattic .navbar-utility>.active>a { + color: #ececec; + background-color: #505050; +} +.navbar-openattic .navbar-utility>li>.open>a, +.navbar-openattic .navbar-utility>li>.open>a:hover, +.navbar-openattic .navbar-utility>li>.open>a:focus { + color: #ececec; + border-color: transparent; + background-color: transparent; +} +@media (min-width: 768px) { + .navbar-openattic .navbar-primary>li>a { + border-bottom: 4px solid transparent; + } + .navbar-openattic .navbar-primary>.active>a, + .navbar-openattic .navbar-primary>.active>a:hover, + .navbar-openattic .navbar-primary>.active>a:focus { + background-color: transparent; + border-bottom: 4px solid #288cea; + } + .navbar-openattic .navbar-utility { + border-bottom: 0; + font-size: 11px; + position: absolute; + right: 0; + top: 0; + } +} +@media (max-width: 767px) { + .navbar-openattic .navbar-nav { + margin: 0; + } + .navbar-openattic .navbar-collapse, + .navbar-openattic .navbar-form { + border-color: #ececec; + } + .navbar-openattic .navbar-collapse { + padding: 0; + } + .navbar-nav .open .dropdown-menu { + padding-top: 0; + padding-bottom: 0; + background-color: #505050; + } + .navbar-nav .open .dropdown-menu .dropdown-header, + .navbar-nav .open .dropdown-menu>li>a { + padding: 5px 15px 5px 35px; + } + .navbar-openattic .navbar-nav .open .dropdown-menu>li>a { + color: #ececec; + } + .navbar-openattic .navbar-nav .open .dropdown-menu>.active>a { + color: #ececec; + background-color: #288cea; + } + .navbar-openattic .navbar-nav>li>a:hover { + background-color: #288cea; + } + .navbar-openattic .navbar-utility { + border-top: 1px solid #ececec; + } + .navbar-openattic .navbar-primary>.active>a, + .navbar-openattic .navbar-primary>.active>a:hover, + .navbar-openattic .navbar-primary>.active>a:focus { + background-color: #288cea; + } +} + /* Navs */ .nav-tabs { margin-bottom: 15px; -- 2.39.5