Skip to content

Commit

Permalink
feat(auth): adds OTP code auth/verification on signin and register
Browse files Browse the repository at this point in the history
  • Loading branch information
rustygreen committed Dec 17, 2024
1 parent ff21b8c commit 941fde2
Show file tree
Hide file tree
Showing 17 changed files with 270 additions and 122 deletions.
8 changes: 0 additions & 8 deletions apps/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { ChangeDetectionStrategy, Component } from '@angular/core';

// 3rd party.
import { Lara } from 'primeng/themes/lara';
import { PrimeNGConfig } from 'primeng/api';

@Component({
standalone: true,
imports: [CommonModule, RouterOutlet],
Expand All @@ -17,8 +13,4 @@ import { PrimeNGConfig } from 'primeng/api';
})
export class AppComponent {
title = 'ng-supabase';

constructor(private config: PrimeNGConfig) {
this.config.theme.set({ preset: Lara });
}
}
7 changes: 7 additions & 0 deletions apps/demo/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { provideRouter } from '@angular/router';
import { ApplicationConfig } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

// 3rd party.
import Aura from '@primeng/themes/aura';
import { providePrimeNG } from 'primeng/config';

// ng-supabase.
import { provideSupabase } from '@ng-supabase/primeng';
import { LogLevel, ALL_SOCIAL_SIGN_INS } from '@ng-supabase/core';
Expand All @@ -15,6 +19,9 @@ export const appConfig: ApplicationConfig = {
providers: [
provideRouter(appRoutes),
provideAnimationsAsync(),
providePrimeNG({
theme: { preset: Aura },
}),
provideSupabase({
// NOTE: You can optionally set "project" instead of "apiUrl".
apiUrl: localStorage.getItem(STORAGE_KEYS.apiUrl) || 'YOUR_PROJECT_URL',
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/app/primeng/register/register.component.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<supabase-register redirectToPath="/primeng/set-password"></supabase-register>
<supabase-register></supabase-register>
Binary file modified assets/primeng-sign-in-message.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions libs/core/src/lib/register/register.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class RegisterComponent implements OnInit {

readonly errorMessage = signal('');
readonly working = signal(false);
readonly verifyingOtp = signal(false);
readonly wait = signal<WaitMessage | null>(null);
readonly form: FormGroup = new FormGroup({
email: new FormControl('', [Validators.required]),
Expand Down Expand Up @@ -99,6 +100,7 @@ export class RegisterComponent implements OnInit {
this.wait.set({
icon: 'pi pi-envelope',
title: 'Check your email',
enableOtp: this.config.signIn.otpEnabled,
message: `An email has been sent to <strong>${email}</strong> with a link to verify your email address. Simply click the link from your email and follow the instructions to continue.`,
});
} finally {
Expand All @@ -107,6 +109,24 @@ export class RegisterComponent implements OnInit {
}
}

async verifyOtp(token: string): Promise<void> {
this.verifyingOtp.set(true);
const email = this.form.value.email as string;
const { error } = await this.supabase.client.auth.verifyOtp({
email,
token,
type: 'email',
});

if (error) {
this.onError(error);
return;
}

const redirectUrl = this.getRedirectTo();
this.routeService.goTo(redirectUrl);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
onError(error: AuthError | string): void {
// Do nothing in here because this is just a hook for child
Expand Down
61 changes: 51 additions & 10 deletions libs/core/src/lib/sign-in/sign-in.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import { UrlTree } from '@angular/router';
import {
Input,
signal,
OnInit,
Component,
inject,
ChangeDetectorRef,
Component,
ChangeDetectionStrategy,
} from '@angular/core';
import {
Expand Down Expand Up @@ -42,23 +42,38 @@ export class SignInComponent implements OnInit {
@Input() redirectTo = '';
@Input() rememberMe: boolean | undefined;

/**
* The absolute route to redirect to from the email link. This should not
* be used in conjunction with "redirectToPath" (use one or the other).
*/
@Input() redirectToUrl = '';

/**
* A route path to redirect to from the email link (as apposed to an absolute path).
* This path will be appended to the app's root URL and will be the URL that is
* targeted from the email link. This should not be used in conjunction with
* "redirectTo" (use one or the other).
*/
@Input() redirectToPath = '';

forgotPassword = false;
wait: WaitMessage | null = null;
signingIn = new Subject<boolean>();
errorMessage = new Subject<string>();
form = new FormGroup({
email: new FormControl('', [Validators.required]),
password: new FormControl(''),
usePassword: new FormControl(false),
rememberMe: new FormControl(true),
});

readonly errorMessage = signal<string | null>(null);
readonly wait = signal<WaitMessage | null>(null);
readonly verifyingOtp = signal(false);

protected readonly log = inject(LogService);
protected readonly config = inject(SupabaseConfig);
protected readonly supabase = inject(SupabaseService);
protected readonly routeService = inject(RouteService);
protected readonly storage = inject(PersistentStorageService);
protected readonly changeDetector = inject(ChangeDetectorRef);

ngOnInit(): void {
this.title = this.title ?? this.config.signIn.title;
Expand Down Expand Up @@ -99,6 +114,24 @@ export class SignInComponent implements OnInit {
: this.signInWithMagicLink();
}

async verifyOtp(token: string): Promise<void> {
this.verifyingOtp.set(true);
const email = this.form.value.email as string;
const { error } = await this.supabase.client.auth.verifyOtp({
email,
token,
type: 'email',
});

if (error) {
this.errorMessage.set(error.message);
return;
}

const redirectUrl = this.getRedirectTo();
this.routeService.goTo(redirectUrl);
}

protected revalidateAllControls(): void {
Object.values(this.form.controls).forEach((control) =>
control.updateValueAndValidity()
Expand All @@ -118,7 +151,7 @@ export class SignInComponent implements OnInit {

if (error) {
this.log.debug(`Sign in failed. ${error.message}`);
this.errorMessage.next(error.message);
this.errorMessage.set(error.message);
return;
}

Expand Down Expand Up @@ -159,16 +192,17 @@ export class SignInComponent implements OnInit {
});

if (error) {
this.errorMessage.next(error.message);
this.errorMessage.set(error.message);
return;
}

this.wait = {
this.wait.set({
icon: 'pi pi-envelope',
title: 'Check your email',
enableOtp: this.config.signIn.otpEnabled,
message: `An email has been sent to <strong>${email}</strong> with a magic link to sign in. Simply click the link from your email and you will automatically be signed into this app.`,
};
this.changeDetector.markForCheck();
});

this.trySaveRememberMe();
} catch (error) {
// TODO: Handle - @russell.green
Expand Down Expand Up @@ -201,4 +235,11 @@ export class SignInComponent implements OnInit {
this.clearRememberMe();
}
}

protected getRedirectTo(): string {
const fallback = this.config.routes.userProfile || this.config.routes.main;
return this.redirectToPath
? this.routeService.appendRoute(this.redirectToPath)
: this.redirectToUrl || this.routeService.appendRoute(fallback);
}
}
4 changes: 4 additions & 0 deletions libs/core/src/lib/supabase-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ interface SignInConfigProperties {
socials?: SocialSignIn[];
socialIconsRoot?: string;
rememberMeStorageKey?: string;
otpEnabled?: boolean;
otpLength?: number;
redirectTo?: string | string[] | UrlTree | null | undefined;
onSocialSignIn?: SocialSignInFn;
}
Expand Down Expand Up @@ -144,6 +146,8 @@ export class SignInConfig implements SignInConfigProperties {
socialSignInItems: SocialSignInItem[] = [];
redirectTo?: string | string[] | UrlTree | null | undefined;
rememberMeStorageKey = 'supabase.auth.info';
otpEnabled = true;
otpLength = 6;
onSocialSignIn?: SocialSignInFn;

constructor(init?: Partial<SignInConfig>) {
Expand Down
3 changes: 2 additions & 1 deletion libs/core/src/lib/wait-message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface WaitMessage {
icon?: string;
title: string;
message: string;
icon?: string;
enableOtp?:boolean;
}
3 changes: 1 addition & 2 deletions libs/primeng/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@
"@angular/core": ">=18.0.0",
"@angular/forms": ">=18.0.0",
"@angular/router": ">=18.0.0",
"primeng": ">=18.0.0-beta.2",
"primeng": ">=18.0.0",
"primeicons": ">=7.0.0",
"rxjs": ">=7.8.0",
"@ng-supabase/core": "*",
"@supabase/supabase-js": ">=2.45.0"
},
Expand Down
3 changes: 2 additions & 1 deletion libs/primeng/src/lib/register/register.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ <h1>{{ title }}</h1>
[message]="wait()"
[showCancel]="false"
subTitle="You can close this window"
[loading]="verifyingOtp()"
(sendAgain)="register()"
(verifyOtp)="verifyOtp($event)"
></supabase-wait-message>

} @else {
<!-- Form -->
<form [formGroup]="form" (ngSubmit)="register()">
Expand Down
36 changes: 12 additions & 24 deletions libs/primeng/src/lib/sign-in/sign-in.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,16 @@ <h4>Logging in. Please wait...</h4>
}

<!-- Wait -->
@if(wait){
<p-fieldset>
<ng-template pTemplate="header">
<div class="flex items-center gap-2 px-2">
<i [ngClass]="wait.icon"></i>
<span class="font-bold">{{ wait.title }}</span>
</div>
</ng-template>
<div>
<p class="m-0" [innerHTML]="wait.message"></p>
<p class="mt-4 text-right">
<small
>Didn't receive it?
<a href="" (click)="signIn(); $event.preventDefault()">send again</a> |
<a href="" (click)="wait = null; $event.preventDefault()"
>cancel</a
></small
>
</p>
</div>
</p-fieldset>

@if(wait()){
<supabase-wait-message
[message]="wait()"
[showCancel]="true"
subTitle="You can close this window"
[loading]="verifyingOtp()"
(sendAgain)="signIn()"
(verifyOtp)="verifyOtp($event)"
(cancel)="wait.set(null)"
></supabase-wait-message>
} @else if(forgotPassword) {
<supabase-reset-password
[email]="form.value.email || ''"
Expand Down Expand Up @@ -120,11 +108,11 @@ <h4>Logging in. Please wait...</h4>
</div>

<!-- Error message -->
<supabase-messages class="mt-4" [messages]="messages"></supabase-messages>
<supabase-messages class="mt-4" [messages]="messages()"></supabase-messages>

<!-- Socials -->
<supabase-socials-grid
(errorMessage)="errorMessage.next($event)"
(errorMessage)="errorMessage.set($event)"
></supabase-socials-grid>
</form>
}
Loading

0 comments on commit 941fde2

Please sign in to comment.