authentification

This commit is contained in:
2019-08-11 01:51:37 +02:00
parent 1e9d111ceb
commit 6d040e17a2
25 changed files with 1471 additions and 101 deletions

1016
WEB/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
"@angular/common": "~8.2.0", "@angular/common": "~8.2.0",
"@angular/compiler": "~8.2.0", "@angular/compiler": "~8.2.0",
"@angular/core": "~8.2.0", "@angular/core": "~8.2.0",
"@angular/fire": "^5.2.1",
"@angular/forms": "~8.2.0", "@angular/forms": "~8.2.0",
"@angular/http": "~7.2.15", "@angular/http": "~7.2.15",
"@angular/material": "^8.1.2", "@angular/material": "^8.1.2",
@@ -28,6 +29,7 @@
"@fortawesome/free-solid-svg-icons": "^5.10.1", "@fortawesome/free-solid-svg-icons": "^5.10.1",
"angular2-uuid": "^1.1.1", "angular2-uuid": "^1.1.1",
"core-js": "^3.1.4", "core-js": "^3.1.4",
"firebase": "^6.3.4",
"ng2-file-upload": "^1.3.0", "ng2-file-upload": "^1.3.0",
"odata-v4-ng": "^1.2.1", "odata-v4-ng": "^1.2.1",
"rxjs": "^6.5.2", "rxjs": "^6.5.2",

View File

@@ -0,0 +1,17 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
{
path: 'login',
loadChildren: () => import('./modules/login/login.module').then(_ => _.LoginModule)
}
]
;
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AccountRoutingModule {
}

View File

@@ -0,0 +1,14 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AccountRoutingModule} from './account-routing.module';
@NgModule({
imports: [
CommonModule,
AccountRoutingModule,
]
})
export class AccountModule {
}

View File

@@ -0,0 +1,21 @@
<mat-card>
<mat-card-header>
<mat-card-title>Login</mat-card-title>
<mat-card-subtitle></mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form>
<mat-form-field>
<input [formControl]="user" matInput placeholder="Benutzername">
</mat-form-field>
<mat-form-field>
<input [formControl]="pass" matInput placeholder="Passwort" type="password">
</mat-form-field>
</form>
</mat-card-content>
<mat-card-actions>
<button (click)="onLogin()" mat-button>Login</button>
<button mat-button>Abbrechen</button>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,5 @@
form {
display: flex;
flex-direction: column;
padding: 20px 0;
}

View File

@@ -0,0 +1,25 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,41 @@
import {Component, OnInit} from '@angular/core';
import {AuthService} from '../../../../../services/auth.service';
import {FormControl} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.less']
})
export class LoginComponent implements OnInit {
public user = new FormControl();
public pass = new FormControl();
public loginError = false;
private redirect: string = null;
constructor(
private authService: AuthService,
private router: Router,
activatedRoute: ActivatedRoute
) {
activatedRoute.queryParams.subscribe(_ => this.redirect = _.redirect);
}
ngOnInit() {
this.loginError = false;
}
public onLogin(): void {
this.authService.login$(this.user.value, this.pass.value).subscribe(_ => {
if (_ === null) {
this.loginError = true;
} else {
if (this.redirect) {
this.router.navigateByUrl('/' + this.redirect);
}
}
});
}
}

View File

