diff --git a/cf_config/headers.conf b/cf_config/headers.conf index a9ddb9f..2bc464c 100644 --- a/cf_config/headers.conf +++ b/cf_config/headers.conf @@ -1 +1,9 @@ add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src https://definma-api.apps.de1.bosch-iot-cloud.com; form-action 'none'; frame-ancestors 'none'; base-uri 'self'"; +add_header X-Frame-Options DENY +add_header X-DNS-Prefetch-Control off +add_header Strict-Transport-Security max-age=15552000 +add_header X-Download-Options noopen +add_header X-Content-Type-Options nosniff +add_header X-Permitted-Cross-Domain-Policies none +add_header Referrer-Policy no-referrer +add_header X-XSS-Protection "1; mode=block" diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 25fcf56..8fa4f6c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -6,6 +6,8 @@ import {SampleComponent} from './sample/sample.component'; import {SamplesComponent} from './samples/samples.component'; import {DocumentationComponent} from './documentation/documentation.component'; import {TemplatesComponent} from './templates/templates.component'; +import {SettingsComponent} from './settings/settings.component'; +import {UsersComponent} from './users/users.component'; const routes: Routes = [ @@ -14,8 +16,10 @@ const routes: Routes = [ {path: 'samples', component: SamplesComponent, canActivate: [LoginService]}, {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]}, {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]}, - {path: 'templates', component: TemplatesComponent}, // TODO: change after development - // {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]}, + {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]}, + // {path: 'users', component: UsersComponent, canActivate: [LoginService]}, + {path: 'users', component: UsersComponent}, // TODO: change + {path: 'settings', component: SettingsComponent, canActivate: [LoginService]}, {path: 'documentation', component: DocumentationComponent}, // if not authenticated diff --git a/src/app/app.component.html b/src/app/app.component.html index 919b47f..884286d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,7 +2,8 @@ @@ -11,12 +12,9 @@ {{loginService.username}} - +
-

- -

