feat : limit each session to 1 hours when checkout

This commit is contained in:
2026-10-01 22:37:52 +07:00 Unverified
parent 61dfc9c9e5
commit d85687c332
11 changed files with 163 additions and 27 deletions
+11 -5
View File
@@ -4,6 +4,7 @@ import { createHash, randomBytes } from "node:crypto";
import { and, eq } from "drizzle-orm";
import { getDb } from "@/db";
import { commissionCheckouts, commissionTickets } from "@/db/schema";
import { checkoutExpired, checkoutExpiresAt } from "@/lib/commission/checkout-expiration";
import { getRedisClient, redisEventChannel } from "@/lib/redis/client";
import { HttpError } from "@/lib/security/http";
@@ -15,19 +16,22 @@ const errorKey = (digest: string) => `${prefix()}:error:${digest}`;
const digestToken = (token: string) => createHash("sha256").update(token).digest("hex");
export async function createMobileSlipLink(checkoutId: string, userId: string) {
const [checkout] = await getDb().select({ id: commissionCheckouts.id }).from(commissionCheckouts)
const [checkout] = await getDb().select({ id: commissionCheckouts.id, createdAt: commissionCheckouts.createdAt }).from(commissionCheckouts)
.where(and(eq(commissionCheckouts.id, checkoutId), eq(commissionCheckouts.userId, userId))).limit(1);
if (!checkout) throw new HttpError(404, "checkout-not-found");
const [ticket] = await getDb().select({ id: commissionTickets.id }).from(commissionTickets)
.where(eq(commissionTickets.checkoutId, checkoutId)).limit(1);
if (ticket) throw new HttpError(409, "checkout-already-paid");
if (checkoutExpired(checkout.createdAt)) throw new HttpError(410, "checkout-expired");
const token = randomBytes(32).toString("base64url");
const digest = digestToken(token);
const redis = await getRedisClient();
await redis.set(tokenKey(digest), JSON.stringify({ checkoutId, userId }), "EX", TTL_SECONDS);
await redis.set(activeKey(checkoutId), digest, "EX", TTL_SECONDS);
return { token, digest, expiresAt: Date.now() + TTL_SECONDS * 1000 };
const expiresAt = Math.min(Date.now() + TTL_SECONDS * 1000, checkoutExpiresAt(checkout.createdAt));
const ttl = Math.max(1, Math.ceil((expiresAt - Date.now()) / 1000));
await redis.set(tokenKey(digest), JSON.stringify({ checkoutId, userId }), "EX", ttl);
await redis.set(activeKey(checkoutId), digest, "EX", ttl);
return { token, digest, expiresAt };
}
export async function authorizeMobileSlip(token: string) {
@@ -44,16 +48,18 @@ export async function authorizeMobileSlip(token: string) {
if (!checkout) throw new HttpError(404, "checkout-not-found");
const [ticket] = await getDb().select({ id: commissionTickets.id }).from(commissionTickets)
.where(eq(commissionTickets.checkoutId, checkoutId)).limit(1);
if (!ticket && checkoutExpired(checkout.createdAt)) throw new HttpError(410, "checkout-expired");
return { checkout, ticketId: ticket?.id ?? null, digest };
}
export async function mobileSlipStatus(checkoutId: string, userId: string, digest: string) {
const [checkout] = await getDb().select({ id: commissionCheckouts.id }).from(commissionCheckouts)
const [checkout] = await getDb().select({ id: commissionCheckouts.id, createdAt: commissionCheckouts.createdAt }).from(commissionCheckouts)
.where(and(eq(commissionCheckouts.id, checkoutId), eq(commissionCheckouts.userId, userId))).limit(1);
if (!checkout) throw new HttpError(404, "checkout-not-found");
const [ticket] = await getDb().select({ id: commissionTickets.id }).from(commissionTickets)
.where(eq(commissionTickets.checkoutId, checkoutId)).limit(1);
if (ticket) return { state: "complete", ticketId: ticket.id };
if (checkoutExpired(checkout.createdAt)) return { state: "expired" };
if (!/^[a-f0-9]{64}$/.test(digest)) throw new HttpError(400, "invalid-upload-link");
const redis = await getRedisClient();
if (await redis.get(activeKey(checkoutId)) !== digest) return { state: "expired" };