fix magic link issue
This commit is contained in:
@@ -3,27 +3,9 @@
|
|||||||
* Handles all /api/auth/* routes for authentication
|
* Handles all /api/auth/* routes for authentication
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { auth } from '@pascal-app/auth/server'
|
|
||||||
import { toNextJsHandler } from 'better-auth/next-js'
|
import { toNextJsHandler } from 'better-auth/next-js'
|
||||||
import type { NextRequest } from 'next/server'
|
import { auth } from '@/lib/auth'
|
||||||
|
|
||||||
// Lazy initialization of the handler
|
const { GET, POST } = toNextJsHandler(auth)
|
||||||
let handler: ReturnType<typeof toNextJsHandler> | null = null
|
|
||||||
|
|
||||||
function getHandler() {
|
export { GET, POST }
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
+70
-119
@@ -1,125 +1,76 @@
|
|||||||
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 { betterAuth } from 'better-auth'
|
||||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
||||||
import { magicLink } from 'better-auth/plugins'
|
import { magicLink } from 'better-auth/plugins'
|
||||||
import { Resend } from 'resend'
|
|
||||||
|
|
||||||
let _auth: ReturnType<typeof betterAuth> | null = null
|
export interface SendMagicLinkParams {
|
||||||
|
|
||||||
function getAuth() {
|
|
||||||
if (!_auth) {
|
|
||||||
const betterAuthSecret = process.env.BETTER_AUTH_SECRET
|
|
||||||
const betterAuthUrl = process.env.BETTER_AUTH_URL
|
|
||||||
|
|
||||||
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, {
|
|
||||||
provider: 'pg',
|
|
||||||
usePlural: true,
|
|
||||||
schema,
|
|
||||||
}),
|
|
||||||
advanced: {
|
|
||||||
database: {
|
|
||||||
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
|
|
||||||
cookieCache: {
|
|
||||||
enabled: true,
|
|
||||||
maxAge: 5 * 60, // 5 minutes
|
|
||||||
},
|
|
||||||
additionalFields: {
|
|
||||||
activePropertyId: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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
|
email: string
|
||||||
emailVerified: boolean
|
url: string
|
||||||
name: string
|
token: string
|
||||||
createdAt: Date
|
|
||||||
updatedAt: Date
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
}),
|
||||||
|
advanced: {
|
||||||
|
database: {
|
||||||
|
generateId: false, // Use our prefixed nanoid IDs from schema
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
// Session caching to reduce database queries
|
||||||
|
cookieCache: {
|
||||||
|
enabled: true,
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Auth = ReturnType<typeof createAuth>
|
||||||
|
|||||||
@@ -1,32 +1,11 @@
|
|||||||
import { createClient } from '@supabase/supabase-js'
|
import { createClient } from '@supabase/supabase-js'
|
||||||
import type { Database } from './types'
|
import type { Database } from './types'
|
||||||
|
|
||||||
let _supabase: ReturnType<typeof createClient<Database>> | null = null
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||||
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supabase client for client-side use with anon key
|
* Supabase client for client-side use with anon key
|
||||||
* Uses Row Level Security (RLS) policies
|
* 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>>, {
|
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
|
||||||
get(_target, prop) {
|
|
||||||
return Reflect.get(getSupabase(), prop)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,38 +1,11 @@
|
|||||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
|
||||||
import postgres from 'postgres'
|
import postgres from 'postgres'
|
||||||
import * as schema from './schema'
|
import * as schema from './schema'
|
||||||
|
|
||||||
let _db: PostgresJsDatabase<typeof schema> | null = null
|
const connectionString = process.env.POSTGRES_URL!
|
||||||
let _client: ReturnType<typeof postgres> | null = null
|
|
||||||
|
|
||||||
function getDb() {
|
const client = postgres(connectionString, { prepare: false })
|
||||||
if (!_db) {
|
|
||||||
const postgresUrl = process.env.POSTGRES_URL
|
|
||||||
|
|
||||||
if (!postgresUrl) {
|
export const db = drizzle({ client, schema })
|
||||||
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 type Database = typeof db
|
export type Database = typeof db
|
||||||
|
|||||||
@@ -1,38 +1,17 @@
|
|||||||
import { createClient } from '@supabase/supabase-js'
|
import { createClient } from '@supabase/supabase-js'
|
||||||
import type { Database } from './types'
|
import type { Database } from './types'
|
||||||
|
|
||||||
let _supabaseAdmin: ReturnType<typeof createClient<Database>> | null = null
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||||
|
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supabase client for server-side use with service role key
|
* Supabase client for server-side use with service role key
|
||||||
* Bypasses Row Level Security (RLS) - use with caution
|
* Bypasses Row Level Security (RLS) - use with caution
|
||||||
* Always filter by user_id to enforce permissions
|
* 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>>, {
|
export const supabaseAdmin = createClient<Database>(supabaseUrl, supabaseServiceRoleKey, {
|
||||||
get(_target, prop) {
|
auth: {
|
||||||
return Reflect.get(getSupabaseAdmin(), prop)
|
persistSession: false,
|
||||||
|
autoRefreshToken: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user