diff --git a/package.json b/package.json index e85611e3e..982090aba 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "diff": "^8.0.2", "markdown-it": "^14.1.0", "markdown-it-video": "^0.6.3", + "ngx-captcha": "^13.0.0", "ngx-cookie-service": "^19.1.2", "ngx-markdown-editor": "^5.3.4", "primeflex": "^4.0.0", diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d9837053e..6cf60eb28 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -40,13 +40,13 @@ export const routes: Routes = [ data: { skipBreadcrumbs: true }, }, { - path: 'sign-up', + path: 'register', loadComponent: () => import('./features/auth/pages/sign-up/sign-up.component').then((mod) => mod.SignUpComponent), data: { skipBreadcrumbs: true }, }, { - path: 'forgot-password', + path: 'forgotpassword', loadComponent: () => import('./features/auth/pages/forgot-password/forgot-password.component').then( (mod) => mod.ForgotPasswordComponent @@ -54,7 +54,7 @@ export const routes: Routes = [ data: { skipBreadcrumbs: true }, }, { - path: 'reset-password', + path: 'resetpassword/:userId/:token', loadComponent: () => import('./features/auth/pages/reset-password/reset-password.component').then( (mod) => mod.ResetPasswordComponent diff --git a/src/app/core/components/header/header.component.ts b/src/app/core/components/header/header.component.ts index 79c173170..030ab1dc4 100644 --- a/src/app/core/components/header/header.component.ts +++ b/src/app/core/components/header/header.component.ts @@ -29,6 +29,12 @@ export class HeaderComponent { command: () => this.router.navigate(['my-profile']), }, { label: 'navigation.settings', command: () => this.router.navigate(['settings']) }, - { label: 'navigation.logOut', command: () => console.log('Log out') }, + { + label: 'navigation.logOut', + command: () => { + document.cookie = 'auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + this.router.navigate(['/']); + }, + }, ]; } diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 1fc6a949c..dae608deb 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -1,5 +1,5 @@ -import { AuthState } from '@core/store/auth'; import { UserState } from '@core/store/user'; +import { AuthState } from '@osf/features/auth/store'; import { MeetingsState } from '@osf/features/meetings/store'; import { ProjectMetadataState } from '@osf/features/project/metadata/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; diff --git a/src/app/core/store/auth/auth.actions.ts b/src/app/core/store/auth/auth.actions.ts deleted file mode 100644 index 112d4d3c5..000000000 --- a/src/app/core/store/auth/auth.actions.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class SetAuthToken { - static readonly type = '[Auth] Set Auth Token'; - - constructor(public accessToken: string) {} -} - -export class ClearAuth { - static readonly type = '[Auth] Clear Auth'; -} diff --git a/src/app/core/store/auth/auth.selectors.ts b/src/app/core/store/auth/auth.selectors.ts deleted file mode 100644 index 51698a086..000000000 --- a/src/app/core/store/auth/auth.selectors.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Selector } from '@ngxs/store'; - -import { AuthStateModel } from './auth.model'; -import { AuthState } from './auth.state'; - -export class AuthSelectors { - @Selector([AuthState]) - static isAuthenticated(state: AuthStateModel): boolean { - return state.isAuthenticated; - } - - @Selector([AuthState]) - static getAuthToken(state: AuthStateModel): string | null { - return state.accessToken; - } -} diff --git a/src/app/core/store/auth/auth.state.ts b/src/app/core/store/auth/auth.state.ts deleted file mode 100644 index 3dc968c7b..000000000 --- a/src/app/core/store/auth/auth.state.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Action, State, StateContext } from '@ngxs/store'; - -import { Injectable } from '@angular/core'; - -import { ClearAuth, SetAuthToken } from './auth.actions'; -import { AuthStateModel } from './auth.model'; - -@State({ - name: 'auth', - defaults: { - accessToken: null, - isAuthenticated: false, - }, -}) -@Injectable() -export class AuthState { - @Action(SetAuthToken) - setAuthToken(ctx: StateContext, action: SetAuthToken) { - ctx.patchState({ - accessToken: action.accessToken, - isAuthenticated: true, - }); - } - - @Action(ClearAuth) - clearAuth(ctx: StateContext) { - ctx.patchState({ - accessToken: null, - isAuthenticated: false, - }); - } -} diff --git a/src/app/features/auth/helpers/index.ts b/src/app/features/auth/helpers/index.ts deleted file mode 100644 index c1d0c55a8..000000000 --- a/src/app/features/auth/helpers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './sign-up.helper'; diff --git a/src/app/features/auth/helpers/sign-up.helper.ts b/src/app/features/auth/helpers/sign-up.helper.ts deleted file mode 100644 index e89b2ab08..000000000 --- a/src/app/features/auth/helpers/sign-up.helper.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; - -export const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\w\s]).{8,}$/; - -export function passwordMatchValidator( - passwordField = 'password', - confirmPasswordField = 'confirmPassword' -): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const password = control.get(passwordField); - const confirmPassword = control.get(confirmPasswordField); - - if (!password || !confirmPassword) { - return null; - } - - if (confirmPassword.errors && !confirmPassword.errors['passwordMismatch']) { - return null; - } - - if (password.value !== confirmPassword.value) { - const error = { passwordMismatch: true }; - confirmPassword.setErrors(error); - return error; - } else { - const errors = { ...confirmPassword.errors }; - delete errors['passwordMismatch']; - confirmPassword.setErrors(Object.keys(errors).length ? errors : null); - return null; - } - }; -} diff --git a/src/app/features/auth/models/auth.model.ts b/src/app/features/auth/models/auth.model.ts index 48cafe8b4..015668185 100644 --- a/src/app/features/auth/models/auth.model.ts +++ b/src/app/features/auth/models/auth.model.ts @@ -1,8 +1,3 @@ -export interface LoginCredentials { - email: string; - password: string; -} - export interface AuthResponse { accessToken: string; } diff --git a/src/app/features/auth/models/forgot-password-form-group.type.ts b/src/app/features/auth/models/forgot-password-form-group.model.ts similarity index 100% rename from src/app/features/auth/models/forgot-password-form-group.type.ts rename to src/app/features/auth/models/forgot-password-form-group.model.ts diff --git a/src/app/features/auth/models/index.ts b/src/app/features/auth/models/index.ts index f804db719..5d9696309 100644 --- a/src/app/features/auth/models/index.ts +++ b/src/app/features/auth/models/index.ts @@ -1,4 +1,6 @@ export * from './auth.model'; -export * from './forgot-password-form-group.type'; +export * from './forgot-password-form-group.model'; export * from './message-info.model'; -export * from './reset-password-form-group.type'; +export * from './reset-password-form-group.model'; +export * from './sign-up.model'; +export * from './sign-up-form.model'; diff --git a/src/app/features/auth/models/reset-password-form-group.type.ts b/src/app/features/auth/models/reset-password-form-group.model.ts similarity index 100% rename from src/app/features/auth/models/reset-password-form-group.type.ts rename to src/app/features/auth/models/reset-password-form-group.model.ts diff --git a/src/app/features/auth/models/sign-up-form.model.ts b/src/app/features/auth/models/sign-up-form.model.ts new file mode 100644 index 000000000..77fa5957d --- /dev/null +++ b/src/app/features/auth/models/sign-up-form.model.ts @@ -0,0 +1,10 @@ +import { FormControl } from '@angular/forms'; + +export interface SignUpForm { + fullName: FormControl; + email1: FormControl; + email2: FormControl; + password: FormControl; + acceptedTermsOfService: FormControl; + recaptcha: FormControl; +} diff --git a/src/app/features/auth/models/sign-up.model.ts b/src/app/features/auth/models/sign-up.model.ts new file mode 100644 index 000000000..07821db06 --- /dev/null +++ b/src/app/features/auth/models/sign-up.model.ts @@ -0,0 +1,8 @@ +export interface SignUpModel { + fullName: string; + email1: string; + email2: string; + password: string; + acceptedTermsOfService: boolean; + recaptcha: string; +} diff --git a/src/app/features/auth/pages/forgot-password/forgot-password.component.html b/src/app/features/auth/pages/forgot-password/forgot-password.component.html index e3d0a3001..1aff6b60e 100644 --- a/src/app/features/auth/pages/forgot-password/forgot-password.component.html +++ b/src/app/features/auth/pages/forgot-password/forgot-password.component.html @@ -1,29 +1,30 @@ -
-

