Skip to content

Commit

Permalink
feat(verification): add verification process anf pages
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuher committed Jan 5, 2025
1 parent 32fb06c commit 8336120
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 25 deletions.
33 changes: 27 additions & 6 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ export const AuthGuard = () => {
return auth;
};

export const VerifiedGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);

const user = authService.getUser();
if (!user?.verified) {
router.navigate(['/verification']);
}
return true;
};

export const AvailableGuard = () => {
const authService = inject(AuthService);
return authService.isAvailable$();
Expand Down Expand Up @@ -44,12 +55,12 @@ export const routes: Routes = [
{
path: 'ride-online',
loadComponent: () => import('./pages/ride-online/ride-online.component').then(m => m.RideOnlineComponent),
canActivate: [AuthGuard, AvailableGuard]
canActivate: [AuthGuard, VerifiedGuard, AvailableGuard]
},
{
path: 'server/:id',
loadComponent: () => import('./pages/server/server.component').then(m => m.ServerComponent),
canActivate: [AuthGuard, AvailableGuard]
canActivate: [AuthGuard, VerifiedGuard, AvailableGuard]
},
{
path: 'local-event',
Expand All @@ -58,13 +69,13 @@ export const routes: Routes = [
{
path: 'online-event/:id',
loadComponent: () => import('./pages/online-event/online-event.component').then(m => m.OnlineEventComponent),
canActivate: [AuthGuard, AvailableGuard]
canActivate: [AuthGuard, VerifiedGuard, AvailableGuard]
},
{
path: 'create-online-event/:serverId',
loadComponent: () =>
import('./pages/create-online-event/create-online-event.component').then(m => m.CreateOnlineEventComponent),
canActivate: [AuthGuard, AvailableGuard]
canActivate: [AuthGuard, VerifiedGuard, AvailableGuard]
},
{
path: 'manage-tracks',
Expand All @@ -75,7 +86,7 @@ export const routes: Routes = [
path: 'manage-online-tracks',
loadComponent: () => import('./pages/manage-tracks/manage-tracks.component').then(m => m.ManageTracksComponent),
data: { type: 'online' },
canActivate: [AuthGuard, AvailableGuard]
canActivate: [AuthGuard, VerifiedGuard, AvailableGuard]
},
{
path: 'race',
Expand All @@ -86,7 +97,7 @@ export const routes: Routes = [
path: 'online-race/:eventId',
loadComponent: () => import('./pages/race/race.component').then(m => m.RaceComponent),
data: { type: 'online' },
canActivate: [AuthGuard, AvailableGuard]
canActivate: [AuthGuard, VerifiedGuard, AvailableGuard]
},
{
path: 'reset-password',
Expand All @@ -100,5 +111,15 @@ export const routes: Routes = [
import('./pages/reset-password/reset-password.component').then(m => m.ResetPasswordComponent),
canActivate: [AvailableGuard]
},
{
path: 'verification',
loadComponent: () => import('./pages/verification/verification.component').then(m => m.VerificationComponent),
canActivate: [AvailableGuard]
},
{
path: 'verification/:token',
loadComponent: () => import('./pages/verification/verification.component').then(m => m.VerificationComponent),
canActivate: [AvailableGuard]
},
{ path: '**', pathMatch: 'full', redirectTo: '' }
];
6 changes: 4 additions & 2 deletions src/app/common/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ export class User {
public id: string;
public name: string;
public email: string;
public verified: boolean;
public premium: boolean;

constructor(id: string, name: string, email: string, premium: boolean) {
constructor(id: string, name: string, email: string, verified: boolean, premium: boolean) {
this.id = id;
this.name = name;
this.email = email;
this.verified = verified;
this.premium = premium;
}

public static buildFromRecord(record: RecordModel): User {
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
return new User(record.id, record['name'], record['email'], record['premium']);
return new User(record.id, record['name'], record['email'], record['verified'], record['premium']);
}
}
23 changes: 21 additions & 2 deletions src/app/common/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { catchError, from, map, of, type Observable } from 'rxjs';
import { catchError, from, map, of, switchMap, type Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import type { RecordAuthResponse, RecordModel } from 'pocketbase';
import { User } from '../models/user';
Expand Down Expand Up @@ -49,7 +49,8 @@ export class AuthService {
map(newUser => {
newUser.email = email;
return newUser;
})
}),
switchMap(user => of(this.sendVerificationMail(user.email)).pipe(map(() => user)))
);
}

Expand All @@ -59,11 +60,29 @@ export class AuthService {
: null;
}

public getRefreshedUser$(): Observable<User | null> {
return from(environment.pb.collection('users').authRefresh()).pipe(
map(() => User.buildFromRecord(environment.pb.authStore.record as RecordModel)),
catchError(() => of(null))
);
}

public sendResetPasswordMail(email: string): Promise<boolean> {
return environment.pb.collection('users').requestPasswordReset(email);
}

public changePassword(password: string, passwordConfirm: string, token: string): Promise<boolean> {
return environment.pb.collection('users').confirmPasswordReset(token, password, passwordConfirm);
}

public verifyUser(token: string): Promise<boolean> {
return environment.pb.collection('users').confirmVerification(token);
}

private sendVerificationMail(email: string): Promise<boolean> {
return environment.pb
.collection('users')
.requestVerification(email)
.catch(() => false);
}
}
41 changes: 32 additions & 9 deletions src/app/pages/profile/profile.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,40 @@
</app-toolbar>
<div class="retro-content">
<div class="retro-title">Profile</div>
<div class="profile-card" [class.premium]="user!.premium">
<div class="name">{{ user?.name }}</div>
<div class="account-badge">{{ user?.premium ? 'Premium rider' : 'Standard rider' }}</div>
<div class="profile-card" [class.premium]="user()?.premium">
<div class="name">{{ user()?.name }}</div>
<div class="email">{{ user()?.email }}</div>
</div>
@if(!user?.premium) {
<button class="retro-button" disabled>Upgrade to premium</button>
<div class="retro-text secondary">Profile upgrade will soon be available to all riders</div>
<div class="retro-subtitle">Verification status</div>
@if(user()?.verified) {
<div class="retro-text">
Your profile is <span class="tertiary">verified</span>.
</div>
} @else {
<div class="retro-text">
You still need to complete the verification process.
</div>
<button class="retro-button tertiary" routerLink="/verification">
Continue verification
</button>
}
<div class="retro-subtitle">Profile type</div>
@if(user()?.premium) {
<div class="retro-text">
You ride with a <span class="tertiary">premium profile</span>.
</div>
} @else { } @if(!user()?.premium) {
<div class="retro-text">
You’re currently riding with a standard profile.
</div>
<div class="retro-text">
Upgrade to Premium to unlock the full community experience! Enjoy
creating your own servers, events and tracks to ride the slopes with
your friends like never before.
</div>
<button class="retro-button tertiary" disabled>Upgrade to premium</button>
}
</div>
<div class="retro-footer">
<button class="retro-button" (click)="logout()">
Logout
</button>
<button class="retro-button" (click)="logout()">Logout</button>
</div>
10 changes: 7 additions & 3 deletions src/app/pages/profile/profile.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
display: flex;
justify-content: center;
flex-direction: column;
padding: 2rem;
gap: 1.5rem;
padding: 1rem;
gap: 1rem;
background-color: var(--color-primary-lightest);
color: var(--color-primary);
align-items: center;
border-radius: var(--border-radius-large);
border-radius: var(--border-radius-large);
margin-bottom: 1rem;

&.premium {
Expand All @@ -31,6 +31,10 @@
font-weight: 500;
}

.email {
font-size: var(--text-small);
}

.account-badge {
font-size: var(--text-small);
font-weight: 500;
Expand Down
7 changes: 4 additions & 3 deletions src/app/pages/profile/profile.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AuthService } from '../../common/services/auth.service';
import { Router } from '@angular/router';
import { Router, RouterLink } from '@angular/router';
import { ToolbarComponent } from '../../common/components/toolbar/toolbar.component';
import { ButtonIconComponent } from '../../common/components/button-icon/button-icon.component';
import { Location } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
selector: 'app-profile',
standalone: true,
imports: [ButtonIconComponent, ToolbarComponent],
imports: [ButtonIconComponent, RouterLink, ToolbarComponent],
templateUrl: './profile.component.html',
styleUrl: './profile.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
Expand All @@ -19,7 +20,7 @@ export class ProfileComponent {
private readonly router = inject(Router);
private readonly location = inject(Location);

protected user = this.authService.getUser();
protected user = toSignal(this.authService.getRefreshedUser$());

protected logout(): void {
this.authService.logout();
Expand Down
45 changes: 45 additions & 0 deletions src/app/pages/verification/verification.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<app-toolbar>
<ng-container ngProjectAs="left-actions">
<app-button-icon icon="arrow_back" routerLink="/"></app-button-icon>
</ng-container>
</app-toolbar>
<div class="retro-content">
@if(token && verificationDone()) {
<div class="retro-placeholder-image">
<img src="assets/icons/verified.svg" alt="mail sent icon" />
</div>
<div class="retro-title placeholder">Profile verified</div>
<div class="retro-text placeholder">
Thank you for completing the verification process!
</div>
<div class="retro-text placeholder">
Welcome to the mountains, you’re now officially a RetroSki community
rider! Enjoy online adventures and connect with your friends.
</div>
<div class="retro-text placeholder">
Start your online riding experience by joining your friends on their
servers. Or take your experience to the next level by
<a class="retro-link" routerLink="/profile"
>upgrading to a premium profile</a
>
to create your own servers, host events, and design custom tracks.
</div>
<button class="retro-button placeholder tertiary" routerLink="/login">
Ride online
</button>
} @else {
<div class="retro-placeholder-image">
<img src="assets/icons/safety_check.svg" alt="mail sent icon" />
</div>
<div class="retro-title placeholder">Almost there!</div>
<div class="retro-text placeholder">
Thank you for joining the RetroSki online community! We’re thrilled to
have you on board.
</div>
<div class="retro-text placeholder">
A verification email has been sent to your inbox. Please check your
email and click the verification button to activate your account. Once
verified, you’ll be ready to hit the slopes and ride with us!
</div>
}
</div>
6 changes: 6 additions & 0 deletions src/app/pages/verification/verification.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:host {
display: flex;
flex: 1 1 auto;
flex-direction: column;
padding: 1rem;
}
30 changes: 30 additions & 0 deletions src/app/pages/verification/verification.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { ButtonIconComponent } from '../../common/components/button-icon/button-icon.component';
import { ToolbarComponent } from '../../common/components/toolbar/toolbar.component';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { AuthService } from '../../common/services/auth.service';
import { from, type Observable, of } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import type { User } from '../../common/models/user';

@Component({
selector: 'app-verification',
imports: [ButtonIconComponent, RouterLink, ToolbarComponent],
templateUrl: './verification.component.html',
styleUrl: './verification.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class VerificationComponent {
private route = inject(ActivatedRoute);
private authService = inject(AuthService);

protected readonly token = (this.route.snapshot.params as { token: string }).token;
protected verificationDone = toSignal(this.verifyToken(this.token));

private verifyToken(token: string): Observable<boolean> {
if (token) {
return from(this.authService.verifyUser(token));
}
return of(false);
}
}
1 change: 1 addition & 0 deletions src/assets/icons/safety_check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/verified.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 8336120

Please sign in to comment.