Skip to content

Commit

Permalink
Merge pull request #318 from e-picsa/ft-password-reset
Browse files Browse the repository at this point in the history
Feat Enable Password Reset
  • Loading branch information
chrismclarke authored Mar 5, 2025
2 parents e0848ad + b1f7c73 commit 0753a26
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- ready status template -->
<div class="status-container" [attr.data-status]="status">
<div class="status-container" [attr.data-status]="status()">
@if(options().showStatusCode){
<span class="status-code" [attr.data-status]="status">{{code() || ''}}</span>
<span class="status-code" [attr.data-status]="status()">{{code() || ''}}</span>
} @if(options().labels?.ready; as readyLabel){
<div class="status-label">{{readyLabel}}</div>
} @if(options().events?.refresh; as refreshEvent){
Expand Down
16 changes: 12 additions & 4 deletions apps/picsa-apps/dashboard/src/app/modules/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import { ErrorHandler, Injectable } from '@angular/core';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';

@Injectable({ providedIn: 'root' })
export class DashboardErrorHandler extends ErrorHandler {
export class DashboardErrorHandler implements ErrorHandler {
constructor(private notificationService: PicsaNotificationService) {
super();
// Ensure errors handled if thrown globally
// https://github.com/angular/angular/issues/56240
window.addEventListener('unhandledrejection', (e) => {
this.handleError(e.reason);
e.preventDefault();
});
window.addEventListener('error', (event) => {
this.handleError(event.error);
event.preventDefault();
});
}

override handleError(error: Error) {
handleError(error: Error) {
console.error(error);
this.notificationService.showErrorNotification(error.message);
super.handleError(error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ <h1 mat-dialog-title>{{ title }}</h1>
<mat-error>Please enter a valid email address</mat-error>
}
</mat-form-field>
<mat-form-field>
@if (template !== 'reset') {<mat-form-field>
<mat-label>Password</mat-label>
<input type="password" matInput autocomplete="picsa-password" formControlName="password" />
</mat-form-field>
@if(template==='register'){
<input type="password" matInput autocomplete="picsa-password" formControlName="password" /> </mat-form-field
>} @if(template==='register'){
<mat-form-field>
<mat-label>Repeat Password</mat-label>
<input
Expand All @@ -35,28 +34,38 @@ <h1 mat-dialog-title>{{ title }}</h1>
}
</form>
</mat-dialog-content>
<mat-dialog-actions align="start">
<mat-dialog-actions class="flex flex-col">
@if(template==='signIn'){
<button mat-button (click)="enableRegisterMode()">Create new account</button>
<button
mat-button
cdkFocusInitial
(click)="handleSignIn()"
style="margin-left: auto"
[disabled]="!form.valid || form.disabled"
>
Sign In
</button>
<div class="action-buttons-container">
<button mat-button (click)="enableRegisterMode()" class="flex-1">Create Account</button>
<button
mat-button
cdkFocusInitial
(click)="handleSignIn()"
class="flex-1"
[disabled]="!form.valid || form.disabled"
>
Sign In
</button>
</div>
<button class="mt-2" mat-button (click)="enableResetMode()">Forgot Password</button>
} @if(template==='register'){
<button
mat-button
cdkFocusInitial
(click)="handleRegister()"
style="margin-left: auto"
[disabled]="!form.valid || form.disabled"
>
Register
</button>

<div class="action-buttons-container">
<button
mat-button
cdkFocusInitial
(click)="handleRegister()"
class="ml-auto"
[disabled]="!form.valid || form.disabled"
>
Register
</button>
</div>
} @if(template==='reset'){
<div class="action-buttons-container">
<button mat-button (click)="handleReset()" class="ml-auto" [disabled]="!form.controls.email.valid || form.disabled">
Reset
</button>
</div>
}
</mat-dialog-actions>
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ input.mat-mdc-input-element.mdc-text-field__input {
font-size: 16px;
height: 48px;
}
.action-buttons-container {
display: flex;
width: 100%;
justify-content: space-between;

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
FormControl,
FormGroup,
Expand Down Expand Up @@ -41,7 +41,7 @@ export class showErrorAfterInteraction implements ErrorStateMatcher {
})
export class SupabaseSignInDialogComponent {
public title = 'Sign In';
public template: 'signIn' | 'register' = 'signIn';
public template: 'signIn' | 'register' | 'reset' = 'signIn';

errorMatcher = new showErrorAfterInteraction();

Expand All @@ -50,12 +50,25 @@ export class SupabaseSignInDialogComponent {
password: new FormControl('', Validators.required),
});

private readonly dialogRef = inject(MatDialogRef<SupabaseSignInDialogComponent>);

constructor(
private dialogRef: MatDialogRef<SupabaseSignInDialogComponent>,
private notificationService: PicsaNotificationService,
private supabaseAuthService: SupabaseAuthService
) {}

public enableResetMode() {
this.template = 'reset';
this.title = 'Reset Password';
this.form.removeControl('passwordConfirm');
}

public enableSignInMode() {
this.template = 'signIn';
this.title = 'Sign In';
this.form.removeControl('passwordConfirm');
}

public enableRegisterMode() {
this.template = 'register';
this.title = 'Register';
Expand All @@ -71,8 +84,8 @@ export class SupabaseSignInDialogComponent {
const { data, error } = await this.supabaseAuthService.signInUser(email, password);
console.log({ data, error });
if (error) {
throw new Error(error.message);
this.form.enable();
throw new Error(error.message);
} else {
this.dialogRef.close();
}
Expand All @@ -82,9 +95,21 @@ export class SupabaseSignInDialogComponent {
const { email, password } = this.form.value;
const { error } = await this.supabaseAuthService.signUpUser(email, password);
if (error) {
this.form.enable();
throw new Error(error.message);
} else {
this.dialogRef.close();
}
}
public async handleReset() {
this.form.disable();
const { email } = this.form.value;
const { error } = await this.supabaseAuthService.resetEmailPassword(email);
if (error) {
this.form.enable();
throw new Error(error.message);
} else {
this.notificationService.showSuccessNotification(`Reset email sent, please check your inbox`,{duration:5000});
this.dialogRef.close();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="page-content">
<h2>Password Reset</h2>
<p>Please enter the new password.</p>
<form [formGroup]="form">
<mat-form-field>
<mat-label>Password</mat-label>
<input type="password" matInput autocomplete="picsa-password" formControlName="password" />
</mat-form-field>
<mat-form-field>
<mat-label>Confirm Password</mat-label>
<input type="password" matInput autocomplete="picsa-password" formControlName="confirmPassword" />
</mat-form-field>
<button mat-button [disabled]="!form.valid || form.disabled" (click)="handlePasswordReset()">Reset Password</button>
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

mat-form-field {
display: block;
max-width: 30rem;
}
input.mat-mdc-input-element.mdc-text-field__input {
font-size: 16px;
height: 48px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { PasswordResetComponent } from './password-reset.component';

describe('PasswordResetComponent', () => {
let component: PasswordResetComponent;
let fixture: ComponentFixture<PasswordResetComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PasswordResetComponent],
}).compileComponents();

fixture = TestBed.createComponent(PasswordResetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
FormControl,
FormGroup,
FormGroupDirective,
FormsModule,
NgForm,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { Router } from '@angular/router';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';
import { SupabaseAuthService } from '@picsa/shared/services/core/supabase/services/supabase-auth.service';

export class showErrorAfterInteraction implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}

@Component({
selector: 'dashboard-password-reset.',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
],
templateUrl: './password-reset.component.html',
styleUrl: './password-reset.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PasswordResetComponent {
public form = new FormGroup<{ password: FormControl; confirmPassword?: FormControl }>({
confirmPassword: new FormControl('', [Validators.required]),
password: new FormControl('', Validators.required),
});

constructor(
private router: Router,
private supabaseAuthService: SupabaseAuthService,
private notificationService: PicsaNotificationService
) {}

public async handlePasswordReset() {
if (this.form.value.password !== this.form.value.confirmPassword) {
this.notificationService.showErrorNotification('Make sure your passwords match');
return;
}
this.form.disable();
const { error } = await this.supabaseAuthService.resetResetUserPassword(this.form.value.password);
if (error) {
this.form.enable();
throw new Error(error.message);
} else {
this.notificationService.showSuccessNotification('Password reset successful');
this.router.navigate(['/login']);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { RouterModule } from '@angular/router';
path: '',
loadComponent: () => import('./pages/user-profile/user-profile.component').then((m) => m.UserProfileComponent),
},
{
path: 'password-reset',
loadComponent: () =>
import('./pages/password-reset/password-reset.component').then((m) => m.PasswordResetComponent),
},
]),
],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject,Injectable, signal } from '@angular/core';
import { ENVIRONMENT } from '@picsa/environments';
// eslint-disable-next-line @nx/enforce-module-boundaries
import type { Database } from '@picsa/server-types';
Expand Down Expand Up @@ -35,7 +36,7 @@ export class SupabaseAuthService extends PicsaAsyncService {

private auth: SupabaseAuthClient;

constructor(private notificationService: PicsaNotificationService) {
constructor(@Inject(DOCUMENT) private document: Document, private notificationService: PicsaNotificationService) {
super();
}

Expand All @@ -62,6 +63,21 @@ export class SupabaseAuthService extends PicsaAsyncService {
return this.auth.signUp({ email, password });
}

public async resetEmailPassword(email: string) {
const baseUrl = this.document.location.origin;
const redirectToUrl = `${baseUrl}/profile/password-reset`;
return this.auth.resetPasswordForEmail(email, {
redirectTo: redirectToUrl,
});
}


// this works automatically since the access token is saved in cookies (really cool)
public async resetResetUserPassword(newPassword: string) {
return this.auth.updateUser({ password: newPassword });
}


public async signOut() {
return this.auth.signOut();
}
Expand Down Expand Up @@ -136,4 +152,6 @@ export class SupabaseAuthService extends PicsaAsyncService {
});
// TODO - trigger auth token refresh on permissions change
}


}

0 comments on commit 0753a26

Please sign in to comment.