fix magic link issue

This commit is contained in:
wass08
2026-02-11 17:09:08 +09:00
parent 64205b1077
commit 92f1c065f8
6 changed files with 129 additions and 221 deletions
+3 -21
View File
@@ -3,27 +3,9 @@
* Handles all /api/auth/* routes for authentication
*/
import { auth } from '@pascal-app/auth/server'
import { toNextJsHandler } from 'better-auth/next-js'
import type { NextRequest } from 'next/server'
import { auth } from '@/lib/auth'
// Lazy initialization of the handler
let handler: ReturnType<typeof toNextJsHandler> | null = null
const { GET, POST } = toNextJsHandler(auth)
function getHandler() {
if (!handler) {
handler = toNextJsHandler(auth)
}
return handler
}
// Export route handlers that initialize lazily
export async function GET(request: NextRequest) {
const handlers = getHandler()
return handlers.GET(request)
}
export async function POST(request: NextRequest) {
const handlers = getHandler()
return handlers.POST(request)
}
export { GET, POST }
+44
View File
@@ -0,0 +1,44 @@
import { createAuth } from '@pascal-app/auth/server'
import { db } from '@pascal-app/db'
import { Resend } from 'resend'
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null
export const auth = createAuth({
db,
appName: 'Pascal Editor',
baseURL: process.env.BETTER_AUTH_URL!,
secret: process.env.BETTER_AUTH_SECRET!,
sendMagicLink: async ({ email, url }) => {
if (!resend) {
console.log(`[DEV] Magic link for ${email}: ${url}`)
return
}
try {
await resend.emails.send({
from: 'Pascal <noreply@pascal.app>',
to: email,
subject: 'Sign in to Pascal Editor',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Sign in to Pascal Editor</h2>
<p>Click the button below to sign in to your account:</p>
<a href="${url}" style="display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;">
Sign In
</a>
<p style="color: #666; font-size: 14px;">This link will expire in 5 minutes.</p>
<p style="color: #666; font-size: 14px;">If you didn't request this email, you can safely ignore it.</p>
</div>
`,
})
console.log(`✓ Magic link email sent to ${email}`)
} catch (error) {
console.error('Failed to send magic link email:', error)
throw error
}
},
})
export type Session = typeof auth.$Infer.Session
export type User = typeof auth.$Infer.Session.user
+48 -97
View File
@@ -1,33 +1,40 @@
import { db, schema } from '@pascal-app/db'
import type { Database } from '@pascal-app/db'
import { schema } from '@pascal-app/db'
import type { BetterAuthOptions } from 'better-auth'
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { magicLink } from 'better-auth/plugins'
import { Resend } from 'resend'
let _auth: ReturnType<typeof betterAuth> | null = null
export interface SendMagicLinkParams {
email: string
url: string
token: string
}
function getAuth() {
if (!_auth) {
const betterAuthSecret = process.env.BETTER_AUTH_SECRET
const betterAuthUrl = process.env.BETTER_AUTH_URL
export interface AuthConfig {
db: Database
appName: string
baseURL: string
secret: string
/** Callback to send magic link emails */
sendMagicLink?: (params: SendMagicLinkParams) => Promise<void>
/** Additional plugins to add (e.g., nextCookies for web) */
additionalPlugins?: BetterAuthOptions['plugins']
}
if (!betterAuthSecret) {
throw new Error(
'Missing BETTER_AUTH_SECRET environment variable. Please configure it in your deployment settings or .env.local file. Generate one with: openssl rand -base64 32',
)
}
if (!betterAuthUrl) {
throw new Error(
'Missing BETTER_AUTH_URL environment variable. Please configure it in your deployment settings or .env.local file. Set it to your app URL (e.g., http://localhost:3000 or https://yourdomain.com)',
)
}
// Initialize Resend for email sending
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null
_auth = betterAuth({
database: drizzleAdapter(db, {
/**
* Creates a Better Auth instance with full configuration including:
* - Magic link authentication
* - Custom session with activePropertyId
* - Session cookie caching
*/
export function createAuth(config: AuthConfig): ReturnType<typeof betterAuth> {
return betterAuth({
appName: config.appName,
baseURL: config.baseURL,
secret: config.secret,
basePath: '/api/auth',
database: drizzleAdapter(config.db, {
provider: 'pg',
usePlural: true,
schema,
@@ -37,89 +44,33 @@ function getAuth() {
generateId: false, // Use our prefixed nanoid IDs from schema
},
},
secret: betterAuthSecret,
baseURL: betterAuthUrl,
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
if (!resend) {
console.log(`[DEV] Magic link for ${email}: ${url}`)
return
}
try {
await resend.emails.send({
from: 'Pascal <noreply@pascal.app>',
to: email,
subject: 'Sign in to Pascal Editor',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Sign in to Pascal Editor</h2>
<p>Click the button below to sign in to your account:</p>
<a href="${url}" style="display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;">
Sign In
</a>
<p style="color: #666; font-size: 14px;">This link will expire in 5 minutes.</p>
<p style="color: #666; font-size: 14px;">If you didn't request this email, you can safely ignore it.</p>
</div>
`,
})
console.log(`✓ Magic link email sent to ${email}`)
} catch (error) {
console.error('Failed to send magic link email:', error)
throw error
}
},
}),
],
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day
// Session caching to reduce database queries
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
maxAge: 5 * 60, // Cache duration in seconds (5 minutes)
},
additionalFields: {
// Additional fields for the session table
activePropertyId: {
type: 'string',
},
},
},
plugins: [
...(config.additionalPlugins ?? []),
// Magic link authentication
...(config.sendMagicLink
? [
magicLink({
sendMagicLink: config.sendMagicLink,
expiresIn: 300, // 5 minutes
disableSignUp: false, // Allow new users to sign up via magic link
}),
]
: []),
],
})
}
return _auth
}
/**
* Better Auth server instance
* Configured with PostgreSQL database (Supabase) and magic link authentication
*
* Initialized lazily to avoid requiring env vars at build time
*/
export const auth = new Proxy({} as ReturnType<typeof betterAuth>, {
get(_target, prop) {
return Reflect.get(getAuth(), prop)
},
})
/**
* Type helpers for better-auth session
* These are generic types that should match Better Auth's session structure
*/
export type Session = {
id: string
userId: string
activePropertyId?: string | null
expiresAt: Date
createdAt: Date
updatedAt: Date
}
export type User = {
id: string
email: string
emailVerified: boolean
name: string
createdAt: Date
updatedAt: Date
}
export type Auth = ReturnType<typeof createAuth>
+3 -24
View File
@@ -1,32 +1,11 @@
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types'
let _supabase: ReturnType<typeof createClient<Database>> | null = null
function getSupabase() {
if (!_supabase) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'Missing required environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. Please configure them in your deployment settings or .env.local file.'
)
}
_supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
}
return _supabase
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
/**
* Supabase client for client-side use with anon key
* Uses Row Level Security (RLS) policies
*
* Initialized lazily to avoid requiring env vars at build time
*/
export const supabase = new Proxy({} as ReturnType<typeof createClient<Database>>, {
get(_target, prop) {
return Reflect.get(getSupabase(), prop)
},
})
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
+3 -30
View File
@@ -1,38 +1,11 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
let _db: PostgresJsDatabase<typeof schema> | null = null
let _client: ReturnType<typeof postgres> | null = null
const connectionString = process.env.POSTGRES_URL!
function getDb() {
if (!_db) {
const postgresUrl = process.env.POSTGRES_URL
const client = postgres(connectionString, { prepare: false })
if (!postgresUrl) {
throw new Error(
'Missing POSTGRES_URL environment variable. Please configure it in your deployment settings or .env.local file.',
)
}
// Create postgres connection
_client = postgres(postgresUrl)
// Create drizzle instance
_db = drizzle(_client, { schema })
}
return _db
}
/**
* Drizzle database instance
* Initialized lazily to avoid requiring env vars at build time
*/
export const db = new Proxy({} as PostgresJsDatabase<typeof schema>, {
get(_target, prop) {
return Reflect.get(getDb(), prop)
},
})
export const db = drizzle({ client, schema })
export type Database = typeof db
+6 -27
View File
@@ -1,38 +1,17 @@
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types'
let _supabaseAdmin: ReturnType<typeof createClient<Database>> | null = null
function getSupabaseAdmin() {
if (!_supabaseAdmin) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error(
'Missing required environment variables: NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY. Please configure them in your deployment settings or .env.local file.'
)
}
_supabaseAdmin = createClient<Database>(supabaseUrl, supabaseServiceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
},
})
}
return _supabaseAdmin
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
/**
* Supabase client for server-side use with service role key
* Bypasses Row Level Security (RLS) - use with caution
* Always filter by user_id to enforce permissions
*
* Initialized lazily to avoid requiring env vars at build time
*/
export const supabaseAdmin = new Proxy({} as ReturnType<typeof createClient<Database>>, {
get(_target, prop) {
return Reflect.get(getSupabaseAdmin(), prop)
export const supabaseAdmin = createClient<Database>(supabaseUrl, supabaseServiceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
},
})