diff --git a/db/config.ts b/db/config.ts new file mode 100644 index 0000000..dc78187 --- /dev/null +++ b/db/config.ts @@ -0,0 +1,20 @@ +import { defineDb, defineTable, column } from "astro:db"; + +const Cap_Challenges = defineTable({ + columns: { + token: column.text({ primaryKey: true }), + data: column.json(), + expires: column.number(), + }, +}); + +const Cap_Tokens = defineTable({ + columns: { + key: column.text({ primaryKey: true }), + expires: column.number(), + }, +}); + +export default defineDb({ + tables: { Cap_Challenges, Cap_Tokens }, +}); diff --git a/src/lib/cap.ts b/src/lib/cap.ts new file mode 100644 index 0000000..81fe97f --- /dev/null +++ b/src/lib/cap.ts @@ -0,0 +1,85 @@ +import Cap, { type ChallengeData } from "@cap.js/server"; +import { db, eq, and, gt, lte, Cap_Challenges, Cap_Tokens } from "astro:db"; + +const cap = new Cap({ + storage: { + challenges: { + store: async (token: string, challengeData: ChallengeData) => { + const expires = challengeData.expires; + const data = challengeData.challenge; + await db + .insert(Cap_Challenges) + .values({ token: token, data: data, expires: expires }) + .onConflictDoUpdate({ + target: Cap_Challenges.token, + set: { data: data, expires: expires }, + }); + }, + read: async (token) => { + const result = await db + .select({ + challenge: Cap_Challenges.data, + expires: Cap_Challenges.expires, + }) + .from(Cap_Challenges) + .where( + and( + eq(Cap_Challenges.token, token), + gt(Cap_Challenges.expires, Date.now()), + ), + ) + .limit(1); + + const data = result[0] as ChallengeData; + + return result ? data : null; + }, + + delete: async (token) => { + await db.delete(Cap_Challenges).where(eq(Cap_Challenges.token, token)); + }, + + deleteExpired: async () => { + await db + .delete(Cap_Challenges) + .where(lte(Cap_Challenges.expires, Date.now())); + }, + }, + tokens: { + store: async (tokenKey, expires) => { + await db + .insert(Cap_Tokens) + .values({ key: tokenKey, expires: expires }) + .onConflictDoUpdate({ + target: Cap_Tokens.key, + set: { expires: expires }, + }); + }, + + get: async (tokenKey) => { + const result = await db + .select({ expires: Cap_Tokens.expires }) + .from(Cap_Tokens) + .where( + and( + eq(Cap_Tokens.key, tokenKey), + gt(Cap_Tokens.expires, Date.now()), + ), + ) + .limit(1); + + return result ? result[0].expires : null; + }, + + delete: async (tokenKey) => { + await db.delete(Cap_Tokens).where(eq(Cap_Tokens.key, tokenKey)); + }, + + deleteExpired: async () => { + await db.delete(Cap_Tokens).where(lte(Cap_Tokens.expires, Date.now())); + }, + }, + }, +}); + +export default cap; diff --git a/src/pages/cap/challenge.ts b/src/pages/cap/challenge.ts new file mode 100644 index 0000000..65a4105 --- /dev/null +++ b/src/pages/cap/challenge.ts @@ -0,0 +1,13 @@ +import type { APIRoute } from "astro"; +import cap from "@lib/cap"; +export const prerender = false; + +export const POST: APIRoute = async ({ request }) => { + try { + return new Response(JSON.stringify(await cap.createChallenge()), { + status: 200, + }); + } catch { + return new Response(JSON.stringify({ success: false }), { status: 400 }); + } +}; diff --git a/src/pages/cap/redeem.ts b/src/pages/cap/redeem.ts new file mode 100644 index 0000000..d66f189 --- /dev/null +++ b/src/pages/cap/redeem.ts @@ -0,0 +1,14 @@ +import type { APIRoute } from "astro"; +import cap from "@lib/cap"; +export const prerender = false; + +export const POST: APIRoute = async ({ request }) => { + const { token, solutions } = await request.json(); + if (!token || !solutions) { + return new Response(JSON.stringify({ success: false }), { status: 400 }); + } + return new Response( + JSON.stringify(await cap.redeemChallenge({ token, solutions })), + { status: 200 }, + ); +}; diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 4bb6347..3ecbe81 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -1,6 +1,7 @@ --- import Layout from "@layouts/BaseLayout.astro"; import SmsClient from "@lib/SmsGatewayClient.ts"; +import CapServer from "@lib/cap"; export const prerender = false; const errors = { name: "", phone: "", msg: "", form: "" }; @@ -9,9 +10,14 @@ if (Astro.request.method === "POST") { try { const data = await Astro.request.formData(); const name = data.get("name")?.toString(); + const capToken = data.get("cap-token")?.toString(); const phone = data.get("phone")?.toString(); const msg = data.get("msg")?.toString(); + if (typeof capToken !== "string" || !(await CapServer.validateToken(capToken)).success) { + throw new Error("invalid cap token"); + } + if (typeof name !== "string" || name.length < 1) { errors.name += "Please enter a name. "; } @@ -38,7 +44,29 @@ if (Astro.request.method === "POST") { } } --- - + + Contact