Add signin page.
This commit is contained in:
@@ -54,7 +54,8 @@ public class SecurityConfiguration {
|
||||
.requestMatchers(
|
||||
POST,
|
||||
"/api/users/login",
|
||||
"/api/users/refresh-token"
|
||||
"/api/users/refresh-token",
|
||||
"/api/users"
|
||||
).permitAll()
|
||||
.requestMatchers(
|
||||
POST,
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
<app-header></app-header>
|
||||
<router-outlet></router-outlet>
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
@@ -5,6 +5,10 @@ export const routes: Routes = [
|
||||
path: 'login',
|
||||
loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent)
|
||||
},
|
||||
{
|
||||
path: 'signin',
|
||||
loadComponent: () => import('./pages/signin/signin.component').then(module => module.SigninComponent)
|
||||
},
|
||||
{
|
||||
path: 'disconnect',
|
||||
loadComponent: () => import('./pages/disconnection/disconnection.component').then(module => module.DisconnectionComponent)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SigninRequest {
|
||||
pseudo?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { LoginRequest, LoginResponse } from "./model/login.model";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { SigninRequest } from "./model/signin.model";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -12,4 +13,8 @@ export class UserRestService {
|
||||
login(request: LoginRequest): Promise<LoginResponse> {
|
||||
return lastValueFrom(this.httpClient.post<LoginResponse>('/api/users/login', request));
|
||||
}
|
||||
|
||||
signin(request: SigninRequest): Promise<void> {
|
||||
return lastValueFrom(this.httpClient.post<void>('/api/users', request));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ $cardBorderRadius: .5em;
|
||||
transition: box-shadow .2s ease-in-out;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
background-color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,.24),0 4px 14px 0 rgba(0,0,0,.16);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="loginForm" (submit)="performLogin()" ngNativeValidate>
|
||||
<h1>Login</h1>
|
||||
<div>
|
||||
<mat-icon>person</mat-icon>
|
||||
<mat-icon>mail</mat-icon>
|
||||
<label for="email">
|
||||
Email address
|
||||
<span class="required">*</span>
|
||||
@@ -18,6 +18,6 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit">Send</button>
|
||||
<a [routerLink]="['/home']">Create an account</a>
|
||||
<a [routerLink]="['/signin']">Create an account</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -15,6 +15,7 @@
|
||||
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
border-radius: .5em;
|
||||
padding: 1em 1.5em;
|
||||
background-color: #ffffff;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
@@ -28,7 +29,7 @@
|
||||
|
||||
mat-icon {
|
||||
position: absolute;
|
||||
top: 1.2em;
|
||||
top: 1.3em;
|
||||
left: .5em;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private loginService = inject(LoginService);
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private subscriptions: Subscription[] = [];
|
||||
emailValue: string | undefined;
|
||||
loginForm: FormGroup = this.formBuilder.group({
|
||||
email: new FormControl<string | undefined>('', [Validators.required, Validators.email]),
|
||||
password: new FormControl<string | undefined>('', [Validators.required])
|
||||
|
||||
39
frontend/src/app/pages/signin/signin.component.html
Normal file
39
frontend/src/app/pages/signin/signin.component.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<form [formGroup]="signinForm" (submit)="performSignin()" ngNativeValidate>
|
||||
<h1>Signin</h1>
|
||||
<div>
|
||||
<mat-icon>person</mat-icon>
|
||||
<label for="pseudo">
|
||||
Pseudo
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="pseudo" formControlName="pseudo" autocomplete="pseudo" required />
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon>mail</mat-icon>
|
||||
<label for="email">
|
||||
Email address
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<input type="email" id="email" formControlName="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon>lock</mat-icon>
|
||||
<label for="password">
|
||||
Password
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<input type="password" id="password" formControlName="password" required />
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon>lock</mat-icon>
|
||||
<label for="confirm-password">
|
||||
Confirm password
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<input type="password" id="confirm-password" formControlName="confirmPassword" required />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit">Send</button>
|
||||
<a [routerLink]="['/login']">I already have an account</a>
|
||||
</div>
|
||||
</form>
|
||||
82
frontend/src/app/pages/signin/signin.component.scss
Normal file
82
frontend/src/app/pages/signin/signin.component.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
|
||||
form {
|
||||
width: 80%;
|
||||
max-width: 20em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||
border-radius: .5em;
|
||||
padding: 1em 1.5em;
|
||||
background-color: #ffffff;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
gap: .1em;
|
||||
|
||||
mat-icon {
|
||||
position: absolute;
|
||||
top: 1.3em;
|
||||
left: .5em;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
&.actions {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
padding: .8em 1.2em;
|
||||
border-radius: 10em;
|
||||
border: none;
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
transition: background-color .2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #5b6ed8;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3f51b5;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
font-style: italic;
|
||||
padding-left: 1em;
|
||||
color: #777;
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
background-color: #eeeeee;
|
||||
border: none;
|
||||
border-radius: 10em;
|
||||
padding: 1em 1em 1em 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
frontend/src/app/pages/signin/signin.component.spec.ts
Normal file
23
frontend/src/app/pages/signin/signin.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SigninComponent } from './signin.component';
|
||||
|
||||
describe('SigninComponent', () => {
|
||||
let component: SigninComponent;
|
||||
let fixture: ComponentFixture<SigninComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SigninComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SigninComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
96
frontend/src/app/pages/signin/signin.component.ts
Normal file
96
frontend/src/app/pages/signin/signin.component.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Subscription, debounceTime, distinctUntilChanged, map } from 'rxjs';
|
||||
import { SigninService } from './signin.service';
|
||||
import { LoginService } from '../login/login.service';
|
||||
import { FormError } from '../../core/model/FormError';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signin',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, MatIconModule, RouterModule],
|
||||
templateUrl: './signin.component.html',
|
||||
styleUrl: './signin.component.scss',
|
||||
providers: [SigninService, LoginService]
|
||||
})
|
||||
export class SigninComponent implements OnInit, OnDestroy {
|
||||
private signinService = inject(SigninService);
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
signinForm: FormGroup = this.formBuilder.group({
|
||||
pseudo: new FormControl<string | undefined>('', Validators.required),
|
||||
email: new FormControl<string | undefined>('', [Validators.required, Validators.email]),
|
||||
password: new FormControl<string | undefined>('', [Validators.required]),
|
||||
confirmPassword: new FormControl<string | undefined>('', [Validators.required])
|
||||
});
|
||||
|
||||
formErrors: FormError[] = []
|
||||
|
||||
ngOnInit(): void {
|
||||
const pseudoSubscription = this.signinForm.controls['pseudo'].valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
map(value => value?.length ? value as string : '')
|
||||
)
|
||||
.subscribe(pseudo => {
|
||||
this.signinService.editPseudo(pseudo);
|
||||
});
|
||||
this.subscriptions.push(pseudoSubscription);
|
||||
|
||||
const emailSubscription = this.signinForm.controls['email'].valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
map(value => value?.length ? value as string : '')
|
||||
)
|
||||
.subscribe(email => {
|
||||
this.signinService.editEmail(email);
|
||||
});
|
||||
this.subscriptions.push(emailSubscription);
|
||||
|
||||
const passwordSubscription = this.signinForm.controls['password'].valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
map(value => value?.length ? value as string : '')
|
||||
)
|
||||
.subscribe(password => {
|
||||
this.signinService.editPassword(password);
|
||||
});
|
||||
this.subscriptions.push(passwordSubscription);
|
||||
|
||||
const confirmPasswordSubscription = this.signinForm.controls['confirmPassword'].valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
map(value => value?.length ? value as string : '')
|
||||
)
|
||||
.subscribe(confirmPassword => {
|
||||
this.signinService.editConfirmPassword(confirmPassword);
|
||||
});
|
||||
this.subscriptions.push(confirmPasswordSubscription);
|
||||
|
||||
const stateSubscription = this.signinService.state$
|
||||
.subscribe(state => {
|
||||
this.signinForm.controls['pseudo'].setValue(state.request.pseudo, { emitEvent: false });
|
||||
this.signinForm.controls['email'].setValue(state.request.email, { emitEvent: false });
|
||||
this.signinForm.controls['password'].setValue(state.request.password, { emitEvent: false });
|
||||
this.signinForm.controls['confirmPassword'].setValue(state.confirmPassword, { emitEvent: false });
|
||||
});
|
||||
this.subscriptions.push(stateSubscription);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
performSignin(): void {
|
||||
if (this.signinForm.valid) {
|
||||
this.signinService.performSignin();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
frontend/src/app/pages/signin/signin.service.ts
Normal file
102
frontend/src/app/pages/signin/signin.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { SigninRequest } from '../../core/rest-services/user/model/signin.model';
|
||||
import { FormError } from '../../core/model/FormError';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { copy } from '../../core/utils/ObjectUtils';
|
||||
import { UserRestService } from '../../core/rest-services/user/user.rest-service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { LoginService } from '../login/login.service';
|
||||
|
||||
export interface SigninState {
|
||||
request: SigninRequest;
|
||||
confirmPassword?: string;
|
||||
errors: FormError[];
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: SigninState = {
|
||||
request: {
|
||||
pseudo: undefined,
|
||||
email: undefined,
|
||||
password: undefined,
|
||||
},
|
||||
confirmPassword: undefined,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SigninService {
|
||||
private stateSubject = new BehaviorSubject<SigninState>(copy(DEFAULT_STATE));
|
||||
private userRestService = inject(UserRestService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private router = inject(Router);
|
||||
private loginService = inject(LoginService);
|
||||
|
||||
get state$(): Observable<SigninState> {
|
||||
return this.stateSubject.asObservable();
|
||||
}
|
||||
|
||||
private get state(): SigninState {
|
||||
return this.stateSubject.value;
|
||||
}
|
||||
|
||||
private save(newState: SigninState): void {
|
||||
this.stateSubject.next(newState);
|
||||
}
|
||||
|
||||
editPseudo(newPseudo: string): void {
|
||||
const state = this.state;
|
||||
|
||||
state.request.pseudo = newPseudo;
|
||||
|
||||
this.save(state);
|
||||
}
|
||||
|
||||
editEmail(newEmail: string): void {
|
||||
const state = this.state;
|
||||
|
||||
state.request.email = newEmail;
|
||||
|
||||
this.save(state);
|
||||
}
|
||||
|
||||
editPassword(newPassword: string): void {
|
||||
const state = this.state;
|
||||
|
||||
state.request.password = newPassword;
|
||||
|
||||
this.save(state);
|
||||
}
|
||||
|
||||
editConfirmPassword(newConfirmPassword: string): void {
|
||||
const state = this.state;
|
||||
|
||||
state.confirmPassword = newConfirmPassword;
|
||||
|
||||
if (state.request.password !== state.confirmPassword) {
|
||||
const confirmPasswordError: FormError = {
|
||||
fieldName: 'confirmPassword',
|
||||
errorMessage: 'Typed password are different.'
|
||||
}
|
||||
state.errors.filter(error => error.fieldName !== 'confirmPassword');
|
||||
state.errors.push(confirmPasswordError)
|
||||
}
|
||||
|
||||
this.save(state);
|
||||
}
|
||||
|
||||
performSignin(): void {
|
||||
const state = this.state;
|
||||
|
||||
// Check state is valid
|
||||
|
||||
this.userRestService
|
||||
.signin(state.request)
|
||||
.then(() => {
|
||||
this.loginService.editEmail(state.request.email!!);
|
||||
this.loginService.editPassword(state.request.password!!);
|
||||
|
||||
this.loginService.performLogin();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user