@@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {LoginComponent} from './components/login/login.component';
const routes: Routes = [
{
path: '',
component: LoginComponent,
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LoginRoutingModule {
}

View File

@@ -0,0 +1,24 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {LoginRoutingModule} from './login-routing.module';
import {LoginComponent} from './components/login/login.component';
import {MatButtonModule, MatCardModule, MatInputModule} from '@angular/material';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@NgModule({
declarations: [LoginComponent],
imports: [
CommonModule,
LoginRoutingModule,
FormsModule,
ReactiveFormsModule,
MatCardModule,
MatInputModule,
MatButtonModule
]
})
export class LoginModule {
}

View File

@@ -1,5 +1,6 @@
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router'; import {RouterModule, Routes} from '@angular/router';
import {AuthGuard} from './guards/auth.guard';
const routes: Routes = [ const routes: Routes = [
@@ -10,7 +11,12 @@ const routes: Routes = [
}, },
{ {
path: 'songs', path: 'songs',
loadChildren: () => import('./songs/songs.module').then(_ => _.SongsModule) loadChildren: () => import('./songs/songs.module').then(_ => _.SongsModule),
canActivate: [AuthGuard],
},
{
path: 'account',
loadChildren: () => import('./account/account.module').then(_ => _.AccountModule)
} }
]; ];

View File

@@ -19,6 +19,11 @@ import {MatSelectModule} from '@angular/material/select';
import {MatTooltipModule} from '@angular/material/tooltip'; import {MatTooltipModule} from '@angular/material/tooltip';
import {MatProgressBarModule} from '@angular/material/progress-bar'; import {MatProgressBarModule} from '@angular/material/progress-bar';
import {FileUploadModule} from 'ng2-file-upload'; import {FileUploadModule} from 'ng2-file-upload';
import {AngularFireModule} from '@angular/fire';
import {AngularFireAuthModule} from '@angular/fire/auth';
import {AngularFirestoreModule} from '@angular/fire/firestore';
import {environment} from '../environments/environment';
import {AngularFireDatabaseModule} from '@angular/fire/database';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -45,6 +50,11 @@ import {FileUploadModule} from 'ng2-file-upload';
FontAwesomeModule, FontAwesomeModule,
FileUploadModule, FileUploadModule,
AppRoutingModule, AppRoutingModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule, // imports firebase/firestore, only needed for database features
AngularFireAuthModule, // imports firebase/auth, only needed for auth features
AngularFireDatabaseModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -0,0 +1,15 @@
import {inject, TestBed} from '@angular/core/testing';
import {AuthGuard} from './auth.guard';
describe('AuthGuard', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthGuard]
});
});
it('should ...', inject([AuthGuard], (guard: AuthGuard) => {
expect(guard).toBeTruthy();
}));
});

View File

@@ -0,0 +1,27 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
import {AuthService} from '../services/auth.service';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {Role} from '../services/roles.model';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService,
private router: Router
) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.authService.userMay$(Role.reader).pipe(
map(_ => {
return _
? true
: this.router.createUrlTree(['/account/login'], {queryParams: {redirect: route.url}});
})
);
}
}

View File

@@ -0,0 +1,41 @@
import {async, TestBed} from '@angular/core/testing';
import {AccessRightService} from './access-right.service';
import {AngularFirestore} from '@angular/fire/firestore';
import {of} from 'rxjs';
import {Role} from './roles.model';
describe('AccessRightService', () => {
const mockAngularFirestore = {
collection: () => ({
doc: () => ({
valueChanges: () => of({
role: 'reader'
})
})
})
};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: AngularFirestore, useValue: mockAngularFirestore}
]
}));
it('should be created', () => {
const service: AccessRightService = TestBed.get(AccessRightService);
expect(service).toBeTruthy();
});
it('should get user', async(() => {
const service: AccessRightService = TestBed.get(AccessRightService);
service.getUserInfo('userid').subscribe(_ => {
expect(_).toEqual({
user: 'userid',
role: Role.reader
});
});
}));
});

View File

