Add disconnection and minor improvements on login page.

This commit is contained in:
Florian THIERRY
2024-03-27 12:15:41 +01:00
parent 13c2cc8118
commit 0900df463a
14 changed files with 193 additions and 76 deletions

View File

@@ -4,5 +4,6 @@
app-header { app-header {
width: 100%; width: 100%;
margin-bottom: 1em;
} }
} }

View File

@@ -1,6 +1,16 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent) }, {
{ path: '**', loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent) } path: 'login',
loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent)
},
{
path: 'disconnect',
loadComponent: () => import('./pages/disconnection/disconnection.component').then(module => module.DisconnectionComponent)
},
{
path: '**',
loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent)
}
]; ];

View File

@@ -2,8 +2,10 @@
<button type="button"> <button type="button">
<mat-icon>menu</mat-icon> <mat-icon>menu</mat-icon>
</button> </button>
<img src="assets/images/codiki.png" alt="logo"/> <a [routerLink]="['/home']">
<span class="title">Codiki</span> <img src="assets/images/codiki.png" alt="logo"/>
<span class="title">Codiki</span>
</a>
</div> </div>
<div> <div>
<input name="search-query" placeholder="Search something..." /> <input name="search-query" placeholder="Search something..." />
@@ -12,5 +14,10 @@
</button> </button>
</div> </div>
<div> <div>
<a [routerLink]="['/login']">Login</a> <ng-container *ngIf="isAuthenticated; else anonymousRightMenu">
<a [routerLink]="['/disconnect']">Disconnect</a>
</ng-container>
<ng-template #anonymousRightMenu>
<a [routerLink]="['/login']">Login</a>
</ng-template>
</div> </div>

View File

@@ -25,14 +25,24 @@ $headerHeight: 3.5em;
gap: 1em; gap: 1em;
padding: 0 1em; padding: 0 1em;
img { a {
$imageSize: 2em; display: flex;
width: $imageSize; flex-direction: row;
height: $imageSize; justify-content: center;
} align-items: center;
color: white;
text-decoration: none;
gap: .5em;
.title { img {
font-size: 1.5em; $imageSize: 2em;
width: $imageSize;
height: $imageSize;
}
.title {
font-size: 1.5em;
}
} }
} }
@@ -76,6 +86,7 @@ $headerHeight: 3.5em;
align-items: center; align-items: center;
min-width: 5em; min-width: 5em;
color: white; color: white;
margin: 0 .5em;
} }
} }
} }

View File

@@ -1,13 +1,21 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { AuthenticationService } from '../../core/service/authentication.service';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
standalone: true, standalone: true,
imports: [MatButtonModule, MatIconModule, RouterModule], imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule],
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrl: './header.component.scss', styleUrl: './header.component.scss',
}) })
export class HeaderComponent {} export class HeaderComponent {
private authenticationService = inject(AuthenticationService);
get isAuthenticated(): boolean {
return this.authenticationService.isAuthenticated();
}
}

View File

@@ -55,7 +55,8 @@ export class AuthenticationService {
const tokenParts = token?.split('.'); const tokenParts = token?.split('.');
if (tokenParts?.length === 3 && tokenParts[1].length) { if (tokenParts?.length === 3 && tokenParts[1].length) {
const userDetails: UserDetails = JSON.parse(tokenParts[1]); const decodedTokenPart = atob(tokenParts[1]);
const userDetails: UserDetails = JSON.parse(decodedTokenPart);
result = userDetails; result = userDetails;
} }

View File

@@ -0,0 +1,2 @@
<h1>Disconnection...</h1>
<mat-spinner></mat-spinner>

View File

@@ -0,0 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,21 @@
import { Component, OnInit, inject } from '@angular/core';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import { AuthenticationService } from '../../core/service/authentication.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-disconnection',
standalone: true,
imports: [MatProgressSpinnerModule],
templateUrl: './disconnection.component.html',
styleUrl: './disconnection.component.scss'
})
export class DisconnectionComponent implements OnInit {
private authenticationService = inject(AuthenticationService);
private router = inject(Router);
ngOnInit(): void {
this.authenticationService.unauthenticate();
this.router.navigate(['/home']);
}
}

View File

@@ -1 +1 @@
<p>home works!</p> <h1>Welcome to Codiki application!</h1>

View File

