feat(membership): replace purchase modal with membership page

This commit is contained in:
Jesse
2026-08-07 18:24:16 +08:00
parent 07223b6b4a
commit 280d3e7a35
20 changed files with 2901 additions and 179 deletions
@@ -0,0 +1,283 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import test from "node:test";
import {
closeLocalPostgresDataPools,
createLocalPostgresDataClient,
} from "../src/lib/db/local-postgres-client-core.ts";
import { formatPaymentOrders, PAYMENT_ORDERS_SELECT } from "../src/lib/payment-orders.ts";
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
const runnerPath = fileURLToPath(
new URL("../scripts/db-migrate.mjs", import.meta.url),
);
function hash(code: string): string {
return createHash("sha256").update(code, "utf8").digest("hex");
}
test("redeem security: case-sensitive hashing, rate limiting, idempotency and order ownership", async () => {
const fixture = startPostgresFixture();
try {
const migration = spawnSync(process.execPath, [runnerPath], {
encoding: "utf8",
env: {
...process.env,
SCHEMA_DATABASE_URL: fixture.connectionUrl(
"schema_owner",
"schema-owner-test-password",
),
},
});
assert.equal(migration.status, 0, migration.stderr);
assert.match(migration.stdout, /applied 20260807020000_redeem_security\.sql/);
assert.match(migration.stdout, /applied 20260807030000_redeem_security\.sql/);
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users (name, email, email_verified, email_verified_at)
values ('User A', 'redeem-a@example.com', true, now()),
('User B', 'redeem-b@example.com', true, now())
`);
const userA = fixture.psql(
"select id from identity.users where email = 'redeem-a@example.com'",
);
const userB = fixture.psql(
"select id from identity.users where email = 'redeem-b@example.com'",
);
const validCodeA = "JYOTISH-TEST-ABCD";
const validCodeB = "JYOTISH-TEST-WXYZ";
fixture.psql(`
insert into public.redemption_codes (code_hash, code_mask, credits)
values ('${hash(validCodeA)}', 'JYOTISH-****-ABCD', 3),
('${hash(validCodeB)}', 'JYOTISH-****-WXYZ', 5)
`);
const local = createLocalPostgresDataClient(
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
{ id: userA, email: "redeem-a@example.com" },
);
// 1) Case-sensitive matching: the lowercase hash must not match.
const lowercase = await local.rpc("redeem_code", {
p_code_hash: hash("jyotish-test-abcd"),
});
assert.deepEqual(lowercase.data, [
{ success: false, credits: null, awarded_credits: null, error_code: "invalid_code" },
]);
// 2) The exact uppercase hash redeems: awarded credits and balance delta.
const redeemed = await local.rpc("redeem_code", {
p_code_hash: hash(validCodeA),
});
assert.deepEqual(redeemed.data, [
{ success: true, credits: 3, awarded_credits: 3, error_code: null },
]);
// A successful redemption deletes all of the account's attempts; no
// permanent success row is kept.
assert.equal(
fixture.psql(
`select count(*) from public.redemption_attempts where user_id='${userA}'`,
),
"0",
);
// 3) Idempotency: reusing the same code is already_redeemed and never
// double-credits (redemption row lock + credit_transactions unique).
const again = await local.rpc("redeem_code", {
p_code_hash: hash(validCodeA),
});
assert.deepEqual(again.data, [
{ success: false, credits: null, awarded_credits: null, error_code: "already_redeemed" },
]);
assert.equal(
fixture.psql(
`select count(*) from public.credit_transactions where user_id='${userA}' and transaction_type='redeem'`,
),
"1",
);
assert.equal(
fixture.psql(`select credits from public.profiles where id='${userA}'`),
"3",
);
// 4) Rate limiting: the 5th failure is recorded, the 6th attempt is
// blocked even with a valid unused code, and nothing is credited.
for (let index = 0; index < 4; index += 1) {
const failed = await local.rpc("redeem_code", {
p_code_hash: hash(`NO-SUCH-${index}`),
});
assert.deepEqual(failed.data, [
{ success: false, credits: null, awarded_credits: null, error_code: "invalid_code" },
]);
}
assert.equal(
fixture.psql(
`select count(*) from public.redemption_attempts where user_id='${userA}'`,
),
"5",
);
const blocked = await local.rpc("redeem_code", {
p_code_hash: hash(validCodeB),
});
assert.deepEqual(blocked.data, [
{ success: false, credits: null, awarded_credits: null, error_code: "rate_limited" },
]);
assert.equal(
fixture.psql(`select credits from public.profiles where id='${userA}'`),
"3",
);
// rate_limited itself is not a new business failure.
assert.equal(
fixture.psql(
`select count(*) from public.redemption_attempts where user_id='${userA}'`,
),
"5",
);
// 5) The window is rolling: aging failures past 10 minutes unblocks.
fixture.psql(
`update public.redemption_attempts set created_at = now() - interval '11 minutes' where user_id='${userA}'`,
);
const afterWindow = await local.rpc("redeem_code", {
p_code_hash: hash(validCodeB),
});
assert.deepEqual(afterWindow.data, [
{ success: true, credits: 8, awarded_credits: 5, error_code: null },
]);
assert.equal(
fixture.psql(
`select count(*) from public.redemption_attempts where user_id='${userA}'`,
),
"0",
);
// 6) Per-account isolation: user B is unaffected by user A's failures.
const userBClient = createLocalPostgresDataClient(
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
{ id: userB, email: "redeem-b@example.com" },
);
for (let index = 0; index < 2; index += 1) {
const bAttempt = await userBClient.rpc("redeem_code", {
p_code_hash: "f".repeat(64),
});
assert.equal(
(bAttempt.data as Array<{ error_code: string }>)[0]?.error_code,
"invalid_code",
);
}
assert.equal(
fixture.psql(
`select count(*) from public.redemption_attempts where user_id='${userB}'`,
),
"2",
);
// 7) Orders: the route's exact query returns only the current user's
// recent orders, most recent first.
fixture.psql(`
insert into public.payment_orders (
order_no, user_id, money_cents, product_code, product_snapshot,
status, grant_status, created_at, paid_at
) values
('${"JY" + "A1".padEnd(14, "0")}', '${userA}', 9900, 'standard_monthly', '{"name":"标准月卡"}'::jsonb, 'paid', 'granted', now() - interval '1 day', now() - interval '1 day'),
('${"JY" + "A2".padEnd(14, "0")}', '${userA}', 990, 'trial_7d', '{"name":"体验卡"}'::jsonb, 'paid', 'granted', now(), now()),
('${"JY" + "B1".padEnd(14, "0")}', '${userB}', 9900, 'standard_monthly', '{"name":"标准月卡"}'::jsonb, 'paid', 'granted', now(), now())
`);
const orders = await local
.from("payment_orders")
.select(PAYMENT_ORDERS_SELECT.join(","))
.eq("user_id", userA)
.order("created_at", { ascending: false })
.limit(20);
assert.equal(orders.error, null);
const orderRows = orders.data as Array<{ order_no: string }>;
assert.deepEqual(
orderRows.map((row) => row.order_no),
["JY" + "A2".padEnd(14, "0"), "JY" + "A1".padEnd(14, "0")],
);
assert.deepEqual(
formatPaymentOrders(orders.data as never).map((order) => order.orderNo),
["JY" + "A2".padEnd(14, "0"), "JY" + "A1".padEnd(14, "0")],
);
// RLS still filters direct selects to the authenticated account.
assert.equal(
fixture.psqlAs(
"app_runtime",
"app-runtime-test-password",
`set role authenticated;
select set_config('request.jwt.claim.sub', '${userA}', true);
select count(*) from public.payment_orders`,
),
`SET\n${userA}\n2`,
);
// 8) The audit table stores no code material and has least privilege.
assert.equal(
fixture.psql(`
select string_agg(column_name, ',' order by ordinal_position)
from information_schema.columns
where table_schema = 'public' and table_name = 'redemption_attempts'
`),
"id,user_id,created_at",
);
assert.equal(
fixture.psql(
`select relrowsecurity from pg_class where relname = 'redemption_attempts'`,
),
"t",
);
assert.equal(
fixture.psql(
`select has_table_privilege('authenticated', 'public.redemption_attempts', 'select')`,
),
"f",
);
assert.equal(
fixture.psql(
`select has_table_privilege('authenticated', 'public.redemption_attempts', 'insert')`,
),
"f",
);
assert.equal(
fixture.psql(
`select has_table_privilege('anon', 'public.redemption_attempts', 'select')`,
),
"f",
);
assert.equal(
fixture.psql(
`select has_table_privilege('service_role', 'public.redemption_attempts', 'select')`,
),
"t",
);
// The identity sequence needs no authenticated USAGE grant: inserts flow
// through the security-definer redeem_code which runs as its owner. The
// count assertions above already prove the definer can insert.
assert.equal(
fixture.psql(
`select has_sequence_privilege('authenticated', 'public.redemption_attempts_id_seq', 'USAGE')`,
),
"f",
);
assert.equal(
fixture.psql(
`select has_function_privilege('authenticated', 'public.redeem_code(text)', 'execute')`,
),
"t",
);
assert.equal(
fixture.psql(
`select has_function_privilege('anon', 'public.redeem_code(text)', 'execute')`,
),
"f",
);
} finally {
await closeLocalPostgresDataPools();
fixture.stop();
}
});