@@ -0,0 +1,43 @@
import {Injectable} from '@angular/core';
import {AngularFirestore} from '@angular/fire/firestore';
import {map} from 'rxjs/operators';
import {User, UserDB} from './user.model';
import {Observable} from 'rxjs';
import {Role} from './roles.model';
import {RoleDefinitions} from './role.definition';
@Injectable({
providedIn: 'root'
})
export class AccessRightService {
constructor(
private angularFirestore: AngularFirestore
) {
}
public getUserInfo(userId: string): Observable<User> {
if (userId === null) {
return null;
}
const user$ = this.angularFirestore
.collection('/user')
.doc<UserDB>(userId)
.valueChanges()
.pipe
(map(user => ({
user: userId,
role: user.role
})));
return user$;
}
public userMay(role: Role, requestedRole: Role): boolean {
const allowedRoles = RoleDefinitions.filter(_ => _.role === requestedRole)[0].when;
const isAllowed = allowedRoles.indexOf(role) !== -1;
return isAllowed;
}
}

View File

@@ -0,0 +1,47 @@
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
import {AuthService} from './auth.service';
import {of} from 'rxjs';
import {Role} from './roles.model';
import {AccessRightService} from './access-right.service';
import {AngularFireAuth} from '@angular/fire/auth';
describe('AuthService', () => {
const mockAccessRightService = {
getUserInfo: () => of({
user: 'userid',
role: Role.reader
})
};
const mockAngularFireAuth = {
auth: {
signInWithEmailAndPassword: Promise.resolve({user: {uid: 'userid'}})
}
};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: AccessRightService, useValue: mockAccessRightService},
{provide: AngularFireAuth, useValue: mockAngularFireAuth},
]
}));
it('should be created', () => {
const service: AuthService = TestBed.get(AuthService);
expect(service).toBeTruthy();
});
it('should be created', fakeAsync(() => {
const service: AuthService = TestBed.get(AuthService);
const authSpy = spyOn(TestBed.get(AngularFireAuth).auth, 'signInWithEmailAndPassword');
const accessSpy = spyOn(TestBed.get(AccessRightService), 'getUserInfo');
service.login$('user', 'pass');
expect(authSpy).toHaveBeenCalledWith('user', 'pass');
tick();
expect(accessSpy).toHaveBeenCalledWith('userid');
tick();
expect(service.user.user).toBe('userid');
expect(service.user.user).toBe(Role.reader);
}));
});

View File

@@ -0,0 +1,64 @@
import {Injectable} from '@angular/core';
import {AngularFireAuth} from '@angular/fire/auth';
import {from, Observable, of, OperatorFunction} from 'rxjs';
import {AccessRightService} from './access-right.service';
import {catchError, map, switchMap, tap} from 'rxjs/operators';
import {User} from './user.model';
import {Role} from './roles.model';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private _user: User = null;
constructor(
private angularFireAuth: AngularFireAuth,
private accessRightService: AccessRightService
) {
angularFireAuth.authState.subscribe(_ => console.log(_));
}
public login$(user: string, pass: string): Observable<User> {
const authPromise = this.angularFireAuth.auth.signInWithEmailAndPassword(user, pass);
const auth$ = from(authPromise).pipe(
map(_ => _.user.uid),
this.processLogin()
);
return auth$;
}
public getUser$(): Observable<User> {
if (this._user) {
return of(this._user);
}
return this.angularFireAuth.authState.pipe(
map(_ => _ ? _.uid : null),
this.processLogin()
);
}
public userMay$(requestedRole: Role): Observable<boolean> {
const allowed$ = this.getUser$().pipe(
map(_ => _ ? this.accessRightService.userMay(_.role, requestedRole) : false)
);
return allowed$;
}
private processLogin(): OperatorFunction<string, User> {
const self = this;
return function (source$: Observable<string>): Observable<User> {
return source$.pipe(
switchMap(_ => self.accessRightService.getUserInfo(_)),
tap((_ => self._user = _)),
catchError(_ => {
self._user = null;
return of(null);
})
);
};
}
}

View File

@@ -0,0 +1,7 @@
import {Role} from './roles.model';
export const RoleDefinitions = [
{role: Role.reader, when: [Role.admin, Role.writer, Role.reader]},
{role: Role.writer, when: [Role.admin, Role.writer]},
{role: Role.admin, when: [Role.admin]},
];

