license resend, license pretified
This commit is contained in:
79
src/routes/api/the-nook/resend-license.test.ts
Normal file
79
src/routes/api/the-nook/resend-license.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it, mock, beforeEach } from "bun:test";
|
||||
|
||||
// The route's two core contracts:
|
||||
// 1. A license key is emailed only when the requested email owns one.
|
||||
// 2. The HTTP response is identical whether or not a license exists —
|
||||
// callers can't learn whether an email is registered.
|
||||
// NookConnectionFactory and the nook helper module are mocked so the test
|
||||
// needs no real DB, env, or outbound email.
|
||||
|
||||
let licenseRows: { key: string }[] = [];
|
||||
const sentEmails: { to: string; key: string }[] = [];
|
||||
const queries: string[] = [];
|
||||
|
||||
const fakeConn = {
|
||||
execute: async (q: { sql: string; args?: unknown[] }) => {
|
||||
queries.push(q.sql);
|
||||
if (q.sql.includes("FROM licenses")) return { rows: licenseRows };
|
||||
return { rows: [] };
|
||||
}
|
||||
};
|
||||
|
||||
mock.module("~/server/db-connections", () => ({
|
||||
NookConnectionFactory: () => fakeConn
|
||||
}));
|
||||
|
||||
mock.module("~/server/nook", () => ({
|
||||
nookSchemaBootstrap: Promise.resolve(),
|
||||
emailLicenseKey: async (to: string, key: string) => {
|
||||
sentEmails.push({ to, key });
|
||||
}
|
||||
}));
|
||||
|
||||
const { POST } = await import("./resend-license");
|
||||
|
||||
function request(body: unknown): any {
|
||||
return { request: { json: async () => body } };
|
||||
}
|
||||
|
||||
describe("POST /api/the-nook/resend-license", () => {
|
||||
beforeEach(() => {
|
||||
licenseRows = [];
|
||||
sentEmails.length = 0;
|
||||
queries.length = 0;
|
||||
});
|
||||
|
||||
it("rejects an invalid email", async () => {
|
||||
const res = await POST(request({ email: "not-an-email" }));
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toEqual({ error: "Invalid email" });
|
||||
expect(sentEmails).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("emails the stored key when a license exists for the email", async () => {
|
||||
licenseRows = [{ key: "NOOK-ABC123" }];
|
||||
const res = await POST(request({ email: "USER@Example.com " }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ success: true });
|
||||
expect(sentEmails).toEqual([{ to: "user@example.com", key: "NOOK-ABC123" }]);
|
||||
expect(queries.some((q) => q.toLowerCase().includes("lower(email)"))).toBe(true);
|
||||
});
|
||||
|
||||
it("sends nothing when no license exists, but still succeeds", async () => {
|
||||
const res = await POST(request({ email: "nobody@example.com" }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ success: true });
|
||||
expect(sentEmails).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns an identical response whether or not a license exists", async () => {
|
||||
const withLicense = await POST(request({ email: "buyer@example.com" }));
|
||||
licenseRows = [{ key: "NOOK-XYZ" }];
|
||||
const withoutLicense = await POST(request({ email: "buyer@example.com" }));
|
||||
|
||||
expect(withLicense.status).toBe(withoutLicense.status);
|
||||
expect(await withLicense.text()).toBe(await withoutLicense.text());
|
||||
});
|
||||
});
|
||||
42
src/routes/api/the-nook/resend-license.ts
Normal file
42
src/routes/api/the-nook/resend-license.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { APIEvent } from "@solidjs/start/server";
|
||||
import { NookConnectionFactory } from "~/server/db-connections";
|
||||
import { nookSchemaBootstrap, emailLicenseKey } from "~/server/nook";
|
||||
import { json, error } from "./_lib";
|
||||
|
||||
/**
|
||||
* POST /api/the-nook/resend-license
|
||||
* Body: { email }
|
||||
*
|
||||
* Re-sends the most recent license key to a buyer who lost it. If the email
|
||||
* owns a license, the key is emailed; otherwise nothing is sent. The response
|
||||
* is identical either way so callers can't learn whether an email has a
|
||||
* license — only validation failures differ.
|
||||
*/
|
||||
export async function POST(event: APIEvent) {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await event.request.json();
|
||||
} catch {
|
||||
return error("Invalid JSON", 400);
|
||||
}
|
||||
const b = (body ?? {}) as Record<string, unknown>;
|
||||
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return error("Invalid email", 400);
|
||||
}
|
||||
|
||||
await nookSchemaBootstrap;
|
||||
const conn = NookConnectionFactory();
|
||||
|
||||
const res = await conn.execute({
|
||||
sql: "SELECT key FROM licenses WHERE lower(email) = lower(?) ORDER BY created_at DESC LIMIT 1",
|
||||
args: [email]
|
||||
});
|
||||
|
||||
if (res.rows.length > 0) {
|
||||
const { key } = res.rows[0] as { key: string };
|
||||
await emailLicenseKey(email, key);
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { APIEvent } from "@solidjs/start/server";
|
||||
import { env } from "~/env/server";
|
||||
import { NookConnectionFactory } from "~/server/db-connections";
|
||||
import { nookSchemaBootstrap, issueLicense } from "~/server/nook";
|
||||
import { nookSchemaBootstrap, issueLicense, emailLicenseKey } from "~/server/nook";
|
||||
import { json } from "../_lib";
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
|
||||
@@ -46,31 +46,6 @@ function verifyStripeSignature(rawBody: string, signatureHeader: string): boolea
|
||||
return timingSafeEqual(provided, expectedBuffer);
|
||||
}
|
||||
|
||||
async function emailLicenseKey(to: string, licenseKey: string): Promise<void> {
|
||||
try {
|
||||
await fetch("https://api.brevo.com/v3/smtp/email", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"api-key": env.SENDINBLUE_KEY,
|
||||
"content-type": "application/json",
|
||||
accept: "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sender: { email: env.EMAIL_FROM },
|
||||
to: [{ email: to }],
|
||||
subject: "Your The Nook license key",
|
||||
textContent:
|
||||
`Your The Nook license key is:\n\n${licenseKey}\n\n` +
|
||||
`Open The Nook, go to Settings, and enter this key to activate.\n` +
|
||||
`You can activate up to 3 devices.\n\n— The Nook`
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
// Purchase must not fail because email delivery did.
|
||||
console.error("Failed to email The Nook license key:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const rawBody = await event.request.text();
|
||||
const signatureHeader = event.request.headers.get("Stripe-Signature");
|
||||
|
||||
Reference in New Issue
Block a user