diff --git a/shared/authentication/src/auth.definition.ts b/shared/authentication/src/auth.definition.ts index 9e61a25..a590b23 100644 --- a/shared/authentication/src/auth.definition.ts +++ b/shared/authentication/src/auth.definition.ts @@ -23,18 +23,21 @@ import { import { eq, sql } from 'drizzle-orm'; import { WXYCRoles } from './auth.roles'; import { sendResetPasswordEmail, sendVerificationEmailMessage } from './email'; +import { rewriteUrlForFrontend } from './url-rewrite'; const buildResetUrl = (url: string, redirectTo?: string) => { + const rewrittenUrl = rewriteUrlForFrontend(url); + if (!redirectTo) { - return url; + return rewrittenUrl; } try { - const parsed = new URL(url); + const parsed = new URL(rewrittenUrl); parsed.searchParams.set('redirectTo', redirectTo); return parsed.toString(); } catch { - return url; + return rewrittenUrl; } }; @@ -90,9 +93,11 @@ export const auth: Auth = betterAuth({ emailVerification: { sendVerificationEmail: async ({ user, url }, request) => { + const verificationUrl = rewriteUrlForFrontend(url); + void sendVerificationEmailMessage({ to: user.email, - verificationUrl: url, + verificationUrl, }).catch((error) => { console.error('Error sending verification email:', error); }); diff --git a/shared/authentication/src/url-rewrite.ts b/shared/authentication/src/url-rewrite.ts new file mode 100644 index 0000000..ddc228d --- /dev/null +++ b/shared/authentication/src/url-rewrite.ts @@ -0,0 +1,18 @@ +/** + * Rewrites a URL to use the frontend host and protocol while preserving path and query params. + * This allows email links to point to the frontend domain while keeping all Better Auth parameters intact. + */ +export const rewriteUrlForFrontend = (url: string): string => { + try { + const parsed = new URL(url); + const frontend = new URL( + process.env.FRONTEND_SOURCE || 'http://localhost:3000' + ); + parsed.host = frontend.host; + parsed.protocol = frontend.protocol; + return parsed.toString(); + } catch { + // If URL parsing fails, return original URL + return url; + } +}; diff --git a/tests/unit/authentication/url-rewrite.test.ts b/tests/unit/authentication/url-rewrite.test.ts new file mode 100644 index 0000000..b82b677 --- /dev/null +++ b/tests/unit/authentication/url-rewrite.test.ts @@ -0,0 +1,75 @@ +import { rewriteUrlForFrontend } from '../../../shared/authentication/src/url-rewrite'; + +describe('rewriteUrlForFrontend', () => { + const originalEnv = process.env.FRONTEND_SOURCE; + + afterEach(() => { + if (originalEnv) { + process.env.FRONTEND_SOURCE = originalEnv; + } else { + delete process.env.FRONTEND_SOURCE; + } + }); + + it('replaces host and protocol while preserving path and query params', () => { + process.env.FRONTEND_SOURCE = 'https://dj.wxyc.org'; + const input = + 'https://api.wxyc.org/auth/verify-email?token=abc123&callbackURL=%2Fonboarding'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe( + 'https://dj.wxyc.org/auth/verify-email?token=abc123&callbackURL=%2Fonboarding' + ); + }); + + it('handles URLs without query params', () => { + process.env.FRONTEND_SOURCE = 'https://dj.wxyc.org'; + const input = 'https://api.wxyc.org/auth/reset-password/token123'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe('https://dj.wxyc.org/auth/reset-password/token123'); + }); + + it('preserves complex query parameters', () => { + process.env.FRONTEND_SOURCE = 'https://dj.wxyc.org'; + const input = + 'https://api.wxyc.org/auth/verify-email?token=xyz&callbackURL=%2Fdashboard&redirectTo=%2Fhome'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe( + 'https://dj.wxyc.org/auth/verify-email?token=xyz&callbackURL=%2Fdashboard&redirectTo=%2Fhome' + ); + }); + + it('handles invalid URLs gracefully by returning original', () => { + process.env.FRONTEND_SOURCE = 'https://dj.wxyc.org'; + const input = 'not-a-valid-url'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe('not-a-valid-url'); + }); + + it('uses default localhost when FRONTEND_SOURCE is not set', () => { + delete process.env.FRONTEND_SOURCE; + const input = 'https://api.wxyc.org/auth/verify-email?token=test'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe('http://localhost:3000/auth/verify-email?token=test'); + }); + + it('handles different protocols correctly', () => { + process.env.FRONTEND_SOURCE = 'http://localhost:3000'; + const input = 'https://api.wxyc.org/auth/verify-email'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe('http://localhost:3000/auth/verify-email'); + }); + + it('preserves port numbers in frontend URL', () => { + process.env.FRONTEND_SOURCE = 'http://localhost:8080'; + const input = 'https://api.wxyc.org/auth/verify-email'; + const result = rewriteUrlForFrontend(input); + + expect(result).toBe('http://localhost:8080/auth/verify-email'); + }); +});