Switch all db storage to session storage
All checks were successful
Build And Deploy / build-and-deploy (push) Successful in 1m27s

Only affects CapAdapter, challenge.ts, and redeem.ts.
This commit is contained in:
badblocks 2026-02-07 18:07:50 -08:00
parent 1fbcbf772a
commit 6c56b203d3
No known key found for this signature in database
4 changed files with 56 additions and 87 deletions

View file

@ -4,7 +4,7 @@ import type { ActionAPIContext } from "astro:actions";
import validator from "validator"; import validator from "validator";
import SmsClient from "@lib/SmsGatewayClient.ts"; import SmsClient from "@lib/SmsGatewayClient.ts";
import Otp, { verifyOtp } from "@lib/Otp.ts"; import Otp, { verifyOtp } from "@lib/Otp.ts";
import CapServer from "@lib/CapAdapter"; import { createCap } from "@lib/CapAdapter";
import { import {
OTP_SUPER_SECRET_SALT, OTP_SUPER_SECRET_SALT,
ANDROID_SMS_GATEWAY_RECIPIENT_PHONE, ANDROID_SMS_GATEWAY_RECIPIENT_PHONE,
@ -81,10 +81,12 @@ const submitActionDefinition = {
}); });
} }
const cap = createCap(context.session ?? null);
if ( if (
!( !(
/^[a-fA-F0-9]{16}:[a-fA-F0-9]{30}$/.test(input.captcha) && /^[a-fA-F0-9]{16}:[a-fA-F0-9]{30}$/.test(input.captcha) &&
(await CapServer.validateToken(input.captcha)) (await cap.validateToken(input.captcha))
) )
) { ) {
throw new ActionError({ throw new ActionError({

View file

@ -1,85 +1,42 @@
import Cap, { type ChallengeData } from "@cap.js/server"; import Cap, { type ChallengeData } from "@cap.js/server";
import { db, eq, and, gt, lte, Cap_Challenges, Cap_Tokens } from "astro:db"; import type { AstroSession } from "astro";
const cap = new Cap({ export function createCap(session: AstroSession<any> | null) {
if (!session) {
throw new Error("Session context is required");
}
return new Cap({
storage: { storage: {
challenges: { challenges: {
store: async (token: string, challengeData: ChallengeData) => { store: async (token: string, challengeData: ChallengeData) => {
const expires = challengeData.expires; session.set(`cap:challenge:${token}`, JSON.stringify(challengeData));
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) => { read: async (token: string) => {
const result = await db const raw = await session.get(`cap:challenge:${token}`);
.select({ return raw ? (JSON.parse(raw) as ChallengeData) : null;
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: string) => {
delete: async (token) => { session.delete(`cap:challenge:${token}`);
await db.delete(Cap_Challenges).where(eq(Cap_Challenges.token, token));
}, },
deleteExpired: async () => { deleteExpired: async () => {
await db // no-op: session store handles TTL itself
.delete(Cap_Challenges)
.where(lte(Cap_Challenges.expires, Date.now()));
}, },
}, },
tokens: { tokens: {
store: async (tokenKey, expires) => { store: async (tokenKey: string, expires: number) => {
await db session.set(`cap:token:${tokenKey}`, String(expires));
.insert(Cap_Tokens)
.values({ key: tokenKey, expires: expires })
.onConflictDoUpdate({
target: Cap_Tokens.key,
set: { expires: expires },
});
}, },
get: async (tokenKey: string) => {
get: async (tokenKey) => { const raw = await session.get(`cap:token:${tokenKey}`);
const result = await db return raw ? Number(raw) : null;
.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: string) => {
delete: async (tokenKey) => { session.delete(`cap:token:${tokenKey}`);
await db.delete(Cap_Tokens).where(eq(Cap_Tokens.key, tokenKey));
}, },
deleteExpired: async () => { deleteExpired: async () => {
await db.delete(Cap_Tokens).where(lte(Cap_Tokens.expires, Date.now())); // no-op: session store handles TTL itself
}, },
}, },
}, },
}); });
}
export default cap;

View file

@ -1,9 +1,10 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import cap from "@lib/CapAdapter"; import { createCap } from "@lib/CapAdapter";
export const prerender = false; export const prerender = false;
export const POST: APIRoute = async () => { export const POST: APIRoute = async (context) => {
try { try {
const cap = createCap(context.session ?? null);
return new Response(JSON.stringify(await cap.createChallenge()), { return new Response(JSON.stringify(await cap.createChallenge()), {
status: 200, status: 200,
}); });

View file

@ -1,12 +1,21 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import cap from "@lib/CapAdapter"; import { createCap } from "@lib/CapAdapter";
export const prerender = false; export const prerender = false;
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async (context) => {
const { token, solutions } = await request.json(); if (!context.session) {
return new Response(
JSON.stringify({ success: false, error: "Session unavailable." }),
{ status: 500 },
);
}
const { token, solutions } = await context.request.json();
if (!token || !solutions) { if (!token || !solutions) {
return new Response(JSON.stringify({ success: false }), { status: 400 }); return new Response(JSON.stringify({ success: false }), { status: 400 });
} }
const cap = createCap(context.session);
return new Response( return new Response(
JSON.stringify(await cap.redeemChallenge({ token, solutions })), JSON.stringify(await cap.redeemChallenge({ token, solutions })),
{ status: 200 }, { status: 200 },