Skip to content

Commit 81fbf97

Browse files
committed
feat(auth): add supabase token verification flow
- Add supabaseVerifyToken method to AuthService for token verification and session setup - Implement verifyToken method in SupabaseAuthService to verify token and create user session - Extend authRouter with supabaseVerifyToken procedure for token verification via API - Modify AppComponent to detect access_token in URL hash and verify using supabaseVerifyToken - Clear URL hash after token verification to improve user experience - Refactor and clean up related authentication code for OAuth and token verification integration
1 parent c06ee77 commit 81fbf97

File tree

4 files changed

+114
-17
lines changed

4 files changed

+114
-17
lines changed

web/src/app/app.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RouteMeta } from '@analogjs/router';
22
import { Component, inject, OnInit } from '@angular/core';
33
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
44
import { LucideAngularModule } from 'lucide-angular';
5-
import { concatMap, first, mergeMap, tap } from 'rxjs';
5+
import { concatMap, first, firstValueFrom, mergeMap, tap } from 'rxjs';
66

77
import { AppInitializerService } from './app.initializer';
88
import { ColorSchemeSwitcherComponent } from './components/theme/color-scheme-switcher.component';
@@ -133,6 +133,16 @@ export class AppComponent implements OnInit {
133133
this.router.events
134134
.pipe(
135135
concatMap(async event => {
136+
if (
137+
typeof location !== 'undefined' &&
138+
location.hash.startsWith('#access_token=')
139+
) {
140+
const token = location.hash
141+
.split('#access_token=')?.[1]
142+
?.split('&')?.[0];
143+
await firstValueFrom(this.authService.supabaseVerifyToken(token));
144+
location.hash = '';
145+
}
136146
if (event instanceof NavigationEnd) {
137147
return await this.appInitializerService.init();
138148
}

web/src/app/services/auth.service.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ export class AuthService {
118118
);
119119
}
120120

121-
supabaseSignInWithOAuth(provider: 'google' | 'github' | 'facebook' | 'twitter') {
121+
supabaseSignInWithOAuth(
122+
provider: 'google' | 'github' | 'facebook' | 'twitter'
123+
) {
122124
return firstValueFrom(
123125
this.trpc.auth.supabaseSignInWithOAuth.mutate({ provider })
124126
).then(redirectUrl => {
@@ -127,7 +129,32 @@ export class AuthService {
127129
});
128130
}
129131

130-
supabaseVerifyEmail(token: string, type: 'signup' | 'recovery' | 'invite' | 'magiclink', email?: string) {
132+
supabaseVerifyToken(token: string) {
133+
return from(this.sessionService.remove()).pipe(
134+
concatMap(async () => {
135+
try {
136+
const { sessionId, user } = await firstValueFrom(
137+
this.trpc.auth.supabaseVerifyToken.mutate({ token })
138+
);
139+
await this.sessionService.set(sessionId);
140+
await this.profileService.set(user as unknown as User);
141+
return { sessionId, user };
142+
} catch (error) {
143+
await this.errorHandler.handleError(
144+
error,
145+
'Failed to verify token with Supabase'
146+
);
147+
throw error;
148+
}
149+
})
150+
);
151+
}
152+
153+
supabaseVerifyEmail(
154+
token: string,
155+
type: 'signup' | 'recovery' | 'invite' | 'magiclink',
156+
email?: string
157+
) {
131158
return from(this.sessionService.remove()).pipe(
132159
concatMap(async () => {
133160
try {

web/src/server/supabase/auth.service.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export class SupabaseAuthService {
5656

5757
async signIn(signInData: SupabaseSignInData): Promise<SupabaseAuthResult> {
5858
const { email, password } = signInData;
59-
console.log({email, password})
6059

6160
// Sign in user with Supabase
6261
const { data, error } = await this.supabase.auth.signInWithPassword({
@@ -83,7 +82,9 @@ export class SupabaseAuthService {
8382
return { user: user as UserType, sessionId: session.id };
8483
}
8584

86-
async signInWithOAuth(provider: 'google' | 'github' | 'facebook' | 'twitter') {
85+
async signInWithOAuth(
86+
provider: 'google' | 'github' | 'facebook' | 'twitter'
87+
) {
8788
const { data, error } = await this.supabase.auth.signInWithOAuth({
8889
provider,
8990
options: {
@@ -98,13 +99,41 @@ export class SupabaseAuthService {
9899
return data.url; // This is the URL to redirect the user to
99100
}
100101

101-
async verifyEmailToken(token: string, type: 'signup' | 'recovery' | 'invite' | 'magiclink', email?: string) {
102+
async verifyToken(token: string) {
103+
const { data, error } = await this.supabase.auth.getUser(token);
104+
105+
if (error) {
106+
throw new Error(`Token verification failed: ${error.message}`);
107+
}
108+
109+
if (!data.user) {
110+
throw new Error('No user returned from token verification');
111+
}
112+
113+
// Create or update user in our database
114+
const user = await this.createOrUpdateUser(data.user);
115+
116+
// Create session in our database
117+
const session = await prisma.session.create({
118+
data: { userId: user.id },
119+
});
120+
121+
return { user: user as UserType, sessionId: session.id };
122+
}
123+
124+
async verifyEmailToken(
125+
token: string,
126+
type: 'signup' | 'recovery' | 'invite' | 'magiclink',
127+
email?: string
128+
) {
102129
let verifyParams;
103-
130+
104131
if (type === 'signup' || type === 'magiclink') {
105132
// For signup and magiclink, we need the email
106133
if (!email) {
107-
throw new Error('Email is required for signup and magiclink verification');
134+
throw new Error(
135+
'Email is required for signup and magiclink verification'
136+
);
108137
}
109138
verifyParams = {
110139
email,
@@ -118,7 +147,7 @@ export class SupabaseAuthService {
118147
type,
119148
};
120149
}
121-
150+
122151
const { data, error } = await this.supabase.auth.verifyOtp(verifyParams);
123152

124153
if (error) {
@@ -176,7 +205,11 @@ export class SupabaseAuthService {
176205
supabaseUserData: {
177206
id: supabaseUser.id,
178207
email: supabaseUser.email,
179-
name: name || supabaseUser.user_metadata?.name || supabaseUser.email?.split('@')[0] || '',
208+
name:
209+
name ||
210+
supabaseUser.user_metadata?.name ||
211+
supabaseUser.email?.split('@')[0] ||
212+
'',
180213
aud: supabaseUser.aud,
181214
role: supabaseUser.role,
182215
emailConfirmedAt: supabaseUser.email_confirmed_at,
@@ -194,7 +227,11 @@ export class SupabaseAuthService {
194227
supabaseUserData: {
195228
id: supabaseUser.id,
196229
email: supabaseUser.email,
197-
name: name || supabaseUser.user_metadata?.name || supabaseUser.email?.split('@')[0] || '',
230+
name:
231+
name ||
232+
supabaseUser.user_metadata?.name ||
233+
supabaseUser.email?.split('@')[0] ||
234+
'',
198235
aud: supabaseUser.aud,
199236
role: supabaseUser.role,
200237
emailConfirmedAt: supabaseUser.email_confirmed_at,
@@ -209,4 +246,4 @@ export class SupabaseAuthService {
209246

210247
return user;
211248
}
212-
}
249+
}

web/src/server/trpc/routers/auth.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { z } from 'zod';
44

55
import { prisma } from '../../prisma';
66
import { UserSchema, UserType } from '../../types/UserSchema';
7-
import { SupabaseAuthService, SupabaseSignUpData, SupabaseSignInData } from '../../supabase/auth.service';
7+
import {
8+
SupabaseAuthService,
9+
SupabaseSignUpData,
10+
SupabaseSignInData,
11+
} from '../../supabase/auth.service';
812
import { publicProcedure, router } from '../trpc';
913

1014
const supabaseAuthService = new SupabaseAuthService();
@@ -73,7 +77,7 @@ export const authRouter = router({
7377
password: input.password,
7478
name: input.name,
7579
};
76-
80+
7781
return await supabaseAuthService.signUp(signUpData);
7882
}),
7983

@@ -95,7 +99,7 @@ export const authRouter = router({
9599
email: input.email,
96100
password: input.password,
97101
};
98-
102+
99103
return await supabaseAuthService.signIn(signInData);
100104
}),
101105

@@ -125,7 +129,26 @@ export const authRouter = router({
125129
})
126130
)
127131
.mutation(async ({ input }) => {
128-
return await supabaseAuthService.verifyEmailToken(input.token, input.type, input.email);
132+
return await supabaseAuthService.verifyEmailToken(
133+
input.token,
134+
input.type,
135+
input.email
136+
);
129137
}),
130-
});
131138

139+
supabaseVerifyToken: publicProcedure
140+
.input(
141+
z.object({
142+
token: z.string(),
143+
})
144+
)
145+
.output(
146+
z.object({
147+
sessionId: z.string().uuid(),
148+
user: UserSchema,
149+
})
150+
)
151+
.mutation(async ({ input }) => {
152+
return await supabaseAuthService.verifyToken(input.token);
153+
}),
154+
});

0 commit comments

Comments
 (0)