20230731-angular-Erwieterung-authantificationAnd

This commit is contained in:
2023-07-31 12:36:17 +02:00
parent c7ba87409a
commit 6ae0d27dd4
24 changed files with 348 additions and 26 deletions

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BookCreateComponent } from './book-create/book-create.component';
const routes: Routes = [
{ path: 'admin', redirectTo: 'admin/create' },
{ path: 'admin/create', component: BookCreateComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AdminRoutingModule {}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import { FormsModule } from '@angular/forms';
import { BookFormComponent } from './book-form/book-form.component';
import { BookCreateComponent } from './book-create/book-create.component';
@NgModule({
declarations: [
BookFormComponent,
BookCreateComponent
],
imports: [CommonModule, AdminRoutingModule, FormsModule],
})
export class AdminModule {}

View File

@@ -0,0 +1,3 @@
<h1>Create Book</h1>
<bm-book-form (submitBook)="create($event)"></bm-book-form>

View File

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

View File

@@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { BookStoreService } from 'src/app/shared/book-store.service';
import { Book } from 'src/shared/book';
@Component({
selector: 'bm-book-create',
templateUrl: './book-create.component.html',
styleUrls: ['./book-create.component.css'],
})
export class BookCreateComponent {
constructor(
private service: BookStoreService,
private router: Router,
) {}
create(book: Book) {
this.service.create(book).subscribe((createdBook) => {
this.router.navigate(['/books', createdBook.isbn]);
});
}
}

View File

@@ -0,0 +1,19 @@
<form (ngSubmit)="submitForm()" #form="ngForm">
<label for="title">Title</label>
<input name="title" id="title" [(ngModel)]="book.title" required />
<label for="isbn">ISBN</label>
<input
name="isbn"
id="isbn"
[(ngModel)]="book.isbn"
required
minlength="10"
maxlength="13"
/>
<label for="author">Author</label>
<input name="author" id="author" [(ngModel)]="book.authors[0]" required />
<button type="submit" [disabled]="form.invalid">Save</button>
</form>

View File

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

View File

@@ -0,0 +1,21 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { Book } from 'src/shared/book';
@Component({
selector: 'bm-book-form',
templateUrl: './book-form.component.html',
styleUrls: ['./book-form.component.css'],
})
export class BookFormComponent {
@Output() submitBook = new EventEmitter<Book>();
submitForm() {
this.submitBook.emit(this.book);
}
book: Book = {
isbn: '',
title: '',
authors: [''],
};
}

View File

@@ -1,10 +1,21 @@
<nav> <nav>
<a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page" <a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page">
>Home</a Home
> </a>
<a routerLink="/books" routerLinkActive="active" ariaCurrentWhenActive="page" <a routerLink="/books" routerLinkActive="active" ariaCurrentWhenActive="page">
>Books</a Books
> </a>
<a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">
Administrator
</a>
<div class="actions">
<button class="green" (click)="auth.login()" *ngIf="!auth.isAuthenticated">
Login
</button>
<button class="red" (click)="auth.logout()" *ngIf="auth.isAuthenticated">
Logout
</button>
</div>
</nav> </nav>
<main> <main>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -1,19 +1,12 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Book } from 'src/shared/book'; import { AuthService } from './shared/auth.service';
@Component({ @Component({
selector: 'bm-root', selector: 'bm-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'], styleUrls: ['./app.component.css'],
}) })
export class AppComponent { export class AppComponent {
title = 'book-monkey'; constructor(public auth: AuthService) {}
book: Book | null = null;
showList() { title = 'book-monkey';
this.book = null;
}
showDetails(book: Book) {
this.book = book;
}
} }

View File

