diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3b66410 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/devday/src/app/app-routing.module.ts b/devday/src/app/app-routing.module.ts deleted file mode 100644 index 06c7342..0000000 --- a/devday/src/app/app-routing.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] -}) -export class AppRoutingModule { } diff --git a/devday/src/app/app.component.html b/devday/src/app/app.component.html index 0f3d9d8..f39219c 100644 --- a/devday/src/app/app.component.html +++ b/devday/src/app/app.component.html @@ -1,21 +1,19 @@ - -
-

- Welcome to {{ title }}! -

- Angular Logo -
-

Here are some links to help you start:

- + + - + +
+
+
+
+ + +
+
+
+
diff --git a/devday/src/app/app.component.ts b/devday/src/app/app.component.ts index 0032be0..5540d69 100644 --- a/devday/src/app/app.component.ts +++ b/devday/src/app/app.component.ts @@ -1,10 +1,20 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { User } from './models/user'; +import { AuthenticationService } from './services/authentication.service'; -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] -}) + + +@Component({ selector: 'app', templateUrl: 'app.component.html' }) export class AppComponent { - title = 'devday'; + currentUser: User; + + constructor(private router: Router, private authenticationService: AuthenticationService) { + this.authenticationService.currentUser.subscribe(x => (this.currentUser = x)); + } + + logout() { + this.authenticationService.logout(); + this.router.navigate(['/login']); + } } diff --git a/devday/src/app/app.module.ts b/devday/src/app/app.module.ts index a6a8237..f9ac4a8 100644 --- a/devday/src/app/app.module.ts +++ b/devday/src/app/app.module.ts @@ -1,21 +1,34 @@ -import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; + +// used to create fake backend +import { fakeBackendProvider } from './shared/helper/fake-backend'; + +import { JwtInterceptor } from './shared/helper/jwt.interceptor'; +import { ErrorInterceptor } from './shared/helper/error.interceptor'; + +import { appRoutingModule } from './routing/app.routing'; -import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { ServiceWorkerModule } from '@angular/service-worker'; -import { environment } from '../environments/environment'; +import { HomeComponent } from './pages/home/home.component'; +import { LoginComponent } from './modules/login/components/login/login.component'; +import { RegisterComponent } from './pages/register/register.component'; +import { AlertComponent } from './modules/alert/components/alert/alert.component'; + + @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - AppRoutingModule, - ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) + imports: [BrowserModule, ReactiveFormsModule, HttpClientModule, appRoutingModule], + declarations: [AppComponent, HomeComponent, LoginComponent, RegisterComponent, AlertComponent], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, + + // provider used to create fake backend + fakeBackendProvider ], - providers: [], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule {} diff --git a/devday/src/app/models/user.ts b/devday/src/app/models/user.ts new file mode 100644 index 0000000..c783594 --- /dev/null +++ b/devday/src/app/models/user.ts @@ -0,0 +1,6 @@ +export class User { + id: number; + email: string; + password: string; + token: string; +} diff --git a/devday/src/app/modules/alert/alert.module.ts b/devday/src/app/modules/alert/alert.module.ts new file mode 100644 index 0000000..488902f --- /dev/null +++ b/devday/src/app/modules/alert/alert.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AlertComponent } from './components/alert/alert.component'; + + + +@NgModule({ + declarations: [AlertComponent], + imports: [ + CommonModule + ] +}) +export class AlertModule { } diff --git a/devday/src/app/modules/alert/components/alert/alert.component.html b/devday/src/app/modules/alert/components/alert/alert.component.html new file mode 100644 index 0000000..f2f369d --- /dev/null +++ b/devday/src/app/modules/alert/components/alert/alert.component.html @@ -0,0 +1 @@ +
{{ message.text }}
diff --git a/devday/src/app/modules/alert/components/alert/alert.component.scss b/devday/src/app/modules/alert/components/alert/alert.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/devday/src/app/modules/alert/components/alert/alert.component.spec.ts b/devday/src/app/modules/alert/components/alert/alert.component.spec.ts new file mode 100644 index 0000000..3921dd6 --- /dev/null +++ b/devday/src/app/modules/alert/components/alert/alert.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AlertComponent } from './alert.component'; + +describe('AlertComponent', () => { + let component: AlertComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AlertComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/devday/src/app/modules/alert/components/alert/alert.component.ts b/devday/src/app/modules/alert/components/alert/alert.component.ts new file mode 100644 index 0000000..1a01bb3 --- /dev/null +++ b/devday/src/app/modules/alert/components/alert/alert.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { AlertService } from 'src/app/services/alert.service'; + + +@Component({ selector: 'alert', templateUrl: 'alert.component.html' }) +export class AlertComponent implements OnInit, OnDestroy { + private subscription: Subscription; + message: any; + + constructor(private alertService: AlertService) { } + + ngOnInit() { + this.subscription = this.alertService.getAlert() + .subscribe(message => { + switch (message && message.type) { + case 'success': + message.cssClass = 'alert alert-success'; + break; + case 'error': + message.cssClass = 'alert alert-danger'; + break; + } + + this.message = message; + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/devday/src/app/modules/login/components/login/login.component.html b/devday/src/app/modules/login/components/login/login.component.html new file mode 100644 index 0000000..53fe068 --- /dev/null +++ b/devday/src/app/modules/login/components/login/login.component.html @@ -0,0 +1,34 @@ +

Anmelden

+
+
+ + +
+
E-Mail ist erforderlich
+
+
+
+ + +
+
Passwort ist erforderlich
+
+
+
+ + Registrieren +
+
diff --git a/devday/src/app/modules/login/components/login/login.component.scss b/devday/src/app/modules/login/components/login/login.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/devday/src/app/modules/login/components/login/login.component.spec.ts b/devday/src/app/modules/login/components/login/login.component.spec.ts new file mode 100644 index 0000000..d6d85a8 --- /dev/null +++ b/devday/src/app/modules/login/components/login/login.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/devday/src/app/modules/login/components/login/login.component.ts b/devday/src/app/modules/login/components/login/login.component.ts new file mode 100644 index 0000000..57b22e9 --- /dev/null +++ b/devday/src/app/modules/login/components/login/login.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { first } from 'rxjs/operators'; +import { AuthenticationService } from 'src/app/services/authentication.service'; +import { AlertService } from 'src/app/services/alert.service'; + +@Component({ templateUrl: 'login.component.html' }) +export class LoginComponent implements OnInit { + loginForm: FormGroup; + loading = false; + submitted = false; + returnUrl: string; + + constructor( + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private authenticationService: AuthenticationService, + private alertService: AlertService + ) { + // redirect to home if already logged in + if (this.authenticationService.currentUserValue) { + this.router.navigate(['/']); + } + } + + ngOnInit() { + this.loginForm = this.formBuilder.group({ + email: ['', Validators.required], + password: ['', Validators.required] + }); + + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/'; + } + + // convenience getter for easy access to form fields + get f() { + return this.loginForm.controls; + } + + onSubmit() { + this.submitted = true; + + // reset alerts on submit + this.alertService.clear(); + + // stop here if form is invalid + if (this.loginForm.invalid) { + return; + } + + this.loading = true; + this.authenticationService + .login(this.f.email.value, this.f.password.value) + .pipe(first()) + .subscribe( + data => { + this.router.navigate([this.returnUrl]); + }, + error => { + this.alertService.error(error); + this.loading = false; + } + ); + } +} diff --git a/devday/src/app/modules/login/login.module.ts b/devday/src/app/modules/login/login.module.ts new file mode 100644 index 0000000..6ca0181 --- /dev/null +++ b/devday/src/app/modules/login/login.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LoginComponent } from './components/login/login.component'; + + + +@NgModule({ + declarations: [LoginComponent], + imports: [ + CommonModule + ] +}) +export class LoginModule { } diff --git a/devday/src/app/pages/home/home.component.html b/devday/src/app/pages/home/home.component.html new file mode 100644 index 0000000..cd7ad67 --- /dev/null +++ b/devday/src/app/pages/home/home.component.html @@ -0,0 +1,9 @@ +

Hallo {{ currentUser.email }}!

+

Sie sind eingeloggt

+

Alle registrierten Benutzer:

+ diff --git a/devday/src/app/pages/home/home.component.scss b/devday/src/app/pages/home/home.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/devday/src/app/pages/home/home.component.spec.ts b/devday/src/app/pages/home/home.component.spec.ts new file mode 100644 index 0000000..490e81b --- /dev/null +++ b/devday/src/app/pages/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/devday/src/app/pages/home/home.component.ts b/devday/src/app/pages/home/home.component.ts new file mode 100644 index 0000000..852ac69 --- /dev/null +++ b/devday/src/app/pages/home/home.component.ts @@ -0,0 +1,34 @@ +import { Component, OnInit } from '@angular/core'; +import { first } from 'rxjs/operators'; +import { AuthenticationService } from 'src/app/services/authentication.service'; +import { User } from 'src/app/models/user'; +import { UserService } from 'src/app/services/user.service'; + + +@Component({ templateUrl: 'home.component.html' }) +export class HomeComponent implements OnInit { + currentUser: User; + users = []; + + constructor(private authenticationService: AuthenticationService, private userService: UserService) { + this.currentUser = this.authenticationService.currentUserValue; + } + + ngOnInit() { + this.loadAllUsers(); + } + + deleteUser(id: number) { + this.userService + .delete(id) + .pipe(first()) + .subscribe(() => this.loadAllUsers()); + } + + private loadAllUsers() { + this.userService + .getAll() + .pipe(first()) + .subscribe(users => (this.users = users)); + } +} diff --git a/devday/src/app/pages/register/register.component.html b/devday/src/app/pages/register/register.component.html new file mode 100644 index 0000000..7c1a914 --- /dev/null +++ b/devday/src/app/pages/register/register.component.html @@ -0,0 +1,49 @@ +

Registrieren

+
+
+ + +
+
E-Mail ist erforderlich
+
Dies ist keine gültige E-Mail Adresse
+
+
+
+ + +
+
Passwort ist erforderlich
+
Passwort muss mindestens 6 Zeichen lang sein
+
+
+
+ + +
+
Passwort ist erforderlich
+
Passwort stimmt nicht überein
+
+
+
+ + Abbrechen +
+
diff --git a/devday/src/app/pages/register/register.component.scss b/devday/src/app/pages/register/register.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/devday/src/app/pages/register/register.component.spec.ts b/devday/src/app/pages/register/register.component.spec.ts new file mode 100644 index 0000000..6c19551 --- /dev/null +++ b/devday/src/app/pages/register/register.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RegisterComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/devday/src/app/pages/register/register.component.ts b/devday/src/app/pages/register/register.component.ts new file mode 100644 index 0000000..4de4d99 --- /dev/null +++ b/devday/src/app/pages/register/register.component.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl } from '@angular/forms'; +import { first } from 'rxjs/operators'; +import { AuthenticationService } from 'src/app/services/authentication.service'; +import { UserService } from 'src/app/services/user.service'; +import { AlertService } from 'src/app/services/alert.service'; + +@Component({ templateUrl: 'register.component.html' }) +export class RegisterComponent implements OnInit { + registerForm: FormGroup; + loading = false; + submitted = false; + + matchPassword(): ValidatorFn { + return (currentControl: AbstractControl): {} => { + if (this.registerForm && currentControl.value === this.registerForm.controls.password.value) { + return null; + } else { + return { matchPassword: 'noMatch' }; + } + }; + } + + constructor( + private formBuilder: FormBuilder, + private router: Router, + private authenticationService: AuthenticationService, + private userService: UserService, + private alertService: AlertService + ) { + // redirect to home if already logged in + if (this.authenticationService.currentUserValue) { + this.router.navigate(['/']); + } + } + + ngOnInit() { + this.registerForm = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + password_confirm: ['', [Validators.required, this.matchPassword()]] + }); + } + + // convenience getter for easy access to form fields + get f() { + return this.registerForm.controls; + } + + onSubmit() { + this.submitted = true; + + // reset alerts on submit + this.alertService.clear(); + + // stop here if form is invalid + if (this.registerForm.invalid) { + return; + } + + this.loading = true; + this.userService + .register(this.registerForm.value) + .pipe(first()) + .subscribe( + data => { + this.alertService.success('Registration successful', true); + this.router.navigate(['/login']); + }, + error => { + this.alertService.error(error); + this.loading = false; + } + ); + } +} diff --git a/devday/src/app/routing/app.routing.ts b/devday/src/app/routing/app.routing.ts new file mode 100644 index 0000000..58d1a05 --- /dev/null +++ b/devday/src/app/routing/app.routing.ts @@ -0,0 +1,17 @@ +import { Routes, RouterModule } from '@angular/router'; +import { HomeComponent } from '../pages/home/home.component'; +import { AuthGuard } from './auth.guard'; +import { LoginComponent } from '../modules/login/components/login/login.component'; +import { RegisterComponent } from '../pages/register/register.component'; + + +const routes: Routes = [ + { path: '', component: HomeComponent, canActivate: [AuthGuard] }, + { path: 'login', component: LoginComponent }, + { path: 'register', component: RegisterComponent }, + + // otherwise redirect to home + { path: '**', redirectTo: '' } +]; + +export const appRoutingModule = RouterModule.forRoot(routes); diff --git a/devday/src/app/routing/auth.guard.spec.ts b/devday/src/app/routing/auth.guard.spec.ts new file mode 100644 index 0000000..7ed05ee --- /dev/null +++ b/devday/src/app/routing/auth.guard.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, async, inject } 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(); + })); +}); diff --git a/devday/src/app/routing/auth.guard.ts b/devday/src/app/routing/auth.guard.ts new file mode 100644 index 0000000..be8bee5 --- /dev/null +++ b/devday/src/app/routing/auth.guard.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, CanActivate, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { AuthenticationService } from '../services/authentication.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor( + private router: Router, + private authenticationService: AuthenticationService +) {} + +canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + const currentUser = this.authenticationService.currentUserValue; + if (currentUser) { + // authorised so return true + return true; + } + + // not logged in so redirect to login page with the return url + this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }}); + return false; +} +} diff --git a/devday/src/app/services/alert.service.spec.ts b/devday/src/app/services/alert.service.spec.ts new file mode 100644 index 0000000..679f77a --- /dev/null +++ b/devday/src/app/services/alert.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { AlertService } from './alert.service'; + +describe('AlertService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: AlertService = TestBed.get(AlertService); + expect(service).toBeTruthy(); + }); +}); diff --git a/devday/src/app/services/alert.service.ts b/devday/src/app/services/alert.service.ts new file mode 100644 index 0000000..5de9e9c --- /dev/null +++ b/devday/src/app/services/alert.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { Subject, Observable } from 'rxjs'; +import { Router, NavigationStart } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class AlertService { + + private subject = new Subject(); + private keepAfterNavigationChange = false; + + constructor(private router: Router) { + // clear alert message on route change + this.router.events.subscribe(event => { + if (event instanceof NavigationStart) { + if (this.keepAfterNavigationChange) { + // only keep for a single location change + this.keepAfterNavigationChange = false; + } else { + // clear alert + this.clear(); + } + } + }); + } + + success(message: string, keepAfterNavigationChange = false) { + this.keepAfterNavigationChange = keepAfterNavigationChange; + this.subject.next({ type: 'success', text: message }); + } + + error(message: string, keepAfterNavigationChange = false) { + this.keepAfterNavigationChange = keepAfterNavigationChange; + this.subject.next({ type: 'error', text: message }); + } + + getAlert(): Observable { + return this.subject.asObservable(); + } + + clear() { + + // clear by calling subject.next() without parameters + + this.subject.next(); + + } +} diff --git a/devday/src/app/services/authentication.service.spec.ts b/devday/src/app/services/authentication.service.spec.ts new file mode 100644 index 0000000..91a1e97 --- /dev/null +++ b/devday/src/app/services/authentication.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthenticationService } from './authentication.service'; + +describe('AuthenticationService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: AuthenticationService = TestBed.get(AuthenticationService); + expect(service).toBeTruthy(); + }); +}); diff --git a/devday/src/app/services/authentication.service.ts b/devday/src/app/services/authentication.service.ts new file mode 100644 index 0000000..fe5e088 --- /dev/null +++ b/devday/src/app/services/authentication.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { User } from '../models/user'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationService { + private currentUserSubject: BehaviorSubject; + public currentUser: Observable; + + constructor(private http: HttpClient) { + this.currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser'))); + this.currentUser = this.currentUserSubject.asObservable(); + } + + public get currentUserValue(): User { + return this.currentUserSubject.value; + } + + login(email: string, password: string) { + return this.http.post(`${environment.apiUrl}/users/authenticate`, { email, password }).pipe( + map(user => { + // login successful if there's a jwt token in the response + if (user && user.token) { + // store user details and jwt token in local storage to keep user logged in between page refreshes + localStorage.setItem('currentUser', JSON.stringify(user)); + this.currentUserSubject.next(user); + } + + return user; + }) + ); + } + + logout() { + // remove user from local storage to log user out + localStorage.removeItem('currentUser'); + this.currentUserSubject.next(null); + } +} diff --git a/devday/src/app/services/user.service.spec.ts b/devday/src/app/services/user.service.spec.ts new file mode 100644 index 0000000..9e7fd1c --- /dev/null +++ b/devday/src/app/services/user.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: UserService = TestBed.get(UserService); + expect(service).toBeTruthy(); + }); +}); diff --git a/devday/src/app/services/user.service.ts b/devday/src/app/services/user.service.ts new file mode 100644 index 0000000..4c44bf3 --- /dev/null +++ b/devday/src/app/services/user.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { User } from '../models/user'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + constructor(private http: HttpClient) {} + + getAll() { + return this.http.get(`${environment.apiUrl}/users`); + } + + getById(id: number) { + return this.http.get(`${environment.apiUrl}/users/${id}`); + } + + register(user: User) { + return this.http.post(`${environment.apiUrl}/users/register`, user); + } + + update(user: User) { + return this.http.put(`${environment.apiUrl}/users/${user.id}`, user); + } + + delete(id: number) { + return this.http.delete(`${environment.apiUrl}/users/${id}`); + } +} diff --git a/devday/src/app/shared/helper/error.interceptor.ts b/devday/src/app/shared/helper/error.interceptor.ts new file mode 100644 index 0000000..536faed --- /dev/null +++ b/devday/src/app/shared/helper/error.interceptor.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { AuthenticationService } from 'src/app/services/authentication.service'; + + + +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + constructor(private authenticationService: AuthenticationService) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe(catchError(err => { + if (err.status === 401) { + // auto logout if 401 response returned from api + this.authenticationService.logout(); + location.reload(true); + } + + const error = err.error.message || err.statusText; + return throwError(error); + })) + } +} \ No newline at end of file diff --git a/devday/src/app/shared/helper/fake-backend.ts b/devday/src/app/shared/helper/fake-backend.ts new file mode 100644 index 0000000..1844778 --- /dev/null +++ b/devday/src/app/shared/helper/fake-backend.ts @@ -0,0 +1,144 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpResponse, + HttpHandler, + HttpEvent, + HttpInterceptor, + HTTP_INTERCEPTORS +} from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators'; + +@Injectable() +export class FakeBackendInterceptor implements HttpInterceptor { + constructor() {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // array in local storage for registered users + const users: any[] = JSON.parse(localStorage.getItem('users')) || []; + + // wrap in delayed observable to simulate server api call + return ( + of(null) + .pipe( + mergeMap(() => { + // authenticate + if (request.url.endsWith('/users/authenticate') && request.method === 'POST') { + // find if any user matches login credentials + const filteredUsers = users.filter(user => { + return user.email === request.body.email && user.password === request.body.password; + }); + + if (filteredUsers.length) { + // if login details are valid return 200 OK with user details and fake jwt token + const user = filteredUsers[0]; + const body = { + id: user.id, + email: user.email, + token: 'fake-jwt-token' + }; + + return of(new HttpResponse({ status: 200, body })); + } else { + // else return 400 bad request + return throwError({ error: { message: 'E-Mail oder Passwort ist nicht korrekt' } }); + } + } + + // get users + if (request.url.endsWith('/users') && request.method === 'GET') { + // check for fake auth token in header and return users if valid, this security is implemented server side in a real application + if (request.headers.get('Authorization') === 'Bearer fake-jwt-token') { + return of(new HttpResponse({ status: 200, body: users })); + } else { + // return 401 not authorised if token is null or invalid + return throwError({ status: 401, error: { message: 'Nicht authorisiert' } }); + } + } + + // get user by id + if (request.url.match(/\/users\/\d+$/) && request.method === 'GET') { + // check for fake auth token in header and return user if valid, this security is implemented server side in a real application + if (request.headers.get('Authorization') === 'Bearer fake-jwt-token') { + // find user by id in users array + const urlParts = request.url.split('/'); + const id = parseInt(urlParts[urlParts.length - 1]); + const matchedUsers = users.filter(user => { + return user.id === id; + }); + const user = matchedUsers.length ? matchedUsers[0] : null; + + return of(new HttpResponse({ status: 200, body: user })); + } else { + // return 401 not authorised if token is null or invalid + return throwError({ status: 401, error: { message: 'Nicht authorisiert' } }); + } + } + + // register user + if (request.url.endsWith('/users/register') && request.method === 'POST') { + // get new user object from post body + const newUser = request.body; + + // validation + const duplicateUser = users.filter(user => { + return user.email === newUser.email; + }).length; + if (duplicateUser) { + return throwError({ error: { message: 'E-Mail "' + newUser.email + '" wird bereits benutzt' } }); + } + + // save new user + newUser.id = users.length + 1; + users.push(newUser); + localStorage.setItem('users', JSON.stringify(users)); + + // respond 200 OK + return of(new HttpResponse({ status: 200 })); + } + + // delete user + if (request.url.match(/\/users\/\d+$/) && request.method === 'DELETE') { + // check for fake auth token in header and return user if valid, this security is implemented server side in a real application + if (request.headers.get('Authorization') === 'Bearer fake-jwt-token') { + // find user by id in users array + const urlParts = request.url.split('/'); + const id = parseInt(urlParts[urlParts.length - 1]); + for (let i = 0; i < users.length; i++) { + const user = users[i]; + if (user.id === id) { + // delete user + users.splice(i, 1); + localStorage.setItem('users', JSON.stringify(users)); + break; + } + } + + // respond 200 OK + return of(new HttpResponse({ status: 200 })); + } else { + // return 401 not authorised if token is null or invalid + return throwError({ status: 401, error: { message: 'Nicht authorisiert' } }); + } + } + + // pass through any requests not handled above + return next.handle(request); + }) + ) + + // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648) + .pipe(materialize()) + .pipe(delay(500)) + .pipe(dematerialize()) + ); + } +} + +export let fakeBackendProvider = { + // use fake backend in place of Http service for backend-less development + provide: HTTP_INTERCEPTORS, + useClass: FakeBackendInterceptor, + multi: true +}; diff --git a/devday/src/app/shared/helper/jwt.interceptor.ts b/devday/src/app/shared/helper/jwt.interceptor.ts new file mode 100644 index 0000000..2339978 --- /dev/null +++ b/devday/src/app/shared/helper/jwt.interceptor.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AuthenticationService } from 'src/app/services/authentication.service'; + + + +@Injectable() +export class JwtInterceptor implements HttpInterceptor { + constructor(private authenticationService: AuthenticationService) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // add authorization header with jwt token if available + let currentUser = this.authenticationService.currentUserValue; + if (currentUser && currentUser.token) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${currentUser.token}` + } + }); + } + + return next.handle(request); + } +} \ No newline at end of file diff --git a/devday/src/environments/environment.prod.ts b/devday/src/environments/environment.prod.ts index 3612073..ef9a6bb 100644 --- a/devday/src/environments/environment.prod.ts +++ b/devday/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + apiUrl: 'http://localhost:4000' }; diff --git a/devday/src/environments/environment.ts b/devday/src/environments/environment.ts index 7b4f817..c77b96d 100644 --- a/devday/src/environments/environment.ts +++ b/devday/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + apiUrl: 'http://localhost:4000' }; /* diff --git a/devday/src/index.html b/devday/src/index.html index 09cc40f..478d5f5 100644 --- a/devday/src/index.html +++ b/devday/src/index.html @@ -1,17 +1,14 @@ - - - - - Devday - + + + + + Angular 8 User Registration and Login Example + - - - - - - - - - + + + + + Loading... + diff --git a/devday/src/main.ts b/devday/src/main.ts index c7b673c..1d91a20 100644 --- a/devday/src/main.ts +++ b/devday/src/main.ts @@ -1,12 +1,6 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import './polyfills'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; - -if (environment.production) { - enableProdMode(); -} -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/devday/src/polyfills.ts b/devday/src/polyfills.ts index aa665d6..346394a 100644 --- a/devday/src/polyfills.ts +++ b/devday/src/polyfills.ts @@ -55,8 +55,8 @@ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js/dist/zone'; // Included with Angular CLI. - +import 'zone.js/dist/zone'; // Included with Angular CLI. +import 'core-js/features/reflect'; /*************************************************************************************************** * APPLICATION IMPORTS