From b8312843ddb5abada3a3324d96fcddc697fd2a19 Mon Sep 17 00:00:00 2001 From: Millicent Amolo Date: Wed, 17 Sep 2025 22:31:17 +1000 Subject: [PATCH] feat: implement password management web updates --- .../change-password-dialog.component.html | 54 +++++++++++ .../change-password-dialog.component.scss | 25 +++++ .../change-password-dialog.component.ts | 76 ++++++++++++++++ .../edit-profile-form.component.html | 13 +++ .../edit-profile-form.component.ts | 28 +++++- src/app/doubtfire-angular.module.ts | 8 ++ src/app/doubtfire.states.ts | 54 +++++++++++ .../forgot-password.component.html | 61 +++++++++++++ .../forgot-password.component.scss | 86 ++++++++++++++++++ .../forgot-password.component.ts | 59 ++++++++++++ .../states/register/register.component.html | 91 +++++++++++++++++++ .../states/register/register.component.scss | 55 +++++++++++ .../states/register/register.component.ts | 90 ++++++++++++++++++ .../reset-password.component.html | 79 ++++++++++++++++ .../reset-password.component.scss | 91 +++++++++++++++++++ .../reset-password.component.ts | 91 +++++++++++++++++++ .../states/sign-in/sign-in.component.html | 24 +++++ .../states/sign-in/sign-in.component.scss | 15 +++ .../states/sign-in/sign-in.component.ts | 8 ++ 19 files changed, 1006 insertions(+), 2 deletions(-) create mode 100644 src/app/common/change-password-dialog/change-password-dialog.component.html create mode 100644 src/app/common/change-password-dialog/change-password-dialog.component.scss create mode 100644 src/app/common/change-password-dialog/change-password-dialog.component.ts create mode 100644 src/app/sessions/states/forgot-password/forgot-password.component.html create mode 100644 src/app/sessions/states/forgot-password/forgot-password.component.scss create mode 100644 src/app/sessions/states/forgot-password/forgot-password.component.ts create mode 100644 src/app/sessions/states/register/register.component.html create mode 100644 src/app/sessions/states/register/register.component.scss create mode 100644 src/app/sessions/states/register/register.component.ts create mode 100644 src/app/sessions/states/reset-password/reset-password.component.html create mode 100644 src/app/sessions/states/reset-password/reset-password.component.scss create mode 100644 src/app/sessions/states/reset-password/reset-password.component.ts diff --git a/src/app/common/change-password-dialog/change-password-dialog.component.html b/src/app/common/change-password-dialog/change-password-dialog.component.html new file mode 100644 index 0000000000..2d94d412e6 --- /dev/null +++ b/src/app/common/change-password-dialog/change-password-dialog.component.html @@ -0,0 +1,54 @@ +

Change Password

