Skip to content
Open
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
104 changes: 104 additions & 0 deletions api/auth/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { VercelRequest, VercelResponse } from '@vercel/node'
import { createClient } from '@supabase/supabase-js'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Secure CORS - only allow same domain or local development
const isProduction = process.env.NODE_ENV === 'production'

res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version')

if (!isProduction) {
// Development: allow localhost for Vite dev server
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173')
}
// Production: no CORS header = same-origin only (most secure)

if (req.method === 'OPTIONS') {
res.status(200).end()
return
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
const { email, password } = req.body

// Basic validation
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
})
}

// Create Supabase client
const supabaseUrl = process.env.SUPABASE_URL
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY

if (!supabaseUrl || !supabaseAnonKey) {
console.error('Missing Supabase environment variables')
return res.status(500).json({
success: false,
error: 'Server configuration error'
})
}

const supabase = createClient(supabaseUrl, supabaseAnonKey)

// Authenticate user with Supabase Auth
const { data: authData, error: authError } = await supabase.auth.signInWithPassword({
email,
password
})

if (authError) {
console.error('Supabase auth error:', authError)
return res.status(400).json({
success: false,
error: authError.message
})
}

if (!authData.user || !authData.session) {
return res.status(400).json({
success: false,
error: 'Login failed'
})
}

// Set secure session cookie
const isProduction = process.env.NODE_ENV === 'production'
const sameSitePolicy = isProduction ? 'Strict' : 'Lax'
const secureFlag = isProduction ? '; Secure' : ''
const maxAge = 24 * 60 * 60 // 24 hours in seconds

// Store session token as base64 encoded JSON
const sessionToken = Buffer.from(JSON.stringify({
user: authData.user,
session: authData.session,
timestamp: Date.now()
})).toString('base64')

res.setHeader('Set-Cookie', `session=${sessionToken}; HttpOnly; Path=/; SameSite=${sameSitePolicy}${secureFlag}; Max-Age=${maxAge}`)

// Login successful
return res.status(200).json({
success: true,
data: {
user: authData.user,
session: authData.session
}
})

} catch (error) {
console.error('Login error:', error)
return res.status(500).json({
success: false,
error: 'Internal server error'
})
}
}
48 changes: 48 additions & 0 deletions api/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { VercelRequest, VercelResponse } from '@vercel/node'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Secure CORS - only allow same domain or local development
const isProduction = process.env.NODE_ENV === 'production'

res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version')

if (!isProduction) {
// Development: allow localhost for Vite dev server
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173')
}
// Production: no CORS header = same-origin only (most secure)

if (req.method === 'OPTIONS') {
res.status(200).end()
return
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
// Clear session cookie with same security settings
const isProduction = process.env.NODE_ENV === 'production'
const sameSitePolicy = isProduction ? 'Strict' : 'Lax'
const secureFlag = isProduction ? '; Secure' : ''

res.setHeader('Set-Cookie', `session=; HttpOnly; Path=/; SameSite=${sameSitePolicy}${secureFlag}; Max-Age=0`)

return res.status(200).json({
success: true,
data: {
message: 'Logout successful'
}
})

} catch (error) {
console.error('Logout error:', error)
return res.status(500).json({
success: false,
error: 'Internal server error'
})
}
}
95 changes: 95 additions & 0 deletions api/auth/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { VercelRequest, VercelResponse } from '@vercel/node'
import { createClient } from '@supabase/supabase-js'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Secure CORS - only allow same domain or local development
const isProduction = process.env.NODE_ENV === 'production'

res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version')

if (!isProduction) {
// Development: allow localhost for Vite dev server
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173')
}
// Production: no CORS header = same-origin only (most secure)

if (req.method === 'OPTIONS') {
res.status(200).end()
return
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
const { email, password, name } = req.body

// Basic validation
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
})
}

// Create Supabase client
const supabaseUrl = process.env.SUPABASE_URL
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY

if (!supabaseUrl || !supabaseAnonKey) {
console.error('Missing Supabase environment variables')
return res.status(500).json({
success: false,
error: 'Server configuration error'
})
}

const supabase = createClient(supabaseUrl, supabaseAnonKey)

// Register user with Supabase Auth
const { data: authData, error: authError } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name: name || ''
}
}
})

if (authError) {
console.error('Supabase auth error:', authError)
return res.status(400).json({
success: false,
error: authError.message
})
}

if (!authData.user) {
return res.status(400).json({
success: false,
error: 'Registration failed'
})
}

// Registration successful
return res.status(200).json({
success: true,
data: {
user: authData.user,
session: authData.session,
message: 'Registration successful. Please check your email for confirmation.'
}
})

} catch (error) {
console.error('Registration error:', error)
return res.status(500).json({
success: false,
error: 'Internal server error'
})
}
}
86 changes: 86 additions & 0 deletions api/auth/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { VercelRequest, VercelResponse } from '@vercel/node'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Secure CORS - only allow same domain or local development
const isProduction = process.env.NODE_ENV === 'production'

res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version')

if (!isProduction) {
// Development: allow localhost for Vite dev server
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173')
}
// Production: no CORS header = same-origin only (most secure)

if (req.method === 'OPTIONS') {
res.status(200).end()
return
}

if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
// Parse cookies for session-based authentication
const cookies: Record<string, string> = {}
const cookieHeader = req.headers.cookie

if (cookieHeader) {
cookieHeader.split(';').forEach(cookie => {
const [name, value] = cookie.trim().split('=')
if (name && value) {
cookies[name] = value
}
})
}

if (!cookies.session) {
return res.status(401).json({
success: false,
error: 'No session found'
})
}

try {
// Parse session data from base64 encoded cookie
const sessionData = JSON.parse(Buffer.from(cookies.session, 'base64').toString())

// Check if session is expired (24 hours)
const sessionAge = Date.now() - (sessionData.timestamp || 0)
const maxAge = 24 * 60 * 60 * 1000 // 24 hours in milliseconds

if (sessionAge > maxAge) {
return res.status(401).json({
success: false,
error: 'Session expired'
})
}

// Return session data
return res.status(200).json({
success: true,
data: {
user: sessionData.user,
session: sessionData.session
}
})

} catch (parseError) {
console.error('Session parse error:', parseError)
return res.status(401).json({
success: false,
error: 'Invalid session'
})
}

} catch (error) {
console.error('Session check error:', error)
return res.status(500).json({
success: false,
error: 'Internal server error'
})
}
}
Loading