diff --git a/apps/demo-integration/src/App.tsx b/apps/demo-integration/src/App.tsx index ac65189..de728a7 100644 --- a/apps/demo-integration/src/App.tsx +++ b/apps/demo-integration/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from "react" /** * The message contract the Aura verification embed posts to `window.parent`. @@ -14,8 +14,8 @@ interface VerificationSignature { } interface AuraMessage { - app: 'aura-get-verified' - type: 'app-ready' | 'verification-success' + app: "aura-get-verified" + type: "app-ready" | "verification-success" data?: { brightId?: string signature?: VerificationSignature @@ -32,8 +32,10 @@ interface LogEntry { } const DEFAULT_BASE_URL = - (import.meta.env.VITE_AURA_EMBED_BASE_URL as string | undefined)?.replace(/\/$/, '') ?? - 'https://aura-get-verified.vercel.app' + (import.meta.env.VITE_AURA_EMBED_BASE_URL as string | undefined)?.replace( + /\/$/, + "", + ) ?? "https://aura-get-verified.vercel.app" const DEFAULT_PROJECT_ID = Number(import.meta.env.VITE_AURA_PROJECT_ID ?? 9) @@ -41,7 +43,7 @@ function originOf(url: string): string { try { return new URL(url).origin } catch { - return '' + return "" } } @@ -49,14 +51,14 @@ function CopyButton({ value }: { value: string }) { const [done, setDone] = useState(false) return ( ) } @@ -64,16 +66,20 @@ function CopyButton({ value }: { value: string }) { export function App() { const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL) const [projectId, setProjectId] = useState(DEFAULT_PROJECT_ID) - const [committed, setCommitted] = useState({ baseUrl: DEFAULT_BASE_URL, projectId: DEFAULT_PROJECT_ID }) + const [committed, setCommitted] = useState({ + baseUrl: DEFAULT_BASE_URL, + projectId: DEFAULT_PROJECT_ID, + }) const [iframeKey, setIframeKey] = useState(0) - const [result, setResult] = useState(null) + const [result, setResult] = useState(null) const [log, setLog] = useState([]) const iframeRef = useRef(null) const embedSrc = useMemo( - () => `${committed.baseUrl.replace(/\/$/, '')}/embed/projects/${committed.projectId}`, - [committed] + () => + `${committed.baseUrl.replace(/\/$/, "")}/embed/projects/${committed.projectId}`, + [committed], ) const expectedOrigin = useMemo(() => originOf(committed.baseUrl), [committed]) @@ -81,7 +87,7 @@ export function App() { (e: MessageEvent) => { // Only trust messages coming from the embed's own origin. if (expectedOrigin && e.origin !== expectedOrigin) return - if (typeof e.data !== 'string') return + if (typeof e.data !== "string") return let parsed: AuraMessage | undefined try { @@ -89,28 +95,36 @@ export function App() { } catch { return } - if (parsed.app !== 'aura-get-verified') return + if (parsed.app !== "aura-get-verified") return setLog((prev) => - [{ ts: new Date().toLocaleTimeString(), origin: e.origin, raw: e.data, parsed }, ...prev].slice(0, 50) + [ + { + ts: new Date().toLocaleTimeString(), + origin: e.origin, + raw: e.data, + parsed, + }, + ...prev, + ].slice(0, 50), ) - if (parsed.type === 'verification-success') { + if (parsed.type === "verification-success") { setResult(parsed.data ?? null) } }, - [expectedOrigin] + [expectedOrigin], ) useEffect(() => { - window.addEventListener('message', onMessage) - return () => window.removeEventListener('message', onMessage) + window.addEventListener("message", onMessage) + return () => window.removeEventListener("message", onMessage) }, [onMessage]) const applyConfig = () => { setResult(null) setLog([]) - setCommitted({ baseUrl: baseUrl.trim().replace(/\/$/, ''), projectId }) + setCommitted({ baseUrl: baseUrl.trim().replace(/\/$/, ""), projectId }) setIframeKey((k) => k + 1) } @@ -129,9 +143,10 @@ export function App() { Demo Integration

- A third-party site embedding the Aura verification iframe. Complete the flow in the frame - below; on success the embed posts the brightId, verification{' '} - signature, level and score back to this page. + A third-party site embedding the Aura verification iframe. Complete + the flow in the frame below; on success the embed posts the{" "} + brightId, verification signature, level + and score back to this page.

@@ -185,7 +200,7 @@ export function App() {
- +

Result

{result ? ( @@ -195,28 +210,32 @@ export function App() {
BrightID
- {result.brightId ?? '—'} + {result.brightId ?? "—"} {result.brightId && }
Aura level
-
{result.auraLevel ?? '—'}
+
{result.auraLevel ?? "—"}
Aura score
-
{result.auraScore ?? '—'}
+
{result.auraScore ?? "—"}
Signature
{result.signature ? ( <> -
{JSON.stringify(result.signature, null, 2)}
+
+                          {JSON.stringify(result.signature, null, 2)}
+                        
) : ( - no signature (unverified / API failed) + + no signature (unverified / API failed) + )}
@@ -236,7 +255,9 @@ export function App() { {log.length === 0 ? (
📡 -

No aura-get-verified messages yet.

+

+ No aura-get-verified messages yet. +

) : (
    @@ -244,7 +265,7 @@ export function App() {
  • {entry.ts} {entry.parsed?.type} diff --git a/apps/interface/api/lib/schema.ts b/apps/interface/api/lib/schema.ts index 7878241..375c1e6 100644 --- a/apps/interface/api/lib/schema.ts +++ b/apps/interface/api/lib/schema.ts @@ -7,6 +7,7 @@ import { serial, text, timestamp, + uniqueIndex, varchar } from 'drizzle-orm/pg-core' @@ -88,7 +89,9 @@ export const verificationsTable = pgTable('verifications', { auraScore: integer(), auraLevel: integer(), verifiedAt: timestamp().notNull().defaultNow() -}) +}, (table) => [ + uniqueIndex('verifications_user_project_uidx').on(table.userId, table.projectId) +]) export const brightIdAppsTable = pgTable('brightid_apps', { key: text('key').primaryKey(), // Unique key diff --git a/apps/interface/api/projects/[id]/verify.ts b/apps/interface/api/projects/[id]/verify.ts index 768d405..bba6a9b 100644 --- a/apps/interface/api/projects/[id]/verify.ts +++ b/apps/interface/api/projects/[id]/verify.ts @@ -1,3 +1,4 @@ +import { VercelRequest, VercelResponse } from '@vercel/node' import { and, eq } from 'drizzle-orm' import { z } from 'zod' import withCors from '../../lib/cors.js' @@ -6,15 +7,30 @@ import { projectsTable, verificationsTable } from '../../lib/schema.js' const verifySchema = z.object({ client: z.string().min(1).max(100), - auraScore: z.number().int().optional(), + auraScore: z.number().optional(), auraLevel: z.number().int().optional(), userId: z.string() }) -async function handler(req: Request, { params }: { params: { id: string } }) { +async function handler(req: VercelRequest, res: VercelResponse) { + const rawId = Array.isArray(req.query['id']) ? req.query['id'][0] : req.query['id'] + const projectId = Number(rawId) + + let body: z.infer + try { + body = verifySchema.parse(req.body) + if (!Number.isInteger(projectId)) { + return res.status(400).json({ error: 'Invalid project id' }) + } + } catch { + return res.status(400).json({ error: 'Invalid request' }) + } + + const log = (msg: string, extra?: unknown) => + console.log(`[verify project=${projectId} user=${body.userId}] ${msg}`, extra ?? '') + try { - const body = verifySchema.parse(await req.json()) - const projectId = Number(params.id) + log('start') const [project] = await db .select({ @@ -34,7 +50,8 @@ async function handler(req: Request, { params }: { params: { id: string } }) { .limit(1) if (!project) { - return Response.json({ error: 'Invalid project or no tokens' }, { status: 400 }) + log('project not found or inactive') + return res.status(400).json({ error: 'Invalid project or no tokens' }) } const now = new Date() @@ -48,61 +65,91 @@ async function handler(req: Request, { params }: { params: { id: string } }) { .limit(1) if (alreadyVerified.length > 0) { - return Response.json({ message: 'Already verified', data: alreadyVerified }, { status: 200 }) + log('already verified') + return res.status(200).json({ message: 'Already verified', data: alreadyVerified }) } - const res = await fetch( - `${process.env['VITE_SOME_AURA_BACKEND_URL']}/brightid/v6/verifications/${project.brightIdAppId}/${body.userId}?signed=nacl` - ) - - const verification = (await res.json())[0] as { - verification: string - unique: true - appUserId: string - app: string - verificationHash: string - sig: { - r: string - s: string - v: number - } - publicKey: string + log('fetching brightid verification') + let apiRes: Response + try { + apiRes = await fetch( + `${process.env['VITE_SOME_AURA_BACKEND_URL']}/brightid/v6/verifications/${project.brightIdAppId}/${body.userId}?signed=nacl`, + { signal: AbortSignal.timeout(10_000) } + ) + } catch (err) { + log('brightid fetch failed/timeout', err) + return res.status(504).json({ error: 'Verification service unavailable' }) } + log(`brightid responded status=${apiRes.status}`) - if (!verification.unique) - return Response.json({ error: 'User is not verified' }, { status: 400 }) + if (!apiRes.ok) { + log('brightid non-ok status') + return res.status(502).json({ error: 'Verification service error' }) + } - await db.transaction(async (tx) => { - await tx.insert(verificationsTable).values({ - userId: body.userId, - projectId, - client: body.client, - auraScore: body.auraScore, - auraLevel: body.auraLevel, - verifiedAt: now, - signature: JSON.stringify(verification.sig) - }) + const verification = (await apiRes.json())[0] as + | { + verification: string + unique: boolean + appUserId: string + app: string + verificationHash: string + sig: { + r: string + s: string + v: number + } + publicKey: string + } + | undefined - await tx - .update(projectsTable) - .set({ remainingtokens: (project.remainingtokens ?? 0) - 1 }) - .where(eq(projectsTable.id, projectId)) - }) + if (!verification?.unique) { + log('user not unique/verified') + return res.status(400).json({ error: 'User is not verified' }) + } + + try { + await db.transaction(async (tx) => { + await tx.insert(verificationsTable).values({ + userId: body.userId, + projectId, + client: body.client, + auraScore: body.auraScore === undefined ? undefined : Math.round(body.auraScore), + auraLevel: body.auraLevel, + verifiedAt: now, + signature: JSON.stringify(verification.sig) + }) + + await tx + .update(projectsTable) + .set({ remainingtokens: (project.remainingtokens ?? 0) - 1 }) + .where(eq(projectsTable.id, projectId)) + }) + } catch (err) { + // 23505 = unique_violation: concurrent request already verified this user+project + if ((err as { code?: string })?.code === '23505') { + log('concurrent verify race, already inserted') + return res.status(200).json({ message: 'Already verified' }) + } + throw err + } - return Response.json({ + log('verification success') + return res.status(200).json({ message: 'verification success', data: { userId: body.userId, projectId, client: body.client, signature: verification.sig, - auraScore: body.auraScore, + auraScore: body.auraScore === undefined ? undefined : Math.round(body.auraScore), auraLevel: body.auraLevel, verifiedAt: now } }) } catch (error) { - return Response.json({ error: 'Invalid request' }, { status: 400 }) + log('unhandled error', error) + return res.status(500).json({ error: 'Internal server error' }) } } diff --git a/apps/interface/src/utils/apis/index.ts b/apps/interface/src/utils/apis/index.ts index 53b4f10..0c3ed72 100644 --- a/apps/interface/src/utils/apis/index.ts +++ b/apps/interface/src/utils/apis/index.ts @@ -1,12 +1,13 @@ +import { QueryClient } from '@aura/query' +import createClient from 'openapi-fetch' import { AURA_NODE_URL_PROXY } from '@/lib/constants/domains' import type { paths } from '@/lib/schema' import type { BrightID } from '@/types/brightid' import type { Project } from '@/types/projects' -import { QueryClient } from '@aura/query' -import createClient from 'openapi-fetch' export const clientAPI = createClient({ - baseUrl: 'https://aura-get-verified.vercel.app/api' + // baseUrl: 'https://aura-get-verified.vercel.app/api' + baseUrl: 'http://localhost:3000/api' }) const baseUrl = AURA_NODE_URL_PROXY @@ -55,12 +56,20 @@ export interface VerifyProjectResult { */ export const verifyProject = async ( projectId: number, - payload: { userId: string; client: string; auraScore?: number; auraLevel?: number } + payload: { + userId: string + client: string + auraScore?: number + auraLevel?: number + } ) => { - const res = await clientAPI.POST('/projects/{id}/verify' as never, { - params: { path: { id: String(projectId) } }, - body: payload - } as never) + const res = await clientAPI.POST( + '/projects/{id}/verify' as never, + { + params: { path: { id: String(projectId) } }, + body: payload + } as never + ) if ((res as { error?: unknown }).error) { throw new Error('Failed to generate verification signature')