From 1e18e3bc522c6f0d027b3bf72f5eeaf7c983752e Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Tue, 11 Jun 2024 12:55:11 +0200 Subject: [PATCH] Add signin page. --- .../security/SecurityConfiguration.java | 3 +- frontend/src/app/app.component.html | 4 +- frontend/src/app/app.routes.ts | 4 + .../rest-services/user/model/signin.model.ts | 5 + .../rest-services/user/user.rest-service.ts | 5 + .../src/app/pages/home/home.component.scss | 1 + .../src/app/pages/login/login.component.html | 4 +- .../src/app/pages/login/login.component.scss | 3 +- .../src/app/pages/login/login.component.ts | 1 - .../app/pages/signin/signin.component.html | 39 +++++++ .../app/pages/signin/signin.component.scss | 82 ++++++++++++++ .../app/pages/signin/signin.component.spec.ts | 23 ++++ .../src/app/pages/signin/signin.component.ts | 96 +++++++++++++++++ .../src/app/pages/signin/signin.service.ts | 102 ++++++++++++++++++ frontend/src/styles.scss | 6 +- 15 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/core/rest-services/user/model/signin.model.ts create mode 100644 frontend/src/app/pages/signin/signin.component.html create mode 100644 frontend/src/app/pages/signin/signin.component.scss create mode 100644 frontend/src/app/pages/signin/signin.component.spec.ts create mode 100644 frontend/src/app/pages/signin/signin.component.ts create mode 100644 frontend/src/app/pages/signin/signin.service.ts diff --git a/backend/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java b/backend/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java index 497b1c5..ef03e88 100644 --- a/backend/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java +++ b/backend/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java @@ -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, diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 1e15754..3cb0394 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,2 +1,4 @@ - +
+ +
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 40a172c..7c674f7 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -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) diff --git a/frontend/src/app/core/rest-services/user/model/signin.model.ts b/frontend/src/app/core/rest-services/user/model/signin.model.ts new file mode 100644 index 0000000..e141b80 --- /dev/null +++ b/frontend/src/app/core/rest-services/user/model/signin.model.ts @@ -0,0 +1,5 @@ +export interface SigninRequest { + pseudo?: string; + email?: string; + password?: string; +} \ No newline at end of file diff --git a/frontend/src/app/core/rest-services/user/user.rest-service.ts b/frontend/src/app/core/rest-services/user/user.rest-service.ts index df354dd..2aeb9ea 100644 --- a/frontend/src/app/core/rest-services/user/user.rest-service.ts +++ b/frontend/src/app/core/rest-services/user/user.rest-service.ts @@ -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 { return lastValueFrom(this.httpClient.post('/api/users/login', request)); } + + signin(request: SigninRequest): Promise { + return lastValueFrom(this.httpClient.post('/api/users', request)); + } } \ No newline at end of file diff --git a/frontend/src/app/pages/home/home.component.scss b/frontend/src/app/pages/home/home.component.scss index 8adaaf9..f9b0533 100644 --- a/frontend/src/app/pages/home/home.component.scss +++ b/frontend/src/app/pages/home/home.component.scss @@ -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); diff --git a/frontend/src/app/pages/login/login.component.html b/frontend/src/app/pages/login/login.component.html index 178ce0d..38b4138 100644 --- a/frontend/src/app/pages/login/login.component.html +++ b/frontend/src/app/pages/login/login.component.html @@ -1,7 +1,7 @@

Login

- person + mail
\ No newline at end of file diff --git a/frontend/src/app/pages/login/login.component.scss b/frontend/src/app/pages/login/login.component.scss index 64a5a5a..a8d5b3d 100644 --- a/frontend/src/app/pages/login/login.component.scss +++ b/frontend/src/app/pages/login/login.component.scss @@ -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; } diff --git a/frontend/src/app/pages/login/login.component.ts b/frontend/src/app/pages/login/login.component.ts index 3fe8a5f..ec93f59 100644 --- a/frontend/src/app/pages/login/login.component.ts +++ b/frontend/src/app/pages/login/login.component.ts @@ -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('', [Validators.required, Validators.email]), password: new FormControl('', [Validators.required]) diff --git a/frontend/src/app/pages/signin/signin.component.html b/frontend/src/app/pages/signin/signin.component.html new file mode 100644 index 0000000..9c8fd88 --- /dev/null +++ b/frontend/src/app/pages/signin/signin.component.html @@ -0,0 +1,39 @@ +
+

Signin

+
+ person + + +
+
+ mail + + +
+
+ lock + + +
+
+ lock + + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/pages/signin/signin.component.scss b/frontend/src/app/pages/signin/signin.component.scss new file mode 100644 index 0000000..a8d5b3d --- /dev/null +++ b/frontend/src/app/pages/signin/signin.component.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/pages/signin/signin.component.spec.ts b/frontend/src/app/pages/signin/signin.component.spec.ts new file mode 100644 index 0000000..b0892f5 --- /dev/null +++ b/frontend/src/app/pages/signin/signin.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SigninComponent } from './signin.component'; + +describe('SigninComponent', () => { + let component: SigninComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SigninComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SigninComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/signin/signin.component.ts b/frontend/src/app/pages/signin/signin.component.ts new file mode 100644 index 0000000..d2b8005 --- /dev/null +++ b/frontend/src/app/pages/signin/signin.component.ts @@ -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('', Validators.required), + email: new FormControl('', [Validators.required, Validators.email]), + password: new FormControl('', [Validators.required]), + confirmPassword: new FormControl('', [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(); + } + } +} diff --git a/frontend/src/app/pages/signin/signin.service.ts b/frontend/src/app/pages/signin/signin.service.ts new file mode 100644 index 0000000..acd7ccd --- /dev/null +++ b/frontend/src/app/pages/signin/signin.service.ts @@ -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(copy(DEFAULT_STATE)); + private userRestService = inject(UserRestService); + private snackBar = inject(MatSnackBar); + private router = inject(Router); + private loginService = inject(LoginService); + + get state$(): Observable { + 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(); + }) + } +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 7e7239a..ce7e640 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -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; +}