+ + +
+ + Current Password + + + + + New Password + + Password must be at least 8 characters long + + + + Confirm New Password + + +
+
+ + + + + + diff --git a/src/app/common/change-password-dialog/change-password-dialog.component.scss b/src/app/common/change-password-dialog/change-password-dialog.component.scss new file mode 100644 index 0000000000..77a2cba435 --- /dev/null +++ b/src/app/common/change-password-dialog/change-password-dialog.component.scss @@ -0,0 +1,25 @@ +.change-password-form { + display: flex; + flex-direction: column; + gap: 1rem; + min-width: 400px; +} + +mat-form-field { + width: 100%; +} + +.mat-hint { + font-size: 12px; + color: #666; +} + +mat-dialog-actions { + padding: 1rem 0 0 0; + margin: 0; +} + +button { + min-width: 100px; +} + diff --git a/src/app/common/change-password-dialog/change-password-dialog.component.ts b/src/app/common/change-password-dialog/change-password-dialog.component.ts new file mode 100644 index 0000000000..84a6286040 --- /dev/null +++ b/src/app/common/change-password-dialog/change-password-dialog.component.ts @@ -0,0 +1,76 @@ +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { HttpClient } from '@angular/common/http'; +import { AlertService } from 'src/app/common/services/alert.service'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { AuthenticationService } from 'src/app/api/services/authentication.service'; + +@Component({ + selector: 'f-change-password-dialog', + templateUrl: './change-password-dialog.component.html', + styleUrls: ['./change-password-dialog.component.scss'], +}) +export class ChangePasswordDialogComponent { + changingPassword: boolean = false; + formData = { + currentPassword: '', + newPassword: '', + confirmPassword: '' + }; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private httpClient: HttpClient, + private alerts: AlertService, + private constants: DoubtfireConstants, + private authService: AuthenticationService + ) {} + + changePassword(): void { + if (this.formData.newPassword !== this.formData.confirmPassword) { + this.alerts.error('New passwords do not match', 6000); + return; + } + + if (this.formData.newPassword.length < 8) { + this.alerts.error('New password must be at least 8 characters long', 6000); + return; + } + + this.changingPassword = true; + + this.httpClient.post(`${this.constants.API_URL}/password/change`, { + current_password: this.formData.currentPassword, + password: this.formData.newPassword, + password_confirmation: this.formData.confirmPassword + }).subscribe({ + next: (response: any) => { + this.changingPassword = false; + this.alerts.success('Password changed successfully!', 6000); + this.dialogRef.close(true); + }, + error: (error) => { + this.changingPassword = false; + this.formData.currentPassword = ''; + this.formData.newPassword = ''; + this.formData.confirmPassword = ''; + + let errorMessage = 'Password change failed'; + if (error.error && error.error.error) { + errorMessage = error.error.error; + if (error.error.details) { + errorMessage += ': ' + error.error.details.join(', '); + } + } + + this.alerts.error(errorMessage, 6000); + }, + }); + } + + cancel(): void { + this.dialogRef.close(false); + } +} + diff --git a/src/app/common/edit-profile-form/edit-profile-form.component.html b/src/app/common/edit-profile-form/edit-profile-form.component.html index b18f430fc8..806c08587e 100644 --- a/src/app/common/edit-profile-form/edit-profile-form.component.html +++ b/src/app/common/edit-profile-form/edit-profile-form.component.html @@ -87,6 +87,19 @@

{{ initialFirstName }}

+ @if (showPasswordManagement) { +
+ +
+ } + @if (canSeeSystemRole) { System Role diff --git a/src/app/common/edit-profile-form/edit-profile-form.component.ts b/src/app/common/edit-profile-form/edit-profile-form.component.ts index a31fa6d581..85cf23c4d0 100644 --- a/src/app/common/edit-profile-form/edit-profile-form.component.ts +++ b/src/app/common/edit-profile-form/edit-profile-form.component.ts @@ -1,11 +1,12 @@ import { Component, Inject, Input, OnInit, Optional } from '@angular/core'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { StateService } from '@uirouter/core'; import { User } from 'src/app/api/models/user/user'; import { AuthenticationService } from 'src/app/api/services/authentication.service'; import { UserService } from 'src/app/api/services/user.service'; import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { ChangePasswordDialogComponent } from '../change-password-dialog/change-password-dialog.component'; @Component({ selector: 'f-edit-profile-form', @@ -19,7 +20,8 @@ export class EditProfileFormComponent implements OnInit { private state: StateService, private authService: AuthenticationService, @Optional() @Inject(MAT_DIALOG_DATA) public data: { user: User; mode: 'edit' | 'create' | 'new' }, - private _snackBar: MatSnackBar + private _snackBar: MatSnackBar, + private dialog: MatDialog ) { this.user = data?.user || this.userService.currentUser; } @@ -78,6 +80,11 @@ export class EditProfileFormComponent implements OnInit { return this.constants.IsTiiEnabled.value; } + public get showPasswordManagement(): boolean { + // Show password management if user is authenticated and not in create mode + return this.authService.isAuthenticated() && this.mode !== 'create'; + } + public submit(): void { this.user.pronouns = this.customPronouns ? this.user.pronouns : this.formPronouns.pronouns; this.user.hasRunFirstTimeSetup = true; @@ -118,4 +125,21 @@ export class EditProfileFormComponent implements OnInit { }); } } + + public openChangePasswordDialog(): void { + const dialogRef = this.dialog.open(ChangePasswordDialogComponent, { + width: '500px', + data: {} + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this._snackBar.open('Password changed successfully', 'dismiss', { + duration: 3000, + horizontalPosition: 'end', + verticalPosition: 'top', + }); + } + }); + } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 9284745988..4625a7ded3 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -179,6 +179,10 @@ import {ObjectSelectComponent} from './common/obect-select/object-select.compone import {WelcomeComponent} from './welcome/welcome.component'; import {HeroSidebarComponent} from './common/hero-sidebar/hero-sidebar.component'; import {SignInComponent} from './sessions/states/sign-in/sign-in.component'; +import {RegisterComponent} from './sessions/states/register/register.component'; +import {ForgotPasswordComponent} from './sessions/states/forgot-password/forgot-password.component'; +import {ResetPasswordComponent} from './sessions/states/reset-password/reset-password.component'; +import {ChangePasswordDialogComponent} from './common/change-password-dialog/change-password-dialog.component'; import {EditProfileFormComponent} from './common/edit-profile-form/edit-profile-form.component'; import {TransitionHooksService} from './sessions/transition-hooks.service'; import {EditProfileComponent} from './account/edit-profile/edit-profile.component'; @@ -298,6 +302,10 @@ import {GradeService} from './common/services/grade.service'; AcceptEulaComponent, HeroSidebarComponent, SignInComponent, + RegisterComponent, + ForgotPasswordComponent, + ResetPasswordComponent, + ChangePasswordDialogComponent, EditProfileFormComponent, EditProfileComponent, UserBadgeComponent, diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index b9f95e88af..0e698c1470 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -3,6 +3,9 @@ import {InstitutionSettingsComponent} from './admin/institution-settings/institu import {HomeComponent} from './home/states/home/home.component'; import {WelcomeComponent} from './welcome/welcome.component'; import {SignInComponent} from './sessions/states/sign-in/sign-in.component'; +import {RegisterComponent} from './sessions/states/register/register.component'; +import {ForgotPasswordComponent} from './sessions/states/forgot-password/forgot-password.component'; +import {ResetPasswordComponent} from './sessions/states/reset-password/reset-password.component'; import {EditProfileComponent} from './account/edit-profile/edit-profile.component'; import {TeachingPeriodListComponent} from './admin/states/teaching-periods/teaching-period-list/teaching-period-list.component'; import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component'; @@ -185,6 +188,54 @@ const SignInState: NgHybridStateDeclaration = { }, }; +/** + * Define the Register state. + */ +const RegisterState: NgHybridStateDeclaration = { + name: 'register', + url: '/register', + views: { + main: { + component: RegisterComponent, + }, + }, + data: { + pageTitle: 'Create Account', + }, +}; + +/** + * Define the Forgot Password state. + */ +const ForgotPasswordState: NgHybridStateDeclaration = { + name: 'forgot-password', + url: '/forgot-password', + views: { + main: { + component: ForgotPasswordComponent, + }, + }, + data: { + pageTitle: 'Forgot Password', + }, +}; + +/** + * Define the Reset Password state. + */ +const ResetPasswordState: NgHybridStateDeclaration = { + name: 'reset-password', + url: '/reset-password?token', + views: { + main: { + component: ResetPasswordComponent, + }, + }, + data: { + pageTitle: 'Reset Password', + }, +}; + /** * Define the Edit Profile state. */ @@ -300,6 +351,9 @@ export const doubtfireStates = [ HomeState, WelcomeState, SignInState, + RegisterState, + ForgotPasswordState, + ResetPasswordState, EditProfileState, EulaState, usersState, diff --git a/src/app/sessions/states/forgot-password/forgot-password.component.html b/src/app/sessions/states/forgot-password/forgot-password.component.html new file mode 100644 index 0000000000..c82321524a --- /dev/null +++ b/src/app/sessions/states/forgot-password/forgot-password.component.html @@ -0,0 +1,61 @@ +
+ +
+
+
+
+ Homepage Logo +

Reset Password

+
+

+ Forgot Your Password? +

+ + @if (!emailSent) { +

+ Enter your email address and we'll send you a link to reset your password. +

+ + + } @else { +
+ email +

Check Your Email

+

We've sent a password reset link to {{ formData.email }}

+

If you don't see the email, check your spam folder.

+
+ } + + +
+
+
+
+ diff --git a/src/app/sessions/states/forgot-password/forgot-password.component.scss b/src/app/sessions/states/forgot-password/forgot-password.component.scss new file mode 100644 index 0000000000..f952df8d4f --- /dev/null +++ b/src/app/sessions/states/forgot-password/forgot-password.component.scss @@ -0,0 +1,86 @@ +// Forgot password form styles +.welcome { + min-height: 100vh; +} + +.container { + padding: 2rem; + max-width: 500px; + margin: 0 auto; +} + +.subcontainer { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.wordmark { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: #666; +} + +.welcome-heading { + font-size: 2rem; + font-weight: 700; + color: #333; + margin: 0; +} + +.description { + color: #666; + font-size: 16px; + line-height: 1.5; + margin: 0; +} + +.sign-in-form { + gap: 1rem; +} + +mat-form-field { + width: 100%; +} + +button { + height: 48px; + font-size: 16px; + font-weight: 500; +} + +.success-message { + text-align: center; + padding: 2rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.success-icon { + font-size: 48px; + width: 48px; + height: 48px; + margin-bottom: 1rem; +} + +.success-message h2 { + color: #333; + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.success-message p { + color: #666; + margin: 0.5rem 0; + line-height: 1.5; +} + +.note { + font-size: 14px; + color: #888; + font-style: italic; +} + diff --git a/src/app/sessions/states/forgot-password/forgot-password.component.ts b/src/app/sessions/states/forgot-password/forgot-password.component.ts new file mode 100644 index 0000000000..b1a7fd9e1c --- /dev/null +++ b/src/app/sessions/states/forgot-password/forgot-password.component.ts @@ -0,0 +1,59 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { StateService } from '@uirouter/core'; +import { AlertService } from 'src/app/common/services/alert.service'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { GlobalStateService } from 'src/app/projects/states/index/global-state.service'; + +@Component({ + selector: 'f-forgot-password', + templateUrl: './forgot-password.component.html', + styleUrls: ['./forgot-password.component.scss'], +}) +export class ForgotPasswordComponent implements OnInit { + requestingReset: boolean = false; + emailSent: boolean = false; + formData = { + email: '' + }; + + constructor( + private httpClient: HttpClient, + private state: StateService, + private constants: DoubtfireConstants, + private globalState: GlobalStateService, + private alerts: AlertService, + ) {} + + ngOnInit(): void { + this.globalState.hideHeader(); + } + + requestPasswordReset(): void { + if (!this.formData.email) { + this.alerts.error('Please enter your email address', 6000); + return; + } + + this.requestingReset = true; + + this.httpClient.post(`${this.constants.API_URL}/password/reset`, { + email: this.formData.email + }).subscribe({ + next: (response: any) => { + this.requestingReset = false; + this.emailSent = true; + this.alerts.success('If an account with that email exists, a password reset link has been sent.', 8000); + }, + error: (error) => { + this.requestingReset = false; + this.alerts.error('Failed to send password reset email. Please try again.', 6000); + }, + }); + } + + goToLogin(): void { + this.state.go('sign_in'); + } +} + diff --git a/src/app/sessions/states/register/register.component.html b/src/app/sessions/states/register/register.component.html new file mode 100644 index 0000000000..2938d990cb --- /dev/null +++ b/src/app/sessions/states/register/register.component.html @@ -0,0 +1,91 @@ +
+ +
+
+
+
+ Homepage Logo +

Create Account

+
+

+ Create Your Account +

+ +
+
+
+
+ diff --git a/src/app/sessions/states/register/register.component.scss b/src/app/sessions/states/register/register.component.scss new file mode 100644 index 0000000000..76c7148866 --- /dev/null +++ b/src/app/sessions/states/register/register.component.scss @@ -0,0 +1,55 @@ +// Registration form styles - inherits from sign-in styles +.welcome { + min-height: 100vh; +} + +.container { + padding: 2rem; + max-width: 500px; + margin: 0 auto; +} + +.subcontainer { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.wordmark { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: #666; +} + +.welcome-heading { + font-size: 2rem; + font-weight: 700; + color: #333; + margin: 0; +} + +.sign-in-form { + gap: 1rem; +} + +mat-form-field { + width: 100%; +} + +button { + height: 48px; + font-size: 16px; + font-weight: 500; +} + +.mat-mdc-button-base { + margin-bottom: 0.5rem; +} + +.mat-hint { + font-size: 12px; + color: #666; +} + diff --git a/src/app/sessions/states/register/register.component.ts b/src/app/sessions/states/register/register.component.ts new file mode 100644 index 0000000000..e9c1f99856 --- /dev/null +++ b/src/app/sessions/states/register/register.component.ts @@ -0,0 +1,90 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { StateService } from '@uirouter/core'; +import { AuthenticationService } from 'src/app/api/services/authentication.service'; +import { AlertService } from 'src/app/common/services/alert.service'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { GlobalStateService } from 'src/app/projects/states/index/global-state.service'; + +@Component({ + selector: 'f-register', + templateUrl: './register.component.html', + styleUrls: ['./register.component.scss'], +}) +export class RegisterComponent implements OnInit { + registering: boolean = false; + formData = { + username: '', + email: '', + password: '', + passwordConfirmation: '', + firstName: '', + lastName: '', + nickname: '' + }; + + constructor( + private httpClient: HttpClient, + private authService: AuthenticationService, + private state: StateService, + private constants: DoubtfireConstants, + private globalState: GlobalStateService, + private alerts: AlertService, + ) {} + + ngOnInit(): void { + this.globalState.hideHeader(); + } + + register(): void { + if (this.formData.password !== this.formData.passwordConfirmation) { + this.alerts.error('Passwords do not match', 6000); + return; + } + + if (this.formData.password.length < 8) { + this.alerts.error('Password must be at least 8 characters long', 6000); + return; + } + + this.registering = true; + + const registrationData = { + username: this.formData.username, + email: this.formData.email, + password: this.formData.password, + password_confirmation: this.formData.passwordConfirmation, + first_name: this.formData.firstName, + last_name: this.formData.lastName, + nickname: this.formData.nickname || this.formData.firstName + }; + + this.httpClient.post(`${this.constants.API_URL}/register`, registrationData).subscribe({ + next: (response: any) => { + this.registering = false; + this.alerts.success('Registration successful! You are now logged in.', 6000); + this.state.go('home'); + }, + error: (error) => { + this.registering = false; + this.formData.password = ''; + this.formData.passwordConfirmation = ''; + + let errorMessage = 'Registration failed'; + if (error.error && error.error.error) { + errorMessage = error.error.error; + if (error.error.details) { + errorMessage += ': ' + error.error.details.join(', '); + } + } + + this.alerts.error(errorMessage, 6000); + }, + }); + } + + goToLogin(): void { + this.state.go('sign_in'); + } +} + diff --git a/src/app/sessions/states/reset-password/reset-password.component.html b/src/app/sessions/states/reset-password/reset-password.component.html new file mode 100644 index 0000000000..e8ac5bf013 --- /dev/null +++ b/src/app/sessions/states/reset-password/reset-password.component.html @@ -0,0 +1,79 @@ +
+ +
+
+
+
+ Homepage Logo +

Reset Password

+
+

+ Set New Password +

+ + @if (!passwordReset) { +

+ Enter your new password below. +

+ + + } @else { +
+ check_circle +

Password Reset Successfully!

+

Your password has been updated. You can now sign in with your new password.

+

Redirecting to sign in page...

+
+ } + + +
+
+
+
+ diff --git a/src/app/sessions/states/reset-password/reset-password.component.scss b/src/app/sessions/states/reset-password/reset-password.component.scss new file mode 100644 index 0000000000..9fe1b40242 --- /dev/null +++ b/src/app/sessions/states/reset-password/reset-password.component.scss @@ -0,0 +1,91 @@ +// Reset password form styles +.welcome { + min-height: 100vh; +} + +.container { + padding: 2rem; + max-width: 500px; + margin: 0 auto; +} + +.subcontainer { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.wordmark { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: #666; +} + +.welcome-heading { + font-size: 2rem; + font-weight: 700; + color: #333; + margin: 0; +} + +.description { + color: #666; + font-size: 16px; + line-height: 1.5; + margin: 0; +} + +.sign-in-form { + gap: 1rem; +} + +mat-form-field { + width: 100%; +} + +button { + height: 48px; + font-size: 16px; + font-weight: 500; +} + +.success-message { + text-align: center; + padding: 2rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.success-icon { + font-size: 48px; + width: 48px; + height: 48px; + margin-bottom: 1rem; +} + +.success-message h2 { + color: #333; + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.success-message p { + color: #666; + margin: 0.5rem 0; + line-height: 1.5; +} + +.note { + font-size: 14px; + color: #888; + font-style: italic; +} + +.mat-hint { + font-size: 12px; + color: #666; +} + diff --git a/src/app/sessions/states/reset-password/reset-password.component.ts b/src/app/sessions/states/reset-password/reset-password.component.ts new file mode 100644 index 0000000000..768a091d84 --- /dev/null +++ b/src/app/sessions/states/reset-password/reset-password.component.ts @@ -0,0 +1,91 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { StateService, Transition } from '@uirouter/core'; +import { AlertService } from 'src/app/common/services/alert.service'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { GlobalStateService } from 'src/app/projects/states/index/global-state.service'; + +@Component({ + selector: 'f-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./reset-password.component.scss'], +}) +export class ResetPasswordComponent implements OnInit { + resettingPassword: boolean = false; + passwordReset: boolean = false; + token: string = ''; + formData = { + password: '', + passwordConfirmation: '' + }; + + constructor( + private httpClient: HttpClient, + private state: StateService, + private transition: Transition, + private constants: DoubtfireConstants, + private globalState: GlobalStateService, + private alerts: AlertService, + ) {} + + ngOnInit(): void { + this.globalState.hideHeader(); + this.token = this.transition.params().token || ''; + + if (!this.token) { + this.alerts.error('Invalid reset link. Please request a new password reset.', 6000); + this.state.go('forgot-password'); + } + } + + resetPassword(): void { + if (this.formData.password !== this.formData.passwordConfirmation) { + this.alerts.error('Passwords do not match', 6000); + return; + } + + if (this.formData.password.length < 8) { + this.alerts.error('Password must be at least 8 characters long', 6000); + return; + } + + this.resettingPassword = true; + + this.httpClient.post(`${this.constants.API_URL}/password/reset/confirm`, { + token: this.token, + password: this.formData.password, + password_confirmation: this.formData.passwordConfirmation + }).subscribe({ + next: (response: any) => { + this.resettingPassword = false; + this.passwordReset = true; + this.alerts.success('Your password has been reset successfully!', 6000); + + // Redirect to login after a short delay + setTimeout(() => { + this.state.go('sign_in'); + }, 3000); + }, + error: (error) => { + this.resettingPassword = false; + this.formData.password = ''; + this.formData.passwordConfirmation = ''; + + let errorMessage = 'Password reset failed'; + if (error.error && error.error.error) { + errorMessage = error.error.error; + if (error.error.details) { + errorMessage += ': ' + error.error.details.join(', '); + } + } + + this.alerts.error(errorMessage, 6000); + }, + }); + } + + goToLogin(): void { + this.state.go('sign_in'); + } +} + diff --git a/src/app/sessions/states/sign-in/sign-in.component.html b/src/app/sessions/states/sign-in/sign-in.component.html index cf7b12c70c..d39b90b7a5 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.html +++ b/src/app/sessions/states/sign-in/sign-in.component.html @@ -48,6 +48,30 @@

> Sign In + + @if (showCredentials) { + + } diff --git a/src/app/sessions/states/sign-in/sign-in.component.scss b/src/app/sessions/states/sign-in/sign-in.component.scss index da91789cc2..8269327503 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.scss +++ b/src/app/sessions/states/sign-in/sign-in.component.scss @@ -19,3 +19,18 @@ height: 100vh !important; } } + +.auth-links { + display: flex; + justify-content: space-between; + margin-top: 1rem; + gap: 1rem; +} + +.link-button { + font-size: 14px; + text-decoration: underline; + min-width: auto; + height: auto; + padding: 0.5rem 0; +} \ No newline at end of file diff --git a/src/app/sessions/states/sign-in/sign-in.component.ts b/src/app/sessions/states/sign-in/sign-in.component.ts index f41b944018..6f7a0a4a50 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.ts +++ b/src/app/sessions/states/sign-in/sign-in.component.ts @@ -137,4 +137,12 @@ export class SignInComponent implements OnInit { }, }); } + + goToForgotPassword(): void { + this.state.go('forgot-password'); + } + + goToRegister(): void { + this.state.go('register'); + } }