@@ -5,12 +5,17 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { BooksModule } from './books/books.module'; import { BooksModule } from './books/books.module';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { HttpClientModule } from '@angular/common/http'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { SearchComponent } from './search/search.component';
import { AuthInterceptor } from './shared/auth.interceptor';
import { AdminModule } from './admin/admin.module';
@NgModule({ @NgModule({
declarations: [AppComponent, HomeComponent], declarations: [AppComponent, HomeComponent, SearchComponent],
imports: [BrowserModule, AppRoutingModule, BooksModule, HttpClientModule], imports: [BrowserModule, AppRoutingModule, BooksModule, HttpClientModule, AdminModule],
providers: [], providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -15,8 +15,4 @@ export class BookListComponent {
constructor(private service: BookStoreService) { constructor(private service: BookStoreService) {
this.books$ = this.service.getAll(); this.books$ = this.service.getAll();
} }
doSelect(book: Book) {
this.selectBook.emit(book);
}
} }

View File

@@ -1,3 +1,6 @@
<h1>Home</h1> <h1>Home</h1>
<a routerLink="/books" class="button red">Show book list</a> <a routerLink="/books" class="button red">Show book list</a>
<h2>Search</h2>
<bm-search></bm-search>

View File

View File

@@ -0,0 +1,18 @@
<input
type="search"
autocomplete="off"
aria-label="Search"
[class.loading]="isLoading"
#searchInput
(input)="input$.next(searchInput.value)"
/>
<ul class="search-results" *ngIF="results$ | async as results">
<li *ngFor="let book of results">
<a [routerLink]="['/books', book.isbn]">
{{ book.title }}
<p role="doc-subtitle">{{ book.subtitle }}></p>
</a>
</li>
<li *ngIf="!results.length">No results</li>
</ul>

View File

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

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import {
Subject,
debounceTime,
distinctUntilChanged,
filter,
switchMap,
tap,
Observable,
} from 'rxjs';
import { BookStoreService } from '../shared/book-store.service';
import { Book } from 'src/shared/book';
@Component({
selector: 'bm-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.css'],
})
export class SearchComponent {
input$ = new Subject<string>();
isLoading = false;
results$: Observable<Book[]>;
constructor(private service: BookStoreService) {
this.results$ = this.input$.pipe(
filter((term) => term.length >= 3),
debounceTime(500),
distinctUntilChanged(),
tap(() => (this.isLoading = true)),
switchMap((term) => this.service.getAllSearch(term)),
tap(() => (this.isLoading = false)),
);
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AuthInterceptor } from './auth.interceptor';
describe('AuthInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
AuthInterceptor
]
}));
it('should be created', () => {
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
expect(interceptor).toBeTruthy();
});
});

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(
request: HttpRequest<unknown>,
next: HttpHandler,
): Observable<HttpEvent<unknown>> {
const token = '1234567890';
if (this.authService.isAuthenticated) {
//token in hearder einfügen
const reqWithToken = request.clone({
setHeaders: { Authorization: `Bearer${token}` },
});
return next.handle(reqWithToken);
} else {
//Request unverändert weitergeben
return next.handle(request);
}
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private _isAuthenticated$ = new BehaviorSubject(true);
readonly isAuthenticated$ = this._isAuthenticated$.asObservable();
constructor() {}
get isAuthenticated() {
return this._isAuthenticated$.value;
}
login() {
this._isAuthenticated$.next(true);
}
logout() {
this._isAuthenticated$.next(false);
}
}

View File

@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable, catchError, of } from 'rxjs';
import { Book } from 'src/shared/book'; import { Book } from 'src/shared/book';
@Injectable({ @Injectable({
@@ -12,7 +12,12 @@ export class BookStoreService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getAll(): Observable<Book[]> { getAll(): Observable<Book[]> {
return this.http.get<Book[]>(`${this.apiUrl}/books`); return this.http.get<Book[]>(`${this.apiUrl}/books`).pipe(
catchError((err) => {
console.error(err);
return of([]);
}),
);
} }
getSingle(isbn: string): Observable<Book> { getSingle(isbn: string): Observable<Book> {
@@ -22,4 +27,17 @@ export class BookStoreService {
remove(isbn: string): Observable<unknown> { remove(isbn: string): Observable<unknown> {
return this.http.delete(`${this.apiUrl}/books/${isbn}`); return this.http.delete(`${this.apiUrl}/books/${isbn}`);
} }
getAllSearch(term: string): Observable<Book[]> {
return this.http.get<Book[]>(`${this.apiUrl}/books/search/${term}`).pipe(
catchError((err) => {
console.error(err);
return of([]);
}),
);
}
create(book: Book): Observable<Book> {
return this.http.post<Book>(`${this.apiUrl}/books`, book);
}
} }