Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions shared/authentication/src/auth.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

Expand Down Expand Up @@ -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);
});
Expand Down
18 changes: 18 additions & 0 deletions shared/authentication/src/url-rewrite.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
75 changes: 75 additions & 0 deletions tests/unit/authentication/url-rewrite.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading