@0x0501/mystack 0.0.0-stage → 0.1.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/assets/auth.sqlite.schema.ts +479 -0
- package/assets/bun-sqlite.test.ts +46 -0
- package/assets/d1.test.ts +41 -0
- package/assets/drizzle.config.ts +2 -0
- package/assets/generate-auth-schema.mjs +25 -0
- package/assets/platform.sqlite.schema.ts +46 -0
- package/assets/polar-checkout.ts +10 -0
- package/assets/polar-route.ts +19 -0
- package/assets/polar.test.ts +30 -0
- package/assets/polar.ts +12 -0
- package/assets/purchase.bun-sqlite.ts +38 -0
- package/assets/purchase.sqlite.ts +39 -0
- package/assets/sqlite-migrations/0000_thin_phalanx.sql +295 -0
- package/assets/sqlite-migrations/meta/0000_snapshot.json +2058 -0
- package/assets/sqlite-migrations/meta/_journal.json +13 -0
- package/assets/turso.test.ts +40 -0
- package/package.json +44 -5
- package/src/adapters/database.mjs +448 -0
- package/src/adapters/deployment.mjs +101 -0
- package/src/adapters/email.mjs +75 -0
- package/src/adapters/payments.mjs +270 -0
- package/src/catalog.mjs +128 -0
- package/src/cli.mjs +294 -0
- package/src/generate.mjs +183 -0
- package/src/template.mjs +93 -0
- package/template/.env.docker +16 -0
- package/template/.env.example +67 -0
- package/template/DESIGN.md +58 -0
- package/template/LICENSE +21 -0
- package/template/biome.json +46 -0
- package/template/docker-compose.yml +47 -0
- package/template/docs/project-guide.md +61 -0
- package/template/drizzle/0000_create-app-role.sql +54 -0
- package/template/drizzle/0001_add-rls-context.sql +21 -0
- package/template/drizzle/0002_platform_tables.sql +320 -0
- package/template/drizzle/meta/0002_snapshot.json +2125 -0
- package/template/drizzle/meta/_journal.json +27 -0
- package/template/drizzle.config.ts +25 -0
- package/template/e2e/authenticated-ui.spec.ts +40 -0
- package/template/e2e/headless-design.spec.ts +66 -0
- package/template/e2e/navigation-loading.spec.ts +70 -0
- package/template/e2e/theme-responsive.spec.ts +260 -0
- package/template/gitignore +24 -0
- package/template/messages/de.json +212 -0
- package/template/messages/en.json +212 -0
- package/template/package.json +101 -0
- package/template/paraglide.config.ts +49 -0
- package/template/playwright.config.ts +58 -0
- package/template/project.inlang/settings.json +12 -0
- package/template/public/drizzle.svg +1 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logo192.png +0 -0
- package/template/public/logo512.png +0 -0
- package/template/public/manifest.json +25 -0
- package/template/public/robots.txt +3 -0
- package/template/scripts/check-client-bundle.ts +150 -0
- package/template/scripts/check-server-fns.ts +290 -0
- package/template/scripts/check-sql-raw.ts +50 -0
- package/template/scripts/compile-paraglide.ts +12 -0
- package/template/scripts/postgres/README.md +76 -0
- package/template/scripts/postgres/create-app-role.sh +60 -0
- package/template/scripts/seed-local.ts +331 -0
- package/template/scripts/validate-prod-env.ts +53 -0
- package/template/src/__tests__/README.md +9 -0
- package/template/src/__tests__/account-security.test.ts +664 -0
- package/template/src/__tests__/admin-http-surface.test.ts +311 -0
- package/template/src/__tests__/admin-peer-protection.test.ts +178 -0
- package/template/src/__tests__/auth-harness.ts +104 -0
- package/template/src/__tests__/auth-rate-limit.test.ts +43 -0
- package/template/src/__tests__/auth-redirect.test.ts +120 -0
- package/template/src/__tests__/body-limit.test.ts +47 -0
- package/template/src/__tests__/creem.test.ts +62 -0
- package/template/src/__tests__/demo-checkout.test.ts +141 -0
- package/template/src/__tests__/email-locale.test.ts +110 -0
- package/template/src/__tests__/global-setup.ts +80 -0
- package/template/src/__tests__/input-rules.test.ts +0 -0
- package/template/src/__tests__/message-parity.test.ts +38 -0
- package/template/src/__tests__/navigation-loading.test.tsx +252 -0
- package/template/src/__tests__/nowpayments.test.ts +106 -0
- package/template/src/__tests__/paraglide-url-contract.test.ts +87 -0
- package/template/src/__tests__/purchase-fulfillment.test.ts +204 -0
- package/template/src/__tests__/query-unauthorized-redirect.test.ts +95 -0
- package/template/src/__tests__/rls-policy.test.ts +120 -0
- package/template/src/__tests__/same-page-locale-href.test.ts +47 -0
- package/template/src/__tests__/search-params-oauth.test.ts +57 -0
- package/template/src/__tests__/security-headers.test.ts +12 -0
- package/template/src/__tests__/server-fn-error-contract.test.ts +173 -0
- package/template/src/__tests__/stripe.test.ts +84 -0
- package/template/src/__tests__/test-db.ts +32 -0
- package/template/src/__tests__/test-rls-db.ts +23 -0
- package/template/src/__tests__/user-update-guard.test.ts +109 -0
- package/template/src/components/LocaleSwitcher.tsx +79 -0
- package/template/src/components/ThemeToggle.tsx +47 -0
- package/template/src/components/auth/social.tsx +90 -0
- package/template/src/components/auth/turnstile.tsx +67 -0
- package/template/src/components/root/navigation-progress.tsx +32 -0
- package/template/src/components/root/not-found.tsx +20 -0
- package/template/src/components/root/pending.tsx +9 -0
- package/template/src/components/root/root-document.tsx +37 -0
- package/template/src/components/root/root-error.tsx +34 -0
- package/template/src/components/shells.tsx +201 -0
- package/template/src/components/ui/styles.ts +9 -0
- package/template/src/db/auth.schema.ts +455 -0
- package/template/src/db/helper.ts +38 -0
- package/template/src/db/index.ts +15 -0
- package/template/src/db/platform.schema.ts +122 -0
- package/template/src/db/schema.ts +2 -0
- package/template/src/env.ts +65 -0
- package/template/src/hooks/use-theme.ts +32 -0
- package/template/src/integrations/better-auth/header-user.tsx +36 -0
- package/template/src/integrations/better-auth/session-query.ts +11 -0
- package/template/src/integrations/react-email/PasswordOtpEmail.tsx +62 -0
- package/template/src/integrations/react-email/ResetPasswordEmail.tsx +64 -0
- package/template/src/integrations/react-email/VerifyEmail.tsx +72 -0
- package/template/src/integrations/react-email/theme.ts +75 -0
- package/template/src/integrations/tanstack-query/devtools.tsx +6 -0
- package/template/src/integrations/tanstack-query/root-provider.tsx +60 -0
- package/template/src/lib/auth-client.ts +27 -0
- package/template/src/lib/auth-paths.ts +94 -0
- package/template/src/lib/auth-plugins/existing-email-signup.ts +86 -0
- package/template/src/lib/auth-plugins/field-rules.ts +84 -0
- package/template/src/lib/auth-plugins/social-two-factor.ts +194 -0
- package/template/src/lib/auth-timing.ts +9 -0
- package/template/src/lib/auth.ts +279 -0
- package/template/src/lib/captcha.ts +11 -0
- package/template/src/lib/email.tsx +162 -0
- package/template/src/lib/github.ts +18 -0
- package/template/src/lib/locale-cookie.ts +12 -0
- package/template/src/lib/localized-pages.ts +25 -0
- package/template/src/lib/oauth-metadata.ts +19 -0
- package/template/src/lib/permissions.ts +29 -0
- package/template/src/lib/security-headers.ts +20 -0
- package/template/src/lib/site.ts +59 -0
- package/template/src/lib/theme.ts +42 -0
- package/template/src/lib/user-update-guard.ts +35 -0
- package/template/src/middlewares/admin.ts +46 -0
- package/template/src/middlewares/auth-rate-limit.ts +37 -0
- package/template/src/middlewares/better-auth.ts +15 -0
- package/template/src/middlewares/body-limit.ts +62 -0
- package/template/src/middlewares/database.ts +13 -0
- package/template/src/middlewares/protected.ts +45 -0
- package/template/src/routeTree.gen.ts +536 -0
- package/template/src/router.tsx +53 -0
- package/template/src/routes/[.]well-known/oauth-authorization-server/api/auth.ts +18 -0
- package/template/src/routes/[.]well-known/oauth-protected-resource/index.ts +32 -0
- package/template/src/routes/__root.tsx +175 -0
- package/template/src/routes/admin/audit.tsx +74 -0
- package/template/src/routes/admin/index.tsx +7 -0
- package/template/src/routes/admin/oauth-apps.tsx +164 -0
- package/template/src/routes/admin/route.tsx +27 -0
- package/template/src/routes/admin/users.tsx +215 -0
- package/template/src/routes/api/auth/$.ts +14 -0
- package/template/src/routes/api/auth/[.]well-known/openid-configuration.ts +15 -0
- package/template/src/routes/api/webhooks/creem.ts +69 -0
- package/template/src/routes/api/webhooks/nowpayments.ts +62 -0
- package/template/src/routes/api/webhooks/stripe.ts +74 -0
- package/template/src/routes/dashboard/account.tsx +430 -0
- package/template/src/routes/dashboard/index.tsx +128 -0
- package/template/src/routes/dashboard/route.tsx +17 -0
- package/template/src/routes/forgot-password.tsx +65 -0
- package/template/src/routes/index.tsx +85 -0
- package/template/src/routes/oauth/consent.tsx +77 -0
- package/template/src/routes/reset-password.tsx +78 -0
- package/template/src/routes/sign-in.tsx +174 -0
- package/template/src/routes/sign-up.tsx +135 -0
- package/template/src/routes/two-factor.tsx +74 -0
- package/template/src/server/README.md +3 -0
- package/template/src/server/account-security.ts +163 -0
- package/template/src/server/admin-audit.ts +37 -0
- package/template/src/server/admin-users.ts +112 -0
- package/template/src/server/auth.ts +44 -0
- package/template/src/server/demo-checkout.ts +67 -0
- package/template/src/server/oauth-clients.ts +83 -0
- package/template/src/server/payment-toggles.ts +39 -0
- package/template/src/server.ts +20 -0
- package/template/src/services/README.md +3 -0
- package/template/src/services/account-security.ts +893 -0
- package/template/src/services/admin-security.ts +164 -0
- package/template/src/services/admin-users.ts +181 -0
- package/template/src/services/auth-rate-limit.ts +32 -0
- package/template/src/services/creem.ts +83 -0
- package/template/src/services/demo-checkout.ts +100 -0
- package/template/src/services/nowpayments.ts +154 -0
- package/template/src/services/oauth-client.ts +85 -0
- package/template/src/services/oauth-clients.ts +15 -0
- package/template/src/services/payment-providers.ts +157 -0
- package/template/src/services/payment-toggles.ts +48 -0
- package/template/src/services/purchase.ts +233 -0
- package/template/src/services/stripe.ts +106 -0
- package/template/src/start.ts +35 -0
- package/template/src/styles/layout.css +93 -0
- package/template/src/styles/theme.css +84 -0
- package/template/src/styles/ui.css +233 -0
- package/template/src/styles.css +90 -0
- package/template/src/utils/api-error.ts +129 -0
- package/template/src/utils/auth-errors.ts +157 -0
- package/template/src/utils/auth-redirect.ts +61 -0
- package/template/src/utils/body-limit.ts +84 -0
- package/template/src/utils/cn.ts +6 -0
- package/template/src/utils/input-rules.ts +172 -0
- package/template/src/utils/locale-paths.ts +18 -0
- package/template/src/utils/same-page-locale-href.ts +20 -0
- package/template/src/utils/search-params.ts +71 -0
- package/template/tsconfig.json +30 -0
- package/template/tsr.config.json +6 -0
- package/template/vite.config.ts +24 -0
- package/template/vitest.config.ts +26 -0
- package/template/worker-configuration.d.ts +14660 -0
- package/template/wrangler.jsonc +39 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
const temporary = "src/lib/.mystack-auth.ts";
|
|
4
|
+
let source = (await readFile("src/lib/auth.ts", "utf8"))
|
|
5
|
+
.replace('import { waitUntil } from "cloudflare:workers";', 'const waitUntil = (promise: Promise<unknown>) => { void promise; };')
|
|
6
|
+
.replaceAll("import.meta.env", '({ DEV: true, VITE_GITHUB_CLIENT_ID: undefined })');
|
|
7
|
+
// The offline generator reads plugin metadata without a connected database.
|
|
8
|
+
// Runtime auth keeps schema validation enabled.
|
|
9
|
+
source = source.includes("\t\tadvanced: {\n")
|
|
10
|
+
? source.replace("\t\tadvanced: {\n", "\t\tadvanced: {\n\t\t\tdatabase: { validateSchema: false },\n")
|
|
11
|
+
: source.replace("\t\ttelemetry: { enabled: false },", "\t\ttelemetry: { enabled: false },\n\t\tadvanced: { database: { validateSchema: false } },");
|
|
12
|
+
source += "\nexport const auth = createAuth({} as unknown as Database);\n";
|
|
13
|
+
await writeFile(temporary, source, { flag: "wx" });
|
|
14
|
+
try {
|
|
15
|
+
await new Promise((resolve, reject) => {
|
|
16
|
+
const child = spawn("bun", ["x", "auth@1.7.3", "generate", "--config", temporary, "--output", "./src/db/auth.schema.ts", "--yes"], { stdio: "inherit" });
|
|
17
|
+
child.on("error", reject);
|
|
18
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Auth schema generation failed (${code})`)));
|
|
19
|
+
});
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
const child = spawn("bun", ["x", "biome", "check", "--write", "src/db/auth.schema.ts"], { stdio: "inherit" });
|
|
22
|
+
child.on("error", reject);
|
|
23
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Auth schema formatting failed (${code})`)));
|
|
24
|
+
});
|
|
25
|
+
} finally { await rm(temporary, { force: true }); }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { user } from "./auth.sqlite.schema";
|
|
4
|
+
|
|
5
|
+
export type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[];
|
|
6
|
+
const createdAt = () => integer("created_at", { mode: "timestamp_ms" }).default(sql`(unixepoch('subsec') * 1000)`).notNull();
|
|
7
|
+
const updatedAt = () => integer("updated_at", { mode: "timestamp_ms" }).default(sql`(unixepoch('subsec') * 1000)`).$onUpdate(() => new Date()).notNull();
|
|
8
|
+
export const systemConfig = sqliteTable("system_config", {
|
|
9
|
+
key: text("key").primaryKey(),
|
|
10
|
+
value: text("value", { mode: "json" }).$type<JsonValue>().notNull(),
|
|
11
|
+
updatedAt: updatedAt(),
|
|
12
|
+
});
|
|
13
|
+
export const adminAction = sqliteTable("admin_action", {
|
|
14
|
+
id: text("id").primaryKey(),
|
|
15
|
+
actorId: text("actor_id").references(() => user.id, { onDelete: "set null" }),
|
|
16
|
+
action: text("action").notNull(),
|
|
17
|
+
targetType: text("target_type"),
|
|
18
|
+
targetId: text("target_id"),
|
|
19
|
+
detail: text("detail", { mode: "json" }).$type<JsonValue>(),
|
|
20
|
+
createdAt: createdAt(),
|
|
21
|
+
}, (table) => [index("admin_action_created_idx").on(table.createdAt)]);
|
|
22
|
+
export const purchase = sqliteTable("purchase", {
|
|
23
|
+
id: text("id").primaryKey(),
|
|
24
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
25
|
+
provider: text("provider").notNull(),
|
|
26
|
+
externalId: text("external_id").notNull(),
|
|
27
|
+
amount: text("amount").notNull(),
|
|
28
|
+
currency: text("currency").notNull().default("usd"),
|
|
29
|
+
status: text("status").notNull().default("pending"),
|
|
30
|
+
metadata: text("metadata", { mode: "json" }).$type<JsonValue>(),
|
|
31
|
+
paidAt: integer("paid_at", { mode: "timestamp_ms" }),
|
|
32
|
+
fulfillmentKey: text("fulfillment_key"),
|
|
33
|
+
createdAt: createdAt(),
|
|
34
|
+
updatedAt: updatedAt(),
|
|
35
|
+
}, (table) => [uniqueIndex("purchase_provider_external_uq").on(table.provider, table.externalId), index("purchase_userId_idx").on(table.userId)]);
|
|
36
|
+
// Durable paid events. D1 commits this row in the same batch as the paid flip.
|
|
37
|
+
// Product-specific fulfillment consumes these events with purchaseId as its
|
|
38
|
+
// idempotency key; no arbitrary async callback pretends to be a D1 transaction.
|
|
39
|
+
export const purchaseDelivery = sqliteTable("purchase_delivery", {
|
|
40
|
+
purchaseId: text("purchase_id").primaryKey().references(() => purchase.id, { onDelete: "cascade" }),
|
|
41
|
+
createdAt: createdAt(),
|
|
42
|
+
processedAt: integer("processed_at", { mode: "timestamp_ms" }),
|
|
43
|
+
});
|
|
44
|
+
export type SystemConfigRow = typeof systemConfig.$inferSelect;
|
|
45
|
+
export type AdminActionRow = typeof adminAction.$inferSelect;
|
|
46
|
+
export type PurchaseRow = typeof purchase.$inferSelect;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
async function createPolarCheckout(input: { userId: string; userEmail: string; amount: number; currency: string }): Promise<{ externalId: string; url: string }> {
|
|
2
|
+
const { createPolar } = await import("@polar-sh/sdk/2026-10");
|
|
3
|
+
if (!env.POLAR_ACCESS_TOKEN || !env.POLAR_PRODUCT_ID) throw new Error("Polar is not configured.");
|
|
4
|
+
const polar = createPolar({ accessToken: env.POLAR_ACCESS_TOKEN, environment: env.POLAR_SANDBOX ? "sandbox" : "production" });
|
|
5
|
+
const checkout = await polar.checkouts.create({ products: [env.POLAR_PRODUCT_ID], customer_email: input.userEmail, external_customer_id: input.userId, success_url: `${env.APP_ORIGIN}/dashboard?checkout=success`, return_url: `${env.APP_ORIGIN}/dashboard?checkout=cancel` });
|
|
6
|
+
// The configured catalog price must match the local purchase ledger. Do not
|
|
7
|
+
// issue a pending purchase with an invented price for a fixed-price product.
|
|
8
|
+
if (checkout.amount !== input.amount || checkout.currency.toLowerCase() !== input.currency.toLowerCase()) throw new Error("Polar product price must match the demo purchase price and currency.");
|
|
9
|
+
return { externalId: checkout.id, url: checkout.url };
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { withRlsService } from "@/db/helper";
|
|
3
|
+
import { env } from "@/env";
|
|
4
|
+
import { webhookBodyLimitMiddleware } from "@/middlewares/body-limit";
|
|
5
|
+
import { databaseMiddleware } from "@/middlewares/database";
|
|
6
|
+
import { handlePolarWebhook } from "@/services/polar";
|
|
7
|
+
import { methodNotAllowed } from "@/utils/api-error";
|
|
8
|
+
export const Route = createFileRoute("/api/webhooks/polar")({
|
|
9
|
+
server: { handlers: ({ createHandlers }) => createHandlers({
|
|
10
|
+
GET: { handler: () => methodNotAllowed("POST") },
|
|
11
|
+
POST: { middleware: [databaseMiddleware, webhookBodyLimitMiddleware], handler: async ({ request, context }) => {
|
|
12
|
+
const secret = env.POLAR_WEBHOOK_SECRET;
|
|
13
|
+
if (!secret) return Response.json({ error: "not_configured" }, { status: 503 });
|
|
14
|
+
const headers = Object.fromEntries(request.headers.entries());
|
|
15
|
+
const result = await withRlsService(context.db, (db) => handlePolarWebhook(db, { rawBody: context.rawBody, headers, secret }));
|
|
16
|
+
return Response.json({ result }, { status: result === "invalid_signature" || result === "invalid_payload" ? 400 : 200 });
|
|
17
|
+
} },
|
|
18
|
+
}) },
|
|
19
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { beforeEach, expect, test, vi } from "vitest";
|
|
3
|
+
import type { Database } from "@/db";
|
|
4
|
+
import { handlePolarWebhook } from "@/services/polar";
|
|
5
|
+
import { markPurchasePaid } from "@/services/purchase";
|
|
6
|
+
vi.mock("@/services/purchase", () => ({ markPurchasePaid: vi.fn() }));
|
|
7
|
+
const db = {} as Database;
|
|
8
|
+
const secret = "whsec_test-signing-secret";
|
|
9
|
+
function signed(rawBody: string) {
|
|
10
|
+
const id = "test-event";
|
|
11
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
12
|
+
const signature = createHmac("sha256", secret).update(`${id}.${timestamp}.${rawBody}`).digest("base64");
|
|
13
|
+
return { "webhook-id": id, "webhook-timestamp": timestamp, "webhook-signature": `v1,${signature}` };
|
|
14
|
+
}
|
|
15
|
+
beforeEach(() => vi.clearAllMocks());
|
|
16
|
+
test("invalid signatures never reach the purchase ledger", async () => {
|
|
17
|
+
expect(await handlePolarWebhook(db, { rawBody: "{}", headers: {}, secret })).toBe("invalid_signature");
|
|
18
|
+
expect(markPurchasePaid).not.toHaveBeenCalled();
|
|
19
|
+
});
|
|
20
|
+
test("verified paid events map the checkout id and subtotal to the ledger", async () => {
|
|
21
|
+
vi.mocked(markPurchasePaid).mockResolvedValue({ status: "not_found" });
|
|
22
|
+
const rawBody = JSON.stringify({ type: "order.paid", timestamp: new Date().toISOString(), data: { checkout_id: "checkout-1", subtotal_amount: 500 } });
|
|
23
|
+
expect(await handlePolarWebhook(db, { rawBody, headers: signed(rawBody), secret })).toBe("unknown_order");
|
|
24
|
+
expect(markPurchasePaid).toHaveBeenCalledWith(db, { provider: "polar", externalId: "checkout-1", reportedAmount: 500 });
|
|
25
|
+
});
|
|
26
|
+
test("a signed malformed payload is refused without ledger effects", async () => {
|
|
27
|
+
const rawBody = "{invalid-json";
|
|
28
|
+
expect(await handlePolarWebhook(db, { rawBody, headers: signed(rawBody), secret })).toBe("invalid_payload");
|
|
29
|
+
expect(markPurchasePaid).not.toHaveBeenCalled();
|
|
30
|
+
});
|
package/assets/polar.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { webhooks } from "@polar-sh/sdk/2026-10";
|
|
2
|
+
import type { Database } from "@/db";
|
|
3
|
+
import { markPurchasePaid } from "./purchase";
|
|
4
|
+
export async function handlePolarWebhook(db: Database, input: { rawBody: string; headers: Record<string, string>; secret: string }) {
|
|
5
|
+
let event: Awaited<ReturnType<typeof webhooks.validateEvent>>;
|
|
6
|
+
try { event = await webhooks.validateEvent(input.rawBody, input.headers, input.secret); }
|
|
7
|
+
catch (error) { if (error instanceof webhooks.PolarWebhookVerificationError) return "invalid_signature"; if (error instanceof webhooks.PolarWebhookError) return "invalid_payload"; throw error; }
|
|
8
|
+
if (event.type !== "order.paid") return "ignored";
|
|
9
|
+
if (!event.data.checkout_id) return "unknown_order";
|
|
10
|
+
const result = await markPurchasePaid(db, { provider: "polar", externalId: event.data.checkout_id, reportedAmount: event.data.subtotal_amount });
|
|
11
|
+
return result.status === "not_found" ? "unknown_order" : result.status;
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import type { Database } from "@/db";
|
|
3
|
+
import { purchaseDelivery, purchase as purchaseTable } from "@/db/schema";
|
|
4
|
+
export type PurchaseProvider = "stripe" | "creem" | "nowpayments";
|
|
5
|
+
export type PurchaseStatus = "pending" | "paid" | "failed";
|
|
6
|
+
export type Purchase = { id: string; userId: string; provider: PurchaseProvider; externalId: string; amount: number; currency: string; status: PurchaseStatus; paidAt: Date | null };
|
|
7
|
+
export type MarkPurchasePaidResult =
|
|
8
|
+
| { status: "fulfilled" | "already_paid" | "not_pending" | "amount_mismatch"; purchase: Purchase }
|
|
9
|
+
| { status: "not_found" };
|
|
10
|
+
function rowToPurchase(row: typeof purchaseTable.$inferSelect): Purchase {
|
|
11
|
+
if (!/^\d+$/.test(row.amount) || !Number.isSafeInteger(Number(row.amount))) throw new Error("Invalid purchase amount.");
|
|
12
|
+
return { id: row.id, userId: row.userId, provider: row.provider as PurchaseProvider, externalId: row.externalId, amount: Number(row.amount), currency: row.currency, status: row.status as PurchaseStatus, paidAt: row.paidAt };
|
|
13
|
+
}
|
|
14
|
+
export async function markPurchasePaid(db: Database, input: { provider: PurchaseProvider; externalId: string; reportedAmount?: number }): Promise<MarkPurchasePaidResult> {
|
|
15
|
+
const where = and(eq(purchaseTable.provider, input.provider), eq(purchaseTable.externalId, input.externalId));
|
|
16
|
+
const [existing] = await db.select().from(purchaseTable).where(where).limit(1);
|
|
17
|
+
if (!existing) return { status: "not_found" };
|
|
18
|
+
const purchase = rowToPurchase(existing);
|
|
19
|
+
if (purchase.status === "paid") return { status: "already_paid", purchase };
|
|
20
|
+
if (purchase.status !== "pending") return { status: "not_pending", purchase };
|
|
21
|
+
if (input.reportedAmount !== undefined && purchase.amount !== input.reportedAmount) return { status: "amount_mismatch", purchase };
|
|
22
|
+
const claim = crypto.randomUUID();
|
|
23
|
+
const now = new Date();
|
|
24
|
+
// The paid flip and delivery event commit together in a synchronous transaction.
|
|
25
|
+
const flipped = db.transaction((tx) => {
|
|
26
|
+
const row = tx.update(purchaseTable).set({ status: "paid", paidAt: now, fulfillmentKey: claim }).where(and(where, eq(purchaseTable.status, "pending"))).returning().get();
|
|
27
|
+
if (row) tx.insert(purchaseDelivery).values({ purchaseId: row.id, createdAt: now }).run();
|
|
28
|
+
return row;
|
|
29
|
+
}, { behavior: "immediate" });
|
|
30
|
+
if (flipped) return { status: "fulfilled", purchase: rowToPurchase(flipped) };
|
|
31
|
+
const [again] = await db.select().from(purchaseTable).where(where).limit(1);
|
|
32
|
+
return { status: again?.status === "paid" ? "already_paid" : "not_pending", purchase: again ? rowToPurchase(again) : purchase };
|
|
33
|
+
}
|
|
34
|
+
export async function createPendingPurchase(db: Database, input: { userId: string; provider: PurchaseProvider; externalId: string; amount: number; currency?: string }): Promise<{ purchaseId: string }> {
|
|
35
|
+
const id = crypto.randomUUID();
|
|
36
|
+
await db.insert(purchaseTable).values({ id, userId: input.userId, provider: input.provider, externalId: input.externalId, amount: String(input.amount), currency: input.currency ?? "usd", status: "pending" });
|
|
37
|
+
return { purchaseId: id };
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
2
|
+
import type { Database } from "@/db";
|
|
3
|
+
import { purchaseDelivery, purchase as purchaseTable } from "@/db/schema";
|
|
4
|
+
export type PurchaseProvider = "stripe" | "creem" | "nowpayments";
|
|
5
|
+
export type PurchaseStatus = "pending" | "paid" | "failed";
|
|
6
|
+
export type Purchase = { id: string; userId: string; provider: PurchaseProvider; externalId: string; amount: number; currency: string; status: PurchaseStatus; paidAt: Date | null };
|
|
7
|
+
export type MarkPurchasePaidResult =
|
|
8
|
+
| { status: "fulfilled" | "already_paid" | "not_pending" | "amount_mismatch"; purchase: Purchase }
|
|
9
|
+
| { status: "not_found" };
|
|
10
|
+
function rowToPurchase(row: typeof purchaseTable.$inferSelect): Purchase {
|
|
11
|
+
if (!/^\d+$/.test(row.amount) || !Number.isSafeInteger(Number(row.amount))) throw new Error("Invalid purchase amount.");
|
|
12
|
+
return { id: row.id, userId: row.userId, provider: row.provider as PurchaseProvider, externalId: row.externalId, amount: Number(row.amount), currency: row.currency, status: row.status as PurchaseStatus, paidAt: row.paidAt };
|
|
13
|
+
}
|
|
14
|
+
export async function markPurchasePaid(db: Database, input: { provider: PurchaseProvider; externalId: string; reportedAmount?: number }): Promise<MarkPurchasePaidResult> {
|
|
15
|
+
const where = and(eq(purchaseTable.provider, input.provider), eq(purchaseTable.externalId, input.externalId));
|
|
16
|
+
const [existing] = await db.select().from(purchaseTable).where(where).limit(1);
|
|
17
|
+
if (!existing) return { status: "not_found" };
|
|
18
|
+
const purchase = rowToPurchase(existing);
|
|
19
|
+
if (purchase.status === "paid") return { status: "already_paid", purchase };
|
|
20
|
+
if (purchase.status !== "pending") return { status: "not_pending", purchase };
|
|
21
|
+
if (input.reportedAmount !== undefined && purchase.amount !== input.reportedAmount) return { status: "amount_mismatch", purchase };
|
|
22
|
+
const claim = crypto.randomUUID();
|
|
23
|
+
const now = new Date();
|
|
24
|
+
// SQLite driver batches are atomic. Only the invocation that flips this pending row
|
|
25
|
+
// owns the claim and can insert the durable delivery event. A failed insert
|
|
26
|
+
// rolls back the flip, so the webhook's retry can try again safely.
|
|
27
|
+
const [flipped] = await db.batch([
|
|
28
|
+
db.update(purchaseTable).set({ status: "paid", paidAt: now, fulfillmentKey: claim }).where(and(where, eq(purchaseTable.status, "pending"))).returning(),
|
|
29
|
+
db.insert(purchaseDelivery).select(sql`SELECT id, ${now.getTime()}, NULL FROM purchase WHERE id = ${existing.id} AND fulfillment_key = ${claim}`),
|
|
30
|
+
]);
|
|
31
|
+
if (flipped[0]) return { status: "fulfilled", purchase: rowToPurchase(flipped[0]) };
|
|
32
|
+
const [again] = await db.select().from(purchaseTable).where(where).limit(1);
|
|
33
|
+
return { status: again?.status === "paid" ? "already_paid" : "not_pending", purchase: again ? rowToPurchase(again) : purchase };
|
|
34
|
+
}
|
|
35
|
+
export async function createPendingPurchase(db: Database, input: { userId: string; provider: PurchaseProvider; externalId: string; amount: number; currency?: string }): Promise<{ purchaseId: string }> {
|
|
36
|
+
const id = crypto.randomUUID();
|
|
37
|
+
await db.insert(purchaseTable).values({ id, userId: input.userId, provider: input.provider, externalId: input.externalId, amount: String(input.amount), currency: input.currency ?? "usd", status: "pending" });
|
|
38
|
+
return { purchaseId: id };
|
|
39
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
CREATE TABLE `account` (
|
|
2
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`account_id` text NOT NULL,
|
|
4
|
+
`provider_id` text NOT NULL,
|
|
5
|
+
`user_id` text NOT NULL,
|
|
6
|
+
`access_token` text,
|
|
7
|
+
`refresh_token` text,
|
|
8
|
+
`id_token` text,
|
|
9
|
+
`access_token_expires_at` integer,
|
|
10
|
+
`refresh_token_expires_at` integer,
|
|
11
|
+
`scope` text,
|
|
12
|
+
`password` text,
|
|
13
|
+
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
14
|
+
`updated_at` integer NOT NULL,
|
|
15
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
16
|
+
);
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
|
|
19
|
+
CREATE TABLE `jwks` (
|
|
20
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
21
|
+
`public_key` text NOT NULL,
|
|
22
|
+
`private_key` text NOT NULL,
|
|
23
|
+
`created_at` integer NOT NULL,
|
|
24
|
+
`expires_at` integer,
|
|
25
|
+
`alg` text,
|
|
26
|
+
`crv` text
|
|
27
|
+
);
|
|
28
|
+
--> statement-breakpoint
|
|
29
|
+
CREATE TABLE `oauth_access_token` (
|
|
30
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
31
|
+
`token` text NOT NULL,
|
|
32
|
+
`client_id` text NOT NULL,
|
|
33
|
+
`session_id` text,
|
|
34
|
+
`user_id` text,
|
|
35
|
+
`reference_id` text,
|
|
36
|
+
`authorization_code_id` text,
|
|
37
|
+
`resources` text,
|
|
38
|
+
`requested_user_info_claims` text,
|
|
39
|
+
`refresh_id` text,
|
|
40
|
+
`expires_at` integer NOT NULL,
|
|
41
|
+
`created_at` integer NOT NULL,
|
|
42
|
+
`revoked` integer,
|
|
43
|
+
`confirmation` text,
|
|
44
|
+
`scopes` text NOT NULL,
|
|
45
|
+
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
|
46
|
+
FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE set null,
|
|
47
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
48
|
+
FOREIGN KEY (`refresh_id`) REFERENCES `oauth_refresh_token`(`id`) ON UPDATE no action ON DELETE cascade
|
|
49
|
+
);
|
|
50
|
+
--> statement-breakpoint
|
|
51
|
+
CREATE UNIQUE INDEX `oauth_access_token_token_unique` ON `oauth_access_token` (`token`);--> statement-breakpoint
|
|
52
|
+
CREATE INDEX `oauthAccessToken_clientId_idx` ON `oauth_access_token` (`client_id`);--> statement-breakpoint
|
|
53
|
+
CREATE INDEX `oauthAccessToken_sessionId_idx` ON `oauth_access_token` (`session_id`);--> statement-breakpoint
|
|
54
|
+
CREATE INDEX `oauthAccessToken_userId_idx` ON `oauth_access_token` (`user_id`);--> statement-breakpoint
|
|
55
|
+
CREATE INDEX `oauthAccessToken_authorizationCodeId_idx` ON `oauth_access_token` (`authorization_code_id`);--> statement-breakpoint
|
|
56
|
+
CREATE INDEX `oauthAccessToken_refreshId_idx` ON `oauth_access_token` (`refresh_id`);--> statement-breakpoint
|
|
57
|
+
CREATE TABLE `oauth_client` (
|
|
58
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
59
|
+
`client_id` text NOT NULL,
|
|
60
|
+
`client_secret` text,
|
|
61
|
+
`client_discovery_id` text,
|
|
62
|
+
`disabled` integer DEFAULT false,
|
|
63
|
+
`skip_consent` integer,
|
|
64
|
+
`enable_end_session` integer,
|
|
65
|
+
`subject_type` text,
|
|
66
|
+
`scopes` text,
|
|
67
|
+
`client_credentials_scopes` text DEFAULT '[]',
|
|
68
|
+
`user_id` text,
|
|
69
|
+
`created_at` integer,
|
|
70
|
+
`updated_at` integer,
|
|
71
|
+
`name` text,
|
|
72
|
+
`uri` text,
|
|
73
|
+
`icon` text,
|
|
74
|
+
`contacts` text,
|
|
75
|
+
`tos` text,
|
|
76
|
+
`policy` text,
|
|
77
|
+
`software_id` text,
|
|
78
|
+
`software_version` text,
|
|
79
|
+
`software_statement` text,
|
|
80
|
+
`redirect_uris` text NOT NULL,
|
|
81
|
+
`post_logout_redirect_uris` text,
|
|
82
|
+
`backchannel_logout_uri` text,
|
|
83
|
+
`backchannel_logout_session_required` integer,
|
|
84
|
+
`token_endpoint_auth_method` text,
|
|
85
|
+
`application_type` text,
|
|
86
|
+
`jwks` text,
|
|
87
|
+
`jwks_uri` text,
|
|
88
|
+
`grant_types` text,
|
|
89
|
+
`response_types` text,
|
|
90
|
+
`require_pkce` integer,
|
|
91
|
+
`dpop_bound_access_tokens` integer DEFAULT false,
|
|
92
|
+
`reference_id` text,
|
|
93
|
+
`metadata` text,
|
|
94
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
95
|
+
);
|
|
96
|
+
--> statement-breakpoint
|
|
97
|
+
CREATE UNIQUE INDEX `oauth_client_client_id_unique` ON `oauth_client` (`client_id`);--> statement-breakpoint
|
|
98
|
+
CREATE INDEX `oauthClient_userId_idx` ON `oauth_client` (`user_id`);--> statement-breakpoint
|
|
99
|
+
CREATE TABLE `oauth_client_assertion` (
|
|
100
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
101
|
+
`expires_at` integer NOT NULL
|
|
102
|
+
);
|
|
103
|
+
--> statement-breakpoint
|
|
104
|
+
CREATE TABLE `oauth_client_resource` (
|
|
105
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
106
|
+
`client_id` text NOT NULL,
|
|
107
|
+
`resource_id` text NOT NULL,
|
|
108
|
+
`metadata` text,
|
|
109
|
+
`created_at` integer,
|
|
110
|
+
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
|
111
|
+
FOREIGN KEY (`resource_id`) REFERENCES `oauth_resource`(`identifier`) ON UPDATE no action ON DELETE cascade
|
|
112
|
+
);
|
|
113
|
+
--> statement-breakpoint
|
|
114
|
+
CREATE UNIQUE INDEX `oauthClientResource_clientId_resourceId_uidx` ON `oauth_client_resource` (`client_id`,`resource_id`);--> statement-breakpoint
|
|
115
|
+
CREATE INDEX `oauthClientResource_clientId_idx` ON `oauth_client_resource` (`client_id`);--> statement-breakpoint
|
|
116
|
+
CREATE INDEX `oauthClientResource_resourceId_idx` ON `oauth_client_resource` (`resource_id`);--> statement-breakpoint
|
|
117
|
+
CREATE TABLE `oauth_consent` (
|
|
118
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
119
|
+
`client_id` text NOT NULL,
|
|
120
|
+
`user_id` text,
|
|
121
|
+
`reference_id` text,
|
|
122
|
+
`resources` text,
|
|
123
|
+
`requested_user_info_claims` text,
|
|
124
|
+
`scopes` text NOT NULL,
|
|
125
|
+
`created_at` integer NOT NULL,
|
|
126
|
+
`updated_at` integer NOT NULL,
|
|
127
|
+
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
|
128
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
129
|
+
);
|
|
130
|
+
--> statement-breakpoint
|
|
131
|
+
CREATE INDEX `oauthConsent_clientId_idx` ON `oauth_consent` (`client_id`);--> statement-breakpoint
|
|
132
|
+
CREATE INDEX `oauthConsent_userId_idx` ON `oauth_consent` (`user_id`);--> statement-breakpoint
|
|
133
|
+
CREATE TABLE `oauth_refresh_token` (
|
|
134
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
135
|
+
`token` text NOT NULL,
|
|
136
|
+
`client_id` text NOT NULL,
|
|
137
|
+
`session_id` text,
|
|
138
|
+
`user_id` text NOT NULL,
|
|
139
|
+
`reference_id` text,
|
|
140
|
+
`authorization_code_id` text,
|
|
141
|
+
`resources` text,
|
|
142
|
+
`requested_user_info_claims` text,
|
|
143
|
+
`expires_at` integer NOT NULL,
|
|
144
|
+
`created_at` integer NOT NULL,
|
|
145
|
+
`revoked` integer,
|
|
146
|
+
`rotated_at` integer,
|
|
147
|
+
`rotation_replay_response` text,
|
|
148
|
+
`rotation_replay_expires_at` integer,
|
|
149
|
+
`auth_time` integer,
|
|
150
|
+
`confirmation` text,
|
|
151
|
+
`scopes` text NOT NULL,
|
|
152
|
+
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
|
153
|
+
FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE set null,
|
|
154
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
155
|
+
);
|
|
156
|
+
--> statement-breakpoint
|
|
157
|
+
CREATE UNIQUE INDEX `oauth_refresh_token_token_unique` ON `oauth_refresh_token` (`token`);--> statement-breakpoint
|
|
158
|
+
CREATE INDEX `oauthRefreshToken_clientId_idx` ON `oauth_refresh_token` (`client_id`);--> statement-breakpoint
|
|
159
|
+
CREATE INDEX `oauthRefreshToken_sessionId_idx` ON `oauth_refresh_token` (`session_id`);--> statement-breakpoint
|
|
160
|
+
CREATE INDEX `oauthRefreshToken_userId_idx` ON `oauth_refresh_token` (`user_id`);--> statement-breakpoint
|
|
161
|
+
CREATE INDEX `oauthRefreshToken_authorizationCodeId_idx` ON `oauth_refresh_token` (`authorization_code_id`);--> statement-breakpoint
|
|
162
|
+
CREATE TABLE `oauth_resource` (
|
|
163
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
164
|
+
`identifier` text NOT NULL,
|
|
165
|
+
`name` text NOT NULL,
|
|
166
|
+
`access_token_ttl` integer,
|
|
167
|
+
`refresh_token_ttl` integer,
|
|
168
|
+
`signing_algorithm` text,
|
|
169
|
+
`signing_key_id` text,
|
|
170
|
+
`allowed_scopes` text,
|
|
171
|
+
`custom_claims` text,
|
|
172
|
+
`dpop_bound_access_tokens_required` integer DEFAULT false,
|
|
173
|
+
`disabled` integer DEFAULT false,
|
|
174
|
+
`created_at` integer,
|
|
175
|
+
`updated_at` integer,
|
|
176
|
+
`policy_version` integer DEFAULT 1,
|
|
177
|
+
`metadata` text
|
|
178
|
+
);
|
|
179
|
+
--> statement-breakpoint
|
|
180
|
+
CREATE UNIQUE INDEX `oauth_resource_identifier_unique` ON `oauth_resource` (`identifier`);--> statement-breakpoint
|
|
181
|
+
CREATE TABLE `passkey` (
|
|
182
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
183
|
+
`name` text,
|
|
184
|
+
`public_key` text NOT NULL,
|
|
185
|
+
`user_id` text NOT NULL,
|
|
186
|
+
`credential_id` text NOT NULL,
|
|
187
|
+
`counter` integer NOT NULL,
|
|
188
|
+
`device_type` text NOT NULL,
|
|
189
|
+
`backed_up` integer NOT NULL,
|
|
190
|
+
`transports` text,
|
|
191
|
+
`created_at` integer,
|
|
192
|
+
`aaguid` text,
|
|
193
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
194
|
+
);
|
|
195
|
+
--> statement-breakpoint
|
|
196
|
+
CREATE INDEX `passkey_userId_idx` ON `passkey` (`user_id`);--> statement-breakpoint
|
|
197
|
+
CREATE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`);--> statement-breakpoint
|
|
198
|
+
CREATE TABLE `session` (
|
|
199
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
200
|
+
`expires_at` integer NOT NULL,
|
|
201
|
+
`token` text NOT NULL,
|
|
202
|
+
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
203
|
+
`updated_at` integer NOT NULL,
|
|
204
|
+
`ip_address` text,
|
|
205
|
+
`user_agent` text,
|
|
206
|
+
`user_id` text NOT NULL,
|
|
207
|
+
`impersonated_by` text,
|
|
208
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
209
|
+
);
|
|
210
|
+
--> statement-breakpoint
|
|
211
|
+
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
|
|
212
|
+
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
|
|
213
|
+
CREATE TABLE `two_factor` (
|
|
214
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
215
|
+
`secret` text NOT NULL,
|
|
216
|
+
`backup_codes` text NOT NULL,
|
|
217
|
+
`user_id` text NOT NULL,
|
|
218
|
+
`verified` integer DEFAULT true,
|
|
219
|
+
`failed_verification_count` integer DEFAULT 0,
|
|
220
|
+
`locked_until` integer,
|
|
221
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
222
|
+
);
|
|
223
|
+
--> statement-breakpoint
|
|
224
|
+
CREATE INDEX `twoFactor_secret_idx` ON `two_factor` (`secret`);--> statement-breakpoint
|
|
225
|
+
CREATE INDEX `twoFactor_userId_idx` ON `two_factor` (`user_id`);--> statement-breakpoint
|
|
226
|
+
CREATE TABLE `user` (
|
|
227
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
228
|
+
`name` text NOT NULL,
|
|
229
|
+
`email` text NOT NULL,
|
|
230
|
+
`email_verified` integer DEFAULT false NOT NULL,
|
|
231
|
+
`image` text,
|
|
232
|
+
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
233
|
+
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
234
|
+
`role` text,
|
|
235
|
+
`banned` integer DEFAULT false,
|
|
236
|
+
`ban_reason` text,
|
|
237
|
+
`ban_expires` integer,
|
|
238
|
+
`two_factor_enabled` integer DEFAULT false,
|
|
239
|
+
`tokens_revoked_at` integer,
|
|
240
|
+
`locale` text
|
|
241
|
+
);
|
|
242
|
+
--> statement-breakpoint
|
|
243
|
+
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
|
|
244
|
+
CREATE TABLE `verification` (
|
|
245
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
246
|
+
`identifier` text NOT NULL,
|
|
247
|
+
`value` text NOT NULL,
|
|
248
|
+
`expires_at` integer NOT NULL,
|
|
249
|
+
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
250
|
+
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
|
251
|
+
);
|
|
252
|
+
--> statement-breakpoint
|
|
253
|
+
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);--> statement-breakpoint
|
|
254
|
+
CREATE TABLE `admin_action` (
|
|
255
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
256
|
+
`actor_id` text,
|
|
257
|
+
`action` text NOT NULL,
|
|
258
|
+
`target_type` text,
|
|
259
|
+
`target_id` text,
|
|
260
|
+
`detail` text,
|
|
261
|
+
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
|
|
262
|
+
FOREIGN KEY (`actor_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null
|
|
263
|
+
);
|
|
264
|
+
--> statement-breakpoint
|
|
265
|
+
CREATE INDEX `admin_action_created_idx` ON `admin_action` (`created_at`);--> statement-breakpoint
|
|
266
|
+
CREATE TABLE `purchase` (
|
|
267
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
268
|
+
`user_id` text NOT NULL,
|
|
269
|
+
`provider` text NOT NULL,
|
|
270
|
+
`external_id` text NOT NULL,
|
|
271
|
+
`amount` text NOT NULL,
|
|
272
|
+
`currency` text DEFAULT 'usd' NOT NULL,
|
|
273
|
+
`status` text DEFAULT 'pending' NOT NULL,
|
|
274
|
+
`metadata` text,
|
|
275
|
+
`paid_at` integer,
|
|
276
|
+
`fulfillment_key` text,
|
|
277
|
+
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
|
|
278
|
+
`updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
|
|
279
|
+
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
|
280
|
+
);
|
|
281
|
+
--> statement-breakpoint
|
|
282
|
+
CREATE UNIQUE INDEX `purchase_provider_external_uq` ON `purchase` (`provider`,`external_id`);--> statement-breakpoint
|
|
283
|
+
CREATE INDEX `purchase_userId_idx` ON `purchase` (`user_id`);--> statement-breakpoint
|
|
284
|
+
CREATE TABLE `purchase_delivery` (
|
|
285
|
+
`purchase_id` text PRIMARY KEY NOT NULL,
|
|
286
|
+
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
|
|
287
|
+
`processed_at` integer,
|
|
288
|
+
FOREIGN KEY (`purchase_id`) REFERENCES `purchase`(`id`) ON UPDATE no action ON DELETE cascade
|
|
289
|
+
);
|
|
290
|
+
--> statement-breakpoint
|
|
291
|
+
CREATE TABLE `system_config` (
|
|
292
|
+
`key` text PRIMARY KEY NOT NULL,
|
|
293
|
+
`value` text NOT NULL,
|
|
294
|
+
`updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL
|
|
295
|
+
);
|