From 32fb06cf4cd3b152ecb816cc2321b41f5b9152c9 Mon Sep 17 00:00:00 2001 From: Mathieu Hermann Date: Sun, 5 Jan 2025 03:12:34 +0100 Subject: [PATCH 1/2] feat(verification): add security for verified account --- .../pb_migrations/1736037536_updated_users.js | 20 ++ .../1736040606_updated_events.js | 20 ++ .../1736040674_updated_ghosts.js | 22 ++ .../1736040727_updated_records.js | 20 ++ .../1736040770_updated_servers.js | 20 ++ .../1736040817_updated_tracks.js | 22 ++ .../1736040841_updated_public_events.js | 122 ++++++++++ .../1736040870_updated_public_records.js | 230 ++++++++++++++++++ 8 files changed, 476 insertions(+) create mode 100644 backend/pb_migrations/1736037536_updated_users.js create mode 100644 backend/pb_migrations/1736040606_updated_events.js create mode 100644 backend/pb_migrations/1736040674_updated_ghosts.js create mode 100644 backend/pb_migrations/1736040727_updated_records.js create mode 100644 backend/pb_migrations/1736040770_updated_servers.js create mode 100644 backend/pb_migrations/1736040817_updated_tracks.js create mode 100644 backend/pb_migrations/1736040841_updated_public_events.js create mode 100644 backend/pb_migrations/1736040870_updated_public_records.js diff --git a/backend/pb_migrations/1736037536_updated_users.js b/backend/pb_migrations/1736037536_updated_users.js new file mode 100644 index 0000000..f9ac61b --- /dev/null +++ b/backend/pb_migrations/1736037536_updated_users.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_") + + // update collection data + unmarshal({ + "createRule": "@request.body.premium:isset = false && @request.body.verified:isset = false" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_") + + // update collection data + unmarshal({ + "createRule": "@request.body.premium:isset = false" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040606_updated_events.js b/backend/pb_migrations/1736040606_updated_events.js new file mode 100644 index 0000000..fecfa02 --- /dev/null +++ b/backend/pb_migrations/1736040606_updated_events.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1687431684") + + // update collection data + unmarshal({ + "viewRule": "@request.auth.id != \"\" && @request.auth.verified = true" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1687431684") + + // update collection data + unmarshal({ + "viewRule": "@request.auth.id != \"\"" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040674_updated_ghosts.js b/backend/pb_migrations/1736040674_updated_ghosts.js new file mode 100644 index 0000000..23fb710 --- /dev/null +++ b/backend/pb_migrations/1736040674_updated_ghosts.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2379182763") + + // update collection data + unmarshal({ + "createRule": "@request.auth.id != '' && @request.auth.verified = true", + "listRule": "@request.auth.id != '' && @request.auth.verified = true && @request.query.track = track && (@request.query.event = \"undefined\" || @request.query.event = event)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2379182763") + + // update collection data + unmarshal({ + "createRule": "@request.auth.id != ''", + "listRule": "@request.auth.id != '' && @request.query.track = track && (@request.query.event = \"undefined\" || @request.query.event = event)" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040727_updated_records.js b/backend/pb_migrations/1736040727_updated_records.js new file mode 100644 index 0000000..0a51184 --- /dev/null +++ b/backend/pb_migrations/1736040727_updated_records.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_231614380") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != '' && @request.auth.verified = true && (@request.query.server = event.server || @request.query.event = event)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_231614380") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != '' && (@request.query.server = event.server || @request.query.event = event)" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040770_updated_servers.js b/backend/pb_migrations/1736040770_updated_servers.js new file mode 100644 index 0000000..f0062be --- /dev/null +++ b/backend/pb_migrations/1736040770_updated_servers.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3738798621") + + // update collection data + unmarshal({ + "viewRule": "@request.auth.id != \"\" && @request.auth.verified = true" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3738798621") + + // update collection data + unmarshal({ + "viewRule": "@request.auth.id != \"\"" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040817_updated_tracks.js b/backend/pb_migrations/1736040817_updated_tracks.js new file mode 100644 index 0000000..c835ec5 --- /dev/null +++ b/backend/pb_migrations/1736040817_updated_tracks.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_327047008") + + // update collection data + unmarshal({ + "createRule": "@request.auth.id != '' && @request.auth.verified = true && owner.id = @request.auth.id", + "viewRule": "@request.auth.id != '' && @request.auth.verified = true" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_327047008") + + // update collection data + unmarshal({ + "createRule": "@request.auth.id != '' && owner.id = @request.auth.id", + "viewRule": "@request.auth.id != ''" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040841_updated_public_events.js b/backend/pb_migrations/1736040841_updated_public_events.js new file mode 100644 index 0000000..aa9434c --- /dev/null +++ b/backend/pb_migrations/1736040841_updated_public_events.js @@ -0,0 +1,122 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_901890428") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != \"\" && @request.auth.verified = true && @request.query.server = server" + }, collection) + + // remove field + collection.fields.removeById("_clone_QKDJ") + + // remove field + collection.fields.removeById("_clone_93dA") + + // remove field + collection.fields.removeById("_clone_mz7f") + + // add field + collection.fields.addAt(1, new Field({ + "hidden": false, + "id": "_clone_Sqfl", + "max": null, + "min": 0, + "name": "racesLimit", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })) + + // add field + collection.fields.addAt(2, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_3VmJ", + "max": 16, + "min": 3, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(4, new Field({ + "hidden": false, + "id": "_clone_vWIZ", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_901890428") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != \"\" && @request.query.server = server" + }, collection) + + // add field + collection.fields.addAt(1, new Field({ + "hidden": false, + "id": "_clone_QKDJ", + "max": null, + "min": 0, + "name": "racesLimit", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })) + + // add field + collection.fields.addAt(2, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_93dA", + "max": 16, + "min": 3, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(4, new Field({ + "hidden": false, + "id": "_clone_mz7f", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + })) + + // remove field + collection.fields.removeById("_clone_Sqfl") + + // remove field + collection.fields.removeById("_clone_3VmJ") + + // remove field + collection.fields.removeById("_clone_vWIZ") + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1736040870_updated_public_records.js b/backend/pb_migrations/1736040870_updated_public_records.js new file mode 100644 index 0000000..5715ef3 --- /dev/null +++ b/backend/pb_migrations/1736040870_updated_public_records.js @@ -0,0 +1,230 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1396804425") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != \"\" && @request.auth.verified = true && (@request.query.server = event.server || @request.query.event = event.id || @request.query.track = event.track.id)" + }, collection) + + // remove field + collection.fields.removeById("_clone_0EYE") + + // remove field + collection.fields.removeById("_clone_rtdW") + + // remove field + collection.fields.removeById("_clone_Tzx6") + + // remove field + collection.fields.removeById("_clone_1VxK") + + // remove field + collection.fields.removeById("_clone_phXy") + + // remove field + collection.fields.removeById("_clone_Xqb8") + + // add field + collection.fields.addAt(1, new Field({ + "cascadeDelete": true, + "collectionId": "pbc_327047008", + "hidden": false, + "id": "_clone_x4uy", + "maxSelect": 1, + "minSelect": 0, + "name": "track", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + })) + + // add field + collection.fields.addAt(2, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_1687431684", + "hidden": false, + "id": "_clone_vkmz", + "maxSelect": 1, + "minSelect": 0, + "name": "event", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + })) + + // add field + collection.fields.addAt(3, new Field({ + "cascadeDelete": true, + "collectionId": "pbc_3738798621", + "hidden": false, + "id": "_clone_w5Yb", + "maxSelect": 1, + "minSelect": 0, + "name": "server", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + })) + + // add field + collection.fields.addAt(4, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_RRSb", + "max": 16, + "min": 3, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(5, new Field({ + "hidden": false, + "id": "_clone_XXH4", + "max": null, + "min": null, + "name": "timing", + "onlyInt": false, + "presentable": false, + "required": true, + "system": false, + "type": "number" + })) + + // add field + collection.fields.addAt(6, new Field({ + "hidden": false, + "id": "_clone_CMzQ", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1396804425") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != \"\" && (@request.query.server = event.server || @request.query.event = event.id || @request.query.track = event.track.id)" + }, collection) + + // add field + collection.fields.addAt(1, new Field({ + "cascadeDelete": true, + "collectionId": "pbc_327047008", + "hidden": false, + "id": "_clone_0EYE", + "maxSelect": 1, + "minSelect": 0, + "name": "track", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + })) + + // add field + collection.fields.addAt(2, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_1687431684", + "hidden": false, + "id": "_clone_rtdW", + "maxSelect": 1, + "minSelect": 0, + "name": "event", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + })) + + // add field + collection.fields.addAt(3, new Field({ + "cascadeDelete": true, + "collectionId": "pbc_3738798621", + "hidden": false, + "id": "_clone_Tzx6", + "maxSelect": 1, + "minSelect": 0, + "name": "server", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + })) + + // add field + collection.fields.addAt(4, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_1VxK", + "max": 16, + "min": 3, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(5, new Field({ + "hidden": false, + "id": "_clone_phXy", + "max": null, + "min": null, + "name": "timing", + "onlyInt": false, + "presentable": false, + "required": true, + "system": false, + "type": "number" + })) + + // add field + collection.fields.addAt(6, new Field({ + "hidden": false, + "id": "_clone_Xqb8", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + })) + + // remove field + collection.fields.removeById("_clone_x4uy") + + // remove field + collection.fields.removeById("_clone_vkmz") + + // remove field + collection.fields.removeById("_clone_w5Yb") + + // remove field + collection.fields.removeById("_clone_RRSb") + + // remove field + collection.fields.removeById("_clone_XXH4") + + // remove field + collection.fields.removeById("_clone_CMzQ") + + return app.save(collection) +}) From 833612082d3417e396275adc796c675a8c29af86 Mon Sep 17 00:00:00 2001 From: Mathieu Hermann Date: Sun, 5 Jan 2025 03:15:21 +0100 Subject: [PATCH 2/2] feat(verification): add verification process anf pages --- src/app/app.routes.ts | 33 +++++++++++--- src/app/common/models/user.ts | 6 ++- src/app/common/services/auth.service.ts | 23 +++++++++- src/app/pages/profile/profile.component.html | 41 +++++++++++++---- src/app/pages/profile/profile.component.scss | 10 +++-- src/app/pages/profile/profile.component.ts | 7 +-- .../verification/verification.component.html | 45 +++++++++++++++++++ .../verification/verification.component.scss | 6 +++ .../verification/verification.component.ts | 30 +++++++++++++ src/assets/icons/safety_check.svg | 1 + src/assets/icons/verified.svg | 1 + 11 files changed, 178 insertions(+), 25 deletions(-) create mode 100644 src/app/pages/verification/verification.component.html create mode 100644 src/app/pages/verification/verification.component.scss create mode 100644 src/app/pages/verification/verification.component.ts create mode 100644 src/assets/icons/safety_check.svg create mode 100644 src/assets/icons/verified.svg diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6bbdeea..1ec3385 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -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$(); @@ -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', @@ -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', @@ -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', @@ -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', @@ -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: '' } ]; diff --git a/src/app/common/models/user.ts b/src/app/common/models/user.ts index 7f13b1e..5ea97c3 100644 --- a/src/app/common/models/user.ts +++ b/src/app/common/models/user.ts @@ -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: - return new User(record.id, record['name'], record['email'], record['premium']); + return new User(record.id, record['name'], record['email'], record['verified'], record['premium']); } } diff --git a/src/app/common/services/auth.service.ts b/src/app/common/services/auth.service.ts index e74b5f8..8f865c0 100644 --- a/src/app/common/services/auth.service.ts +++ b/src/app/common/services/auth.service.ts @@ -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'; @@ -49,7 +49,8 @@ export class AuthService { map(newUser => { newUser.email = email; return newUser; - }) + }), + switchMap(user => of(this.sendVerificationMail(user.email)).pipe(map(() => user))) ); } @@ -59,6 +60,13 @@ export class AuthService { : null; } + public getRefreshedUser$(): Observable { + 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 { return environment.pb.collection('users').requestPasswordReset(email); } @@ -66,4 +74,15 @@ export class AuthService { public changePassword(password: string, passwordConfirm: string, token: string): Promise { return environment.pb.collection('users').confirmPasswordReset(token, password, passwordConfirm); } + + public verifyUser(token: string): Promise { + return environment.pb.collection('users').confirmVerification(token); + } + + private sendVerificationMail(email: string): Promise { + return environment.pb + .collection('users') + .requestVerification(email) + .catch(() => false); + } } diff --git a/src/app/pages/profile/profile.component.html b/src/app/pages/profile/profile.component.html index b78c9c0..99ad8c2 100644 --- a/src/app/pages/profile/profile.component.html +++ b/src/app/pages/profile/profile.component.html @@ -5,17 +5,40 @@
Profile
-
-
{{ user?.name }}
- +
+
{{ user()?.name }}
+
- @if(!user?.premium) { - -
Profile upgrade will soon be available to all riders
+
Verification status
+ @if(user()?.verified) { +
+ Your profile is verified. +
+ } @else { +
+ You still need to complete the verification process. +
+ + } +
Profile type
+ @if(user()?.premium) { +
+ You ride with a premium profile. +
+ } @else { } @if(!user()?.premium) { +
+ You’re currently riding with a standard profile. +
+
+ 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. +
+ }
diff --git a/src/app/pages/profile/profile.component.scss b/src/app/pages/profile/profile.component.scss index 62c3cc4..835d57a 100644 --- a/src/app/pages/profile/profile.component.scss +++ b/src/app/pages/profile/profile.component.scss @@ -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 { @@ -31,6 +31,10 @@ font-weight: 500; } + .email { + font-size: var(--text-small); + } + .account-badge { font-size: var(--text-small); font-weight: 500; diff --git a/src/app/pages/profile/profile.component.ts b/src/app/pages/profile/profile.component.ts index 27eb9bb..5ba8087 100644 --- a/src/app/pages/profile/profile.component.ts +++ b/src/app/pages/profile/profile.component.ts @@ -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 @@ -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(); diff --git a/src/app/pages/verification/verification.component.html b/src/app/pages/verification/verification.component.html new file mode 100644 index 0000000..11f3d33 --- /dev/null +++ b/src/app/pages/verification/verification.component.html @@ -0,0 +1,45 @@ + + + + + +
+ @if(token && verificationDone()) { +
+ mail sent icon +
+
Profile verified
+
+ Thank you for completing the verification process! +
+
+ Welcome to the mountains, you’re now officially a RetroSki community + rider! Enjoy online adventures and connect with your friends. +
+
+ Start your online riding experience by joining your friends on their + servers. Or take your experience to the next level by + upgrading to a premium profile + to create your own servers, host events, and design custom tracks. +
+ + } @else { +
+ mail sent icon +
+
Almost there!
+
+ Thank you for joining the RetroSki online community! We’re thrilled to + have you on board. +
+
+ 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! +
+ } +
diff --git a/src/app/pages/verification/verification.component.scss b/src/app/pages/verification/verification.component.scss new file mode 100644 index 0000000..6d0d248 --- /dev/null +++ b/src/app/pages/verification/verification.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + flex: 1 1 auto; + flex-direction: column; + padding: 1rem; +} diff --git a/src/app/pages/verification/verification.component.ts b/src/app/pages/verification/verification.component.ts new file mode 100644 index 0000000..525b492 --- /dev/null +++ b/src/app/pages/verification/verification.component.ts @@ -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 { + if (token) { + return from(this.authService.verifyUser(token)); + } + return of(false); + } +} diff --git a/src/assets/icons/safety_check.svg b/src/assets/icons/safety_check.svg new file mode 100644 index 0000000..3887b36 --- /dev/null +++ b/src/assets/icons/safety_check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/verified.svg b/src/assets/icons/verified.svg new file mode 100644 index 0000000..ff404e4 --- /dev/null +++ b/src/assets/icons/verified.svg @@ -0,0 +1 @@ + \ No newline at end of file