@@ -0,0 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -4,7 +4,7 @@
<label for="email"> <label for="email">
Email address Email address
</label> </label>
<input type="email" formControlName="email" required /> <input type="email" formControlName="email" autocomplete="email" required />
</div> </div>
<div> <div>
<label for="password"> <label for="password">
@@ -12,5 +12,7 @@
</label> </label>
<input type="password" formControlName="password" required /> <input type="password" formControlName="password" required />
</div> </div>
<button type="submit">Send</button> <div class="actions">
<button type="submit">Send</button>
</div>
</form> </form>

View File

@@ -0,0 +1,37 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
form {
min-width: 40em;
max-width: 90%;
display: flex;
flex-direction: column;
justify-content: center;
gap: .5em;
div {
display: flex;
gap: .5em;
&.actions {
justify-content: end;
button {
width: 10em;
height: 2em;
}
}
label {
flex: 1 30%;
}
input {
flex: 1 70%;
}
}
}
}

View File

@@ -1,76 +1,81 @@
import { Injectable, inject } from "@angular/core"; import { Injectable, inject } from '@angular/core';
import { BehaviorSubject, Observable } from "rxjs"; import { BehaviorSubject, Observable } from 'rxjs';
import { copy } from "../../core/utils/ObjectUtils"; import { copy } from '../../core/utils/ObjectUtils';
import { FormError } from "../../core/model/FormError"; import { FormError } from '../../core/model/FormError';
import { UserRestService } from "../../core/rest-services/user.rest-service"; import { UserRestService } from '../../core/rest-services/user.rest-service';
import { LoginRequest } from "../../core/rest-services/model/login.model"; import { LoginRequest } from '../../core/rest-services/model/login.model';
import { AuthenticationService } from "../../core/service/authentication.service"; import { AuthenticationService } from '../../core/service/authentication.service';
import { MatSnackBar } from "@angular/material/snack-bar"; import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
export interface LoginState { export interface LoginState {
request: LoginRequest; request: LoginRequest;
errors: FormError[] errors: FormError[];
} }
const DEFAULT_STATE: LoginState = { const DEFAULT_STATE: LoginState = {
request: { request: {
email: undefined, email: undefined,
password: undefined password: undefined,
}, },
errors: [] errors: [],
} };
@Injectable() @Injectable()
export class LoginService { export class LoginService {
private stateSubject = new BehaviorSubject<LoginState>(copy(DEFAULT_STATE)); private stateSubject = new BehaviorSubject<LoginState>(copy(DEFAULT_STATE));
private userRestService = inject(UserRestService); private userRestService = inject(UserRestService);
private authenticationService = inject(AuthenticationService); private authenticationService = inject(AuthenticationService);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private router = inject(Router); private router = inject(Router);
get state$(): Observable<LoginState> {
return this.stateSubject.asObservable();
}
private get state(): LoginState { get state$(): Observable<LoginState> {
return this.stateSubject.value; return this.stateSubject.asObservable();
} }
private save(newState: LoginState): void { private get state(): LoginState {
this.stateSubject.next(newState); return this.stateSubject.value;
} }
editEmail(newEmail: string): void { private save(newState: LoginState): void {
const state = this.state; this.stateSubject.next(newState);
}
state.request.email = newEmail; editEmail(newEmail: string): void {
const state = this.state;
this.save(state); state.request.email = newEmail;
}
editPassword(newPassword: string): void { this.save(state);
const state = this.state; }
state.request.password = newPassword; editPassword(newPassword: string): void {
const state = this.state;
this.save(state); state.request.password = newPassword;
}
performLogin(): void { this.save(state);
const state = this.state; }
// Check state is valid performLogin(): void {
const state = this.state;
this.userRestService.login(state.request) // Check state is valid
.then(response => {
this.authenticationService.authenticate(response.accessToken); this.userRestService
this.snackBar.open('Authentication succeeded!', 'Close', { duration: 5000 }); .login(state.request)
this.router.navigate(['/home']); .then((response) => {
}) this.authenticationService.authenticate(response.accessToken);
.catch(error => { this.snackBar.open('Authentication succeeded!', 'Close', {
console.error(error) duration: 5000,
this.snackBar.open('Authentication failed.', 'Close', { duration: 5000 }); });
}); this.router.navigate(['/home']);
} })
} .catch((error) => {
console.error(error);
this.snackBar.open('Authentication failed.', 'Close', {
duration: 5000,
});
});
}
}