fix magic link issue
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user