- +   Settings
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 31ad797..dd5d5f3 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -3,3 +3,9 @@ font-size: 32px; margin-right: 40px; } + +.spacing { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c78cc33..18590d1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -23,6 +23,8 @@ import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component'; import { ExistsPipe } from './exists.pipe'; import { TemplatesComponent } from './templates/templates.component'; import { ParametersPipe } from './parameters.pipe'; +import { SettingsComponent } from './settings/settings.component'; +import { UsersComponent } from './users/users.component'; @NgModule({ declarations: [ @@ -38,7 +40,9 @@ import { ParametersPipe } from './parameters.pipe'; ImgMagnifierComponent, ExistsPipe, TemplatesComponent, - ParametersPipe + ParametersPipe, + SettingsComponent, + UsersComponent ], imports: [ LocalStorageModule.forRoot({ diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index e765fc9..6de06e5 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -6,10 +6,14 @@ {{usernameInput.errors.failure}} - + {{passwordInput.errors.failure}} - - {{message}} + + {{emailInput.errors.failure}} + + Forgot password + +
{{message}}
diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss index 52566ab..9f3ea33 100644 --- a/src/app/login/login.component.scss +++ b/src/app/login/login.component.scss @@ -4,9 +4,14 @@ .message { font-size: 13px; - white-space: pre-line; + margin-top: 10px; } .login-button { margin-right: 10px; } + +.forgot-pass { + display: block; + margin-bottom: 1rem; +} diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 6c28dcc..c1bc69f 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -2,6 +2,7 @@ import {Component, OnInit, ViewChild} from '@angular/core'; import {ValidationService} from '../services/validation.service'; import {LoginService} from '../services/login.service'; import {Router} from '@angular/router'; +import {ApiService} from '../services/api.service'; @Component({ @@ -13,13 +14,17 @@ export class LoginComponent implements OnInit { username = ''; // credentials password = ''; + email = ''; message = ''; // message below login fields + passreset = false; + @ViewChild('loginForm') loginForm; constructor( private validate: ValidationService, private loginService: LoginService, + private api: ApiService, private router: Router ) { } @@ -27,14 +32,26 @@ export class LoginComponent implements OnInit { } login() { - this.loginService.login(this.username, this.password).then(ok => { - if (ok) { - this.message = 'Login successful'; - this.router.navigate(['/samples']); - } - else { - this.message = 'Wrong credentials!'; - } - }); + if (this.passreset) { + this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => { + if (err) { + this.message = 'Could not find a valid user'; + } + else { + this.message = 'Password reset, check your inbox'; + } + }); + } + else { + this.loginService.login(this.username, this.password).then(ok => { + if (ok) { + this.message = 'Login successful'; + this.router.navigate(['/samples']); + } + else { + this.message = 'Wrong credentials!'; + } + }); + } } } diff --git a/src/app/models/user.model.spec.ts b/src/app/models/user.model.spec.ts new file mode 100644 index 0000000..3ba7e0b --- /dev/null +++ b/src/app/models/user.model.spec.ts @@ -0,0 +1,7 @@ +import { UserModel } from './user.model'; + +describe('User.Model', () => { + it('should create an instance', () => { + expect(new UserModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts new file mode 100644 index 0000000..66f62fe --- /dev/null +++ b/src/app/models/user.model.ts @@ -0,0 +1,28 @@ +import _ from 'lodash'; +import {BaseModel} from './base.model'; +import {IdModel} from './id.model'; + +export class UserModel extends BaseModel{ + _id: IdModel = null; + name = ''; + origName = ''; + email = ''; + level = ''; + location = ''; + device_name = ''; + edit = false; + + deserialize(input: any): this { + Object.assign(this, input); + this.origName = this.name; + return this; + } + + sendFormat(mode = 'user') { + const keys = ['name', 'email', 'location', 'device_name']; + if (mode === 'admin') { + keys.push('level'); + } + return _.pick(this, keys); + } +} diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html index 33826bc..6e4f70d 100644 --- a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html @@ -1,4 +1,4 @@ - diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts index f48ebf7..fb02b84 100644 --- a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts @@ -13,6 +13,7 @@ export class RbIconButtonComponent implements OnInit { @Input() icon: string; @Input() mode: string; @Input() disabled; + @Input() type = 'button'; constructor() { } diff --git a/src/app/sample/sample.component.html b/src/app/sample/sample.component.html index 7401416..19306d6 100644 --- a/src/app/sample/sample.component.html +++ b/src/app/sample/sample.component.html @@ -1,4 +1,4 @@ -

{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}

+

{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}

@@ -6,7 +6,7 @@
- + Cannot be empty Unknown material, add properties for new material @@ -57,7 +57,7 @@
Sample references
- + Unknown sample number
diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts index 5f68668..985ea69 100644 --- a/src/app/sample/sample.component.ts +++ b/src/app/sample/sample.component.ts @@ -27,7 +27,7 @@ import {Observable} from 'rxjs'; // TODO: material properties, color (in material and sample (not required)) -// TODO: API $in Regex +// TODO: device autocomplete @Component({ selector: 'app-sample', diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index 6de63eb..6f66d51 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -9,7 +9,17 @@ import {Observable} from 'rxjs'; }) export class LoginService implements CanActivate { - private maintainPaths = ['templates']; + private pathPermissions = [ + {path: 'templates', permission: 'maintain'}, + {path: 'users', permission: 'admin'} + ]; + readonly levels = [ + 'read', + 'write', + 'maintain', + 'dev', + 'admin' + ]; private loggedIn; private level; @@ -23,9 +33,28 @@ export class LoginService implements CanActivate { login(username = '', password = '') { return new Promise(resolve => { - if (username !== '') { - this.storage.set('basicAuth', btoa(username + ':' + password)); + console.log(username); + console.log(password); + if (username !== '' || password !== '') { // some credentials given + let credentials: string[]; + const credentialString: string = this.storage.get('basicAuth'); + if (credentialString) { // found stored credentials + credentials = atob(credentialString).split(':'); + } + else { + credentials = ['', '']; + } + if (username !== '' && password !== '') { // all credentials given + this.storage.set('basicAuth', btoa(username + ':' + password)); + } + else if (username !== '') { // username given + this.storage.set('basicAuth', btoa(username + ':' + credentials[1])); + } + else if (password !== '') { // password given + this.storage.set('basicAuth', btoa(credentials[0] + ':' + password)); + } } + console.log(this.storage.get('basicAuth')); this.api.get('/authorized', (data: any, error) => { if (!error) { if (data.status === 'Authorization successful') { @@ -53,8 +82,8 @@ export class LoginService implements CanActivate { canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable { return new Observable(observer => { - const isMaintainPath = this.maintainPaths.indexOf(route.url[0].path) >= 0; - if (!isMaintainPath || (isMaintainPath && this.isMaintain)) { + const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0); + if (!pathPermission || this.is(pathPermission.permission)) { // check if level is permitted for path if (this.loggedIn === undefined) { this.login().then(res => { observer.next(res as any); @@ -77,8 +106,8 @@ export class LoginService implements CanActivate { return this.loggedIn; } - get isMaintain() { - return this.level === 'maintain' || this.level === 'admin'; + is(level) { + return this.levels.indexOf(this.level) >= this.levels.indexOf(level); } get username() { diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts index 8dd273a..a58ec72 100644 --- a/src/app/services/validation.service.ts +++ b/src/app/services/validation.service.ts @@ -66,10 +66,16 @@ export class ValidationService { return {ok: true, error: ''}; } - string(data) { - const {ignore, error} = Joi.string().max(128).allow('').validate(data); + string(data, option = null) { + let validator = Joi.string().max(128).allow(''); + let errorMsg = 'must contain max 128 characters'; + if (option === 'alphanum') { + validator = validator.alphanum(); + errorMsg = 'must contain max 128 alphanumerical characters'; + } + const {ignore, error} = validator.validate(data); if (error) { - return {ok: false, error: 'must contain max 128 characters'}; + return {ok: false, error: errorMsg}; } return {ok: true, error: ''}; } @@ -122,6 +128,13 @@ export class ValidationService { return {ok: true, error: ''}; } + equal(data, compare) { + if (data !== compare) { + return {ok: false, error: `must be equal`}; + } + return {ok: true, error: ''}; + } + parameterName(data) { const {ignore, error} = Joi.string() .max(128) diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html new file mode 100644 index 0000000..8e73ba1 --- /dev/null +++ b/src/app/settings/settings.component.html @@ -0,0 +1,45 @@ +

Settings

+ +
+ + {{nameInput.errors.failure}} + Cannot be empty + + + Invalid email + Cannot be empty + + + {{locationInput.errors.failure}} + Cannot be empty + + + {{deviceInput.errors.failure}} + + + Save change + + {{messageUser}} +
+ + +

Change password

+ +
+ + {{passAInput.errors.failure}} + + + {{passBInput.errors.failure}} + + + {{messagePass}} +
+ diff --git a/src/app/settings/settings.component.scss b/src/app/settings/settings.component.scss new file mode 100644 index 0000000..3651625 --- /dev/null +++ b/src/app/settings/settings.component.scss @@ -0,0 +1,7 @@ +.pass-heading { + margin-top: 40px; +} + +.message { + margin-left: 20px; +} diff --git a/src/app/settings/settings.component.spec.ts b/src/app/settings/settings.component.spec.ts new file mode 100644 index 0000000..91588f3 --- /dev/null +++ b/src/app/settings/settings.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; + +describe('SettingsComponent', () => { + let component: SettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts new file mode 100644 index 0000000..313d727 --- /dev/null +++ b/src/app/settings/settings.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import {ApiService} from '../services/api.service'; +import {UserModel} from '../models/user.model'; +import {Router} from '@angular/router'; +import {LoginService} from '../services/login.service'; + + +@Component({ + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.scss'] +}) +export class SettingsComponent implements OnInit { + + user: UserModel = new UserModel(); + password = ''; + messageUser = ''; + messagePass = ''; + + constructor( + private api: ApiService, + private login: LoginService, + private router: Router + ) { } + + ngOnInit(): void { + this.api.get('/user', data => { + this.user.deserialize(data); + }); + } + + saveUser() { + this.api.put('/user', this.user.sendFormat(), (data, err) => { + if (err) { + this.messageUser = err.error.status; + } + else { + this.login.login(data.name).then(res => { + if (res) { + this.router.navigate(['/samples']); + } + else { + this.messageUser = 'request not successful, try again'; + } + }); + } + }); + } + + savePass() { + this.api.put('/user', {pass: this.password}, (ignore, err) => { + if (err) { + this.messagePass = err.error.status; + } + else { + this.login.login('', this.password).then(res => { + if (res) { + this.router.navigate(['/samples']); + } + else { + this.messagePass = 'request not successful, try again'; + } + }); + } + }); + } + +} diff --git a/src/app/templates/templates.component.scss b/src/app/templates/templates.component.scss index eb79c15..ede71e8 100644 --- a/src/app/templates/templates.component.scss +++ b/src/app/templates/templates.component.scss @@ -38,7 +38,3 @@ } } } - -.clickable { - cursor: pointer; -} diff --git a/src/app/users/users.component.html b/src/app/users/users.component.html new file mode 100644 index 0000000..b176428 --- /dev/null +++ b/src/app/users/users.component.html @@ -0,0 +1,94 @@ +

Users

+ +New user + +
+ + {{nameInput.errors.failure}} + Cannot be empty + + + Invalid email + Cannot be empty + + + + Cannot be empty + + + {{locationInput.errors.failure}} + Cannot be empty + + + {{deviceInput.errors.failure}} + + + {{passAInput.errors.failure}} + + + {{passBInput.errors.failure}} + + + Save user + +
+ + + + Name + Email + Level + Location + Device + + + + + + {{user.name}} + {{user.email}} + {{user.level}} + {{user.location}} + {{user.device_name}} + + + + + + {{nameInput.errors.failure}} + Cannot be empty + + + + + Invalid email + Cannot be empty + + + + + + + + + + {{locationInput.errors.failure}} + Cannot be empty + + + + + {{deviceInput.errors.failure}} + + + Save + + + diff --git a/src/app/users/users.component.scss b/src/app/users/users.component.scss new file mode 100644 index 0000000..9ba4fcf --- /dev/null +++ b/src/app/users/users.component.scss @@ -0,0 +1,3 @@ +::ng-deep td .error-messages { + position: absolute; +} diff --git a/src/app/users/users.component.spec.ts b/src/app/users/users.component.spec.ts new file mode 100644 index 0000000..909b5ba --- /dev/null +++ b/src/app/users/users.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UsersComponent } from './users.component'; + +describe('UsersComponent', () => { + let component: UsersComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UsersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/users/users.component.ts b/src/app/users/users.component.ts new file mode 100644 index 0000000..d80817b --- /dev/null +++ b/src/app/users/users.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import {ApiService} from '../services/api.service'; +import {UserModel} from '../models/user.model'; +import {LoginService} from '../services/login.service'; + + +@Component({ + selector: 'app-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'] +}) +export class UsersComponent implements OnInit { + + users: UserModel[] = []; + newUser: UserModel | null = null; + newUserPass = ''; + + constructor( + private api: ApiService, + public login: LoginService + ) { } + + ngOnInit(): void { + this.api.get('/users', data => { + this.users = data.map(e => new UserModel().deserialize(e)); + }); + } + + saveUser(user: UserModel) { + this.api.put('/user/' + user.origName, user.sendFormat('admin'), data => { + user.deserialize(data); + user.edit = false; + }); + } + + saveNewUser() { + this.api.post('/user/new', {...this.newUser.sendFormat('admin'), pass: this.newUserPass}, data => { + this.newUser = null; + this.users.push(new UserModel().deserialize(data)); + }); + } + + addNewUser() { + this.newUser = this.newUser ? null : new UserModel(); + } + +} diff --git a/src/styles.scss b/src/styles.scss index ae773e1..a0f6fd5 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -19,3 +19,7 @@ button::-moz-focus-inner { .supergraphic { background-image: url("assets/imgs/supergraphic.svg"); } + +.clickable { + cursor: pointer; +}