{{ 'auth.forgotPassword.title' | translate }}

-

{{ 'auth.forgotPassword.description' | translate }}

+
+
+

{{ 'auth.forgotPassword.title' | translate }}

+

{{ 'auth.forgotPassword.description' | translate }}

-
- - + + - -
+ + +
-
+
@if (message()) { {{ 'auth.forgotPassword.title' | translate }} /> }
-
+ diff --git a/src/app/features/auth/pages/forgot-password/forgot-password.component.scss b/src/app/features/auth/pages/forgot-password/forgot-password.component.scss index 211f2bff1..d4d4dac88 100644 --- a/src/app/features/auth/pages/forgot-password/forgot-password.component.scss +++ b/src/app/features/auth/pages/forgot-password/forgot-password.component.scss @@ -1,5 +1,4 @@ @use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; :host { @include mix.flex-center; @@ -8,20 +7,11 @@ } .forgot-password-container { - position: relative; - background: var.$white; - border-radius: mix.rem(12px); - box-shadow: 0 2px 4px var.$grey-outline; @include mix.flex-column; - color: var.$dark-blue-1; - flex: 1; - gap: mix.rem(24px); + background: var(--white); + border-radius: mix.rem(12px); + box-shadow: 0 2px 4px var(--grey-outline); max-width: mix.rem(448px); - - .message-container { - width: 100%; - position: absolute; - bottom: -4.5rem; - left: 0; - } + gap: mix.rem(24px); + flex: 1; } diff --git a/src/app/features/auth/pages/forgot-password/forgot-password.component.ts b/src/app/features/auth/pages/forgot-password/forgot-password.component.ts index ca7f78be7..8f56d3161 100644 --- a/src/app/features/auth/pages/forgot-password/forgot-password.component.ts +++ b/src/app/features/auth/pages/forgot-password/forgot-password.component.ts @@ -1,40 +1,53 @@ +import { createDispatchMap } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; -import { InputText } from 'primeng/inputtext'; import { Message } from 'primeng/message'; import { Component, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; +import { CustomValidators } from '@osf/shared/utils'; + import { ForgotPasswordFormGroupType, MessageInfo } from '../../models'; +import { ForgotPassword } from '../../store'; @Component({ selector: 'osf-forgot-password', - imports: [InputText, ReactiveFormsModule, Button, Message, TranslatePipe], + imports: [ReactiveFormsModule, Button, Message, TextInputComponent, TranslatePipe], templateUrl: './forgot-password.component.html', styleUrl: './forgot-password.component.scss', }) export class ForgotPasswordComponent { - #fb = inject(FormBuilder); - forgotPasswordForm: ForgotPasswordFormGroupType = this.#fb.group({ - email: ['', [Validators.required, Validators.email]], + private readonly fb = inject(FormBuilder); + private readonly actions = createDispatchMap({ forgotPassword: ForgotPassword }); + + readonly emailLimit = InputLimits.email.maxLength; + + forgotPasswordForm: ForgotPasswordFormGroupType = this.fb.group({ + email: ['', [CustomValidators.requiredTrimmed(), Validators.email]], }); + message = signal(null); onSubmit(): void { - // [NS] TODO: Implement password reset logic - if (this.forgotPasswordForm.valid) { + if (this.forgotPasswordForm.invalid) { + return; + } + + const emailForm = this.forgotPasswordForm.getRawValue(); + + this.actions.forgotPassword(emailForm.email).subscribe(() => { + this.forgotPasswordForm.reset(); + this.message.set({ severity: 'success', content: 'auth.forgotPassword.messages.success', }); - - // this.message.set({ - // severity: 'error', - // content: 'auth.forgotPassword.messages.error' - // }); - } + }); } onCloseMessage(): void { diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.html b/src/app/features/auth/pages/reset-password/reset-password.component.html index 7e6738f9b..691fae8f7 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.html +++ b/src/app/features/auth/pages/reset-password/reset-password.component.html @@ -1,42 +1,41 @@ @if (!isFormSubmitted()) { -
-

{{ 'auth.resetPassword.title' | translate }}

+
+

{{ 'auth.resetPassword.title' | translate }}

-
-
+ +
- +
-
+
- @if ( - resetPasswordForm.get('confirmNewPassword')?.errors?.['passwordMismatch'] && - resetPasswordForm.get('confirmNewPassword')?.touched - ) { - {{ 'auth.common.password.mismatch' | translate }} + @if (isMismatchError) { + + {{ 'auth.common.password.mismatch' | translate }} + }
{{ 'auth.resetPassword.title' | translate }}
} @else { -
+

{{ 'auth.resetPassword.success.title' | translate }}

-

+

{{ 'auth.resetPassword.success.message' | translate }}

} diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.scss b/src/app/features/auth/pages/reset-password/reset-password.component.scss index 0914a6af2..b231ba8b2 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.scss +++ b/src/app/features/auth/pages/reset-password/reset-password.component.scss @@ -1,5 +1,4 @@ @use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; :host { @include mix.flex-center; @@ -11,48 +10,10 @@ .reset-password-container, .message-container { @include mix.flex-column; - color: var.$dark-blue-1; - max-width: 32rem; - flex: 1; - gap: 1.2rem; padding: 2rem; - background: var.$white; + background: var(--white); border-radius: 0.6rem; - box-shadow: 0 2px 4px var.$grey-outline; - - h2, - .message-text { - text-align: center; - } - - .message-text { - margin-bottom: 1.5rem; - } - - form { - @include mix.flex-column; - flex: 1; - - .field { - margin-bottom: 1rem; - - .text-danger { - color: var.$red-1; - font-weight: 600; - } - } - - small { - color: var.$pr-blue-1; - font-weight: 600; - } - - .btn-full-width { - margin-top: 1.5rem; - } - } -} - -.mobile { - max-width: 24.5rem; + box-shadow: 0 2px 4px var(--grey-outline); + max-width: 32rem; + flex: 1; } diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.ts b/src/app/features/auth/pages/reset-password/reset-password.component.ts index 813078c5f..12f1bf9d6 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.ts +++ b/src/app/features/auth/pages/reset-password/reset-password.component.ts @@ -1,45 +1,68 @@ +import { createDispatchMap } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; +import { Message } from 'primeng/message'; import { Password } from 'primeng/password'; import { Component, inject, signal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { CustomValidators, PASSWORD_REGEX } from '@osf/shared/utils'; import { PasswordInputHintComponent } from '@shared/components'; -import { IS_XSMALL } from '@shared/utils'; -import { PASSWORD_REGEX, passwordMatchValidator } from '../../helpers'; import { ResetPasswordFormGroupType } from '../../models'; +import { ResetPassword } from '../../store'; @Component({ selector: 'osf-reset-password', - imports: [Button, Password, ReactiveFormsModule, RouterLink, PasswordInputHintComponent, TranslatePipe], + imports: [Button, Password, ReactiveFormsModule, RouterLink, PasswordInputHintComponent, Message, TranslatePipe], templateUrl: './reset-password.component.html', styleUrl: './reset-password.component.scss', }) export class ResetPasswordComponent { - #fb = inject(FormBuilder); - #isMobile$ = inject(IS_XSMALL); + private readonly fb = inject(FormBuilder); + private readonly route = inject(ActivatedRoute); + private readonly actions = createDispatchMap({ resetPassword: ResetPassword }); + + isFormSubmitted = signal(false); passwordRegex = PASSWORD_REGEX; - resetPasswordForm: ResetPasswordFormGroupType = this.#fb.group( + + resetPasswordForm: ResetPasswordFormGroupType = this.fb.group( { - newPassword: ['', [Validators.required, Validators.pattern(this.passwordRegex)]], - confirmNewPassword: ['', Validators.required], + newPassword: ['', [CustomValidators.requiredTrimmed(), Validators.pattern(this.passwordRegex)]], + confirmNewPassword: ['', CustomValidators.requiredTrimmed()], }, { - validators: passwordMatchValidator(), + validators: CustomValidators.passwordMatchValidator('newPassword', 'confirmNewPassword'), } ); - isFormSubmitted = signal(false); - isMobile = toSignal(this.#isMobile$); + + get isNewPasswordError() { + return this.resetPasswordForm.get('newPassword')?.errors && this.resetPasswordForm.get('newPassword')?.touched; + } + + get isMismatchError(): boolean { + return ( + this.resetPasswordForm.get('confirmNewPassword')?.dirty && + this.resetPasswordForm.get('newPassword')?.dirty && + this.resetPasswordForm.errors?.['passwordMismatch'] + ); + } onSubmit(): void { - if (this.resetPasswordForm.valid) { - // TODO: Implement password reset logic - this.isFormSubmitted.set(true); + if (this.resetPasswordForm.invalid) { + return; } + + const userId = this.route.snapshot.params['userId']; + const token = this.route.snapshot.params['token']; + const newPassword = this.resetPasswordForm.getRawValue().newPassword; + + this.actions.resetPassword(userId, token, newPassword).subscribe(() => { + this.isFormSubmitted.set(true); + }); } } diff --git a/src/app/features/auth/pages/sign-up/sign-up.component.html b/src/app/features/auth/pages/sign-up/sign-up.component.html index 8932e499b..b11064281 100644 --- a/src/app/features/auth/pages/sign-up/sign-up.component.html +++ b/src/app/features/auth/pages/sign-up/sign-up.component.html @@ -1,9 +1,9 @@ @if (!isFormSubmitted()) { -