Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
VITE_SUPABASE_URL=https://ichdmajupiwycrdcstoe.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImljaGRtYWp1cGl3eWNyZGNzdG9lIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjI4MTY4OTksImV4cCI6MjA3ODM5Mjg5OX0.RJD3ppHt1YtpFJbUiJolYNWULmPJTeFkimuPoFZz7DA

# Serverless (/api/*) runtime env vars
SUPABASE_URL=https://ichdmajupiwycrdcstoe.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImljaGRtYWp1cGl3eWNyZGNzdG9lIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjI4MTY4OTksImV4cCI6MjA3ODM5Mjg5OX0.RJD3ppHt1YtpFJbUiJolYNWULmPJTeFkimuPoFZz7DA

# Defaults for this repo
VITE_ANALYTICS_TABLE=analytics_events
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@

VITE_SUPABASE_URL=your-supabase-url
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key
VITE_ANALYTICS_TABLE=analytics_events

# Serverless (Vercel /api/*) environment variables
SUPABASE_URL=your-supabase-url
SUPABASE_ANON_KEY=your-supabase-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key

# Swiss Ephemeris (for personalized daily transits)
SWISS_EPHEMERIS_URL=https://api.swissephemeris.com/v1/calculate
SWISS_EPHEMERIS_API_KEY=your_swiss_ephemeris_api_key
88 changes: 88 additions & 0 deletions api/auth/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { createClient } from '@supabase/supabase-js'

const json = (res, status, body) => {
res.statusCode = status
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(body))
}

const withCors = (req, res) => {
const origin = typeof req.headers?.origin === 'string' ? req.headers.origin : '*'
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Vary', 'Origin')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
}

const readBody = async (req) => {
if (req.body && typeof req.body === 'object') return req.body
const chunks = []
for await (const chunk of req) chunks.push(chunk)
const raw = Buffer.concat(chunks).toString('utf8')
if (!raw) return {}
return JSON.parse(raw)
}

const isValidEmail = (email) => typeof email === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())

export default async function handler(req, res) {
withCors(req, res)

if (req.method === 'OPTIONS') {
res.statusCode = 204
res.end()
return
}

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

const supabaseUrl = process.env.SUPABASE_URL
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
json(res, 500, { error: 'Server not configured' })
return
}

let body
try {
body = await readBody(req)
} catch {
json(res, 400, { error: 'Invalid JSON body' })
return
}

const email = typeof body?.email === 'string' ? body.email.trim() : ''
const password = typeof body?.password === 'string' ? body.password : ''

if (!isValidEmail(email)) {
json(res, 400, { error: 'Valid email is required' })
return
}
if (!password) {
json(res, 400, { error: 'Password is required' })
return
}

const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: { persistSession: false, autoRefreshToken: false }
})

const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})

if (error) {
json(res, 401, { error: error.message })
return
}

const jwt = data?.session?.access_token || null
const user = data?.user || null

json(res, 200, { jwt, user })
}

88 changes: 88 additions & 0 deletions api/auth/signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { createClient } from '@supabase/supabase-js'

const json = (res, status, body) => {
res.statusCode = status
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(body))
}

const withCors = (req, res) => {
const origin = typeof req.headers?.origin === 'string' ? req.headers.origin : '*'
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Vary', 'Origin')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
}

const readBody = async (req) => {
if (req.body && typeof req.body === 'object') return req.body
const chunks = []
for await (const chunk of req) chunks.push(chunk)
const raw = Buffer.concat(chunks).toString('utf8')
if (!raw) return {}
return JSON.parse(raw)
}

const isValidEmail = (email) => typeof email === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())

export default async function handler(req, res) {
withCors(req, res)

if (req.method === 'OPTIONS') {
res.statusCode = 204
res.end()
return
}

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

const supabaseUrl = process.env.SUPABASE_URL
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
json(res, 500, { error: 'Server not configured' })
return
}

let body
try {
body = await readBody(req)
} catch {
json(res, 400, { error: 'Invalid JSON body' })
return
}

const email = typeof body?.email === 'string' ? body.email.trim() : ''
const password = typeof body?.password === 'string' ? body.password : ''

if (!isValidEmail(email)) {
json(res, 400, { error: 'Valid email is required' })
return
}
if (!password || password.length < 6) {
json(res, 400, { error: 'Password must be at least 6 characters' })
return
}

const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: { persistSession: false, autoRefreshToken: false }
})

const { data, error } = await supabase.auth.signUp({
email,
password
})

if (error) {
json(res, 400, { error: error.message })
return
}

const jwt = data?.session?.access_token || null
const user = data?.user || null

json(res, 200, { jwt, user })
}

120 changes: 120 additions & 0 deletions api/leads.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { createClient } from '@supabase/supabase-js'

const json = (res, status, body) => {
res.statusCode = status
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(body))
}

const getOrigin = (req) => {
const origin = req.headers?.origin
return typeof origin === 'string' ? origin : '*'
}

const withCors = (req, res) => {
res.setHeader('Access-Control-Allow-Origin', getOrigin(req))
res.setHeader('Vary', 'Origin')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
}

const readBody = async (req) => {
if (req.body && typeof req.body === 'object') return req.body

const chunks = []
for await (const chunk of req) chunks.push(chunk)
const raw = Buffer.concat(chunks).toString('utf8')
if (!raw) return {}
return JSON.parse(raw)
}

const isValidEmail = (email) => {
if (typeof email !== 'string') return false
const v = email.trim()
if (v.length < 5 || v.length > 254) return false
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
}

export default async function handler(req, res) {
withCors(req, res)

if (req.method === 'OPTIONS') {
res.statusCode = 204
res.end()
return
}

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

const supabaseUrl = process.env.SUPABASE_URL
const serviceKey =
process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.SUPABASE_SERVICE_KEY ||
process.env.SUPABASE_SERVICE_ROLE

if (!supabaseUrl || !serviceKey) {
json(res, 500, { error: 'Server not configured' })
return
}

let body
try {
body = await readBody(req)
} catch {
json(res, 400, { error: 'Invalid JSON body' })
return
}

// Honeypot: bots often fill hidden fields.
if (body?.website) {
json(res, 200, { ok: true })
return
}

const email = typeof body?.email === 'string' ? body.email.trim() : ''
if (!isValidEmail(email)) {
json(res, 400, { error: 'Valid email is required' })
return
}

const firstName = typeof body?.firstName === 'string' ? body.firstName.trim() : null
const source = typeof body?.source === 'string' ? body.source.trim() : 'essence_quiz'

const metadata =
body?.metadata && typeof body.metadata === 'object' && !Array.isArray(body.metadata)
? body.metadata
: {}

const record = {
email,
first_name: firstName || null,
source,
metadata: {
...metadata,
user_agent: req.headers?.['user-agent'],
ip: req.headers?.['x-forwarded-for'] || req.socket?.remoteAddress
}
}

const supabase = createClient(supabaseUrl, serviceKey, {
auth: { persistSession: false, autoRefreshToken: false }
})

// Upsert by email so repeat “unlock” attempts don’t 500.
const { data, error } = await supabase
.from('leads')
.upsert([record], { onConflict: 'email' })
.select('id')
.single()

if (error) {
json(res, 500, { error: 'Failed to save lead' })
return
}

json(res, 200, { ok: true, lead_id: data?.id })
}

Loading