mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(backend): add standalone data plane
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
This commit is contained in:
1 parent
01bb8225ae
commit
006761d3b0
294 files changed
+16259
-6138
No files matched your search
@@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Gift, Mail, Key, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Gift, Mail, Key, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { redeemCode } from './redemptionApi'
|
||||
|
||||
export default function RedeemPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
@@ -32,24 +33,10 @@ export default function RedeemPage() {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/redeem-code`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email.trim(),
|
||||
code: code.trim(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const data = await redeemCode(email.trim(), code.trim())
|
||||
|
||||
if (data.success) {
|
||||
setIsRedeemed(true)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { redeemCode } from './redemptionApi'
|
||||
|
||||
describe('redeemCode', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('posts only the public redemption fields to the standalone API', async () => {
|
||||
vi.stubEnv('NEXT_PUBLIC_API_URL', 'http://127.0.0.1:3003/')
|
||||
const request = vi.fn(async () => Response.json({ success: true, message: 'redeemed' }))
|
||||
vi.stubGlobal('fetch', request)
|
||||
|
||||
await expect(redeemCode('alice@example.test', 'PROMO')).resolves.toEqual({
|
||||
success: true,
|
||||
message: 'redeemed',
|
||||
})
|
||||
expect(request).toHaveBeenCalledWith('http://127.0.0.1:3003/public/v1/redemptions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'alice@example.test', code: 'PROMO' }),
|
||||
})
|
||||
})
|
||||
|
||||
it('fails closed instead of falling back to hosted Supabase', async () => {
|
||||
vi.stubEnv('NEXT_PUBLIC_API_URL', '')
|
||||
const request = vi.fn()
|
||||
vi.stubGlobal('fetch', request)
|
||||
await expect(redeemCode('alice@example.test', 'PROMO')).rejects.toThrow('NEXT_PUBLIC_API_URL')
|
||||
expect(request).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export type RedemptionResponse =
|
||||
| { success: true; message: string }
|
||||
| { success: false; error: string }
|
||||
|
||||
export async function redeemCode(email: string, code: string): Promise<RedemptionResponse> {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, '')
|
||||
if (!apiUrl) throw new Error('NEXT_PUBLIC_API_URL is not configured')
|
||||
|
||||
const response = await fetch(`${apiUrl}/public/v1/redemptions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, code }),
|
||||
})
|
||||
return response.json() as Promise<RedemptionResponse>
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user