View File

@@ -0,0 +1,5 @@
export enum Role {
admin = 'admin',
reader = 'reader',
writer = 'writer'
}

View File

@@ -0,0 +1,10 @@
import {Role} from './roles.model';
export interface User {
user: string;
role: Role;
}
export interface UserDB {
role: Role;
}

View File

@@ -1,24 +1,4 @@
table {
border-radius: 8px;
background: #fffe;
tr.selected {
background-color: #0002;
}
tr:hover {
cursor: pointer;
background-color: #0001;
td {
color: #ff9900;
}
}
td.mat-cell {
padding: 0 5px;
}
}
.table-container { .table-container {
overflow: auto; overflow: auto;

View File

@@ -4,7 +4,16 @@
export const environment = { export const environment = {
production: false, production: false,
api: 'http://test.benjamin-ifland.de' api: 'http://test.benjamin-ifland.de',
firebase: {
apiKey: 'AIzaSyBcIa6m8F7fT4gRLTx2zcBufUEE81gWVFg',
authDomain: 'worshipgenerator.firebaseapp.com',
databaseURL: 'https://worshipgenerator.firebaseio.com',
projectId: 'worshipgenerator',
storageBucket: 'worshipgenerator.appspot.com',
messagingSenderId: '317378238562',
appId: '1:317378238562:web:552ca27bc5e9086e'
}
}; };
/* /*

View File

@@ -2,7 +2,14 @@
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; @import "~@angular/material/prebuilt-themes/indigo-pink.css";
html { html {
background-image: url(https://images.unsplash.com/photo-1476136236990-838240be4859?ixlib=rb-1.2.1&auto=format&fit=crop&w=2167&q=80); background: #2c3e50; /* fallback for old browsers */
background: -webkit-linear-gradient(to top, #2c3e50, #3498db); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to top, #2c3e50, #3498db); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
height: 100vh;
}
.card-3 {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
} }
body { body {
@@ -14,53 +21,28 @@ body {
} }
.page-container { .page-container {
padding: 10px; .card-3;
box-sizing: border-box; margin: 10px;
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: 8px;
.mat-table tbody { table.mat-table thead,
table.mat-table tbody {
background: none; background: none;
} }
th.mat-header-cell:first-of-type {
border-top-left-radius: 8px;
}
th.mat-header-cell:last-of-type {
border-top-right-radius: 8px;
}
.mat-table thead {
border-top-right-radius: 8px;
border-top-left-radius: 8px;
}
&.pinned { &.pinned {
padding: 0; padding: 0;
max-width: 300px; max-width: 300px;
border-radius: 0;
th.mat-header-cell:first-of-type {
border-top-left-radius: 0;
}
th.mat-header-cell:last-of-type {
border-top-right-radius: 0;
}
.mat-table thead {
border-top-right-radius: 0;
border-top-left-radius: 0;
}
} }
} }
.mat-card { .mat-card {
.card-3;
width: 600px; width: 600px;
border-radius: 8px; border-radius: 8px;
background: #fffd; background: #fffa;
margin: 10px; margin: 10px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -92,7 +74,7 @@ body {
padding: 0; padding: 0;
} }
table { table.mat-table {
width: 100%; width: 100%;
background: none; background: none;
box-shadow: none; box-shadow: none;
@@ -105,4 +87,35 @@ body {
} }
} }
.mat-paginator {
background: #fffa;
}
table.mat-table {
background: #fffa;
tr.selected {
background-color: #0002;
}
tr:hover {
cursor: pointer;
background-color: #0001;
td {
color: #ff9900;
}
}
tr:focus {
outline: none;
}
td.mat-cell {
padding: 0 5px;
}
}
} }

View File

@@ -10,7 +10,6 @@
true, true,
"check-space" "check-space"
], ],
"curly": true,
"deprecation": { "deprecation": {
"severity": "warn" "severity": "warn"
}, },