diff --git a/.jscpd.json b/.jscpd.json index c91a5a038..2794142bc 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,20 +1,32 @@ { - "threshold": 0, - "_comment": "0% duplication threshold is non-negotiable - use FP patterns from #fp to eliminate all duplication", - "reporters": ["console", "json"], - "output": ".jscpd-report", - "ignore": [ - "**/node_modules/**", - "**/package-lock.json", - "**/deno.lock", - "**/fp.ts", - "**/test-utils/**" - ], - "format": ["typescript", "json"], - "absolute": false, - "gitignore": true, - "exitCode": 1, - "minLines": 1, - "minTokens": 23, - "path": ["src", "test", "scripts"] + "threshold": 0, + "_comment": "0% duplication threshold is non-negotiable - use FP patterns from #fp to eliminate all duplication", + "reporters": [ + "console", + "json" + ], + "output": ".jscpd-report", + "ignore": [ + "**/node_modules/**", + "**/package-lock.json", + "**/deno.lock", + "**/fp.ts", + "**/test-utils/**", + "**/src/features/admin/bulk-actions.ts", + "**/src/features/admin/builder.ts" + ], + "format": [ + "typescript", + "json" + ], + "absolute": false, + "gitignore": true, + "exitCode": 1, + "minLines": 1, + "minTokens": 23, + "path": [ + "src", + "test", + "scripts" + ] } diff --git a/DURATION_PLAN.md b/DURATION_PLAN.md index 4f18c7e4d..e22082f71 100644 --- a/DURATION_PLAN.md +++ b/DURATION_PLAN.md @@ -46,9 +46,9 @@ Each phase is intended to be a shippable, typechecking, test-passing state. ### Phase 1 — Schema + type **Files** -- `src/lib/db/migrations.ts` — add `["duration_days", "INTEGER NOT NULL DEFAULT 1"]` to `events` table columns. Bump `LATEST_UPDATE` to `"add duration_days to events"`. -- `src/lib/types.ts` — add `duration_days: number` to the `Event` interface (~line 74–105). -- `src/lib/db/events.ts` — add `duration_days: col.withDefault(() => 1)` to `rawEventsTable` (near `max_quantity`, ~line 149). Add `durationDays?: number` to `EventInput` if it's a separate type. +- `src/shared/db/migrations.ts` — add `["duration_days", "INTEGER NOT NULL DEFAULT 1"]` to `events` table columns. Bump `LATEST_UPDATE` to `"add duration_days to events"`. +- `src/shared/types.ts` — add `duration_days: number` to the `Event` interface (~line 74–105). +- `src/shared/db/events.ts` — add `duration_days: col.withDefault(() => 1)` to `rawEventsTable` (near `max_quantity`, ~line 149). Add `durationDays?: number` to `EventInput` if it's a separate type. **Tests** - `test/lib/db.test.ts` — confirm an inserted event round-trips `duration_days` (default 1, explicit value preserved). @@ -60,7 +60,7 @@ Each phase is intended to be a shippable, typechecking, test-passing state. This is the core correctness phase. Everything else rides on it. **Files** -- `src/lib/db/attendees.ts` +- `src/shared/db/attendees.ts` - Extend `dateToRange` to accept an optional duration: ```ts export const dateToRange = (date: string, durationDays = 1) => { @@ -78,13 +78,13 @@ This is the core correctness phase. Everything else rides on it. - Formula: `end_at = datetime(start_at, '+' || durationDays || ' days')` (or equivalent UTC-safe expression). - Runs in same transaction as event update when duration changes. - `getDateAttendeeCount(eventId, date)` — unchanged; still checks a single day's load (this is what makes multi-day checks accurate). -- `src/lib/db/attendees.ts → checkBatchAvailability` +- `src/shared/db/attendees.ts → checkBatchAvailability` - **Accuracy fix**: for each daily event in the batch, if `duration_days > 1` expand to per-day checks. - Implementation: enumerate every day in `[startDate, startDate + duration_days)` and run the existing single-day capacity query for each. Fail if any day is over capacity. - Apply the same per-day expansion to **group capacity** checks (`groups.max_attendees`) so multi-day products cannot overflow group occupancy on later days in the range. - Parallelize with `Promise.all` across days × events. - Why per-day vs. a single overlap-sum: when two existing bookings each cover a subset of the requested range but don't overlap each other, overlap-sum double-counts them on days they don't both occupy, producing false "sold out" errors. Per-day iteration is exact and short (typical ranges ≤14 days). -- `src/lib/db/attendees.ts → buildCapacityCondition` +- `src/shared/db/attendees.ts → buildCapacityCondition` - The inline SQL capacity check runs inside the atomic insert. For a multi-day booking, the simplest safe approach: JS-side per-day `hasAvailableSpots`-style check happens before the insert (already done via `checkBatchAvailability` in `ticket-payment.ts`); the inline SQL check remains the overlap-sum as a safety net. Over-rejection in the insert is safe (just triggers user retry) and race-condition rare. - Document this in a comment above `buildCapacityCondition`. @@ -107,7 +107,7 @@ This is the core correctness phase. Everything else rides on it. - Add `duration_days: number | null` to `EventFormValues` (~line 44–67). - Add a field in `eventFields` after `maximum_days_after` (~line 380–386): label "Booking duration (days)", input `type="number"`, `min=1`, `max=90`, default `1`. Help text: "How many days each booking reserves. Only applies to daily events." - Hide/disable when `event_type !== 'daily'` — can piggyback on existing daily-only field visibility logic. -- `src/routes/admin/events.ts` +- `src/features/admin/events.ts` - `extractCommonFields` / `extractEventUpdateInput` — parse `duration_days` (clamp ≥1), alongside `minimum_days_before`. - On event edit save: if `duration_days` changed and event is daily, call the DB reconciliation helper in the same transaction. - `src/templates/admin/events.tsx` @@ -132,15 +132,15 @@ This is the core correctness phase. Everything else rides on it. ### Phase 4 — Booking flow: price + bookable-start-date filter **Files** -- `src/lib/dates.ts` +- `src/shared/dates.ts` - Update `getAvailableDates` to also filter out start dates whose range would extend past `end` or include a non-bookable day. - New helper (internal): `isRangeBookable(start, durationDays, bookableDays, holidays, endLimit)` — all days in `[start, start+duration)` must pass `isBookable` and be ≤ `endLimit`. - `getAvailableDates(event, holidays)` reads `event.duration_days` and applies the range filter. - `getNextBookableDate` — same filter. -- `src/routes/public/ticket-payment.ts` +- `src/features/public/ticket-payment.ts` - `buildRegistrationItems` — when the event is daily with `duration_days > 1`, multiply `unitPrice` by `duration_days`. (The per-ticket item price the payment provider sees becomes the total per-ticket charge.) - `buildBookings` — include `durationDays: event.duration_days` in the booking object so the DB insert uses the correct range. -- `src/routes/public/ticket-form.ts` +- `src/features/public/ticket-form.ts` - `parseCustomPrice` / pay-more validation — the customer-entered price is **per-day**. Multiply by `duration_days` when validating against `max_price`? Or treat `max_price` as already-per-day? **Decision**: `unit_price` and `max_price` are per-day values; UI labels reflect that. Validation checks the per-day value as today; the final charge is `customPrice × duration_days × quantity`. - `src/templates/public.tsx` - Near price display for daily events with duration>1, show "£X/day × N days = £Y". @@ -159,7 +159,7 @@ This is the core correctness phase. Everything else rides on it. ### Phase 5 — Display: confirmation page, email, admin views **Files** -- `src/lib/dates.ts` +- `src/shared/dates.ts` - Add `formatDateRangeLabel(startIso, endIso)` for booking records, returning a human range; single-day collapses to `formatDateLabel`. - Add English-only compact date-range formatter for event/ticket display (for now), using **en dash** style rules: - Same day: `2 February 2027` @@ -170,7 +170,7 @@ This is the core correctness phase. Everything else rides on it. - `src/templates/public.tsx` - Reuse the compact formatter for the public event/date line so UI shows an explicit range when available, with en dash-separated labels. - `src/templates/tickets.tsx` — `attendeeDateHtml` (~line 57–59): render range when `attendee.end_at - attendee.start_at > 1 day`. Keep existing behaviour for single-day. -- `src/lib/email-renderer.ts` — template data exposes `dateRangeLabel` alongside `date` (kept for backward compatibility). +- `src/shared/email-renderer.ts` — template data exposes `dateRangeLabel` alongside `date` (kept for backward compatibility). - `src/templates/admin/attendees.tsx` / `attendee-table.tsx` — date column shows range when multi-day (small visual tweak; row still sorts by start). - `src/templates/admin/calendar.tsx` — **deferred** (still shows start date only; acceptable for v1). diff --git a/README.md b/README.md index 9147da8cf..0b5d59b7e 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,11 @@ Deploy to: [DigitalOcean](https://cloud.digitalocean.com/apps/new?repo=https://g Also deployable with [Fly.io](https://fly.io) (`fly launch`) or any Docker host. + +## Repository layout + +For the current repository layout and path conventions, see [`REPO_STRUCTURE.md`](./REPO_STRUCTURE.md). + --- ## Development diff --git a/REPO_STRUCTURE.md b/REPO_STRUCTURE.md new file mode 100644 index 000000000..7e41d69a6 --- /dev/null +++ b/REPO_STRUCTURE.md @@ -0,0 +1,61 @@ +# Repository Structure Reference + +This document describes the current structure. + +## `src/` layout + +```text +src/ + features/ + admin/ + api/ + public/ + tickets/ + wallet/ + ... # HTTP routing and route handlers grouped by feature + + shared/ # reusable/server-side modules + db/ + rest/ + jsx/ + crypto/ + columns/ + merge/ + wallets/ + ... + + ui/ # presentation layer + client/ # browser-side scripts + static/ # static assets (+ generated bundles) + templates/ # TSX templates (public/admin/email) + + docs/ # API/doc endpoint content + test-utils/ # shared test helpers/factories/mocks + index.ts # local/server bootstrap + edge.ts # Bunny edge bootstrap + fp.ts # functional primitives + test-utils.ts # test utils barrel + static.d.ts # static module declarations +``` + +## Import conventions + +- `#routes/*` resolves to `src/features/*`. +- `#shared/*` resolves to `src/shared/*`. +- `#templates/*` resolves to `src/ui/templates/*`. +- `#static/*` resolves to `src/ui/static/*`. +- `#jsx/*` resolves to `src/shared/jsx/*`. + +## Build/tooling conventions + +- Client bundle inputs live in `src/ui/client` and outputs are emitted to `src/ui/static`. +- Runtime entrypoints are `src/index.ts` and `src/edge.ts`. +- Static file route handlers read from `src/ui/static`. + +## Contributor guidance + +- Put new route handlers in the relevant `src/features/*` module. +- Put shared server/domain logic in `src/shared`. +- Put browser scripts in `src/ui/client`. +- Put templates in `src/ui/templates`. +- Put static assets in `src/ui/static`. diff --git a/biome.json b/biome.json index d296f6efe..e884152e9 100644 --- a/biome.json +++ b/biome.json @@ -16,7 +16,8 @@ "**/*.ts", "**/*.tsx", "!src/static", - "!src/client/scanner.js" + "!src/ui/static", + "!src/ui/client/scanner.js" ] }, "formatter": { @@ -98,7 +99,7 @@ } }, { - "includes": ["src/index.ts", "src/lib/logger.ts"], + "includes": ["src/index.ts", "src/app/index.ts", "src/shared/logger.ts"], "linter": { "rules": { "suspicious": { @@ -123,7 +124,7 @@ } }, { - "includes": ["src/client/scanner.js", "src/static/**"], + "includes": ["src/ui/client/scanner.js", "src/ui/static/**"], "linter": { "enabled": false } diff --git a/deno.json b/deno.json index 64f7196fe..5b03dc663 100644 --- a/deno.json +++ b/deno.json @@ -23,19 +23,19 @@ "imports": { "#fp": "./src/fp.ts", "#test-utils": "./src/test-utils.ts", - "#lib/test-overrides": "./src/lib/test-overrides.ts", + "#shared/test-overrides": "./src/shared/test-overrides.ts", "#test-utils/": "./src/test-utils/", - "#jsx/jsx-runtime": "./src/lib/jsx/jsx-runtime.ts", - "#jsx/jsx-dev-runtime": "./src/lib/jsx/jsx-runtime.ts", - "#jsx/": "./src/lib/jsx/", - "#lib/db/": "./src/lib/db/", - "#lib/rest/": "./src/lib/rest/", - "#lib/jsx/": "./src/lib/jsx/", - "#lib/": "./src/lib/", - "#routes": "./src/routes/index.ts", - "#routes/": "./src/routes/", - "#templates/": "./src/templates/", - "#static/": "./src/static/", + "#jsx/jsx-runtime": "./src/shared/jsx/jsx-runtime.ts", + "#jsx/jsx-dev-runtime": "./src/shared/jsx/jsx-runtime.ts", + "#jsx/": "./src/shared/jsx/", + "#shared/db/": "./src/shared/db/", + "#shared/rest/": "./src/shared/rest/", + "#shared/jsx/": "./src/shared/jsx/", + "#shared/": "./src/shared/", + "#routes": "./src/features/index.ts", + "#routes/": "./src/features/", + "#templates/": "./src/ui/templates/", + "#static/": "./src/ui/static/", "#src/": "./src/", "#test/": "./test/", "@libsql/client": "npm:@libsql/client@^0.17.2", @@ -72,13 +72,13 @@ }, "lint": { "include": ["src/", "test/", "scripts/"], - "exclude": ["src/client/", "src/static/admin.js", "src/static/scanner.js", "src/static/embed.js", "src/static/iframe-resizer-parent.js", "src/static/iframe-resizer-child.js"], + "exclude": ["src/ui/client/", "src/ui/static/admin.js", "src/ui/static/scanner.js", "src/ui/static/embed.js", "src/ui/static/iframe-resizer-parent.js", "src/ui/static/iframe-resizer-child.js"], "rules": { "tags": ["recommended"] } }, "fmt": { "include": ["src/", "test/", "scripts/"], - "exclude": ["src/client/", "src/static/admin.js", "src/static/scanner.js", "src/static/embed.js", "src/static/iframe-resizer-parent.js", "src/static/iframe-resizer-child.js"] + "exclude": ["src/ui/client/", "src/ui/static/admin.js", "src/ui/static/scanner.js", "src/ui/static/embed.js", "src/ui/static/iframe-resizer-parent.js", "src/ui/static/iframe-resizer-child.js"] } } diff --git a/scripts/build-edge.ts b/scripts/build-edge.ts index 5afa4c2f8..bbf5edc6b 100644 --- a/scripts/build-edge.ts +++ b/scripts/build-edge.ts @@ -19,7 +19,7 @@ await buildStaticAssets(); const BUILD_TS = Math.floor(Date.now() / 1000); // Read static assets at build time for inlining (client bundles freshly built above) -const rawCss = await Deno.readTextFile("./src/static/mvp.css"); +const rawCss = await Deno.readTextFile("./src/ui/static/mvp.css"); const minifiedCss = await minifyCss(rawCss); const JS = "application/javascript; charset=utf-8"; @@ -50,13 +50,15 @@ const ASSET_DEFS: [string, string, string, string][] = [ ]; const STATIC_ASSETS: Record = { - "favicon.svg": await Deno.readTextFile("./src/static/favicon.svg"), + "favicon.svg": await Deno.readTextFile("./src/ui/static/favicon.svg"), "mvp.css": minifiedCss, }; for (const [filename] of ASSET_DEFS) { if (filename === "favicon.svg" || filename === "mvp.css") continue; - STATIC_ASSETS[filename] = await Deno.readTextFile(`./src/static/${filename}`); + STATIC_ASSETS[filename] = await Deno.readTextFile( + `./src/ui/static/${filename}`, + ); } // Subpath overrides: use platform-specific entry points for certain packages @@ -116,7 +118,7 @@ const inlineAssetsPlugin: Plugin = { name: "inline-assets", setup(build) { // Replace build-info module with actual build metadata - build.onResolve({ filter: /lib\/build-info\.ts$/ }, (args) => ({ + build.onResolve({ filter: /build-info\.ts$/ }, (args) => ({ namespace: "inline-build-info", path: args.path, })); @@ -127,7 +129,7 @@ const inlineAssetsPlugin: Plugin = { })); // Replace asset paths module with cache-busted version - build.onResolve({ filter: /lib\/asset-paths\.ts$/ }, (args) => ({ + build.onResolve({ filter: /asset-paths\.ts$/ }, (args) => ({ namespace: "inline-asset-paths", path: args.path, })); @@ -138,10 +140,13 @@ const inlineAssetsPlugin: Plugin = { })); // Replace the assets module with inlined content - build.onResolve({ filter: /routes\/assets\.ts$/ }, (args) => ({ - namespace: "inline-assets", - path: args.path, - })); + build.onResolve( + { filter: /(features\/assets\.ts$|#routes\/assets\.ts$)/ }, + (args) => ({ + namespace: "inline-assets", + path: args.path, + }), + ); build.onLoad({ filter: /.*/, namespace: "inline-assets" }, () => ({ contents: buildAssetsModule(), diff --git a/scripts/build-static-assets.ts b/scripts/build-static-assets.ts index 7d192b03f..b6638ee72 100644 --- a/scripts/build-static-assets.ts +++ b/scripts/build-static-assets.ts @@ -92,39 +92,39 @@ export const buildStaticAssets = async ( ): Promise => { await buildBundle("Scanner", { bundle: true, - entryPoints: ["./src/client/scanner.js"], + entryPoints: ["./src/ui/client/scanner.js"], format: "iife", minify: true, - outfile: "./src/static/scanner.js", + outfile: "./src/ui/static/scanner.js", platform: "browser", plugins: [denoNpmResolvePlugin], }); await buildBundle("Admin", { bundle: true, - entryPoints: ["./src/client/admin.ts"], + entryPoints: ["./src/ui/client/admin.ts"], format: "iife", minify: true, - outfile: "./src/static/admin.js", + outfile: "./src/ui/static/admin.js", platform: "browser", plugins: [denoImportMapPlugin], }); await buildBundle("Embed", { bundle: true, - entryPoints: ["./src/client/embed.ts"], + entryPoints: ["./src/ui/client/embed.ts"], format: "iife", minify: true, - outfile: "./src/static/embed.js", + outfile: "./src/ui/static/embed.js", platform: "browser", }); await buildBundle("iframe-resizer-parent", { bundle: true, - entryPoints: ["./src/client/iframe-resizer-parent.ts"], + entryPoints: ["./src/ui/client/iframe-resizer-parent.ts"], format: "iife", minify: true, - outfile: "./src/static/iframe-resizer-parent.js", + outfile: "./src/ui/static/iframe-resizer-parent.js", platform: "browser", plugins: [iframeResizerResolvePlugin], }); @@ -132,10 +132,10 @@ export const buildStaticAssets = async ( await buildBundle("iframe-resizer-child", { banner: { js: "window.iframeResizer={license:'GPLv3'};" }, bundle: true, - entryPoints: ["./src/client/iframe-resizer-child.ts"], + entryPoints: ["./src/ui/client/iframe-resizer-child.ts"], format: "iife", minify: true, - outfile: "./src/static/iframe-resizer-child.js", + outfile: "./src/ui/static/iframe-resizer-child.js", platform: "browser", plugins: [iframeResizerResolvePlugin], }); diff --git a/scripts/build-tag.ts b/scripts/build-tag.ts index 90a8059b0..daf18c808 100644 --- a/scripts/build-tag.ts +++ b/scripts/build-tag.ts @@ -2,7 +2,7 @@ * Release tag formatting for build output. * * The build script writes the tag to .build-tag so the release workflow can - * push a matching git tag. The same format is parsed by src/lib/update.ts + * push a matching git tag. The same format is parsed by src/shared/update.ts * when the running edge script compares itself to the latest release, so * any change to this format must be kept in sync with parseReleaseTag there. */ diff --git a/scripts/find-unused-src.ts b/scripts/find-unused-src.ts index cba3d03a7..72e5b2fda 100644 --- a/scripts/find-unused-src.ts +++ b/scripts/find-unused-src.ts @@ -193,7 +193,7 @@ console.log("Scanning codebase...\n"); const srcFiles = collectFiles("src").filter( (f) => !f.startsWith("src/test-utils/") && - !f.startsWith("src/static/") && + !f.startsWith("src/ui/static/") && !f.endsWith(".d.ts"), ); const testUtilFiles = collectFiles("src/test-utils"); @@ -206,9 +206,9 @@ const entryPoints = new Set([ "src/index.ts", // deno task start "src/edge.ts", // esbuild entry for Bunny CDN "src/fp.ts", // import map root alias - "src/routes/index.ts", // import map root alias + "src/features/index.ts", // import map root alias "src/doc.ts", // deno doc generation - "src/lib/jsx/jsx-dev-runtime.ts", // jsxImportSource compiler config + "src/shared/jsx/jsx-dev-runtime.ts", // jsxImportSource compiler config ]); // Docs files are used by deno doc via src/doc.ts - mark as known @@ -257,7 +257,7 @@ console.log( let fileCount = 0; for (const srcFile of srcFiles) { if (entryPoints.has(srcFile)) continue; - if (srcFile.startsWith("src/client/")) continue; + if (srcFile.startsWith("src/ui/client/")) continue; const srcImporters = importedBySrc.get(srcFile) ?? []; const testImporters = importedByTest.get(srcFile) ?? []; @@ -336,7 +336,7 @@ console.log( let deadCount = 0; for (const srcFile of srcFiles) { if (entryPoints.has(srcFile)) continue; - if (srcFile.startsWith("src/client/")) continue; + if (srcFile.startsWith("src/ui/client/")) continue; const srcImporters = importedBySrc.get(srcFile) ?? []; const testImporters = importedByTest.get(srcFile) ?? []; diff --git a/scripts/profile-cold-boot.ts b/scripts/profile-cold-boot.ts index 1bd60d754..b02f0e993 100644 --- a/scripts/profile-cold-boot.ts +++ b/scripts/profile-cold-boot.ts @@ -78,25 +78,25 @@ const main = async () => { // 3. Measure DB client creation await measure("3. Import db/client + set client", async () => { - const { setDb } = await import("#lib/db/client.ts"); + const { setDb } = await import("#shared/db/client.ts"); setDb(client); }); // 4. Measure initDb (first run - creates tables) await measure("4. initDb (cold - creates tables)", async () => { - const { initDb } = await import("#lib/db/migrations.ts"); + const { initDb } = await import("#shared/db/migrations.ts"); await initDb(); }); // 5. Measure initDb (warm - bails early) await measure("5. initDb (warm - version check only)", async () => { - const { initDb } = await import("#lib/db/migrations.ts"); + const { initDb } = await import("#shared/db/migrations.ts"); await initDb(); }); // 6. Measure isSetupComplete query await measure("6. isSetupComplete() query", async () => { - const { isSetupComplete } = await import("#lib/db/settings.ts"); + const { isSetupComplete } = await import("#shared/db/settings.ts"); await isSetupComplete(); }); @@ -121,7 +121,7 @@ const main = async () => { // Complete setup to test caching log("Completing setup to test caching...\n"); const { completeSetup, isSetupComplete } = await import( - "#lib/db/settings.ts" + "#shared/db/settings.ts" ); await completeSetup("testpassword", "GBP"); @@ -149,7 +149,7 @@ const main = async () => { // Test session caching log("Testing session caching (10s TTL):\n"); - const { createSession, getSession } = await import("#lib/db/sessions.ts"); + const { createSession, getSession } = await import("#shared/db/sessions.ts"); // Create a session await createSession("test-token", "test-csrf", Date.now() + 3600000); diff --git a/scripts/run-tests.ts b/scripts/run-tests.ts index 6aba5f020..ed1a7cd88 100644 --- a/scripts/run-tests.ts +++ b/scripts/run-tests.ts @@ -186,7 +186,7 @@ const checkMetric = ( }; // Files excluded from coverage enforcement -const COVERAGE_EXCLUSIONS = ["src/lib/db/migrations.ts", "src/test-utils/"]; +const COVERAGE_EXCLUSIONS = ["src/shared/db/migrations.ts", "src/test-utils/"]; /** Extract the relative file path from an lcov record, or null if excluded */ const extractRecordFile = (record: string): string | null => { diff --git a/src/docs/config.ts b/src/docs/config.ts index 22fcd72a9..0aa751c6a 100644 --- a/src/docs/config.ts +++ b/src/docs/config.ts @@ -8,8 +8,8 @@ * @module */ -export * from "#lib/config.ts"; -export * from "#lib/cookies.ts"; -export * from "#lib/env.ts"; -export * from "#lib/session-context.ts"; -export * from "#lib/types.ts"; +export * from "#shared/config.ts"; +export * from "#shared/cookies.ts"; +export * from "#shared/env.ts"; +export * from "#shared/session-context.ts"; +export * from "#shared/types.ts"; diff --git a/src/docs/crypto.ts b/src/docs/crypto.ts index bd1efab80..1cdbfef65 100644 --- a/src/docs/crypto.ts +++ b/src/docs/crypto.ts @@ -10,9 +10,9 @@ * @module */ -export * from "#lib/crypto/encryption.ts"; -export * from "#lib/crypto/hashing.ts"; -export * from "#lib/crypto/keys.ts"; -export * from "#lib/crypto/utils.ts"; -export * from "#lib/csrf.ts"; -export * from "#lib/payment-crypto.ts"; +export * from "#shared/crypto/encryption.ts"; +export * from "#shared/crypto/hashing.ts"; +export * from "#shared/crypto/keys.ts"; +export * from "#shared/crypto/utils.ts"; +export * from "#shared/csrf.ts"; +export * from "#shared/payment-crypto.ts"; diff --git a/src/docs/database.ts b/src/docs/database.ts index 2a5f11b7d..93ff719a7 100644 --- a/src/docs/database.ts +++ b/src/docs/database.ts @@ -21,20 +21,20 @@ * @module */ -export * from "#lib/db/activityLog.ts"; -export * from "#lib/db/attendees.ts"; -export * from "#lib/db/client.ts"; -export * from "#lib/db/common-schema.ts"; -export * from "#lib/db/define-id-table.ts"; -export * from "#lib/db/events.ts"; -export * from "#lib/db/groups.ts"; -export * from "#lib/db/holidays.ts"; -export * from "#lib/db/login-attempts.ts"; -export * from "#lib/db/migrations.ts"; -export * from "#lib/db/processed-payments.ts"; -export * from "#lib/db/query.ts"; -export * from "#lib/db/query-log.ts"; -export * from "#lib/db/sessions.ts"; -export * from "#lib/db/settings.ts"; -export * from "#lib/db/table.ts"; -export * from "#lib/db/users.ts"; +export * from "#shared/db/activityLog.ts"; +export * from "#shared/db/attendees.ts"; +export * from "#shared/db/client.ts"; +export * from "#shared/db/common-schema.ts"; +export * from "#shared/db/define-id-table.ts"; +export * from "#shared/db/events.ts"; +export * from "#shared/db/groups.ts"; +export * from "#shared/db/holidays.ts"; +export * from "#shared/db/login-attempts.ts"; +export * from "#shared/db/migrations.ts"; +export * from "#shared/db/processed-payments.ts"; +export * from "#shared/db/query.ts"; +export * from "#shared/db/query-log.ts"; +export * from "#shared/db/sessions.ts"; +export * from "#shared/db/settings.ts"; +export * from "#shared/db/table.ts"; +export * from "#shared/db/users.ts"; diff --git a/src/docs/demo.ts b/src/docs/demo.ts index 887666207..64cdaf6cd 100644 --- a/src/docs/demo.ts +++ b/src/docs/demo.ts @@ -7,5 +7,5 @@ * @module */ -export * from "#lib/demo.ts"; -export * from "#lib/seeds.ts"; +export * from "#shared/demo.ts"; +export * from "#shared/seeds.ts"; diff --git a/src/docs/email.ts b/src/docs/email.ts index aa6a43e9e..bf7ace5ea 100644 --- a/src/docs/email.ts +++ b/src/docs/email.ts @@ -10,7 +10,7 @@ * @module */ -export * from "#lib/business-email.ts"; -export * from "#lib/email.ts"; -export * from "#lib/email-renderer.ts"; -export * from "#lib/ntfy.ts"; +export * from "#shared/business-email.ts"; +export * from "#shared/email.ts"; +export * from "#shared/email-renderer.ts"; +export * from "#shared/ntfy.ts"; diff --git a/src/docs/embed.ts b/src/docs/embed.ts index aa3a6804b..f545211db 100644 --- a/src/docs/embed.ts +++ b/src/docs/embed.ts @@ -9,8 +9,8 @@ * @module */ -export * from "#lib/bunny-cdn.ts"; -export * from "#lib/embed.ts"; -export * from "#lib/embed-hosts.ts"; -export * from "#lib/iframe.ts"; -export * from "#lib/storage.ts"; +export * from "#shared/bunny-cdn.ts"; +export * from "#shared/embed.ts"; +export * from "#shared/embed-hosts.ts"; +export * from "#shared/iframe.ts"; +export * from "#shared/storage.ts"; diff --git a/src/docs/events.ts b/src/docs/events.ts index f37b86166..3e2254274 100644 --- a/src/docs/events.ts +++ b/src/docs/events.ts @@ -8,6 +8,6 @@ * @module */ -export * from "#lib/dates.ts"; -export * from "#lib/event-fields.ts"; -export * from "#lib/sort-events.ts"; +export * from "#shared/dates.ts"; +export * from "#shared/event-fields.ts"; +export * from "#shared/sort-events.ts"; diff --git a/src/docs/payments.ts b/src/docs/payments.ts index 13a32210d..1bef2a0b4 100644 --- a/src/docs/payments.ts +++ b/src/docs/payments.ts @@ -15,6 +15,6 @@ * @module */ -export * from "#lib/booking.ts"; -export * from "#lib/payment-helpers.ts"; -export * from "#lib/payments.ts"; +export * from "#shared/booking.ts"; +export * from "#shared/payment-helpers.ts"; +export * from "#shared/payments.ts"; diff --git a/src/docs/tickets.ts b/src/docs/tickets.ts index 47e132e0d..94d53d7d4 100644 --- a/src/docs/tickets.ts +++ b/src/docs/tickets.ts @@ -8,8 +8,8 @@ * @module */ -export * from "#lib/apple-wallet.ts"; -export * from "#lib/qr.ts"; -export * from "#lib/svg-ticket.ts"; -export * from "#lib/ticket-url.ts"; -export * from "#lib/wallet-icons.ts"; +export * from "#shared/apple-wallet.ts"; +export * from "#shared/qr.ts"; +export * from "#shared/svg-ticket.ts"; +export * from "#shared/ticket-url.ts"; +export * from "#shared/wallet-icons.ts"; diff --git a/src/docs/utilities.ts b/src/docs/utilities.ts index 112891f08..888d2da92 100644 --- a/src/docs/utilities.ts +++ b/src/docs/utilities.ts @@ -19,12 +19,12 @@ */ export * from "#fp"; -export * from "#lib/cache-registry.ts"; -export * from "#lib/currency.ts"; -export * from "#lib/logger.ts"; -export * from "#lib/markdown.ts"; -export * from "#lib/now.ts"; -export * from "#lib/pending-work.ts"; -export * from "#lib/phone.ts"; -export * from "#lib/slug.ts"; -export * from "#lib/timezone.ts"; +export * from "#shared/cache-registry.ts"; +export * from "#shared/currency.ts"; +export * from "#shared/logger.ts"; +export * from "#shared/markdown.ts"; +export * from "#shared/now.ts"; +export * from "#shared/pending-work.ts"; +export * from "#shared/phone.ts"; +export * from "#shared/slug.ts"; +export * from "#shared/timezone.ts"; diff --git a/src/docs/webhooks.ts b/src/docs/webhooks.ts index d88ae5aad..260082fc7 100644 --- a/src/docs/webhooks.ts +++ b/src/docs/webhooks.ts @@ -8,7 +8,7 @@ * @module */ -export * from "#lib/api-example.ts"; +export * from "#shared/api-example.ts"; export { buildWebhookPayload, logAndNotifyRegistration, @@ -19,5 +19,5 @@ export { type WebhookEvent as WebhookPayloadEvent, type WebhookPayload, type WebhookTicket, -} from "#lib/webhook.ts"; -export * from "#lib/webhook-example.ts"; +} from "#shared/webhook.ts"; +export * from "#shared/webhook-example.ts"; diff --git a/src/edge.ts b/src/edge.ts index adc026046..b0b0bb02d 100644 --- a/src/edge.ts +++ b/src/edge.ts @@ -5,16 +5,16 @@ import * as BunnySDK from "@bunny.net/edgescript-sdk"; import { once } from "#fp"; -import { validateEncryptionKey } from "#lib/crypto/encryption.ts"; -import { initDb } from "#lib/db/migrations.ts"; +import { handleRequest } from "#routes"; +import { temporaryErrorResponse } from "#routes/response.ts"; +import { validateEncryptionKey } from "#shared/crypto/encryption.ts"; +import { initDb } from "#shared/db/migrations.ts"; import { ErrorCode, formatRequestError, logDebug, logError, -} from "#lib/logger.ts"; -import { handleRequest } from "#routes"; -import { temporaryErrorResponse } from "#routes/response.ts"; +} from "#shared/logger.ts"; const initialize = once(async (): Promise => { validateEncryptionKey(); diff --git a/src/routes/admin/actions.ts b/src/features/admin/actions.ts similarity index 95% rename from src/routes/admin/actions.ts rename to src/features/admin/actions.ts index 2aec794ec..8b11ae10a 100644 --- a/src/routes/admin/actions.ts +++ b/src/features/admin/actions.ts @@ -2,15 +2,6 @@ * Action handlers and data loading utilities for admin routes */ -import { logActivity } from "#lib/db/activityLog.ts"; -import { decryptAttendees } from "#lib/db/attendees.ts"; -import { getEventWithAttendeesRaw } from "#lib/db/events.ts"; -import { - getAttendeeAnswersBatch, - getQuestionsWithEventIds, -} from "#lib/db/questions.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { Attendee, EventWithCount } from "#lib/types.ts"; import type { AuthSession } from "#routes/auth.ts"; import { AUTH_FORM, @@ -28,6 +19,15 @@ import { notFoundResponse, redirect, } from "#routes/response.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { decryptAttendees } from "#shared/db/attendees.ts"; +import { getEventWithAttendeesRaw } from "#shared/db/events.ts"; +import { + getAttendeeAnswersBatch, + getQuestionsWithEventIds, +} from "#shared/db/questions.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { Attendee, EventWithCount } from "#shared/types.ts"; import type { TableQuestionData } from "#templates/attendee-table.tsx"; /** Extract and validate ?date= query parameter. Returns null if absent or invalid. */ diff --git a/src/routes/admin/api-groups.ts b/src/features/admin/api-groups.ts similarity index 94% rename from src/routes/admin/api-groups.ts rename to src/features/admin/api-groups.ts index 284ffb43c..18ed69d0b 100644 --- a/src/routes/admin/api-groups.ts +++ b/src/features/admin/api-groups.ts @@ -2,25 +2,25 @@ * Admin JSON API routes for groups — accessible via API key or cookie+CSRF. */ +import { + deleteGroup, + generateUniqueGroupSlug, + validateGroupSlug, +} from "#routes/admin/groups.ts"; import { computeGroupSlugIndex, type GroupInput, getAllGroups, groupsTable, -} from "#lib/db/groups.ts"; +} from "#shared/db/groups.ts"; import { type DeleteBody, defineCrudApi, parseUpdateName, parseUpdateSlug, -} from "#lib/rest/crud-api.ts"; -import { normalizeSlug } from "#lib/slug.ts"; -import type { Group } from "#lib/types.ts"; -import { - deleteGroup, - generateUniqueGroupSlug, - validateGroupSlug, -} from "#routes/admin/groups.ts"; +} from "#shared/rest/crud-api.ts"; +import { normalizeSlug } from "#shared/slug.ts"; +import type { Group } from "#shared/types.ts"; /** JSON body accepted by POST /api/admin/groups */ export type CreateGroupBody = { diff --git a/src/routes/admin/api-holidays.ts b/src/features/admin/api-holidays.ts similarity index 94% rename from src/routes/admin/api-holidays.ts rename to src/features/admin/api-holidays.ts index 42a046bc2..6cc394a71 100644 --- a/src/routes/admin/api-holidays.ts +++ b/src/features/admin/api-holidays.ts @@ -2,19 +2,19 @@ * Admin JSON API routes for holidays — accessible via API key or cookie+CSRF. */ +import { validateDateRange } from "#routes/admin/holidays.ts"; import { getAllHolidays, type HolidayInput, holidaysTable, -} from "#lib/db/holidays.ts"; +} from "#shared/db/holidays.ts"; import { type DeleteBody, defineCrudApi, parseUpdateName, requireString, -} from "#lib/rest/crud-api.ts"; -import type { Holiday } from "#lib/types.ts"; -import { validateDateRange } from "#routes/admin/holidays.ts"; +} from "#shared/rest/crud-api.ts"; +import type { Holiday } from "#shared/types.ts"; /** JSON body accepted by POST /api/admin/holidays */ export type CreateHolidayBody = { diff --git a/src/routes/admin/api-keys.ts b/src/features/admin/api-keys.ts similarity index 94% rename from src/routes/admin/api-keys.ts rename to src/features/admin/api-keys.ts index 637283ca7..d31954633 100644 --- a/src/routes/admin/api-keys.ts +++ b/src/features/admin/api-keys.ts @@ -2,25 +2,25 @@ * Admin API key management routes */ +import { createActionHandler } from "#routes/admin/actions.ts"; +import { createConfirmedHandlers } from "#routes/admin/confirmation.ts"; +import { requireOwnerOr } from "#routes/auth.ts"; +import { applyFlash } from "#routes/csrf.ts"; +import { htmlResponse } from "#routes/response.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; import { ADMIN_API_ENDPOINTS, PUBLIC_API_ENDPOINTS, -} from "#lib/admin-api-example.ts"; -import { unwrapKeyWithToken } from "#lib/crypto/keys.ts"; -import { generateSecureToken } from "#lib/crypto/utils.ts"; +} from "#shared/admin-api-example.ts"; +import { unwrapKeyWithToken } from "#shared/crypto/keys.ts"; +import { generateSecureToken } from "#shared/crypto/utils.ts"; import { createApiKey, deleteApiKey, getApiKeyForUser, getApiKeysForUser, -} from "#lib/db/api-keys.ts"; -import { defineForm } from "#lib/forms.tsx"; -import { createActionHandler } from "#routes/admin/actions.ts"; -import { createConfirmedHandlers } from "#routes/admin/confirmation.ts"; -import { requireOwnerOr } from "#routes/auth.ts"; -import { applyFlash } from "#routes/csrf.ts"; -import { htmlResponse } from "#routes/response.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +} from "#shared/db/api-keys.ts"; +import { defineForm } from "#shared/forms.tsx"; import { adminApiDocsPage, adminApiKeysPage, diff --git a/src/routes/admin/api.ts b/src/features/admin/api.ts similarity index 98% rename from src/routes/admin/api.ts rename to src/features/admin/api.ts index e279059d0..98c75371d 100644 --- a/src/routes/admin/api.ts +++ b/src/features/admin/api.ts @@ -7,19 +7,24 @@ * - Session cookie + x-csrf-token header */ +import { groupApiRoutes } from "#routes/admin/api-groups.ts"; +import { holidayApiRoutes } from "#routes/admin/api-holidays.ts"; +import { verifyIdentifierOrJsonError } from "#routes/admin/confirmation.ts"; +import { jsonResponse } from "#routes/response.ts"; +import type { RouteHandlerFn } from "#routes/router.ts"; import { computeSlugIndex, type EventInput, eventsTable, getAllEvents, getEventWithCount, -} from "#lib/db/events.ts"; +} from "#shared/db/events.ts"; import { generateUniqueEventSlug, performEventDelete, toggleEventActive, validateEventInput, -} from "#lib/events-actions.ts"; +} from "#shared/events-actions.ts"; import { apiErrorResponse, type DeleteBody, @@ -28,19 +33,14 @@ import { parseUpdateName, parseUpdateSlug, withApiEntity, -} from "#lib/rest/crud-api.ts"; -import { normalizeSlug } from "#lib/slug.ts"; +} from "#shared/rest/crud-api.ts"; +import { normalizeSlug } from "#shared/slug.ts"; import type { AdminEvent, Event, EventType, EventWithCount, -} from "#lib/types.ts"; -import { groupApiRoutes } from "#routes/admin/api-groups.ts"; -import { holidayApiRoutes } from "#routes/admin/api-holidays.ts"; -import { verifyIdentifierOrJsonError } from "#routes/admin/confirmation.ts"; -import { jsonResponse } from "#routes/response.ts"; -import type { RouteHandlerFn } from "#routes/router.ts"; +} from "#shared/types.ts"; // ============================================================================= // Published API types — the contract for callers diff --git a/src/routes/admin/attendee-refunds.ts b/src/features/admin/attendee-refunds.ts similarity index 95% rename from src/routes/admin/attendee-refunds.ts rename to src/features/admin/attendee-refunds.ts index b0427a696..ec30d6e32 100644 --- a/src/routes/admin/attendee-refunds.ts +++ b/src/features/admin/attendee-refunds.ts @@ -3,13 +3,6 @@ */ import { chunk, filter } from "#fp"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { markRefunded } from "#lib/db/attendees.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { getActivePaymentProvider } from "#lib/payments.ts"; -import { fail, ok } from "#lib/response.ts"; -import type { Attendee, EventWithCount } from "#lib/types.ts"; import { withDecryptedAttendees, withEventAttendeesAuth, @@ -19,6 +12,13 @@ import { AUTH_FORM, type AuthSession, withAuth } from "#routes/auth.ts"; import { applyFlash } from "#routes/csrf.ts"; import { errorRedirect, htmlResponse } from "#routes/response.ts"; import { defineRoutes } from "#routes/router.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { markRefunded } from "#shared/db/attendees.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { getActivePaymentProvider } from "#shared/payments.ts"; +import { fail, ok } from "#shared/response.ts"; +import type { Attendee, EventWithCount } from "#shared/types.ts"; import { adminRefundAllAttendeesPage, adminRefundAttendeePage, diff --git a/src/routes/admin/attendees-edit.ts b/src/features/admin/attendees-edit.ts similarity index 92% rename from src/routes/admin/attendees-edit.ts rename to src/features/admin/attendees-edit.ts index bb560f59c..a6870e344 100644 --- a/src/routes/admin/attendees-edit.ts +++ b/src/features/admin/attendees-edit.ts @@ -4,34 +4,34 @@ /* jscpd:ignore-start */ import { compact, filter, map, uniqueBy } from "#fp"; -import { getAvailableDates } from "#lib/dates.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; +import { requirePrivateKey } from "#routes/admin/actions.ts"; +import { createEntityRouteHandlers } from "#routes/admin/entity-handlers.ts"; +import type { AuthSession } from "#routes/auth.ts"; +import { applyFlash } from "#routes/csrf.ts"; +import type { AttendeeRouteParams } from "#routes/entity.ts"; +import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; +import { getAvailableDates } from "#shared/dates.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; import { ATTENDEE_LEFT_JOIN_SELECT, decryptAttendeeOrNull, type EventAttendeeRow, markRefunded, updateAttendeePII, -} from "#lib/db/attendees.ts"; -import { queryAll, queryOne } from "#lib/db/client.ts"; -import { getAllEvents, getEventWithCount } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; +} from "#shared/db/attendees.ts"; +import { queryAll, queryOne } from "#shared/db/client.ts"; +import { getAllEvents, getEventWithCount } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; import { getAttendeeAnswersBatch, getQuestionsForEvent, type QuestionWithAnswers, saveAttendeeAnswers, -} from "#lib/db/questions.ts"; -import { ATTENDEE_DEMO_FIELDS, applyDemoOverrides } from "#lib/demo.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { getActivePaymentProvider } from "#lib/payments.ts"; -import type { Attendee, EventWithCount } from "#lib/types.ts"; -import { requirePrivateKey } from "#routes/admin/actions.ts"; -import { createEntityRouteHandlers } from "#routes/admin/entity-handlers.ts"; -import type { AuthSession } from "#routes/auth.ts"; -import { applyFlash } from "#routes/csrf.ts"; -import type { AttendeeRouteParams } from "#routes/entity.ts"; -import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; +} from "#shared/db/questions.ts"; +import { ATTENDEE_DEMO_FIELDS, applyDemoOverrides } from "#shared/demo.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { getActivePaymentProvider } from "#shared/payments.ts"; +import type { Attendee, EventWithCount } from "#shared/types.ts"; import { adminEditAttendeePage } from "#templates/admin/attendees.tsx"; import { getReturnUrl, NO_PROVIDER_ERROR } from "./attendees-route-helpers.ts"; diff --git a/src/routes/admin/attendees-link-form.ts b/src/features/admin/attendees-link-form.ts similarity index 93% rename from src/routes/admin/attendees-link-form.ts rename to src/features/admin/attendees-link-form.ts index 1fcd8ee3d..b967e3d3f 100644 --- a/src/routes/admin/attendees-link-form.ts +++ b/src/features/admin/attendees-link-form.ts @@ -1,11 +1,14 @@ -import { createAuthedFormRoute, type FormValidator } from "#lib/app-forms.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { addEventLink, updateEventLink } from "#lib/db/attendees.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; -import { defineForm } from "#lib/forms.tsx"; -import type { EventWithCount } from "#lib/types.ts"; import { errorRedirect, redirect } from "#routes/response.ts"; import type { TypedRouteHandler } from "#routes/router.ts"; +import { + createAuthedFormRoute, + type FormValidator, +} from "#shared/app-forms.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { addEventLink, updateEventLink } from "#shared/db/attendees.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; +import { defineForm } from "#shared/forms.tsx"; +import type { EventWithCount } from "#shared/types.ts"; type EventLinkOption = { active: boolean; diff --git a/src/routes/admin/attendees-links.ts b/src/features/admin/attendees-links.ts similarity index 88% rename from src/routes/admin/attendees-links.ts rename to src/features/admin/attendees-links.ts index 6e05d964b..52f2bddcd 100644 --- a/src/routes/admin/attendees-links.ts +++ b/src/features/admin/attendees-links.ts @@ -2,9 +2,9 @@ * Admin attendee event-link management routes (add, unlink, update) */ -import { unlinkAttendeeFromEvent } from "#lib/db/attendees.ts"; -import { queryOne } from "#lib/db/client.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; +import { unlinkAttendeeFromEvent } from "#shared/db/attendees.ts"; +import { queryOne } from "#shared/db/client.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; export { handleAddEventLink, diff --git a/src/routes/admin/attendees-merge.ts b/src/features/admin/attendees-merge.ts similarity index 97% rename from src/routes/admin/attendees-merge.ts rename to src/features/admin/attendees-merge.ts index 0d721ce2f..fc9639787 100644 --- a/src/routes/admin/attendees-merge.ts +++ b/src/features/admin/attendees-merge.ts @@ -4,7 +4,14 @@ /* jscpd:ignore-start */ import { filter, map, pipe } from "#fp"; -import { logActivity } from "#lib/db/activityLog.ts"; +import { requirePrivateKey } from "#routes/admin/actions.ts"; +import { createEntityRouteHandlers } from "#routes/admin/entity-handlers.ts"; +import type { AuthSession } from "#routes/auth.ts"; +import { applyFlash } from "#routes/csrf.ts"; +import type { AttendeeRouteParams } from "#routes/entity.ts"; +import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; +import { getSearchParam } from "#routes/url.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; import { ATTENDEE_LEFT_JOIN_SELECT, decryptAttendeeOrNull, @@ -12,31 +19,24 @@ import { type EventAttendeeRow, getAttendeesByTokens, updateAttendeePII, -} from "#lib/db/attendees.ts"; -import { queryAll, queryOne } from "#lib/db/client.ts"; -import { getQuestionsWithEventIds } from "#lib/db/questions.ts"; -import type { FormParams } from "#lib/form-data.ts"; +} from "#shared/db/attendees.ts"; +import { queryAll, queryOne } from "#shared/db/client.ts"; +import { getQuestionsWithEventIds } from "#shared/db/questions.ts"; +import type { FormParams } from "#shared/form-data.ts"; import { applyAttendeeMerge, bookingKey, buildAttendeeMergeDiff, validateAttendeeMergeDecision, -} from "#lib/merge/attendee-merge.ts"; +} from "#shared/merge/attendee-merge.ts"; import type { AttendeeMergeDecisionInput, AttendeeMergeDiff, MergeAnswerChoice, MergeBookingChoice, MergeValueChoice, -} from "#lib/merge/attendee-merge-types.ts"; -import type { Attendee } from "#lib/types.ts"; -import { requirePrivateKey } from "#routes/admin/actions.ts"; -import { createEntityRouteHandlers } from "#routes/admin/entity-handlers.ts"; -import type { AuthSession } from "#routes/auth.ts"; -import { applyFlash } from "#routes/csrf.ts"; -import type { AttendeeRouteParams } from "#routes/entity.ts"; -import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; -import { getSearchParam } from "#routes/url.ts"; +} from "#shared/merge/attendee-merge-types.ts"; +import type { Attendee } from "#shared/types.ts"; import { adminMergeAttendeePage } from "#templates/admin/attendees.tsx"; /* jscpd:ignore-end */ diff --git a/src/routes/admin/attendees-route-helpers.ts b/src/features/admin/attendees-route-helpers.ts similarity index 94% rename from src/routes/admin/attendees-route-helpers.ts rename to src/features/admin/attendees-route-helpers.ts index 2ee32178d..b004d249f 100644 --- a/src/routes/admin/attendees-route-helpers.ts +++ b/src/features/admin/attendees-route-helpers.ts @@ -2,10 +2,6 @@ * Shared utilities for admin attendee route handlers */ -import { decryptAttendeeOrNull } from "#lib/db/attendees.ts"; -import { getEventWithAttendeeRaw } from "#lib/db/events.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { Attendee, EventWithCount } from "#lib/types.ts"; import { requirePrivateKey } from "#routes/admin/actions.ts"; import { verifyOrRedirect } from "#routes/admin/confirmation.ts"; import { withEntityLoader } from "#routes/admin/entity-handlers.ts"; @@ -16,6 +12,10 @@ import { withAuth, } from "#routes/auth.ts"; import { getSearchParam } from "#routes/url.ts"; +import { decryptAttendeeOrNull } from "#shared/db/attendees.ts"; +import { getEventWithAttendeeRaw } from "#shared/db/events.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { Attendee, EventWithCount } from "#shared/types.ts"; /** Attendee with event data */ export type AttendeeWithEvent = { attendee: Attendee; event: EventWithCount }; diff --git a/src/routes/admin/attendees.ts b/src/features/admin/attendees.ts similarity index 94% rename from src/routes/admin/attendees.ts rename to src/features/admin/attendees.ts index bb608e3b0..24765e7e9 100644 --- a/src/routes/admin/attendees.ts +++ b/src/features/admin/attendees.ts @@ -2,26 +2,26 @@ * Admin attendee management routes */ -import { createAuthedFormRoute } from "#lib/app-forms.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; +import { applyFlash } from "#routes/csrf.ts"; +import { htmlResponse, redirect, redirectResponse } from "#routes/response.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { createAuthedFormRoute } from "#shared/app-forms.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; import { createAttendeeAtomic, deleteAttendee, updateCheckedIn, -} from "#lib/db/attendees.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; -import { ATTENDEE_DEMO_FIELDS, applyDemoOverrides } from "#lib/demo.ts"; -import { validateForm } from "#lib/forms.tsx"; -import { ErrorCode, logError } from "#lib/logger.ts"; +} from "#shared/db/attendees.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; +import { ATTENDEE_DEMO_FIELDS, applyDemoOverrides } from "#shared/demo.ts"; +import { validateForm } from "#shared/forms.tsx"; +import { ErrorCode, logError } from "#shared/logger.ts"; import { type AdminSession, type EventWithCount, isPaidEvent, -} from "#lib/types.ts"; -import { logAndNotifyRegistration } from "#lib/webhook.ts"; -import { applyFlash } from "#routes/csrf.ts"; -import { htmlResponse, redirect, redirectResponse } from "#routes/response.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +} from "#shared/types.ts"; +import { logAndNotifyRegistration } from "#shared/webhook.ts"; import { adminDeleteAttendeePage, adminResendNotificationPage, diff --git a/src/routes/admin/auth.ts b/src/features/admin/auth.ts similarity index 89% rename from src/routes/admin/auth.ts rename to src/features/admin/auth.ts index 5fc059693..ba7cdcf2d 100644 --- a/src/routes/admin/auth.ts +++ b/src/features/admin/auth.ts @@ -2,24 +2,6 @@ * Admin authentication routes - login and logout */ -import { - buildSessionCookie, - clearSessionCookie, - getSessionCookieName, -} from "#lib/cookies.ts"; -import { deriveKEK, unwrapKey, wrapKeyWithToken } from "#lib/crypto/keys.ts"; -import { verifySignedCsrfToken } from "#lib/csrf.ts"; -import { - clearLoginAttempts, - isLoginRateLimited, - recordFailedLogin, -} from "#lib/db/login-attempts.ts"; -import { createSession, deleteSession } from "#lib/db/sessions.ts"; -import { getUserByUsername, verifyUserPassword } from "#lib/db/users.ts"; -import { validateForm } from "#lib/forms.tsx"; -import { nowMs } from "#lib/now.ts"; -import { fail, ok } from "#lib/response.ts"; -import { getSkipLoginDelay } from "#lib/test-overrides.ts"; import { loginResponse } from "#routes/admin/dashboard.ts"; import { AUTH_FORM, @@ -32,6 +14,24 @@ import { redirect } from "#routes/response.ts"; import { defineRoutes } from "#routes/router.ts"; import type { ServerContext } from "#routes/types.ts"; import { getClientIp, parseCookies } from "#routes/url.ts"; +import { + buildSessionCookie, + clearSessionCookie, + getSessionCookieName, +} from "#shared/cookies.ts"; +import { deriveKEK, unwrapKey, wrapKeyWithToken } from "#shared/crypto/keys.ts"; +import { verifySignedCsrfToken } from "#shared/csrf.ts"; +import { + clearLoginAttempts, + isLoginRateLimited, + recordFailedLogin, +} from "#shared/db/login-attempts.ts"; +import { createSession, deleteSession } from "#shared/db/sessions.ts"; +import { getUserByUsername, verifyUserPassword } from "#shared/db/users.ts"; +import { validateForm } from "#shared/forms.tsx"; +import { nowMs } from "#shared/now.ts"; +import { fail, ok } from "#shared/response.ts"; +import { getSkipLoginDelay } from "#shared/test-overrides.ts"; import { type LoginFormValues, loginFields } from "#templates/fields.ts"; /** Random delay between 100-200ms to prevent timing attacks */ diff --git a/src/routes/admin/backup.ts b/src/features/admin/backup.ts similarity index 97% rename from src/routes/admin/backup.ts rename to src/features/admin/backup.ts index 6e6638e04..9cf283037 100644 --- a/src/routes/admin/backup.ts +++ b/src/features/admin/backup.ts @@ -6,8 +6,13 @@ * storage since the sensitive data is already encrypted at the field level. */ -import { getEncryptionKeyString } from "#lib/crypto/encryption.ts"; - +import { createActionHandler } from "#routes/admin/actions.ts"; +import { verifyOrRedirect } from "#routes/admin/confirmation.ts"; +import { OWNER_MULTIPART, requireOwnerOr, withAuth } from "#routes/auth.ts"; +import { applyFlash } from "#routes/csrf.ts"; +import { htmlResponse, redirect } from "#routes/response.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { getEncryptionKeyString } from "#shared/crypto/encryption.ts"; import { countZipStatements, createAndUploadBackup, @@ -15,21 +20,15 @@ import { isRemoteDatabase, readManifest, restoreFromZip, -} from "#lib/db/backup.ts"; -import { SCHEMA_HASH } from "#lib/db/migrations.ts"; +} from "#shared/db/backup.ts"; +import { SCHEMA_HASH } from "#shared/db/migrations.ts"; import { deleteFile, downloadRaw, isStorageEnabled, listFiles, uploadRaw, -} from "#lib/storage.ts"; -import { createActionHandler } from "#routes/admin/actions.ts"; -import { verifyOrRedirect } from "#routes/admin/confirmation.ts"; -import { OWNER_MULTIPART, requireOwnerOr, withAuth } from "#routes/auth.ts"; -import { applyFlash } from "#routes/csrf.ts"; -import { htmlResponse, redirect } from "#routes/response.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +} from "#shared/storage.ts"; import { adminBackupPage, adminRestoreConfirmPage, diff --git a/src/routes/admin/builder.ts b/src/features/admin/builder.ts similarity index 90% rename from src/routes/admin/builder.ts rename to src/features/admin/builder.ts index ac4147783..fa7faf466 100644 --- a/src/routes/admin/builder.ts +++ b/src/features/admin/builder.ts @@ -3,13 +3,6 @@ * Owner-only access, gated behind CAN_BUILD_SITES=true env var */ -import { createAuthedFormRoute } from "#lib/app-forms.ts"; -import { builderApi } from "#lib/builder.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { getAllBuiltSites, insertBuiltSite } from "#lib/db/built-sites.ts"; -import { settings } from "#lib/db/settings.ts"; -import { getEnv } from "#lib/env.ts"; -import { defineForm } from "#lib/forms.tsx"; import { OWNER_FORM, requireOwnerOr } from "#routes/auth.ts"; import { applyFlash } from "#routes/csrf.ts"; import { @@ -19,6 +12,13 @@ import { redirect, } from "#routes/response.ts"; import { defineRoutes } from "#routes/router.ts"; +import { createAuthedFormRoute } from "#shared/app-forms.ts"; +import { builderApi } from "#shared/builder.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { getAllBuiltSites, insertBuiltSite } from "#shared/db/built-sites.ts"; +import { settings } from "#shared/db/settings.ts"; +import { getEnv } from "#shared/env.ts"; +import { defineForm } from "#shared/forms.tsx"; import { adminBuilderPage, type BuiltSiteDisplay, diff --git a/src/routes/admin/built-sites.ts b/src/features/admin/built-sites.ts similarity index 93% rename from src/routes/admin/built-sites.ts rename to src/features/admin/built-sites.ts index 671324b9b..fbc6c1a40 100644 --- a/src/routes/admin/built-sites.ts +++ b/src/features/admin/built-sites.ts @@ -2,13 +2,13 @@ * Admin built site management routes - owner only */ +import { createOwnerCrudHandlers } from "#routes/admin/owner-crud.ts"; import { type BuiltSiteFormInput, builtSitesCrudTable, getAllBuiltSites, -} from "#lib/db/built-sites.ts"; -import { defineNamedResource } from "#lib/rest/resource.ts"; -import { createOwnerCrudHandlers } from "#routes/admin/owner-crud.ts"; +} from "#shared/db/built-sites.ts"; +import { defineNamedResource } from "#shared/rest/resource.ts"; import { adminBuiltSiteDeletePage, adminBuiltSiteEditPage, diff --git a/src/routes/admin/bulk-actions.ts b/src/features/admin/bulk-actions.ts similarity index 92% rename from src/routes/admin/bulk-actions.ts rename to src/features/admin/bulk-actions.ts index 7086a4e28..65d8e49f0 100644 --- a/src/routes/admin/bulk-actions.ts +++ b/src/features/admin/bulk-actions.ts @@ -8,22 +8,6 @@ * derived from two reference dates. */ -import { - applyNameReplacement, - computeDayOffset, - shiftUtcIsoByDays, -} from "#lib/bulk-replace.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { eventsTable } from "#lib/db/events.ts"; -import { - getEventsByGroupId, - groupsTable, - setGroupEventsActive, -} from "#lib/db/groups.ts"; -import { buildDuplicateEventInput } from "#lib/events-actions.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { sortEvents } from "#lib/sort-events.ts"; -import type { AdminSession, EventWithCount, Group } from "#lib/types.ts"; import { createVerifiedFormRoute } from "#routes/admin/confirmation.ts"; import { generateUniqueGroupSlug, @@ -33,6 +17,22 @@ import { import { requireSessionOr } from "#routes/auth.ts"; import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { + applyNameReplacement, + computeDayOffset, + shiftUtcIsoByDays, +} from "#shared/bulk-replace.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { eventsTable } from "#shared/db/events.ts"; +import { + getEventsByGroupId, + groupsTable, + setGroupEventsActive, +} from "#shared/db/groups.ts"; +import { buildDuplicateEventInput } from "#shared/events-actions.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { sortEvents } from "#shared/sort-events.ts"; +import type { AdminSession, EventWithCount, Group } from "#shared/types.ts"; import { adminBulkActionsPage, adminDeactivateGroupPage, diff --git a/src/routes/admin/calendar.ts b/src/features/admin/calendar.ts similarity index 94% rename from src/routes/admin/calendar.ts rename to src/features/admin/calendar.ts index a7da5c076..6913cf801 100644 --- a/src/routes/admin/calendar.ts +++ b/src/features/admin/calendar.ts @@ -3,33 +3,37 @@ */ import { filter, flatMap, map, pipe, reduce, sort, unique } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; +import { + csvResponse, + getDateFilter, + loadQuestionData, +} from "#routes/admin/actions.ts"; +import { getPrivateKey, requireSessionOr } from "#routes/auth.ts"; +import { htmlResponse, redirect } from "#routes/response.ts"; +import { defineRoutes } from "#routes/router.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; import { eventDateToCalendarDate, formatDateLabel, getAvailableDates, -} from "#lib/dates.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { decryptAttendees } from "#lib/db/attendees.ts"; +} from "#shared/dates.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { decryptAttendees } from "#shared/db/attendees.ts"; import { getAllDailyEvents, getAllStandardEvents, getAttendeesByEventIds, getDailyEventAttendeeDates, getDailyEventAttendeesByDate, -} from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { settings } from "#lib/db/settings.ts"; -import { todayInTz } from "#lib/timezone.ts"; -import { type Attendee, type EventWithCount, isPaidEvent } from "#lib/types.ts"; +} from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { settings } from "#shared/db/settings.ts"; +import { todayInTz } from "#shared/timezone.ts"; import { - csvResponse, - getDateFilter, - loadQuestionData, -} from "#routes/admin/actions.ts"; -import { getPrivateKey, requireSessionOr } from "#routes/auth.ts"; -import { htmlResponse, redirect } from "#routes/response.ts"; -import { defineRoutes } from "#routes/router.ts"; + type Attendee, + type EventWithCount, + isPaidEvent, +} from "#shared/types.ts"; import { adminCalendarPage, type CalendarAttendeeRow, diff --git a/src/routes/admin/confirmation.ts b/src/features/admin/confirmation.ts similarity index 98% rename from src/routes/admin/confirmation.ts rename to src/features/admin/confirmation.ts index b21bd60aa..067acebc8 100644 --- a/src/routes/admin/confirmation.ts +++ b/src/features/admin/confirmation.ts @@ -4,9 +4,6 @@ /* jscpd:ignore-start */ import { asString } from "#fp"; -import { type AuthedBase, createAuthedHandler } from "#lib/app-forms.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import type { FormParams } from "#lib/form-data.ts"; import type { AuthSession } from "#routes/auth.ts"; import { AUTH_FORM, @@ -23,6 +20,9 @@ import { redirect, } from "#routes/response.ts"; import type { RouteHandlerFn } from "#routes/router.ts"; +import { type AuthedBase, createAuthedHandler } from "#shared/app-forms.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import type { FormParams } from "#shared/form-data.ts"; /* jscpd:ignore-end */ /** Form guard: require auth + CSRF, call handler with session and form */ diff --git a/src/routes/admin/dashboard.ts b/src/features/admin/dashboard.ts similarity index 86% rename from src/routes/admin/dashboard.ts rename to src/features/admin/dashboard.ts index a2ae03f8c..4e3eaecb7 100644 --- a/src/routes/admin/dashboard.ts +++ b/src/features/admin/dashboard.ts @@ -2,24 +2,24 @@ * Admin dashboard route */ -import { signCsrfToken } from "#lib/csrf.ts"; -import { getAllActivityLog } from "#lib/db/activityLog.ts"; -import { - decryptAttendees, - getActiveEventStats, - getNewestAttendeesRaw, -} from "#lib/db/attendees.ts"; -import { getAllEvents } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { settings } from "#lib/db/settings.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { sortEvents } from "#lib/sort-events.ts"; import { requirePrivateKey } from "#routes/admin/actions.ts"; import { sessionPage, withSession } from "#routes/auth.ts"; import { applyFlash } from "#routes/csrf.ts"; import { htmlResponse } from "#routes/response.ts"; /* jscpd:ignore-start */ import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { getAllActivityLog } from "#shared/db/activityLog.ts"; +import { + decryptAttendees, + getActiveEventStats, + getNewestAttendeesRaw, +} from "#shared/db/attendees.ts"; +import { getAllEvents } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { settings } from "#shared/db/settings.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { sortEvents } from "#shared/sort-events.ts"; /* jscpd:ignore-end */ import { adminGlobalActivityLogPage } from "#templates/admin/activityLog.tsx"; import { adminDashboardPage } from "#templates/admin/dashboard.tsx"; diff --git a/src/routes/admin/database-reset.ts b/src/features/admin/database-reset.ts similarity index 83% rename from src/routes/admin/database-reset.ts rename to src/features/admin/database-reset.ts index 243d954cc..b418e0226 100644 --- a/src/routes/admin/database-reset.ts +++ b/src/features/admin/database-reset.ts @@ -3,15 +3,6 @@ * Access is strictly restricted to demo mode (DEMO_MODE=true). */ -/* jscpd:ignore-start */ -import { createFormRoute } from "#lib/app-forms.ts"; -import { clearSessionCookie } from "#lib/cookies.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; -import { getAllEvents } from "#lib/db/events.ts"; -import { resetDatabase } from "#lib/db/migrations.ts"; -import { isDemoMode } from "#lib/demo.ts"; -import { defineForm } from "#lib/forms.tsx"; -import { deleteAllEventStorageFiles, isStorageEnabled } from "#lib/storage.ts"; import { applyFlash } from "#routes/csrf.ts"; import { errorRedirect, @@ -20,6 +11,18 @@ import { redirect, } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; +/* jscpd:ignore-start */ +import { createFormRoute } from "#shared/app-forms.ts"; +import { clearSessionCookie } from "#shared/cookies.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { getAllEvents } from "#shared/db/events.ts"; +import { resetDatabase } from "#shared/db/migrations.ts"; +import { isDemoMode } from "#shared/demo.ts"; +import { defineForm } from "#shared/forms.tsx"; +import { + deleteAllEventStorageFiles, + isStorageEnabled, +} from "#shared/storage.ts"; import { demoResetPage, RESET_DATABASE_PHRASE, diff --git a/src/routes/admin/debug.ts b/src/features/admin/debug.ts similarity index 92% rename from src/routes/admin/debug.ts rename to src/features/admin/debug.ts index 763fca12a..2462c3a03 100644 --- a/src/routes/admin/debug.ts +++ b/src/features/admin/debug.ts @@ -3,26 +3,26 @@ * Owner-only access enforced via requireOwnerOr */ +import { ownerPage } from "#routes/auth.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; import { isValidPemCertificate, isValidPemPrivateKey, -} from "#lib/apple-wallet.ts"; -import { BUILD_COMMIT, BUILD_TIMESTAMP } from "#lib/build-info.ts"; -import { getCdnHostname } from "#lib/bunny-cdn.ts"; +} from "#shared/apple-wallet.ts"; +import { BUILD_COMMIT, BUILD_TIMESTAMP } from "#shared/build-info.ts"; +import { getCdnHostname } from "#shared/bunny-cdn.ts"; import { getBunnyDnsSubdomainSuffix, getEffectiveDomain, isBunnyCdnEnabled, isBunnyDnsEnabled, -} from "#lib/config.ts"; -import { settings } from "#lib/db/settings.ts"; -import { getHostEmailConfig } from "#lib/email.ts"; -import { getEnv } from "#lib/env.ts"; -import { isValidGooglePrivateKey } from "#lib/google-wallet.ts"; -import { LIMIT_ENTRIES } from "#lib/limits.ts"; -import { getStorageBackend } from "#lib/storage.ts"; -import { ownerPage } from "#routes/auth.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +} from "#shared/config.ts"; +import { settings } from "#shared/db/settings.ts"; +import { getHostEmailConfig } from "#shared/email.ts"; +import { getEnv } from "#shared/env.ts"; +import { isValidGooglePrivateKey } from "#shared/google-wallet.ts"; +import { LIMIT_ENTRIES } from "#shared/limits.ts"; +import { getStorageBackend } from "#shared/storage.ts"; import { adminDebugPage, type DebugPageState, diff --git a/src/routes/admin/entity-handlers.ts b/src/features/admin/entity-handlers.ts similarity index 99% rename from src/routes/admin/entity-handlers.ts rename to src/features/admin/entity-handlers.ts index de0ba86e4..387dd7954 100644 --- a/src/routes/admin/entity-handlers.ts +++ b/src/features/admin/entity-handlers.ts @@ -2,12 +2,12 @@ * Entity loading patterns for admin route handlers */ -import type { FormParams } from "#lib/form-data.ts"; import type { AuthSession } from "#routes/auth.ts"; import { AUTH_FORM, requireSessionOr, withAuth } from "#routes/auth.ts"; import type { EntityHandler } from "#routes/entity.ts"; import { withEntity } from "#routes/entity.ts"; import { notFoundResponse } from "#routes/response.ts"; +import type { FormParams } from "#shared/form-data.ts"; /** * Curried factory: creates a wrapper that takes load params, then a handler. diff --git a/src/routes/admin/event-qr.ts b/src/features/admin/event-qr.ts similarity index 91% rename from src/routes/admin/event-qr.ts rename to src/features/admin/event-qr.ts index 67096c1cb..4e4030f91 100644 --- a/src/routes/admin/event-qr.ts +++ b/src/features/admin/event-qr.ts @@ -8,20 +8,23 @@ * can refresh the QR client-side (every minute) without a full reload. */ -import { createAuthedFormRoute, type FormValidator } from "#lib/app-forms.ts"; -import { getEffectiveDomain } from "#lib/config.ts"; -import { validatePrice } from "#lib/currency.ts"; -import { getAvailableDates } from "#lib/dates.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { FormParams } from "#lib/form-data.ts"; -import { eventSupportsDirectCheckout, generateQrSvg } from "#lib/qr.ts"; -import { buildQrBookPayload, signQrBookToken } from "#lib/qr-token.ts"; -import type { AdminSession, EventWithCount } from "#lib/types.ts"; import { withEntityLoader } from "#routes/admin/entity-handlers.ts"; import { requireSessionOr } from "#routes/auth.ts"; import { htmlResponse, jsonResponse } from "#routes/response.ts"; import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { + createAuthedFormRoute, + type FormValidator, +} from "#shared/app-forms.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { validatePrice } from "#shared/currency.ts"; +import { getAvailableDates } from "#shared/dates.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { FormParams } from "#shared/form-data.ts"; +import { eventSupportsDirectCheckout, generateQrSvg } from "#shared/qr.ts"; +import { buildQrBookPayload, signQrBookToken } from "#shared/qr-token.ts"; +import type { AdminSession, EventWithCount } from "#shared/types.ts"; import type { AdminEventQrResult, AdminEventQrValues, diff --git a/src/routes/admin/events.ts b/src/features/admin/events.ts similarity index 96% rename from src/routes/admin/events.ts rename to src/features/admin/events.ts index 37d6dcf07..712204668 100644 --- a/src/routes/admin/events.ts +++ b/src/features/admin/events.ts @@ -3,38 +3,60 @@ */ import { compact, filter, map, pipe, sort, unique } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; -import { toMinorUnits } from "#lib/currency.ts"; -import { formatDateLabel, normalizeDatetime } from "#lib/dates.ts"; -import { getEventWithActivityLog, logActivity } from "#lib/db/activityLog.ts"; -import { getGroupRemainingByGroupId } from "#lib/db/attendees.ts"; +import { + csvResponse, + eventAttendeesLoader, + getDateFilter, +} from "#routes/admin/actions.ts"; +import { isBuilderEnabled } from "#routes/admin/builder.ts"; +import { createConfirmedHandlers } from "#routes/admin/confirmation.ts"; +import { + AUTH_FORM, + AUTH_MULTIPART, + requireSessionOr, + withAuth, +} from "#routes/auth.ts"; +import { formDataToParams } from "#routes/csrf.ts"; +import { authenticatedGetById } from "#routes/entity.ts"; +import { htmlResponse, notFoundResponse, redirect } from "#routes/response.ts"; +import type { TypedRouteHandler } from "#routes/router.ts"; +import { defineRoutes } from "#routes/router.ts"; +import { getSearchParam } from "#routes/url.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { toMinorUnits } from "#shared/currency.ts"; +import { formatDateLabel, normalizeDatetime } from "#shared/dates.ts"; +import { + getEventWithActivityLog, + logActivity, +} from "#shared/db/activityLog.ts"; +import { getGroupRemainingByGroupId } from "#shared/db/attendees/capacity.ts"; import { computeSlugIndex, type EventInput, eventsTable, getEventWithCount, -} from "#lib/db/events.ts"; -import { getAllGroups, groupsTable } from "#lib/db/groups.ts"; -import { deleteAllStaleReservations } from "#lib/db/processed-payments.ts"; +} from "#shared/db/events.ts"; +import { getAllGroups, groupsTable } from "#shared/db/groups.ts"; +import { deleteAllStaleReservations } from "#shared/db/processed-payments.ts"; import { getAttendeeAnswersBatch, getQuestionsForEvent, -} from "#lib/db/questions.ts"; -import { settings } from "#lib/db/settings.ts"; +} from "#shared/db/questions.ts"; +import { settings } from "#shared/db/settings.ts"; import { applyDemoOverrides, EVENT_DEMO_FIELDS, isDemoMode, -} from "#lib/demo.ts"; +} from "#shared/demo.ts"; import { generateUniqueEventSlug, performEventDelete, validateEventInput, -} from "#lib/events-actions.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { ErrorCode, logDebug, logError } from "#lib/logger.ts"; -import { defineResource } from "#lib/rest/resource.ts"; -import { normalizeSlug } from "#lib/slug.ts"; +} from "#shared/events-actions.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { ErrorCode, logDebug, logError } from "#shared/logger.ts"; +import { defineResource } from "#shared/rest/resource.ts"; +import { normalizeSlug } from "#shared/slug.ts"; import { ATTACHMENT_ERROR_MESSAGES, deleteFile, @@ -46,32 +68,13 @@ import { uploadImage, validateAttachment, validateImage, -} from "#lib/storage.ts"; +} from "#shared/storage.ts"; import type { AdminSession, Attendee, EventWithCount, Group, -} from "#lib/types.ts"; -import { - csvResponse, - eventAttendeesLoader, - getDateFilter, -} from "#routes/admin/actions.ts"; -import { isBuilderEnabled } from "#routes/admin/builder.ts"; -import { createConfirmedHandlers } from "#routes/admin/confirmation.ts"; -import { - AUTH_FORM, - AUTH_MULTIPART, - requireSessionOr, - withAuth, -} from "#routes/auth.ts"; -import { formDataToParams } from "#routes/csrf.ts"; -import { authenticatedGetById } from "#routes/entity.ts"; -import { htmlResponse, notFoundResponse, redirect } from "#routes/response.ts"; -import type { TypedRouteHandler } from "#routes/router.ts"; -import { defineRoutes } from "#routes/router.ts"; -import { getSearchParam } from "#routes/url.ts"; +} from "#shared/types.ts"; import { adminEventActivityLogPage } from "#templates/admin/activityLog.tsx"; import { type AttendeeFilter, diff --git a/src/routes/admin/groups.ts b/src/features/admin/groups.ts similarity index 89% rename from src/routes/admin/groups.ts rename to src/features/admin/groups.ts index 08bda8782..d59178507 100644 --- a/src/routes/admin/groups.ts +++ b/src/features/admin/groups.ts @@ -3,11 +3,16 @@ */ import { map } from "#fp"; -import { createAuthedHandler } from "#lib/app-forms.ts"; -import { getEffectiveDomain } from "#lib/config.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { decryptAttendees } from "#lib/db/attendees.ts"; -import { getAttendeesByEventIds, getEvent } from "#lib/db/events.ts"; +import { loadQuestionData, requirePrivateKey } from "#routes/admin/actions.ts"; +import { createCrudHandlers } from "#routes/admin/owner-crud.ts"; +import { requireSessionOr } from "#routes/auth.ts"; +import { htmlResponse, redirect } from "#routes/response.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { createAuthedHandler } from "#shared/app-forms.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { decryptAttendees } from "#shared/db/attendees.ts"; +import { getAttendeesByEventIds, getEvent } from "#shared/db/events.ts"; import { assignEventsToGroup, computeGroupSlugIndex, @@ -19,21 +24,16 @@ import { isGroupSlugTaken, resetGroupEvents, validateGroupEventType, -} from "#lib/db/groups.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { settings } from "#lib/db/settings.ts"; -import { GROUP_DEMO_FIELDS, wrapResourceForDemo } from "#lib/demo.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { defineNamedResource } from "#lib/rest/resource.ts"; -import { generateUniqueSlug, normalizeSlug } from "#lib/slug.ts"; -import { sortEvents } from "#lib/sort-events.ts"; -import { type Attendee, type Group, isPaidEvent } from "#lib/types.ts"; -import { loadQuestionData, requirePrivateKey } from "#routes/admin/actions.ts"; -import { createCrudHandlers } from "#routes/admin/owner-crud.ts"; -import { requireSessionOr } from "#routes/auth.ts"; -import { htmlResponse, redirect } from "#routes/response.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +} from "#shared/db/groups.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { settings } from "#shared/db/settings.ts"; +import { GROUP_DEMO_FIELDS, wrapResourceForDemo } from "#shared/demo.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { defineNamedResource } from "#shared/rest/resource.ts"; +import { generateUniqueSlug, normalizeSlug } from "#shared/slug.ts"; +import { sortEvents } from "#shared/sort-events.ts"; +import { type Attendee, type Group, isPaidEvent } from "#shared/types.ts"; import { adminGroupDeletePage, adminGroupDetailPage, diff --git a/src/routes/admin/guide.ts b/src/features/admin/guide.ts similarity index 82% rename from src/routes/admin/guide.ts rename to src/features/admin/guide.ts index a08066354..479952198 100644 --- a/src/routes/admin/guide.ts +++ b/src/features/admin/guide.ts @@ -2,12 +2,15 @@ * Admin guide route */ -import { getBunnyDnsSubdomainSuffix, isBunnyDnsEnabled } from "#lib/config.ts"; -import { settings } from "#lib/db/settings.ts"; -import { EMAIL_PROVIDER_LABELS, getHostEmailConfig } from "#lib/email.ts"; import { isBuilderEnabled } from "#routes/admin/builder.ts"; import { sessionPage } from "#routes/auth.ts"; import { defineRoutes } from "#routes/router.ts"; +import { + getBunnyDnsSubdomainSuffix, + isBunnyDnsEnabled, +} from "#shared/config.ts"; +import { settings } from "#shared/db/settings.ts"; +import { EMAIL_PROVIDER_LABELS, getHostEmailConfig } from "#shared/email.ts"; import { adminGuidePage } from "#templates/admin/guide.tsx"; /** diff --git a/src/routes/admin/holidays.ts b/src/features/admin/holidays.ts similarity index 90% rename from src/routes/admin/holidays.ts rename to src/features/admin/holidays.ts index 76be3450d..7ea2241f7 100644 --- a/src/routes/admin/holidays.ts +++ b/src/features/admin/holidays.ts @@ -2,14 +2,14 @@ * Admin holiday management routes - owner only */ +import { createOwnerCrudHandlers } from "#routes/admin/owner-crud.ts"; import { getAllHolidays, type HolidayInput, holidaysTable, -} from "#lib/db/holidays.ts"; -import { HOLIDAY_DEMO_FIELDS, wrapResourceForDemo } from "#lib/demo.ts"; -import { defineNamedResource } from "#lib/rest/resource.ts"; -import { createOwnerCrudHandlers } from "#routes/admin/owner-crud.ts"; +} from "#shared/db/holidays.ts"; +import { HOLIDAY_DEMO_FIELDS, wrapResourceForDemo } from "#shared/demo.ts"; +import { defineNamedResource } from "#shared/rest/resource.ts"; import { adminHolidayDeletePage, adminHolidayEditPage, diff --git a/src/routes/admin/index.ts b/src/features/admin/index.ts similarity index 96% rename from src/routes/admin/index.ts rename to src/features/admin/index.ts index fd303db2e..280eec354 100644 --- a/src/routes/admin/index.ts +++ b/src/features/admin/index.ts @@ -8,8 +8,6 @@ */ import { reduce } from "#fp"; -import { enableQueryLog } from "#lib/db/query-log.ts"; -import { settings } from "#lib/db/settings.ts"; import { apiKeysRoutes } from "#routes/admin/api-keys.ts"; import { attendeeRefundRoutes } from "#routes/admin/attendee-refunds.ts"; import { attendeesRoutes } from "#routes/admin/attendees.ts"; @@ -36,6 +34,8 @@ import { updateRoutes } from "#routes/admin/update.ts"; import { usersRoutes } from "#routes/admin/users.ts"; import { getAuthenticatedSession } from "#routes/auth.ts"; import { createRouter, type RouteHandlerFn } from "#routes/router.ts"; +import { enableQueryLog } from "#shared/db/query-log.ts"; +import { settings } from "#shared/db/settings.ts"; /** Route maps merged in order (later keys override earlier on conflict) */ const adminRouteModules: Record[] = [ diff --git a/src/routes/admin/owner-crud.ts b/src/features/admin/owner-crud.ts similarity index 94% rename from src/routes/admin/owner-crud.ts rename to src/features/admin/owner-crud.ts index 3af92ded5..a76b1b50d 100644 --- a/src/routes/admin/owner-crud.ts +++ b/src/features/admin/owner-crud.ts @@ -1,8 +1,3 @@ -import { logActivity } from "#lib/db/activityLog.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { NamedResource } from "#lib/rest/resource.ts"; -import type { AdminSession } from "#lib/types.ts"; import { createConfirmedHandlers, type FormGuard, @@ -25,6 +20,11 @@ import { redirect, } from "#routes/response.ts"; import type { RouteHandlerFn } from "#routes/router.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { NamedResource } from "#shared/rest/resource.ts"; +import type { AdminSession } from "#shared/types.ts"; type CrudConfig = { singular: string; diff --git a/src/routes/admin/questions.ts b/src/features/admin/questions.ts similarity index 95% rename from src/routes/admin/questions.ts rename to src/features/admin/questions.ts index 2056f45ff..f4b9ecf8d 100644 --- a/src/routes/admin/questions.ts +++ b/src/features/admin/questions.ts @@ -2,10 +2,26 @@ * Admin routes for custom questions management (owner-only) */ +import { + createConfirmedHandlers, + createVerifiedFormRoute, +} from "#routes/admin/confirmation.ts"; +import { OWNER_FORM, ownerPage, requireOwnerOr } from "#routes/auth.ts"; +import { ownerFormById, ownerGetById } from "#routes/entity.ts"; +import { + errorRedirect, + htmlResponse, + notFoundResponse, + redirect, +} from "#routes/response.ts"; +import { defineRoutes } from "#routes/router.ts"; /* jscpd:ignore-start */ -import { createAuthedFormRoute, createAuthedHandler } from "#lib/app-forms.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; +import { + createAuthedFormRoute, + createAuthedHandler, +} from "#shared/app-forms.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; import { type Answer, answersTable, @@ -20,23 +36,10 @@ import { questionsTable, setEventQuestions, swapAnswerOrder, -} from "#lib/db/questions.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { defineForm } from "#lib/forms.tsx"; -import type { AdminSession } from "#lib/types.ts"; -import { - createConfirmedHandlers, - createVerifiedFormRoute, -} from "#routes/admin/confirmation.ts"; -import { OWNER_FORM, ownerPage, requireOwnerOr } from "#routes/auth.ts"; -import { ownerFormById, ownerGetById } from "#routes/entity.ts"; -import { - errorRedirect, - htmlResponse, - notFoundResponse, - redirect, -} from "#routes/response.ts"; -import { defineRoutes } from "#routes/router.ts"; +} from "#shared/db/questions.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { defineForm } from "#shared/forms.tsx"; +import type { AdminSession } from "#shared/types.ts"; import { adminAnswerDeletePage, adminEventQuestionsPage, diff --git a/src/routes/admin/scanner.ts b/src/features/admin/scanner.ts similarity index 95% rename from src/routes/admin/scanner.ts rename to src/features/admin/scanner.ts index 0d0b87609..28a9fd53e 100644 --- a/src/routes/admin/scanner.ts +++ b/src/features/admin/scanner.ts @@ -5,17 +5,6 @@ */ import { filter, map, pipe } from "#fp"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { - type AttendeeWithBookings, - decryptAttendees, - getAttendeesByTokens, - getAttendeesRaw, - updateCheckedIn, -} from "#lib/db/attendees.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import type { Attendee } from "#lib/types.ts"; import { requirePrivateKey } from "#routes/admin/actions.ts"; import { withEntityLoader } from "#routes/admin/entity-handlers.ts"; import { @@ -31,7 +20,18 @@ import { decryptTokenEntries, resolveEntries, type TokenEntry, -} from "#routes/token-utils.ts"; +} from "#routes/tickets/token-utils.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { + type AttendeeWithBookings, + decryptAttendees, + getAttendeesByTokens, + getAttendeesRaw, + updateCheckedIn, +} from "#shared/db/attendees.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import type { Attendee } from "#shared/types.ts"; import { adminScannerPage } from "#templates/admin/scanner.tsx"; const withEvent = withEntityLoader(getEventWithCount); diff --git a/src/routes/admin/seeds.ts b/src/features/admin/seeds.ts similarity index 91% rename from src/routes/admin/seeds.ts rename to src/features/admin/seeds.ts index 205c45bcc..46140a820 100644 --- a/src/routes/admin/seeds.ts +++ b/src/features/admin/seeds.ts @@ -2,14 +2,14 @@ * Admin seed data routes - populate database with sample events and attendees */ -import { createAuthedFormRoute } from "#lib/app-forms.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { defineForm } from "#lib/forms.tsx"; -import { createSeeds, SEED_MAX_ATTENDEES } from "#lib/seeds.ts"; import { OWNER_FORM, ownerPage } from "#routes/auth.ts"; import { redirect } from "#routes/response.ts"; import type { TypedRouteHandler } from "#routes/router.ts"; import { defineRoutes } from "#routes/router.ts"; +import { createAuthedFormRoute } from "#shared/app-forms.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { defineForm } from "#shared/forms.tsx"; +import { createSeeds, SEED_MAX_ATTENDEES } from "#shared/seeds.ts"; import { adminSeedsPage } from "#templates/admin/seeds.tsx"; /** Max events that can be created in a single seed operation */ diff --git a/src/routes/admin/sessions.ts b/src/features/admin/sessions.ts similarity index 86% rename from src/routes/admin/sessions.ts rename to src/features/admin/sessions.ts index 29f8d809f..b9eb299ea 100644 --- a/src/routes/admin/sessions.ts +++ b/src/features/admin/sessions.ts @@ -2,12 +2,12 @@ * Admin session management routes */ -import { hashSessionToken } from "#lib/crypto/hashing.ts"; -import { deleteOtherSessions, getAllSessions } from "#lib/db/sessions.ts"; -import { getFlash } from "#lib/flash-context.ts"; import { OWNER_FORM, ownerPage, withAuth } from "#routes/auth.ts"; import { redirect } from "#routes/response.ts"; import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { hashSessionToken } from "#shared/crypto/hashing.ts"; +import { deleteOtherSessions, getAllSessions } from "#shared/db/sessions.ts"; +import { getFlash } from "#shared/flash-context.ts"; import { adminSessionsPage } from "#templates/admin/sessions.tsx"; /** diff --git a/src/routes/admin/settings-helpers.ts b/src/features/admin/settings-helpers.ts similarity index 98% rename from src/routes/admin/settings-helpers.ts rename to src/features/admin/settings-helpers.ts index 151306d5f..5628b722a 100644 --- a/src/routes/admin/settings-helpers.ts +++ b/src/features/admin/settings-helpers.ts @@ -9,11 +9,11 @@ * return SettingsFormHandler for use with settingsRoute/advancedSettingsRoute. */ -import { logActivity } from "#lib/db/activityLog.ts"; -import { isMaskSentinel } from "#lib/db/settings.ts"; -import type { FormParams } from "#lib/form-data.ts"; import { type AuthSession, OWNER_FORM, withAuth } from "#routes/auth.ts"; import { errorRedirect, redirect } from "#routes/response.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { isMaskSentinel } from "#shared/db/settings.ts"; +import type { FormParams } from "#shared/form-data.ts"; // ── Types ─────────────────────────────────────────────────────────── diff --git a/src/routes/admin/settings.ts b/src/features/admin/settings.ts similarity index 96% rename from src/routes/admin/settings.ts rename to src/features/admin/settings.ts index cc8de6f59..e10a2bb0b 100644 --- a/src/routes/admin/settings.ts +++ b/src/features/admin/settings.ts @@ -3,71 +3,91 @@ * Owner-only access enforced via requireOwnerOr / withAuth */ +import { demoResetForm } from "#routes/admin/database-reset.ts"; +import { + advancedSettingsRoute, + processSecretField, + type SecretFieldResult, + settingsClearable, + settingsHandler, + settingsRoute, + settingsSecret, + settingsToggle, +} from "#routes/admin/settings-helpers.ts"; +import { + type AuthSession, + OWNER_FORM, + OWNER_MULTIPART, + ownerPage, + withAuth, +} from "#routes/auth.ts"; +import { errorRedirect, jsonResponse } from "#routes/response.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; import { isValidPemCertificate, isValidPemPrivateKey, -} from "#lib/apple-wallet.ts"; +} from "#shared/apple-wallet.ts"; import { checkSubdomainAvailable, getCdnHostname, registerBunnySubdomain, validateCustomDomain, -} from "#lib/bunny-cdn.ts"; +} from "#shared/bunny-cdn.ts"; import { isValidBusinessEmail, updateBusinessEmail, -} from "#lib/business-email.ts"; -import { validateColumnTemplate } from "#lib/column-order.ts"; -import { ATTENDEE_TABLE_COLUMNS } from "#lib/columns/attendee-columns.ts"; -import { EVENT_TABLE_COLUMNS } from "#lib/columns/event-columns.ts"; +} from "#shared/business-email.ts"; +import { validateColumnTemplate } from "#shared/column-order.ts"; +import { ATTENDEE_TABLE_COLUMNS } from "#shared/columns/attendee-columns.ts"; +import { EVENT_TABLE_COLUMNS } from "#shared/columns/event-columns.ts"; import { getBunnyDnsSubdomainSuffix, getEffectiveDomain, isBunnyCdnEnabled, isBunnyDnsEnabled, -} from "#lib/config.ts"; -import { clearSessionCookie } from "#lib/cookies.ts"; -import { isValidCountry } from "#lib/countries.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { getAllEvents } from "#lib/db/events.ts"; -import { resetDatabase } from "#lib/db/migrations.ts"; +} from "#shared/config.ts"; +import { clearSessionCookie } from "#shared/cookies.ts"; +import { isValidCountry } from "#shared/countries.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { getAllEvents } from "#shared/db/events.ts"; +import { resetDatabase } from "#shared/db/migrations.ts"; import { type EmailTemplateType, MAX_EMAIL_TEMPLATE_LENGTH, settings, -} from "#lib/db/settings.ts"; -import { getUserById, verifyUserPassword } from "#lib/db/users.ts"; +} from "#shared/db/settings.ts"; +import { getUserById, verifyUserPassword } from "#shared/db/users.ts"; import { applyDemoOverrides, isDemoMode, TERMS_DEMO_FIELDS, -} from "#lib/demo.ts"; +} from "#shared/demo.ts"; import { EMAIL_PROVIDER_LABELS, getEmailConfig, getHostEmailConfig, isEmailProvider, sendTestEmail, -} from "#lib/email.ts"; +} from "#shared/email.ts"; import { buildTemplateData, renderTemplate, validateTemplate, -} from "#lib/email-renderer.ts"; +} from "#shared/email-renderer.ts"; import { DOMAIN_PATTERN, parseEmbedHosts, validateEmbedHosts, -} from "#lib/embed-hosts.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { validateForm } from "#lib/forms.tsx"; -import { isValidGooglePrivateKey } from "#lib/google-wallet.ts"; -import { MAX_TEXTAREA_LENGTH } from "#lib/limits.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import type { PaymentProviderType } from "#lib/payments.ts"; -import { fail, ok } from "#lib/response.ts"; -import { testSquareConnection } from "#lib/square.ts"; +} from "#shared/embed-hosts.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { validateForm } from "#shared/forms.tsx"; +import { isValidGooglePrivateKey } from "#shared/google-wallet.ts"; +import { MAX_TEXTAREA_LENGTH } from "#shared/limits.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import type { PaymentProviderType } from "#shared/payments.ts"; +import { fail, ok } from "#shared/response.ts"; +import { testSquareConnection } from "#shared/square.ts"; import { deleteAllEventStorageFiles, deleteFile, @@ -76,33 +96,13 @@ import { tryDeleteFile, uploadImage, validateImage, -} from "#lib/storage.ts"; +} from "#shared/storage.ts"; import { detectStripeKeyMode, setupWebhookEndpoint, testStripeConnection, -} from "#lib/stripe.ts"; -import type { Theme } from "#lib/types.ts"; -import { demoResetForm } from "#routes/admin/database-reset.ts"; -import { - advancedSettingsRoute, - processSecretField, - type SecretFieldResult, - settingsClearable, - settingsHandler, - settingsRoute, - settingsSecret, - settingsToggle, -} from "#routes/admin/settings-helpers.ts"; -import { - type AuthSession, - OWNER_FORM, - OWNER_MULTIPART, - ownerPage, - withAuth, -} from "#routes/auth.ts"; -import { errorRedirect, jsonResponse } from "#routes/response.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +} from "#shared/stripe.ts"; +import type { Theme } from "#shared/types.ts"; import { adminSettingsPage } from "#templates/admin/settings.tsx"; import { adminAdvancedSettingsPage } from "#templates/admin/settings-advanced.tsx"; import { diff --git a/src/routes/admin/site.ts b/src/features/admin/site.ts similarity index 95% rename from src/routes/admin/site.ts rename to src/features/admin/site.ts index 5aadf1de8..4cbaa0b77 100644 --- a/src/routes/admin/site.ts +++ b/src/features/admin/site.ts @@ -3,19 +3,19 @@ * Owner-only access */ -import { MAX_WEBSITE_TITLE_LENGTH, settings } from "#lib/db/settings.ts"; -import { - applyDemoOverrides, - SITE_CONTACT_DEMO_FIELDS, - SITE_HOME_DEMO_FIELDS, -} from "#lib/demo.ts"; -import { defineForm } from "#lib/forms.tsx"; -import { MAX_TEXTAREA_LENGTH } from "#lib/limits.ts"; import { settingsHandler } from "#routes/admin/settings-helpers.ts"; import { type AuthSession, requireOwnerOr } from "#routes/auth.ts"; import { applyFlash } from "#routes/csrf.ts"; import { htmlResponse } from "#routes/response.ts"; import { defineRoutes } from "#routes/router.ts"; +import { MAX_WEBSITE_TITLE_LENGTH, settings } from "#shared/db/settings.ts"; +import { + applyDemoOverrides, + SITE_CONTACT_DEMO_FIELDS, + SITE_HOME_DEMO_FIELDS, +} from "#shared/demo.ts"; +import { defineForm } from "#shared/forms.tsx"; +import { MAX_TEXTAREA_LENGTH } from "#shared/limits.ts"; import { adminSiteContactPage, adminSiteHomePage, diff --git a/src/routes/admin/update.ts b/src/features/admin/update.ts similarity index 91% rename from src/routes/admin/update.ts rename to src/features/admin/update.ts index fbec61094..0f7ad6c53 100644 --- a/src/routes/admin/update.ts +++ b/src/features/admin/update.ts @@ -3,20 +3,20 @@ * Owner-only access */ -import { BUILD_COMMIT, BUILD_TIMESTAMP } from "#lib/build-info.ts"; -import { isBunnyCdnEnabled } from "#lib/config.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { settings } from "#lib/db/settings.ts"; -import { getFlash } from "#lib/flash-context.ts"; +import { OWNER_FORM, ownerPage, withAuth } from "#routes/auth.ts"; +import { errorRedirect, redirect } from "#routes/response.ts"; +import { defineRoutes } from "#routes/router.ts"; +import { BUILD_COMMIT, BUILD_TIMESTAMP } from "#shared/build-info.ts"; +import { isBunnyCdnEnabled } from "#shared/config.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { settings } from "#shared/db/settings.ts"; +import { getFlash } from "#shared/flash-context.ts"; import { deployRelease, fetchLatestRelease, formatBuildDate, isNewerVersion, -} from "#lib/update.ts"; -import { OWNER_FORM, ownerPage, withAuth } from "#routes/auth.ts"; -import { errorRedirect, redirect } from "#routes/response.ts"; -import { defineRoutes } from "#routes/router.ts"; +} from "#shared/update.ts"; import { adminUpdatePage, type UpdatePageState, diff --git a/src/routes/admin/users.ts b/src/features/admin/users.ts similarity index 93% rename from src/routes/admin/users.ts rename to src/features/admin/users.ts index c1ceb18ed..f7f814fcd 100644 --- a/src/routes/admin/users.ts +++ b/src/features/admin/users.ts @@ -2,11 +2,23 @@ * Admin user management routes - owner only */ -import { createAuthedFormRoute } from "#lib/app-forms.ts"; +import { createConfirmedHandlers } from "#routes/admin/confirmation.ts"; +import { + type AuthSession, + generateSecureToken, + OWNER_FORM, + ownerPage, + requireOwnerOr, + withAuth, +} from "#routes/auth.ts"; +import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; +import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; +import { getSearchParam } from "#routes/url.ts"; +import { createAuthedFormRoute } from "#shared/app-forms.ts"; /* jscpd:ignore-start */ -import { getEffectiveDomain } from "#lib/config.ts"; -import { unwrapKeyWithToken } from "#lib/crypto/keys.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { unwrapKeyWithToken } from "#shared/crypto/keys.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; import { activateUser, createInvitedUser, @@ -19,23 +31,11 @@ import { hasPassword, isInviteExpired, isUsernameTaken, -} from "#lib/db/users.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { validateForm } from "#lib/forms.tsx"; -import { nowMs } from "#lib/now.ts"; -import type { User } from "#lib/types.ts"; -import { createConfirmedHandlers } from "#routes/admin/confirmation.ts"; -import { - type AuthSession, - generateSecureToken, - OWNER_FORM, - ownerPage, - requireOwnerOr, - withAuth, -} from "#routes/auth.ts"; -import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; -import { defineRoutes, type TypedRouteHandler } from "#routes/router.ts"; -import { getSearchParam } from "#routes/url.ts"; +} from "#shared/db/users.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { validateForm } from "#shared/forms.tsx"; +import { nowMs } from "#shared/now.ts"; +import type { User } from "#shared/types.ts"; import { adminUserDeletePage, @@ -206,7 +206,7 @@ const handleUserActivate: UserActionHandler = async ( ); // Decrypt user's password hash to derive their KEK - const { decrypt } = await import("#lib/crypto/encryption.ts"); + const { decrypt } = await import("#shared/crypto/encryption.ts"); const decryptedPasswordHash = await decrypt(user.password_hash); await activateUser(user.id, dataKey, decryptedPasswordHash); diff --git a/src/routes/health.ts b/src/features/api/health.ts similarity index 100% rename from src/routes/health.ts rename to src/features/api/health.ts diff --git a/src/routes/api.ts b/src/features/api/index.ts similarity index 94% rename from src/routes/api.ts rename to src/features/api/index.ts index 3a9928be7..b9468f572 100644 --- a/src/routes/api.ts +++ b/src/features/api/index.ts @@ -6,23 +6,23 @@ */ import { filter, pipe } from "#fp"; -import { processBooking } from "#lib/booking.ts"; -import { getAvailableDates } from "#lib/dates.ts"; -import { - getGroupRemainingByEventId, - getGroupRemainingForEvent, - hasAvailableSpots, -} from "#lib/db/attendees.ts"; -import { getAllEvents, getEventWithCountBySlug } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { FormParams } from "#lib/form-data.ts"; -import { sortEvents } from "#lib/sort-events.ts"; -import { type EventWithCount, isPaidEvent } from "#lib/types.ts"; import { isRegistrationClosed } from "#routes/format.ts"; import { parseCustomPrice } from "#routes/public/ticket-form.ts"; import { jsonResponse } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; import { getBaseUrl } from "#routes/url.ts"; +import { processBooking } from "#shared/booking.ts"; +import { getAvailableDates } from "#shared/dates.ts"; +import { + getGroupRemainingByEventId, + getGroupRemainingForEvent, +} from "#shared/db/attendees/capacity.ts"; +import { hasAvailableSpots } from "#shared/db/attendees.ts"; +import { getAllEvents, getEventWithCountBySlug } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { FormParams } from "#shared/form-data.ts"; +import { sortEvents } from "#shared/sort-events.ts"; +import { type EventWithCount, isPaidEvent } from "#shared/types.ts"; import { extractContact, tryValidateTicketFields } from "#templates/fields.ts"; // ============================================================================= @@ -210,7 +210,7 @@ const toFormParams = (body: Record): FormParams => /** Map a BookingResult to an API JSON response */ const bookingResultToResponse = ( - result: import("#lib/booking.ts").BookingResult, + result: import("#shared/booking.ts").BookingResult, ): Response => { switch (result.type) { case "success": diff --git a/src/routes/webhook-types.ts b/src/features/api/webhook-types.ts similarity index 91% rename from src/routes/webhook-types.ts rename to src/features/api/webhook-types.ts index f5aa7fa19..55b86f7bd 100644 --- a/src/routes/webhook-types.ts +++ b/src/features/api/webhook-types.ts @@ -2,8 +2,11 @@ * Types for webhook route handlers (payment callbacks and provider webhooks) */ -import type { BookingIntent, ValidatedPaymentSession } from "#lib/payments.ts"; -import type { Attendee, EventWithCount } from "#lib/types.ts"; +import type { + BookingIntent, + ValidatedPaymentSession, +} from "#shared/payments.ts"; +import type { Attendee, EventWithCount } from "#shared/types.ts"; export type { BookingIntent }; diff --git a/src/routes/webhooks.ts b/src/features/api/webhooks.ts similarity index 97% rename from src/routes/webhooks.ts rename to src/features/api/webhooks.ts index 3482ada43..4c1518294 100644 --- a/src/routes/webhooks.ts +++ b/src/features/api/webhooks.ts @@ -15,30 +15,15 @@ */ import { unique } from "#fp"; -import { calculateBookingFee } from "#lib/booking-fee.ts"; -import { getBookingFee, getEffectiveDomain } from "#lib/config.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { - createAttendeeAtomic, - getAttendeesByTokens, -} from "#lib/db/attendees.ts"; -import { getEvent, getEventWithCount } from "#lib/db/events.ts"; -import { - clearSessionTokens, - decryptSessionTokens, - finalizeSession, - type ProcessedPayment, - reserveSession, -} from "#lib/db/processed-payments.ts"; -import { saveEventAnswers } from "#lib/db/questions.ts"; -import { ErrorCode, logDebug, logError } from "#lib/logger.ts"; -import { - type BookingItem, - getActivePaymentProvider, - type ValidatedPaymentSession, -} from "#lib/payments.ts"; -import type { EventWithCount } from "#lib/types.ts"; -import { logAndNotifyRegistration } from "#lib/webhook.ts"; +import type { + BookingIntent, + EventPriceValidation, + EventValidation, + PaymentFailureResult, + PaymentResult, + SessionValidation, + ValidatedSession, +} from "#routes/api/webhook-types.ts"; import { capacityErrorFormatter, isRegistrationClosed, @@ -53,17 +38,32 @@ import { redirectResponse, } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; -import { parseTokens } from "#routes/token-utils.ts"; +import { parseTokens } from "#routes/tickets/token-utils.ts"; import { getSearchParam } from "#routes/url.ts"; -import type { - BookingIntent, - EventPriceValidation, - EventValidation, - PaymentFailureResult, - PaymentResult, - SessionValidation, - ValidatedSession, -} from "#routes/webhook-types.ts"; +import { calculateBookingFee } from "#shared/booking-fee.ts"; +import { getBookingFee, getEffectiveDomain } from "#shared/config.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { + createAttendeeAtomic, + getAttendeesByTokens, +} from "#shared/db/attendees.ts"; +import { getEvent, getEventWithCount } from "#shared/db/events.ts"; +import { + clearSessionTokens, + decryptSessionTokens, + finalizeSession, + type ProcessedPayment, + reserveSession, +} from "#shared/db/processed-payments.ts"; +import { saveEventAnswers } from "#shared/db/questions.ts"; +import { ErrorCode, logDebug, logError } from "#shared/logger.ts"; +import { + type BookingItem, + getActivePaymentProvider, + type ValidatedPaymentSession, +} from "#shared/payments.ts"; +import type { EventWithCount } from "#shared/types.ts"; +import { logAndNotifyRegistration } from "#shared/webhook.ts"; import { paymentCancelPage, successPage } from "#templates/payment.tsx"; /** User-facing message when the event price changed between checkout and payment */ diff --git a/src/routes/assets.ts b/src/features/assets.ts similarity index 96% rename from src/routes/assets.ts rename to src/features/assets.ts index 9e6e3a2ae..434d7f780 100644 --- a/src/routes/assets.ts +++ b/src/features/assets.ts @@ -6,7 +6,7 @@ import { dirname, fromFileUrl, join } from "@std/path"; import { encodeBody } from "#routes/response.ts"; const currentDir = dirname(fromFileUrl(import.meta.url)); -const staticDir = join(currentDir, "..", "static"); +const staticDir = join(currentDir, "..", "ui", "static"); /** Cache for 1 year (immutable assets) */ const CACHE_HEADERS = { diff --git a/src/routes/attachments.ts b/src/features/attachments.ts similarity index 94% rename from src/routes/attachments.ts rename to src/features/attachments.ts index a2759bbf0..ce0559c37 100644 --- a/src/routes/attachments.ts +++ b/src/features/attachments.ts @@ -6,16 +6,16 @@ * Each download increments the attendee's attachment_downloads counter. */ -import { verifyAttachmentUrl } from "#lib/attachment-url.ts"; -import { - getAttendeeRaw, - incrementAttachmentDownloads, -} from "#lib/db/attendees.ts"; -import { getEvent } from "#lib/db/events.ts"; -import { downloadImage, isStorageEnabled } from "#lib/storage.ts"; import { notFoundResponse } from "#routes/response.ts"; import type { TypedRouteHandler } from "#routes/router.ts"; import { defineRoutes } from "#routes/router.ts"; +import { verifyAttachmentUrl } from "#shared/attachment-url.ts"; +import { + getAttendeeRaw, + incrementAttachmentDownloads, +} from "#shared/db/attendees.ts"; +import { getEvent } from "#shared/db/events.ts"; +import { downloadImage, isStorageEnabled } from "#shared/storage.ts"; /** Common MIME types by file extension */ const EXT_MIME_MAP: Record = { diff --git a/src/routes/auth.ts b/src/features/auth.ts similarity index 94% rename from src/routes/auth.ts rename to src/features/auth.ts index d7f3832ce..0d94659eb 100644 --- a/src/routes/auth.ts +++ b/src/features/auth.ts @@ -2,22 +2,6 @@ * Authentication and session utilities */ -import { getSessionCookieName } from "#lib/cookies.ts"; -import { - getPrivateKeyFromSession, - unwrapKeyWithToken, -} from "#lib/crypto/keys.ts"; -import { generateSecureToken } from "#lib/crypto/utils.ts"; -import { signCsrfToken, verifySignedCsrfToken } from "#lib/csrf.ts"; -import { getApiKeyByToken, touchApiKeyLastUsed } from "#lib/db/api-keys.ts"; -import { deleteSession, getSession } from "#lib/db/sessions.ts"; -import { settings } from "#lib/db/settings.ts"; -import { decryptAdminLevel, getUserById } from "#lib/db/users.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { nowMs } from "#lib/now.ts"; -import { getCachedSession, setCachedSession } from "#lib/session-context.ts"; -import type { AdminLevel } from "#lib/types.ts"; import { parseFormData } from "#routes/csrf.ts"; import { htmlResponse, @@ -25,6 +9,22 @@ import { redirectResponse, } from "#routes/response.ts"; import { parseCookies } from "#routes/url.ts"; +import { getSessionCookieName } from "#shared/cookies.ts"; +import { + getPrivateKeyFromSession, + unwrapKeyWithToken, +} from "#shared/crypto/keys.ts"; +import { generateSecureToken } from "#shared/crypto/utils.ts"; +import { signCsrfToken, verifySignedCsrfToken } from "#shared/csrf.ts"; +import { getApiKeyByToken, touchApiKeyLastUsed } from "#shared/db/api-keys.ts"; +import { deleteSession, getSession } from "#shared/db/sessions.ts"; +import { settings } from "#shared/db/settings.ts"; +import { decryptAdminLevel, getUserById } from "#shared/db/users.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { nowMs } from "#shared/now.ts"; +import { getCachedSession, setCachedSession } from "#shared/session-context.ts"; +import type { AdminLevel } from "#shared/types.ts"; // Re-export for callers that need it export { generateSecureToken }; diff --git a/src/routes/checkin.ts b/src/features/checkin.ts similarity index 94% rename from src/routes/checkin.ts rename to src/features/checkin.ts index 41637a5de..8699436f0 100644 --- a/src/routes/checkin.ts +++ b/src/features/checkin.ts @@ -5,10 +5,6 @@ */ import { filter, map } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; -import { updateCheckedIn } from "#lib/db/attendees.ts"; -import { settings } from "#lib/db/settings.ts"; -import type { Attendee } from "#lib/types.ts"; import { AUTH_FORM, type AuthSession, @@ -27,8 +23,12 @@ import { lookupAttendees, resolveEntries, type TokenEntry, -} from "#routes/token-utils.ts"; +} from "#routes/tickets/token-utils.ts"; import { getSearchParam } from "#routes/url.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { updateCheckedIn } from "#shared/db/attendees.ts"; +import { settings } from "#shared/db/settings.ts"; +import type { Attendee } from "#shared/types.ts"; import { checkinAdminPage, checkinPublicPage } from "#templates/checkin.tsx"; const formatTicketCount = (count: number): string => { diff --git a/src/routes/csrf.ts b/src/features/csrf.ts similarity index 91% rename from src/routes/csrf.ts rename to src/features/csrf.ts index 40b6dbd65..515633861 100644 --- a/src/routes/csrf.ts +++ b/src/features/csrf.ts @@ -2,17 +2,21 @@ * Form parsing and CSRF utilities */ +import { getSearchParam } from "#routes/url.ts"; import { CSRF_INVALID_FORM_MESSAGE, signCsrfToken, verifySignedCsrfToken, -} from "#lib/csrf.ts"; -import { getFlash } from "#lib/flash-context.ts"; -import { FormParams } from "#lib/form-data.ts"; -import { setFormError, setFormSuccess, setSavedFormData } from "#lib/forms.tsx"; -import { getSearchParam } from "#routes/url.ts"; +} from "#shared/csrf.ts"; +import { getFlash } from "#shared/flash-context.ts"; +import { FormParams } from "#shared/form-data.ts"; +import { + setFormError, + setFormSuccess, + setSavedFormData, +} from "#shared/forms.tsx"; -export { FormParams } from "#lib/form-data.ts"; +export { FormParams } from "#shared/form-data.ts"; /** * Parse form data from request diff --git a/src/routes/entity.ts b/src/features/entity.ts similarity index 93% rename from src/routes/entity.ts rename to src/features/entity.ts index e08f7d2ac..d60fc5a72 100644 --- a/src/routes/entity.ts +++ b/src/features/entity.ts @@ -2,14 +2,14 @@ * Entity loading patterns for route handlers */ -import { createAuthedHandler } from "#lib/app-forms.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { AdminLevel } from "#lib/types.ts"; import { type AuthSession, OWNER_FORM, requireSessionOr, } from "#routes/auth.ts"; +import { createAuthedHandler } from "#shared/app-forms.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { AdminLevel } from "#shared/types.ts"; /** * Resolve a nullable promise, calling handler if found or returning 404. diff --git a/src/routes/feeds.ts b/src/features/feeds.ts similarity index 96% rename from src/routes/feeds.ts rename to src/features/feeds.ts index 732811666..1edce33c2 100644 --- a/src/routes/feeds.ts +++ b/src/features/feeds.ts @@ -4,9 +4,6 @@ */ import { map, pipe } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; -import { settings } from "#lib/db/settings.ts"; -import { type EventWithCount, loadSortedEvents } from "#lib/sort-events.ts"; import { isRegistrationClosed } from "#routes/format.ts"; import { icsResponse, @@ -14,6 +11,9 @@ import { rssResponse, } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { settings } from "#shared/db/settings.ts"; +import { type EventWithCount, loadSortedEvents } from "#shared/sort-events.ts"; import { escapeHtml } from "#templates/layout.tsx"; /** Escape text for ICS (RFC 5545): backslash-escape special characters */ diff --git a/src/routes/format.ts b/src/features/format.ts similarity index 97% rename from src/routes/format.ts rename to src/features/format.ts index c8b6f5418..68f0ba6b1 100644 --- a/src/routes/format.ts +++ b/src/features/format.ts @@ -2,7 +2,7 @@ * Formatting utilities for dates, errors, and display text */ -import { nowMs } from "#lib/now.ts"; +import { nowMs } from "#shared/now.ts"; /** Check if an event's registration period has closed */ export const isRegistrationClosed = (event: { diff --git a/src/routes/images.ts b/src/features/images.ts similarity index 97% rename from src/routes/images.ts rename to src/features/images.ts index 1f8d36a61..de3ce5873 100644 --- a/src/routes/images.ts +++ b/src/features/images.ts @@ -3,13 +3,13 @@ * GET /image/:filename — downloads, decrypts, and serves the image. */ +import { notFoundResponse } from "#routes/response.ts"; +import type { createRouter } from "#routes/router.ts"; import { downloadImage, getMimeTypeFromFilename, isStorageEnabled, -} from "#lib/storage.ts"; -import { notFoundResponse } from "#routes/response.ts"; -import type { createRouter } from "#routes/router.ts"; +} from "#shared/storage.ts"; type RouterFn = ReturnType; diff --git a/src/routes/index.ts b/src/features/index.ts similarity index 93% rename from src/routes/index.ts rename to src/features/index.ts index 11935a32e..f347fedec 100644 --- a/src/routes/index.ts +++ b/src/features/index.ts @@ -4,35 +4,6 @@ */ import { once, reduce } from "#fp"; -import { loadEffectiveDomain } from "#lib/config.ts"; -import { - clearFlashCookie, - clearSessionCookie, - parseFlashValue, -} from "#lib/cookies.ts"; -import { maybeRunPrunes } from "#lib/db/prune.ts"; -import { runWithQueryLogContext } from "#lib/db/query-log.ts"; -import { settings } from "#lib/db/settings.ts"; -import { isReadOnly } from "#lib/env.ts"; -import { - hasFlash, - runWithFlashContext, - setFlashContext, -} from "#lib/flash-context.ts"; -import { clearSavedFormData } from "#lib/forms.tsx"; -import { detectIframeMode } from "#lib/iframe.ts"; -import { - createRequestTimer, - ErrorCode, - formatRequestError, - logError, - logRequest, - runWithRequestId, -} from "#lib/logger.ts"; -import { addPendingWork, flushPendingWork } from "#lib/pending-work.ts"; -import { runWithRequestCache } from "#lib/request-cache.ts"; -import { runWithSessionContext } from "#lib/session-context.ts"; -import { getRethrowErrors } from "#lib/test-overrides.ts"; import { SessionKeyError } from "#routes/auth.ts"; import { applySecurityHeaders, @@ -54,6 +25,35 @@ import { createRouter } from "#routes/router.ts"; import { routeStatic } from "#routes/static.ts"; import type { ServerContext } from "#routes/types.ts"; import { normalizePath, parseCookies, parseRequest } from "#routes/url.ts"; +import { loadEffectiveDomain } from "#shared/config.ts"; +import { + clearFlashCookie, + clearSessionCookie, + parseFlashValue, +} from "#shared/cookies.ts"; +import { maybeRunPrunes } from "#shared/db/prune.ts"; +import { runWithQueryLogContext } from "#shared/db/query-log.ts"; +import { settings } from "#shared/db/settings.ts"; +import { isReadOnly } from "#shared/env.ts"; +import { + hasFlash, + runWithFlashContext, + setFlashContext, +} from "#shared/flash-context.ts"; +import { clearSavedFormData } from "#shared/forms.tsx"; +import { detectIframeMode } from "#shared/iframe.ts"; +import { + createRequestTimer, + ErrorCode, + formatRequestError, + logError, + logRequest, + runWithRequestId, +} from "#shared/logger.ts"; +import { addPendingWork, flushPendingWork } from "#shared/pending-work.ts"; +import { runWithRequestCache } from "#shared/request-cache.ts"; +import { runWithSessionContext } from "#shared/session-context.ts"; +import { getRethrowErrors } from "#shared/test-overrides.ts"; import { readOnlyPage } from "#templates/public.tsx"; /** Router function type - reuse from router.ts */ @@ -95,7 +95,7 @@ const loadSetupRoutes = once(async () => { /** Lazy-load payment/webhook routes */ const loadPaymentRoutes = once(async () => { - const { routePayment } = await import("#routes/webhooks.ts"); + const { routePayment } = await import("#routes/api/webhooks.ts"); return routePayment; }); @@ -107,7 +107,7 @@ const loadJoinRoutes = once(async () => { /** Lazy-load ticket view routes */ const loadTicketViewRoutes = once(async () => { - const { routeTicketView } = await import("#routes/tickets.ts"); + const { routeTicketView } = await import("#routes/tickets/index.ts"); return routeTicketView; }); @@ -145,27 +145,27 @@ const loadAttachmentRoutes = once(async () => { /** Lazy-load Apple Wallet pass routes */ const loadWalletRoutes = once(async () => { - const { routeWallet } = await import("#routes/wallet.ts"); + const { routeWallet } = await import("#routes/wallet/index.ts"); return routeWallet; }); /** Lazy-load Google Wallet pass routes */ const loadGoogleWalletRoutes = once(async () => { - const { routeGoogleWallet } = await import("#routes/google-wallet.ts"); + const { routeGoogleWallet } = await import("#routes/wallet/google.ts"); return routeGoogleWallet; }); /** Lazy-load Apple Wallet web service routes (v1 API for pass updates) */ const loadWalletWebserviceRoutes = once(async () => { const { routeWalletWebservice } = await import( - "#routes/wallet-webservice.ts" + "#routes/wallet/webservice.ts" ); return routeWalletWebservice; }); /** Lazy-load public API routes */ const loadApiRoutes = once(async () => { - const { routeApi } = await import("#routes/api.ts"); + const { routeApi } = await import("#routes/api/index.ts"); return routeApi; }); diff --git a/src/routes/join.ts b/src/features/join.ts similarity index 94% rename from src/routes/join.ts rename to src/features/join.ts index 86295a21c..c0379f7c2 100644 --- a/src/routes/join.ts +++ b/src/features/join.ts @@ -2,19 +2,19 @@ * Join routes - public invite acceptance flow */ -import { createFormRoute } from "#lib/app-forms.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; +import { applyFlash } from "#routes/csrf.ts"; +import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; +import { createRouter, defineRoutes } from "#routes/router.ts"; +import { createFormRoute } from "#shared/app-forms.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; import { decryptUsername, getUserByInviteCode, isInviteValid, setUserPassword, -} from "#lib/db/users.ts"; -import { defineForm } from "#lib/forms.tsx"; -import type { User } from "#lib/types.ts"; -import { applyFlash } from "#routes/csrf.ts"; -import { errorRedirect, htmlResponse, redirect } from "#routes/response.ts"; -import { createRouter, defineRoutes } from "#routes/router.ts"; +} from "#shared/db/users.ts"; +import { defineForm } from "#shared/forms.tsx"; +import type { User } from "#shared/types.ts"; import { joinCompletePage, joinErrorPage, joinPage } from "#templates/join.tsx"; export const joinForm = defineForm({ diff --git a/src/routes/middleware.ts b/src/features/middleware.ts similarity index 97% rename from src/routes/middleware.ts rename to src/features/middleware.ts index 78b0ccd6f..68c51d8bf 100644 --- a/src/routes/middleware.ts +++ b/src/features/middleware.ts @@ -2,11 +2,11 @@ * Middleware functions for request processing */ -import { getEffectiveDomain, getEmbedHosts } from "#lib/config.ts"; -import { settings } from "#lib/db/settings.ts"; -import { buildFrameAncestors } from "#lib/embed-hosts.ts"; import { SCAN_API_PATTERN } from "#routes/admin/scanner.ts"; import { encodeBody } from "#routes/response.ts"; +import { getEffectiveDomain, getEmbedHosts } from "#shared/config.ts"; +import { settings } from "#shared/db/settings.ts"; +import { buildFrameAncestors } from "#shared/embed-hosts.ts"; /** * Security headers for all responses diff --git a/src/routes/public/groups.ts b/src/features/public/groups.ts similarity index 88% rename from src/routes/public/groups.ts rename to src/features/public/groups.ts index 4feb4831c..c4fb4bd9a 100644 --- a/src/routes/public/groups.ts +++ b/src/features/public/groups.ts @@ -2,15 +2,15 @@ * Group ticket context and routing */ +import { notFoundResponse } from "#routes/response.ts"; import { computeGroupSlugIndex, getActiveEventsByGroupId, getGroupBySlugIndex, -} from "#lib/db/groups.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { sortEvents } from "#lib/sort-events.ts"; -import type { Group } from "#lib/types.ts"; -import { notFoundResponse } from "#routes/response.ts"; +} from "#shared/db/groups.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { sortEvents } from "#shared/sort-events.ts"; +import type { Group } from "#shared/types.ts"; import type { TicketEvent } from "#templates/public.tsx"; import { buildTicketEventsWithGroupCapacity } from "./ticket-events.ts"; import { getTicketContext } from "./ticket-payment.ts"; diff --git a/src/routes/public/pages.ts b/src/features/public/pages.ts similarity index 91% rename from src/routes/public/pages.ts rename to src/features/public/pages.ts index 132e640da..075b0f9db 100644 --- a/src/routes/public/pages.ts +++ b/src/features/public/pages.ts @@ -2,15 +2,15 @@ * Public pages - home, events, terms, contact */ -import { getAllGroups } from "#lib/db/groups.ts"; -import { settings } from "#lib/db/settings.ts"; -import { loadSortedEvents } from "#lib/sort-events.ts"; -import type { EventWithCount, Group } from "#lib/types.ts"; import { htmlResponse, notFoundResponse, redirectResponse, } from "#routes/response.ts"; +import { getAllGroups } from "#shared/db/groups.ts"; +import { settings } from "#shared/db/settings.ts"; +import { loadSortedEvents } from "#shared/sort-events.ts"; +import type { EventWithCount, Group } from "#shared/types.ts"; import { homepagePage, type PublicPageType, diff --git a/src/routes/public/qr-book.ts b/src/features/public/qr-book.ts similarity index 89% rename from src/routes/public/qr-book.ts rename to src/features/public/qr-book.ts index 17e296bb8..bead872bb 100644 --- a/src/routes/public/qr-book.ts +++ b/src/features/public/qr-book.ts @@ -7,16 +7,16 @@ * with the token's values pre-filled. */ -import { getAvailableDates } from "#lib/dates.ts"; -import { getGroupRemainingForEvent } from "#lib/db/attendees.ts"; -import { getEventWithCountBySlug } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import type { CheckoutIntent } from "#lib/payments.ts"; -import { eventSupportsDirectCheckout } from "#lib/qr.ts"; -import { type QrBookPayload, verifyQrBookToken } from "#lib/qr-token.ts"; -import type { EventWithCount } from "#lib/types.ts"; import { isRegistrationClosed } from "#routes/format.ts"; import { htmlResponse } from "#routes/response.ts"; +import { getAvailableDates } from "#shared/dates.ts"; +import { getGroupRemainingForEvent } from "#shared/db/attendees/capacity.ts"; +import { getEventWithCountBySlug } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import type { CheckoutIntent } from "#shared/payments.ts"; +import { eventSupportsDirectCheckout } from "#shared/qr.ts"; +import { type QrBookPayload, verifyQrBookToken } from "#shared/qr-token.ts"; +import type { EventWithCount } from "#shared/types.ts"; import { buildTicketEvent, type QrPrefill, diff --git a/src/routes/public/ticket-events.ts b/src/features/public/ticket-events.ts similarity index 77% rename from src/routes/public/ticket-events.ts rename to src/features/public/ticket-events.ts index 1e74f086e..b4ccae42b 100644 --- a/src/routes/public/ticket-events.ts +++ b/src/features/public/ticket-events.ts @@ -1,6 +1,6 @@ -import { getGroupRemainingByEventId } from "#lib/db/attendees.ts"; -import type { EventWithCount } from "#lib/types.ts"; import { isRegistrationClosed } from "#routes/format.ts"; +import { getGroupRemainingByEventId } from "#shared/db/attendees.ts"; +import type { EventWithCount } from "#shared/types.ts"; import { buildTicketEvent, type TicketEvent } from "#templates/public.tsx"; export const buildTicketEventsWithGroupCapacity = async ( diff --git a/src/routes/public/ticket-form.ts b/src/features/public/ticket-form.ts similarity index 96% rename from src/routes/public/ticket-form.ts rename to src/features/public/ticket-form.ts index 47dad8754..bdab0781a 100644 --- a/src/routes/public/ticket-form.ts +++ b/src/features/public/ticket-form.ts @@ -3,15 +3,15 @@ */ import { filter, map } from "#fp"; -import { validatePrice } from "#lib/currency.ts"; +import { capacityErrorFormatter } from "#routes/format.ts"; +import { errorRedirect, htmlResponse } from "#routes/response.ts"; +import { validatePrice } from "#shared/currency.ts"; import type { QuestionEventMap, QuestionWithAnswers, -} from "#lib/db/questions.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { EventFields } from "#lib/types.ts"; -import { capacityErrorFormatter } from "#routes/format.ts"; -import { errorRedirect, htmlResponse } from "#routes/response.ts"; +} from "#shared/db/questions.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { EventFields } from "#shared/types.ts"; import { extractContact, mergeEventFields } from "#templates/fields.ts"; import { type TicketEvent, ticketPage } from "#templates/public.tsx"; import type { EventQty, TicketCtx } from "./types.ts"; diff --git a/src/routes/public/ticket-payment.ts b/src/features/public/ticket-payment.ts similarity index 91% rename from src/routes/public/ticket-payment.ts rename to src/features/public/ticket-payment.ts index f4fd3c3af..7ceb415a6 100644 --- a/src/routes/public/ticket-payment.ts +++ b/src/features/public/ticket-payment.ts @@ -3,33 +3,33 @@ */ import { compact } from "#fp"; -import { isPaymentsEnabled } from "#lib/config.ts"; -import { getAvailableDates } from "#lib/dates.ts"; -import type { CreateAttendeeResult } from "#lib/db/attendee-types.ts"; +import { + checkoutResponse, + errorRedirect, + notFoundResponse, +} from "#routes/response.ts"; +import { getBaseUrl } from "#routes/url.ts"; +import { isPaymentsEnabled } from "#shared/config.ts"; +import { getAvailableDates } from "#shared/dates.ts"; +import type { CreateAttendeeResult } from "#shared/db/attendee-types.ts"; import { checkBatchAvailability, createAttendeeAtomic, deleteAttendee, -} from "#lib/db/attendees.ts"; -import { getEventsBySlugsBatch } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import { getQuestionsWithEventIds } from "#lib/db/questions.ts"; -import { settings } from "#lib/db/settings.ts"; -import type { EmailEntry } from "#lib/email.ts"; -import { logDebug } from "#lib/logger.ts"; +} from "#shared/db/attendees.ts"; +import { getEventsBySlugsBatch } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import { getQuestionsWithEventIds } from "#shared/db/questions.ts"; +import { settings } from "#shared/db/settings.ts"; +import type { EmailEntry } from "#shared/email.ts"; +import { logDebug } from "#shared/logger.ts"; import { type CheckoutIntent, type CheckoutItem, getActivePaymentProvider, -} from "#lib/payments.ts"; -import type { ContactInfo, Group } from "#lib/types.ts"; -import { logAndNotifyRegistration } from "#lib/webhook.ts"; -import { - checkoutResponse, - errorRedirect, - notFoundResponse, -} from "#routes/response.ts"; -import { getBaseUrl } from "#routes/url.ts"; +} from "#shared/payments.ts"; +import type { ContactInfo, Group } from "#shared/types.ts"; +import { logAndNotifyRegistration } from "#shared/webhook.ts"; import type { TicketEvent } from "#templates/public.tsx"; import { buildTicketEventsWithGroupCapacity } from "./ticket-events.ts"; import { eventsWithQuantity, formatAtomicError } from "./ticket-form.ts"; @@ -69,7 +69,7 @@ export const runCheckoutFlow = ( createSession: ( provider: Awaited> & object, baseUrl: string, - ) => Promise, + ) => Promise, onError: (msg: string, status: number) => Response, ): Promise => { logDebug("Payment", `Starting ${label} checkout`); diff --git a/src/routes/public/ticket-routes.ts b/src/features/public/ticket-routes.ts similarity index 90% rename from src/routes/public/ticket-routes.ts rename to src/features/public/ticket-routes.ts index 925568c65..4182d08b6 100644 --- a/src/routes/public/ticket-routes.ts +++ b/src/features/public/ticket-routes.ts @@ -2,13 +2,16 @@ * Route definitions, endpoint handlers, and the routeTicket router */ -import { getEffectiveDomain } from "#lib/config.ts"; -import { getEventWithCountBySlug } from "#lib/db/events.ts"; -import { computeGroupSlugIndex, getGroupBySlugIndex } from "#lib/db/groups.ts"; -import { getEmailConfig, getHostEmailConfig } from "#lib/email.ts"; -import { generateQrSvg } from "#lib/qr.ts"; import { htmlResponse, notFoundResponse } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { getEventWithCountBySlug } from "#shared/db/events.ts"; +import { + computeGroupSlugIndex, + getGroupBySlugIndex, +} from "#shared/db/groups.ts"; +import { getEmailConfig, getHostEmailConfig } from "#shared/email.ts"; +import { generateQrSvg } from "#shared/qr.ts"; import { successPage } from "#templates/payment.tsx"; import { handleGroupTicketBySlug } from "./groups.ts"; import { handleQrBookGet } from "./qr-book.ts"; diff --git a/src/routes/public/ticket-submit.ts b/src/features/public/ticket-submit.ts similarity index 96% rename from src/routes/public/ticket-submit.ts rename to src/features/public/ticket-submit.ts index 86ee72f88..4e73d2589 100644 --- a/src/routes/public/ticket-submit.ts +++ b/src/features/public/ticket-submit.ts @@ -3,17 +3,17 @@ */ import { reduce } from "#fp"; -import { signCsrfToken } from "#lib/csrf.ts"; -import { countAssignableSites } from "#lib/db/built-sites.ts"; -import { saveEventAnswers } from "#lib/db/questions.ts"; -import { ATTENDEE_DEMO_FIELDS, applyDemoOverrides } from "#lib/demo.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { verifyQrBookToken } from "#lib/qr-token.ts"; -import { isPaidEvent } from "#lib/types.ts"; import { isBuilderEnabled } from "#routes/admin/builder.ts"; import { applyFlash, withCsrfForm } from "#routes/csrf.ts"; import { errorRedirect, redirectResponse } from "#routes/response.ts"; import { getBaseUrl } from "#routes/url.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { countAssignableSites } from "#shared/db/built-sites.ts"; +import { saveEventAnswers } from "#shared/db/questions.ts"; +import { ATTENDEE_DEMO_FIELDS, applyDemoOverrides } from "#shared/demo.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { verifyQrBookToken } from "#shared/qr-token.ts"; +import { isPaidEvent } from "#shared/types.ts"; import { type TicketFormValues, tryValidateTicketFields, diff --git a/src/routes/public/types.ts b/src/features/public/types.ts similarity index 95% rename from src/routes/public/types.ts rename to src/features/public/types.ts index aecfff219..057f9d7ea 100644 --- a/src/routes/public/types.ts +++ b/src/features/public/types.ts @@ -5,8 +5,8 @@ import type { QuestionEventMap, QuestionWithAnswers, -} from "#lib/db/questions.ts"; -import type { EventWithCount } from "#lib/types.ts"; +} from "#shared/db/questions.ts"; +import type { EventWithCount } from "#shared/types.ts"; import type { QrPrefill, TicketEvent } from "#templates/public.tsx"; /** Shared rendering context for ticket pages */ diff --git a/src/routes/response.ts b/src/features/response.ts similarity index 96% rename from src/routes/response.ts rename to src/features/response.ts index 8351cffed..255ad097a 100644 --- a/src/routes/response.ts +++ b/src/features/response.ts @@ -2,9 +2,9 @@ * Response builder utilities for route handlers */ -import { buildFlashCookie } from "#lib/cookies.ts"; -import { appendIframeParam, getIframeMode } from "#lib/iframe.ts"; -import { getRequestId } from "#lib/logger.ts"; +import { buildFlashCookie } from "#shared/cookies.ts"; +import { appendIframeParam, getIframeMode } from "#shared/iframe.ts"; +import { getRequestId } from "#shared/logger.ts"; import { checkoutPopupPage, paymentErrorPage } from "#templates/payment.tsx"; import { notFoundPage, diff --git a/src/routes/router.ts b/src/features/router.ts similarity index 100% rename from src/routes/router.ts rename to src/features/router.ts diff --git a/src/routes/setup.ts b/src/features/setup.ts similarity index 92% rename from src/routes/setup.ts rename to src/features/setup.ts index bcf85da2b..a83ced351 100644 --- a/src/routes/setup.ts +++ b/src/features/setup.ts @@ -2,13 +2,6 @@ * Setup routes - initial system configuration */ -import { isValidCountry } from "#lib/countries.ts"; -import { signCsrfToken, verifySignedCsrfToken } from "#lib/csrf.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { settings } from "#lib/db/settings.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { validateForm } from "#lib/forms.tsx"; -import { ErrorCode, logDebug, logError } from "#lib/logger.ts"; import { applyFlash, parseFormData } from "#routes/csrf.ts"; import { errorRedirect, @@ -16,6 +9,13 @@ import { redirectResponse, } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; +import { isValidCountry } from "#shared/countries.ts"; +import { signCsrfToken, verifySignedCsrfToken } from "#shared/csrf.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { settings } from "#shared/db/settings.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { validateForm } from "#shared/forms.tsx"; +import { ErrorCode, logDebug, logError } from "#shared/logger.ts"; import { type SetupFormValues, setupFields } from "#templates/fields.ts"; import { setupCompletePage, setupPage } from "#templates/setup.tsx"; diff --git a/src/routes/static.ts b/src/features/static.ts similarity index 94% rename from src/routes/static.ts rename to src/features/static.ts index fdf81a90d..08c571a53 100644 --- a/src/routes/static.ts +++ b/src/features/static.ts @@ -2,6 +2,7 @@ * Static routes - health check and assets (always available) */ +import { handleHealthCheck } from "#routes/api/health.ts"; import { handleAdminJs, handleEmbedJs, @@ -12,7 +13,6 @@ import { handleRobotsTxt, handleScannerJs, } from "#routes/assets.ts"; -import { handleHealthCheck } from "#routes/health.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; /** Static routes definition */ diff --git a/src/routes/tickets.ts b/src/features/tickets/index.ts similarity index 91% rename from src/routes/tickets.ts rename to src/features/tickets/index.ts index b527dad80..44250cf29 100644 --- a/src/routes/tickets.ts +++ b/src/features/tickets/index.ts @@ -4,10 +4,6 @@ * The SVG endpoint serves individual QR codes for CDN caching. */ -import { signAttachmentUrl } from "#lib/attachment-url.ts"; -import { settings } from "#lib/db/settings.ts"; -import { generateQrSvg } from "#lib/qr.ts"; -import { buildCheckinUrl } from "#lib/ticket-url.ts"; import { htmlResponse } from "#routes/response.ts"; import { createTokenRoute, @@ -16,7 +12,11 @@ import { type TokenEntry, type TokenRouteFn, withTokenRateLimit, -} from "#routes/token-utils.ts"; +} from "#routes/tickets/token-utils.ts"; +import { signAttachmentUrl } from "#shared/attachment-url.ts"; +import { settings } from "#shared/db/settings.ts"; +import { generateQrSvg } from "#shared/qr.ts"; +import { buildCheckinUrl } from "#shared/ticket-url.ts"; import { type TicketCard, ticketViewPage } from "#templates/tickets.tsx"; /** Build a ticket card for a single token/entry pair */ diff --git a/src/routes/token-utils.ts b/src/features/tickets/token-utils.ts similarity index 94% rename from src/routes/token-utils.ts rename to src/features/tickets/token-utils.ts index 29be8397b..7b50705f1 100644 --- a/src/routes/token-utils.ts +++ b/src/features/tickets/token-utils.ts @@ -3,26 +3,26 @@ */ import { compact, unique } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; +import { notFoundResponse, rateLimitedResponse } from "#routes/response.ts"; +import type { PathMethodRoute, ServerContext } from "#routes/types.ts"; +import { getClientIp } from "#routes/url.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; import { type AttendeeWithBookings, decryptAttendees, type EventAttendeeRow, getAttendeesByTokens, -} from "#lib/db/attendees.ts"; -import { getEventWithCount } from "#lib/db/events.ts"; -import { settings } from "#lib/db/settings.ts"; +} from "#shared/db/attendees.ts"; +import { getEventWithCount } from "#shared/db/events.ts"; +import { settings } from "#shared/db/settings.ts"; import { clearTokenAttempts, isTokenRateLimited, recordTokenFailure, -} from "#lib/db/token-attempts.ts"; -import { addPendingWork } from "#lib/pending-work.ts"; -import { buildCheckinUrl } from "#lib/ticket-url.ts"; -import type { Attendee, EventWithCount } from "#lib/types.ts"; -import { notFoundResponse, rateLimitedResponse } from "#routes/response.ts"; -import type { PathMethodRoute, ServerContext } from "#routes/types.ts"; -import { getClientIp } from "#routes/url.ts"; +} from "#shared/db/token-attempts.ts"; +import { addPendingWork } from "#shared/pending-work.ts"; +import { buildCheckinUrl } from "#shared/ticket-url.ts"; +import type { Attendee, EventWithCount } from "#shared/types.ts"; /** Attendee paired with its event */ export type TokenEntry = { diff --git a/src/routes/types.ts b/src/features/types.ts similarity index 100% rename from src/routes/types.ts rename to src/features/types.ts diff --git a/src/routes/url.ts b/src/features/url.ts similarity index 100% rename from src/routes/url.ts rename to src/features/url.ts diff --git a/src/routes/google-wallet.ts b/src/features/wallet/google.ts similarity index 86% rename from src/routes/google-wallet.ts rename to src/features/wallet/google.ts index 1213fdd3e..d4c622536 100644 --- a/src/routes/google-wallet.ts +++ b/src/features/wallet/google.ts @@ -3,14 +3,14 @@ * Generates a signed JWT and redirects to the Google Wallet save URL. */ -import { settings } from "#lib/db/settings.ts"; -import { buildGoogleWalletUrl } from "#lib/google-wallet.ts"; import { notFoundResponse } from "#routes/response.ts"; import { createTokenRoute, lookupSingleTokenPassData, WALLET_CACHE_CONTROL, -} from "#routes/token-utils.ts"; +} from "#routes/tickets/token-utils.ts"; +import { settings } from "#shared/db/settings.ts"; +import { buildGoogleWalletUrl } from "#shared/google-wallet.ts"; /** Handle GET /gwallet/:token — redirect to Google Wallet save URL */ const handleGoogleWalletGet = async ( diff --git a/src/routes/wallet.ts b/src/features/wallet/index.ts similarity index 90% rename from src/routes/wallet.ts rename to src/features/wallet/index.ts index 1b8501220..2d7c9fd7a 100644 --- a/src/routes/wallet.ts +++ b/src/features/wallet/index.ts @@ -4,15 +4,15 @@ * CDN-cacheable — passes are deterministic for a given token + settings. */ -import { buildPkpass, type SigningCredentials } from "#lib/apple-wallet.ts"; -import { getEffectiveDomain } from "#lib/config.ts"; -import { settings } from "#lib/db/settings.ts"; import { notFoundResponse } from "#routes/response.ts"; import { createTokenRoute, lookupSingleTokenPassData, WALLET_CACHE_CONTROL, -} from "#routes/token-utils.ts"; +} from "#routes/tickets/token-utils.ts"; +import { buildPkpass, type SigningCredentials } from "#shared/apple-wallet.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { settings } from "#shared/db/settings.ts"; /** MIME type for Apple Wallet passes */ const PKPASS_CONTENT_TYPE = "application/vnd.apple.pkpass"; diff --git a/src/routes/wallet-webservice.ts b/src/features/wallet/webservice.ts similarity index 92% rename from src/routes/wallet-webservice.ts rename to src/features/wallet/webservice.ts index 774b27f2e..d999768ef 100644 --- a/src/routes/wallet-webservice.ts +++ b/src/features/wallet/webservice.ts @@ -12,11 +12,14 @@ * so devices re-download the pass on every manual refresh. */ -import { type SigningCredentials, trimAuthToken } from "#lib/apple-wallet.ts"; -import { settings } from "#lib/db/settings.ts"; -import { logDebug } from "#lib/logger.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; -import { buildPkpassForToken } from "#routes/wallet.ts"; +import { buildPkpassForToken } from "#routes/wallet/index.ts"; +import { + type SigningCredentials, + trimAuthToken, +} from "#shared/apple-wallet.ts"; +import { settings } from "#shared/db/settings.ts"; +import { logDebug } from "#shared/logger.ts"; const JSON_HEADERS = { "Content-Type": "application/json" } as const; diff --git a/src/index.ts b/src/index.ts index c0b1e1611..8cd03ab24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,14 +2,15 @@ * Entry point for ticket reservation system */ -import { validateEncryptionKey } from "#lib/crypto/encryption.ts"; -import { initDb } from "#lib/db/migrations.ts"; import { handleRequest } from "#routes/index.ts"; +import { validateEncryptionKey } from "#shared/crypto/encryption.ts"; +import { initDb } from "#shared/db/migrations.ts"; +import { logDebug } from "#shared/logger.ts"; const startServer = async (port = 3000): Promise => { validateEncryptionKey(); await initDb(); - console.log(`Server starting on http://localhost:${port}`); + logDebug("Setup", `Server starting on http://localhost:${port}`); Deno.serve({ port }, (request) => handleRequest(request)); }; diff --git a/src/lib/admin-api-example.ts b/src/shared/admin-api-example.ts similarity index 97% rename from src/lib/admin-api-example.ts rename to src/shared/admin-api-example.ts index d8d82dd05..37547d380 100644 --- a/src/lib/admin-api-example.ts +++ b/src/shared/admin-api-example.ts @@ -6,17 +6,17 @@ * output, so a shape change will break the test and force an update. */ -import { - API_EXAMPLE_EVENT, - API_EXAMPLE_PUBLIC_EVENT, -} from "#lib/api-example.ts"; -import type { AdminEvent, EventWithCount } from "#lib/types.ts"; import { type CreateEventBody, type DeleteEventBody, toAdminEvent, type UpdateEventBody, } from "#routes/admin/api.ts"; +import { + API_EXAMPLE_EVENT, + API_EXAMPLE_PUBLIC_EVENT, +} from "#shared/api-example.ts"; +import type { AdminEvent, EventWithCount } from "#shared/types.ts"; /** Example EventWithCount used as the source for admin API examples */ export const ADMIN_API_EXAMPLE_EVENT: EventWithCount = API_EXAMPLE_EVENT; diff --git a/src/lib/api-example.ts b/src/shared/api-example.ts similarity index 93% rename from src/lib/api-example.ts rename to src/shared/api-example.ts index 7cbde689e..b4f6d1513 100644 --- a/src/lib/api-example.ts +++ b/src/shared/api-example.ts @@ -6,9 +6,9 @@ * output, so a shape change will break the test and force an update. */ -import type { EventWithCount } from "#lib/types.ts"; -import { EXAMPLE_EVENT } from "#lib/webhook-example.ts"; -import { type PublicEvent, toPublicEvent } from "#routes/api.ts"; +import { type PublicEvent, toPublicEvent } from "#routes/api/index.ts"; +import type { EventWithCount } from "#shared/types.ts"; +import { EXAMPLE_EVENT } from "#shared/webhook-example.ts"; /** Example event matching the webhook example data */ export const API_EXAMPLE_EVENT: EventWithCount = { diff --git a/src/lib/app-forms.ts b/src/shared/app-forms.ts similarity index 96% rename from src/lib/app-forms.ts rename to src/shared/app-forms.ts index f44034ed9..a2b2efc4c 100644 --- a/src/lib/app-forms.ts +++ b/src/shared/app-forms.ts @@ -1,6 +1,3 @@ -import { CSRF_INVALID_FORM_MESSAGE } from "#lib/csrf.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { ValidationResult } from "#lib/forms.tsx"; import { AUTH_FORM, type AuthPolicy, @@ -9,6 +6,9 @@ import { } from "#routes/auth.ts"; import { requireCsrfForm } from "#routes/csrf.ts"; import { notFoundResponse } from "#routes/response.ts"; +import { CSRF_INVALID_FORM_MESSAGE } from "#shared/csrf.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { ValidationResult } from "#shared/forms.tsx"; export type FormValidator = { validate: (form: FormParams) => ValidationResult; diff --git a/src/lib/apple-wallet.ts b/src/shared/apple-wallet.ts similarity index 98% rename from src/lib/apple-wallet.ts rename to src/shared/apple-wallet.ts index bc9900e1b..4a381b7d9 100644 --- a/src/lib/apple-wallet.ts +++ b/src/shared/apple-wallet.ts @@ -10,9 +10,9 @@ import { zipSync } from "fflate"; import forge from "node-forge"; -import { getDecimalPlaces } from "#lib/currency.ts"; -import { startOfHour } from "#lib/dates.ts"; -import { WALLET_ICONS } from "#lib/wallet-icons.ts"; +import { getDecimalPlaces } from "#shared/currency.ts"; +import { startOfHour } from "#shared/dates.ts"; +import { WALLET_ICONS } from "#shared/wallet-icons.ts"; // Force pure-JS mode so node-forge never attempts require("crypto"). // The Bunny Edge runtime sets process.versions.node (via the node:process diff --git a/src/lib/asset-paths.ts b/src/shared/asset-paths.ts similarity index 100% rename from src/lib/asset-paths.ts rename to src/shared/asset-paths.ts diff --git a/src/lib/attachment-url.ts b/src/shared/attachment-url.ts similarity index 87% rename from src/lib/attachment-url.ts rename to src/shared/attachment-url.ts index 9d446746a..01c8e7c1d 100644 --- a/src/lib/attachment-url.ts +++ b/src/shared/attachment-url.ts @@ -6,10 +6,10 @@ * their ticket page to get a fresh URL (prevents sharing). */ -import { hmacHash } from "#lib/crypto/hashing.ts"; -import { base64ToBase64Url, constantTimeEqual } from "#lib/crypto/utils.ts"; -import { ATTACHMENT_URL_MAX_AGE_S } from "#lib/limits.ts"; -import { nowMs } from "#lib/now.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; +import { base64ToBase64Url, constantTimeEqual } from "#shared/crypto/utils.ts"; +import { ATTACHMENT_URL_MAX_AGE_S } from "#shared/limits.ts"; +import { nowMs } from "#shared/now.ts"; /** Build the HMAC message for an attachment download */ const buildMessage = ( diff --git a/src/lib/band-name-generator.ts b/src/shared/band-name-generator.ts similarity index 79% rename from src/lib/band-name-generator.ts rename to src/shared/band-name-generator.ts index 8f6f16021..dcd5ddde1 100644 --- a/src/lib/band-name-generator.ts +++ b/src/shared/band-name-generator.ts @@ -8,32 +8,32 @@ * yet the variety is enormous. */ -import { BAND_ADJECTIVES } from "#lib/band-name-generator/adjectives.ts"; -import { AGE_NOTES } from "#lib/band-name-generator/age-notes.ts"; -import { AUDIENCE_OUTCOMES } from "#lib/band-name-generator/audience-outcomes.ts"; -import { BAND_DESCRIPTORS } from "#lib/band-name-generator/band-descriptors.ts"; -import { BAND_VERBS } from "#lib/band-name-generator/band-verbs.ts"; -import { BUILDING_STATES } from "#lib/band-name-generator/building-states.ts"; -import { CONNECTORS } from "#lib/band-name-generator/connectors.ts"; -import { CROSSOVERS } from "#lib/band-name-generator/crossovers.ts"; -import { EVENT_TYPES } from "#lib/band-name-generator/event-types.ts"; -import { FESTIVAL_TYPES } from "#lib/band-name-generator/festival-types.ts"; -import { GENRES } from "#lib/band-name-generator/genres.ts"; -import { INTENSITIES } from "#lib/band-name-generator/intensities.ts"; -import { NOISE_VERBS } from "#lib/band-name-generator/noise-verbs.ts"; -import { BAND_NOUNS } from "#lib/band-name-generator/nouns.ts"; -import { NUMBER_WORDS } from "#lib/band-name-generator/number-words.ts"; -import { ODD_INSTRUMENTS } from "#lib/band-name-generator/odd-instruments.ts"; -import { BAND_PERSON_NAMES } from "#lib/band-name-generator/person-names.ts"; -import { PRIZES } from "#lib/band-name-generator/prizes.ts"; -import { PROHIBITIONS } from "#lib/band-name-generator/prohibitions.ts"; -import { REQUIREMENTS } from "#lib/band-name-generator/requirements.ts"; -import { SHOW_ITEMS } from "#lib/band-name-generator/show-items.ts"; -import { BAND_SUFFIXES } from "#lib/band-name-generator/suffixes.ts"; -import { TIMES } from "#lib/band-name-generator/times.ts"; -import { TOUR_ADJECTIVES } from "#lib/band-name-generator/tour-adjectives.ts"; -import { VENUE_TYPES } from "#lib/band-name-generator/venue-types.ts"; -import { WEIRD_VENUES } from "#lib/band-name-generator/weird-venues.ts"; +import { BAND_ADJECTIVES } from "#shared/band-name-generator/adjectives.ts"; +import { AGE_NOTES } from "#shared/band-name-generator/age-notes.ts"; +import { AUDIENCE_OUTCOMES } from "#shared/band-name-generator/audience-outcomes.ts"; +import { BAND_DESCRIPTORS } from "#shared/band-name-generator/band-descriptors.ts"; +import { BAND_VERBS } from "#shared/band-name-generator/band-verbs.ts"; +import { BUILDING_STATES } from "#shared/band-name-generator/building-states.ts"; +import { CONNECTORS } from "#shared/band-name-generator/connectors.ts"; +import { CROSSOVERS } from "#shared/band-name-generator/crossovers.ts"; +import { EVENT_TYPES } from "#shared/band-name-generator/event-types.ts"; +import { FESTIVAL_TYPES } from "#shared/band-name-generator/festival-types.ts"; +import { GENRES } from "#shared/band-name-generator/genres.ts"; +import { INTENSITIES } from "#shared/band-name-generator/intensities.ts"; +import { NOISE_VERBS } from "#shared/band-name-generator/noise-verbs.ts"; +import { BAND_NOUNS } from "#shared/band-name-generator/nouns.ts"; +import { NUMBER_WORDS } from "#shared/band-name-generator/number-words.ts"; +import { ODD_INSTRUMENTS } from "#shared/band-name-generator/odd-instruments.ts"; +import { BAND_PERSON_NAMES } from "#shared/band-name-generator/person-names.ts"; +import { PRIZES } from "#shared/band-name-generator/prizes.ts"; +import { PROHIBITIONS } from "#shared/band-name-generator/prohibitions.ts"; +import { REQUIREMENTS } from "#shared/band-name-generator/requirements.ts"; +import { SHOW_ITEMS } from "#shared/band-name-generator/show-items.ts"; +import { BAND_SUFFIXES } from "#shared/band-name-generator/suffixes.ts"; +import { TIMES } from "#shared/band-name-generator/times.ts"; +import { TOUR_ADJECTIVES } from "#shared/band-name-generator/tour-adjectives.ts"; +import { VENUE_TYPES } from "#shared/band-name-generator/venue-types.ts"; +import { WEIRD_VENUES } from "#shared/band-name-generator/weird-venues.ts"; // Re-export every pool so existing call sites and tests can keep using // `import { BAND_ADJECTIVES } from "#lib/band-name-generator.ts"`. diff --git a/src/lib/band-name-generator/adjectives.ts b/src/shared/band-name-generator/adjectives.ts similarity index 100% rename from src/lib/band-name-generator/adjectives.ts rename to src/shared/band-name-generator/adjectives.ts diff --git a/src/lib/band-name-generator/age-notes.ts b/src/shared/band-name-generator/age-notes.ts similarity index 100% rename from src/lib/band-name-generator/age-notes.ts rename to src/shared/band-name-generator/age-notes.ts diff --git a/src/lib/band-name-generator/audience-outcomes.ts b/src/shared/band-name-generator/audience-outcomes.ts similarity index 100% rename from src/lib/band-name-generator/audience-outcomes.ts rename to src/shared/band-name-generator/audience-outcomes.ts diff --git a/src/lib/band-name-generator/band-descriptors.ts b/src/shared/band-name-generator/band-descriptors.ts similarity index 100% rename from src/lib/band-name-generator/band-descriptors.ts rename to src/shared/band-name-generator/band-descriptors.ts diff --git a/src/lib/band-name-generator/band-verbs.ts b/src/shared/band-name-generator/band-verbs.ts similarity index 100% rename from src/lib/band-name-generator/band-verbs.ts rename to src/shared/band-name-generator/band-verbs.ts diff --git a/src/lib/band-name-generator/building-states.ts b/src/shared/band-name-generator/building-states.ts similarity index 100% rename from src/lib/band-name-generator/building-states.ts rename to src/shared/band-name-generator/building-states.ts diff --git a/src/lib/band-name-generator/connectors.ts b/src/shared/band-name-generator/connectors.ts similarity index 100% rename from src/lib/band-name-generator/connectors.ts rename to src/shared/band-name-generator/connectors.ts diff --git a/src/lib/band-name-generator/crossovers.ts b/src/shared/band-name-generator/crossovers.ts similarity index 100% rename from src/lib/band-name-generator/crossovers.ts rename to src/shared/band-name-generator/crossovers.ts diff --git a/src/lib/band-name-generator/event-types.ts b/src/shared/band-name-generator/event-types.ts similarity index 100% rename from src/lib/band-name-generator/event-types.ts rename to src/shared/band-name-generator/event-types.ts diff --git a/src/lib/band-name-generator/festival-types.ts b/src/shared/band-name-generator/festival-types.ts similarity index 100% rename from src/lib/band-name-generator/festival-types.ts rename to src/shared/band-name-generator/festival-types.ts diff --git a/src/lib/band-name-generator/genres.ts b/src/shared/band-name-generator/genres.ts similarity index 100% rename from src/lib/band-name-generator/genres.ts rename to src/shared/band-name-generator/genres.ts diff --git a/src/lib/band-name-generator/intensities.ts b/src/shared/band-name-generator/intensities.ts similarity index 100% rename from src/lib/band-name-generator/intensities.ts rename to src/shared/band-name-generator/intensities.ts diff --git a/src/lib/band-name-generator/noise-verbs.ts b/src/shared/band-name-generator/noise-verbs.ts similarity index 100% rename from src/lib/band-name-generator/noise-verbs.ts rename to src/shared/band-name-generator/noise-verbs.ts diff --git a/src/lib/band-name-generator/nouns.ts b/src/shared/band-name-generator/nouns.ts similarity index 100% rename from src/lib/band-name-generator/nouns.ts rename to src/shared/band-name-generator/nouns.ts diff --git a/src/lib/band-name-generator/number-words.ts b/src/shared/band-name-generator/number-words.ts similarity index 100% rename from src/lib/band-name-generator/number-words.ts rename to src/shared/band-name-generator/number-words.ts diff --git a/src/lib/band-name-generator/odd-instruments.ts b/src/shared/band-name-generator/odd-instruments.ts similarity index 100% rename from src/lib/band-name-generator/odd-instruments.ts rename to src/shared/band-name-generator/odd-instruments.ts diff --git a/src/lib/band-name-generator/person-names.ts b/src/shared/band-name-generator/person-names.ts similarity index 100% rename from src/lib/band-name-generator/person-names.ts rename to src/shared/band-name-generator/person-names.ts diff --git a/src/lib/band-name-generator/prizes.ts b/src/shared/band-name-generator/prizes.ts similarity index 100% rename from src/lib/band-name-generator/prizes.ts rename to src/shared/band-name-generator/prizes.ts diff --git a/src/lib/band-name-generator/prohibitions.ts b/src/shared/band-name-generator/prohibitions.ts similarity index 100% rename from src/lib/band-name-generator/prohibitions.ts rename to src/shared/band-name-generator/prohibitions.ts diff --git a/src/lib/band-name-generator/requirements.ts b/src/shared/band-name-generator/requirements.ts similarity index 100% rename from src/lib/band-name-generator/requirements.ts rename to src/shared/band-name-generator/requirements.ts diff --git a/src/lib/band-name-generator/show-items.ts b/src/shared/band-name-generator/show-items.ts similarity index 100% rename from src/lib/band-name-generator/show-items.ts rename to src/shared/band-name-generator/show-items.ts diff --git a/src/lib/band-name-generator/suffixes.ts b/src/shared/band-name-generator/suffixes.ts similarity index 100% rename from src/lib/band-name-generator/suffixes.ts rename to src/shared/band-name-generator/suffixes.ts diff --git a/src/lib/band-name-generator/times.ts b/src/shared/band-name-generator/times.ts similarity index 100% rename from src/lib/band-name-generator/times.ts rename to src/shared/band-name-generator/times.ts diff --git a/src/lib/band-name-generator/tour-adjectives.ts b/src/shared/band-name-generator/tour-adjectives.ts similarity index 100% rename from src/lib/band-name-generator/tour-adjectives.ts rename to src/shared/band-name-generator/tour-adjectives.ts diff --git a/src/lib/band-name-generator/venue-types.ts b/src/shared/band-name-generator/venue-types.ts similarity index 100% rename from src/lib/band-name-generator/venue-types.ts rename to src/shared/band-name-generator/venue-types.ts diff --git a/src/lib/band-name-generator/weird-venues.ts b/src/shared/band-name-generator/weird-venues.ts similarity index 100% rename from src/lib/band-name-generator/weird-venues.ts rename to src/shared/band-name-generator/weird-venues.ts diff --git a/src/lib/booking-fee.ts b/src/shared/booking-fee.ts similarity index 94% rename from src/lib/booking-fee.ts rename to src/shared/booking-fee.ts index e3d0de5af..f9db04310 100644 --- a/src/lib/booking-fee.ts +++ b/src/shared/booking-fee.ts @@ -3,7 +3,7 @@ * Used by Stripe, Square, and webhook validation. */ -import { getBookingFee } from "#lib/config.ts"; +import { getBookingFee } from "#shared/config.ts"; /** Calculate the booking fee amount in minor units from a subtotal and percentage. */ export const calculateBookingFee = ( diff --git a/src/lib/booking.ts b/src/shared/booking.ts similarity index 86% rename from src/lib/booking.ts rename to src/shared/booking.ts index 278159d7f..9eb65d59b 100644 --- a/src/lib/booking.ts +++ b/src/shared/booking.ts @@ -6,12 +6,15 @@ * Callers handle input parsing/validation and response formatting. */ -import { isPaymentsEnabled } from "#lib/config.ts"; -import { createAttendeeAtomic, hasAvailableSpots } from "#lib/db/attendees.ts"; -import { singleEventAnswerIds } from "#lib/payment-helpers.ts"; -import { getActivePaymentProvider } from "#lib/payments.ts"; -import type { Attendee, ContactInfo, EventWithCount } from "#lib/types.ts"; -import { logAndNotifyRegistration } from "#lib/webhook.ts"; +import { isPaymentsEnabled } from "#shared/config.ts"; +import { + createAttendeeAtomic, + hasAvailableSpots, +} from "#shared/db/attendees.ts"; +import { singleEventAnswerIds } from "#shared/payment-helpers.ts"; +import { getActivePaymentProvider } from "#shared/payments.ts"; +import type { Attendee, ContactInfo, EventWithCount } from "#shared/types.ts"; +import { logAndNotifyRegistration } from "#shared/webhook.ts"; /** Booking result — callers map this to their response format */ export type BookingResult = diff --git a/src/lib/build-info.ts b/src/shared/build-info.ts similarity index 100% rename from src/lib/build-info.ts rename to src/shared/build-info.ts diff --git a/src/lib/builder.ts b/src/shared/builder.ts similarity index 94% rename from src/lib/builder.ts rename to src/shared/builder.ts index 47725ef82..2287283cd 100644 --- a/src/lib/builder.ts +++ b/src/shared/builder.ts @@ -12,11 +12,11 @@ * 7. Record the built site in the local database */ -import { bunnyCdnApi } from "#lib/bunny-cdn.ts"; -import { toBase64 } from "#lib/crypto/utils.ts"; -import { getEnv } from "#lib/env.ts"; -import { fetchText } from "#lib/fetch.ts"; -import { fetchLatestRelease } from "#lib/update.ts"; +import { bunnyCdnApi } from "#shared/bunny-cdn.ts"; +import { toBase64 } from "#shared/crypto/utils.ts"; +import { getEnv } from "#shared/env.ts"; +import { fetchText } from "#shared/fetch.ts"; +import { fetchLatestRelease } from "#shared/update.ts"; /** Secrets copied from the host environment (if set) */ const HOST_SECRET_KEYS = [ diff --git a/src/lib/bulk-replace.ts b/src/shared/bulk-replace.ts similarity index 100% rename from src/lib/bulk-replace.ts rename to src/shared/bulk-replace.ts diff --git a/src/lib/bunny-cdn.ts b/src/shared/bunny-cdn.ts similarity index 99% rename from src/lib/bunny-cdn.ts rename to src/shared/bunny-cdn.ts index f2df66ae9..94d70dcd1 100644 --- a/src/lib/bunny-cdn.ts +++ b/src/shared/bunny-cdn.ts @@ -10,9 +10,9 @@ import { getBunnyDnsSubdomainSuffix, getBunnyDnsZoneId, getBunnyScriptId, -} from "#lib/config.ts"; -import { type FetchResult, fetchText } from "#lib/fetch.ts"; -import { ErrorCode, logDebug, logError } from "#lib/logger.ts"; +} from "#shared/config.ts"; +import { type FetchResult, fetchText } from "#shared/fetch.ts"; +import { ErrorCode, logDebug, logError } from "#shared/logger.ts"; const BUNNY_API_BASE = "https://api.bunny.net"; diff --git a/src/lib/business-email.ts b/src/shared/business-email.ts similarity index 95% rename from src/lib/business-email.ts rename to src/shared/business-email.ts index 12b8e46d6..5daacc9c6 100644 --- a/src/lib/business-email.ts +++ b/src/shared/business-email.ts @@ -1,4 +1,4 @@ -import { settings } from "#lib/db/settings.ts"; +import { settings } from "#shared/db/settings.ts"; /** * Validates a basic email format: something@something.something diff --git a/src/lib/cache-registry.ts b/src/shared/cache-registry.ts similarity index 100% rename from src/lib/cache-registry.ts rename to src/shared/cache-registry.ts diff --git a/src/lib/column-order.ts b/src/shared/column-order.ts similarity index 99% rename from src/lib/column-order.ts rename to src/shared/column-order.ts index eb1c92060..b57302ebd 100644 --- a/src/lib/column-order.ts +++ b/src/shared/column-order.ts @@ -11,7 +11,7 @@ */ import { Liquid } from "liquidjs"; -import { formatCurrency } from "#lib/currency.ts"; +import { formatCurrency } from "#shared/currency.ts"; // --------------------------------------------------------------------------- // Types diff --git a/src/lib/columns/attendee-columns.ts b/src/shared/columns/attendee-columns.ts similarity index 95% rename from src/lib/columns/attendee-columns.ts rename to src/shared/columns/attendee-columns.ts index 57bb17984..11ec4d680 100644 --- a/src/lib/columns/attendee-columns.ts +++ b/src/shared/columns/attendee-columns.ts @@ -5,10 +5,10 @@ * iterates the ordered columns and calls each one's cell() function. */ -import type { ColumnDef, ColumnGenerators } from "#lib/column-order.ts"; -import { formatDateLabel, formatDatetimeShort } from "#lib/dates.ts"; -import { normalizePhone } from "#lib/phone.ts"; -import type { AttendeeTableRow } from "#lib/types.ts"; +import type { ColumnDef, ColumnGenerators } from "#shared/column-order.ts"; +import { formatDateLabel, formatDatetimeShort } from "#shared/dates.ts"; +import { normalizePhone } from "#shared/phone.ts"; +import type { AttendeeTableRow } from "#shared/types.ts"; import type { AttendeeColumnOpts } from "#templates/attendee-table.tsx"; import { escapeHtml } from "#templates/layout.tsx"; diff --git a/src/lib/columns/event-columns.ts b/src/shared/columns/event-columns.ts similarity index 94% rename from src/lib/columns/event-columns.ts rename to src/shared/columns/event-columns.ts index 4d2135f49..4bb4fd7a8 100644 --- a/src/lib/columns/event-columns.ts +++ b/src/shared/columns/event-columns.ts @@ -6,8 +6,8 @@ * cell rendering, and guide documentation. */ -import type { ColumnDef, ColumnGenerators } from "#lib/column-order.ts"; -import type { EventWithCount } from "#lib/types.ts"; +import type { ColumnDef, ColumnGenerators } from "#shared/column-order.ts"; +import type { EventWithCount } from "#shared/types.ts"; import { escapeHtml } from "#templates/layout.tsx"; import { renderEventImage } from "#templates/public.tsx"; diff --git a/src/lib/config.ts b/src/shared/config.ts similarity index 95% rename from src/lib/config.ts rename to src/shared/config.ts index 91e80b10e..eacc6dc22 100644 --- a/src/lib/config.ts +++ b/src/shared/config.ts @@ -4,8 +4,8 @@ * Payment provider and keys are configured via admin settings (stored encrypted in DB) */ -import { settings } from "#lib/db/settings.ts"; -import { getEnv, requireEnv } from "#lib/env.ts"; +import { settings } from "#shared/db/settings.ts"; +import { getEnv, requireEnv } from "#shared/env.ts"; /** * Check if payments are enabled (any provider configured with valid keys) @@ -66,7 +66,7 @@ export const setEffectiveDomainForTest = (domain: string): void => { export const getEmbedHosts = async (): Promise => { const raw = settings.embedHosts; if (!raw) return []; - const { parseEmbedHosts } = await import("#lib/embed-hosts.ts"); + const { parseEmbedHosts } = await import("#shared/embed-hosts.ts"); return parseEmbedHosts(raw); }; diff --git a/src/lib/cookies.ts b/src/shared/cookies.ts similarity index 95% rename from src/lib/cookies.ts rename to src/shared/cookies.ts index f4ab6d5b3..11883b8d8 100644 --- a/src/lib/cookies.ts +++ b/src/shared/cookies.ts @@ -1,5 +1,5 @@ -import { getEffectiveDomain } from "#lib/config.ts"; -import { SESSION_MAX_AGE_S } from "#lib/limits.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { SESSION_MAX_AGE_S } from "#shared/limits.ts"; export const isSecureMode = (): boolean => getEffectiveDomain() !== "localhost"; diff --git a/src/lib/countries.ts b/src/shared/countries.ts similarity index 100% rename from src/lib/countries.ts rename to src/shared/countries.ts diff --git a/src/lib/crypto/encryption.ts b/src/shared/crypto/encryption.ts similarity index 99% rename from src/lib/crypto/encryption.ts rename to src/shared/crypto/encryption.ts index c60bc64bc..8451360be 100644 --- a/src/lib/crypto/encryption.ts +++ b/src/shared/crypto/encryption.ts @@ -3,7 +3,7 @@ */ import { lazyRef } from "#fp"; -import { getEnv } from "#lib/env.ts"; +import { getEnv } from "#shared/env.ts"; import { fromBase64, getRandomBytes, toBase64 } from "./utils.ts"; /** diff --git a/src/lib/crypto/hashing.ts b/src/shared/crypto/hashing.ts similarity index 100% rename from src/lib/crypto/hashing.ts rename to src/shared/crypto/hashing.ts diff --git a/src/lib/crypto/keys.ts b/src/shared/crypto/keys.ts similarity index 99% rename from src/lib/crypto/keys.ts rename to src/shared/crypto/keys.ts index c24b3922a..8bd9f8fce 100644 --- a/src/lib/crypto/keys.ts +++ b/src/shared/crypto/keys.ts @@ -3,7 +3,7 @@ */ import { lazyRef, ttlCache } from "#fp"; -import { registerCache } from "#lib/cache-registry.ts"; +import { registerCache } from "#shared/cache-registry.ts"; import { aesGcmDecryptRaw, aesGcmEncryptRaw, diff --git a/src/lib/crypto/utils.ts b/src/shared/crypto/utils.ts similarity index 100% rename from src/lib/crypto/utils.ts rename to src/shared/crypto/utils.ts diff --git a/src/lib/csrf.ts b/src/shared/csrf.ts similarity index 95% rename from src/lib/csrf.ts rename to src/shared/csrf.ts index 3b314bd5e..7e334c1e0 100644 --- a/src/lib/csrf.ts +++ b/src/shared/csrf.ts @@ -7,13 +7,13 @@ * Instagram) where Safari/WebKit blocks third-party cookies. */ -import { hmacHash } from "#lib/crypto/hashing.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; import { base64ToBase64Url, constantTimeEqual, generateSecureToken, -} from "#lib/crypto/utils.ts"; -import { nowMs } from "#lib/now.ts"; +} from "#shared/crypto/utils.ts"; +import { nowMs } from "#shared/now.ts"; const SIGNED_PREFIX = "s1."; const DEFAULT_MAX_AGE_S = 3600; // 1 hour diff --git a/src/lib/currency.ts b/src/shared/currency.ts similarity index 98% rename from src/lib/currency.ts rename to src/shared/currency.ts index 2c0a50a0d..e9434ec22 100644 --- a/src/lib/currency.ts +++ b/src/shared/currency.ts @@ -5,7 +5,7 @@ * and currency symbols. Reads the currency code directly from settings. */ -import { settings } from "#lib/db/settings.ts"; +import { settings } from "#shared/db/settings.ts"; /** Get the number of decimal places for a currency code */ export const getDecimalPlaces = (currencyCode: string): number => diff --git a/src/lib/dates.ts b/src/shared/dates.ts similarity index 97% rename from src/lib/dates.ts rename to src/shared/dates.ts index 8d8cfffa4..f10de491d 100644 --- a/src/lib/dates.ts +++ b/src/shared/dates.ts @@ -4,14 +4,14 @@ import { fromAbsolute } from "@internationalized/date"; import { filter } from "#fp"; -import { settings } from "#lib/db/settings.ts"; +import { settings } from "#shared/db/settings.ts"; import { formatDatetimeInTz, formatDatetimeShortInTz, localToUtc, todayInTz, -} from "#lib/timezone.ts"; -import type { Event, Holiday } from "#lib/types.ts"; +} from "#shared/timezone.ts"; +import type { Event, Holiday } from "#shared/types.ts"; /** Day name lookup from Date.getUTCDay() index (Sunday=0) */ export const DAY_NAMES = [ diff --git a/src/lib/db/activityLog.ts b/src/shared/db/activityLog.ts similarity index 91% rename from src/lib/db/activityLog.ts rename to src/shared/db/activityLog.ts index 54b25c23e..af9bf3d29 100644 --- a/src/lib/db/activityLog.ts +++ b/src/shared/db/activityLog.ts @@ -5,12 +5,12 @@ * Messages are encrypted - only admins can read them. */ -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { queryAll, queryBatch, resultRows } from "#lib/db/client.ts"; -import { eventsTable } from "#lib/db/events.ts"; -import { col, defineTable } from "#lib/db/table.ts"; -import { nowIso } from "#lib/now.ts"; -import type { Event, EventWithCount } from "#lib/types.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { queryAll, queryBatch, resultRows } from "#shared/db/client.ts"; +import { eventsTable } from "#shared/db/events.ts"; +import { col, defineTable } from "#shared/db/table.ts"; +import { nowIso } from "#shared/now.ts"; +import type { Event, EventWithCount } from "#shared/types.ts"; /** Activity log entry */ export interface ActivityLogEntry { diff --git a/src/lib/db/api-keys.ts b/src/shared/db/api-keys.ts similarity index 91% rename from src/lib/db/api-keys.ts rename to src/shared/db/api-keys.ts index 8a14a86b8..7b58319c4 100644 --- a/src/lib/db/api-keys.ts +++ b/src/shared/db/api-keys.ts @@ -9,19 +9,19 @@ * Keys inherit admin_level from their parent user. */ -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { hmacHash } from "#lib/crypto/hashing.ts"; -import { wrapKeyWithToken } from "#lib/crypto/keys.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; +import { wrapKeyWithToken } from "#shared/crypto/keys.ts"; import { deleteByField, getDb, insert, queryAll, queryOne, -} from "#lib/db/client.ts"; -import { nowIso } from "#lib/now.ts"; -import { getTouchOverride } from "#lib/test-overrides.ts"; -import type { ApiKey } from "#lib/types.ts"; +} from "#shared/db/client.ts"; +import { nowIso } from "#shared/now.ts"; +import { getTouchOverride } from "#shared/test-overrides.ts"; +import type { ApiKey } from "#shared/types.ts"; /** * Create a new API key for a user. diff --git a/src/lib/db/attendee-types.ts b/src/shared/db/attendee-types.ts similarity index 97% rename from src/lib/db/attendee-types.ts rename to src/shared/db/attendee-types.ts index f3b40170d..c8e4e6b67 100644 --- a/src/lib/db/attendee-types.ts +++ b/src/shared/db/attendee-types.ts @@ -2,7 +2,7 @@ * Types for attendee operations */ -import type { Attendee, ContactFields, ContactInfo } from "#lib/types.ts"; +import type { Attendee, ContactFields, ContactInfo } from "#shared/types.ts"; /** Aggregated statistics for active events */ export type ActiveEventStats = { diff --git a/src/lib/db/attendees.ts b/src/shared/db/attendees.ts similarity index 82% rename from src/lib/db/attendees.ts rename to src/shared/db/attendees.ts index c27684cb0..d63963491 100644 --- a/src/lib/db/attendees.ts +++ b/src/shared/db/attendees.ts @@ -15,12 +15,12 @@ import type { AttendeeInput, BatchAvailabilityItem, CreateAttendeeResult, -} from "#lib/db/attendee-types.ts"; +} from "#shared/db/attendee-types.ts"; import { checkBatchAvailabilityImpl, hasAvailableSpotsImpl, -} from "#lib/db/attendees/capacity.ts"; -import { createAttendeeAtomicImpl } from "#lib/db/attendees/create.ts"; +} from "#shared/db/attendees/capacity.ts"; +import { createAttendeeAtomicImpl } from "#shared/db/attendees/create.ts"; export type { ActiveEventStats, @@ -33,7 +33,7 @@ export type { UpdateAttendeePIIInput, UpdateEventLinkInput, UpdateEventLinkResult, -} from "#lib/db/attendee-types.ts"; +} from "#shared/db/attendee-types.ts"; export { buildCapacityCheckedInsert, CAPACITY_EXCEEDED, @@ -43,12 +43,12 @@ export { getGroupRemainingByEventId, getGroupRemainingByGroupId, getGroupRemainingForEvent, -} from "#lib/db/attendees/capacity.ts"; -export { buildAttendeeInsert } from "#lib/db/attendees/create.ts"; +} from "#shared/db/attendees/capacity.ts"; +export { buildAttendeeInsert } from "#shared/db/attendees/create.ts"; export { deleteAttendee, unlinkAttendeeFromEvent, -} from "#lib/db/attendees/delete.ts"; +} from "#shared/db/attendees/delete.ts"; export { buildPiiBlob, contactFields, @@ -60,7 +60,7 @@ export { encryptPiiBlob, PII_BLOB_VERSION, parsePiiBlob, -} from "#lib/db/attendees/pii.ts"; +} from "#shared/db/attendees/pii.ts"; export { ATTENDEE_JOIN_SELECT, ATTENDEE_LEFT_JOIN_SELECT, @@ -69,8 +69,8 @@ export { getAttendeesByTokens, getAttendeesRaw, getNewestAttendeesRaw, -} from "#lib/db/attendees/queries.ts"; -export { getActiveEventStats } from "#lib/db/attendees/stats.ts"; +} from "#shared/db/attendees/queries.ts"; +export { getActiveEventStats } from "#shared/db/attendees/stats.ts"; export { addEventLink, @@ -79,7 +79,7 @@ export { updateAttendeePII, updateCheckedIn, updateEventLink, -} from "#lib/db/attendees/update.ts"; +} from "#shared/db/attendees/update.ts"; /** Stubbable API for testing atomic operations */ export const attendeesApi = { diff --git a/src/lib/db/attendees/capacity.ts b/src/shared/db/attendees/capacity.ts similarity index 96% rename from src/lib/db/attendees/capacity.ts rename to src/shared/db/attendees/capacity.ts index f61cfe895..e8bb1ed74 100644 --- a/src/lib/db/attendees/capacity.ts +++ b/src/shared/db/attendees/capacity.ts @@ -8,15 +8,15 @@ import type { BatchAvailabilityItem, EventBooking, UpdateEventLinkResult, -} from "#lib/db/attendee-types.ts"; +} from "#shared/db/attendee-types.ts"; import { buildCapacityCondition, buildGroupAttendeePredicate, dateToRange, -} from "#lib/db/capacity.ts"; -import { inPlaceholders, queryAll } from "#lib/db/client.ts"; -import { getEventWithCount, invalidateEventsCache } from "#lib/db/events.ts"; -import type { EventType } from "#lib/types.ts"; +} from "#shared/db/capacity.ts"; +import { inPlaceholders, queryAll } from "#shared/db/client.ts"; +import { getEventWithCount, invalidateEventsCache } from "#shared/db/events.ts"; +import type { EventType } from "#shared/types.ts"; /** Shared failure result for capacity-exceeded */ export const CAPACITY_EXCEEDED = { diff --git a/src/lib/db/attendees/create.ts b/src/shared/db/attendees/create.ts similarity index 92% rename from src/lib/db/attendees/create.ts rename to src/shared/db/attendees/create.ts index 3aaf6c245..f6b5ee0b2 100644 --- a/src/lib/db/attendees/create.ts +++ b/src/shared/db/attendees/create.ts @@ -8,12 +8,15 @@ import type { BuildAttendeeInput, CreateAttendeeResult, EncryptedAttendeeData, -} from "#lib/db/attendee-types.ts"; -import { buildCapacityCheckedInsert } from "#lib/db/attendees/capacity.ts"; -import { contactFields, encryptAttendeeFields } from "#lib/db/attendees/pii.ts"; -import { executeBatchWithResults, insert } from "#lib/db/client.ts"; -import { invalidateEventsCache } from "#lib/db/events.ts"; -import type { Attendee } from "#lib/types.ts"; +} from "#shared/db/attendee-types.ts"; +import { buildCapacityCheckedInsert } from "#shared/db/attendees/capacity.ts"; +import { + contactFields, + encryptAttendeeFields, +} from "#shared/db/attendees/pii.ts"; +import { executeBatchWithResults, insert } from "#shared/db/client.ts"; +import { invalidateEventsCache } from "#shared/db/events.ts"; +import type { Attendee } from "#shared/types.ts"; /** Build an INSERT statement for the attendees table from encrypted fields. */ export const buildAttendeeInsert = (enc: EncryptedAttendeeData) => diff --git a/src/lib/db/attendees/delete.ts b/src/shared/db/attendees/delete.ts similarity index 92% rename from src/lib/db/attendees/delete.ts rename to src/shared/db/attendees/delete.ts index a35b41e7f..3530087ee 100644 --- a/src/lib/db/attendees/delete.ts +++ b/src/shared/db/attendees/delete.ts @@ -2,8 +2,8 @@ * Deletion and event-link unlinking for attendees. */ -import { executeBatch, getDb, queryOne } from "#lib/db/client.ts"; -import { invalidateEventsCache } from "#lib/db/events.ts"; +import { executeBatch, getDb, queryOne } from "#shared/db/client.ts"; +import { invalidateEventsCache } from "#shared/db/events.ts"; /** Delete an attendee and all dependent data (payments, answers, event links) */ const purgeAttendee = (attendeeId: number): Promise => diff --git a/src/lib/db/attendees/pii.ts b/src/shared/db/attendees/pii.ts similarity index 91% rename from src/lib/db/attendees/pii.ts rename to src/shared/db/attendees/pii.ts index b9b6b75fc..8e21e9c53 100644 --- a/src/lib/db/attendees/pii.ts +++ b/src/shared/db/attendees/pii.ts @@ -7,16 +7,16 @@ */ import { map } from "#fp"; -import { computeTicketTokenIndex } from "#lib/crypto/hashing.ts"; -import { decryptAttendeePII, encryptAttendeePII } from "#lib/crypto/keys.ts"; -import { generateTicketToken } from "#lib/crypto/utils.ts"; +import { computeTicketTokenIndex } from "#shared/crypto/hashing.ts"; +import { decryptAttendeePII, encryptAttendeePII } from "#shared/crypto/keys.ts"; +import { generateTicketToken } from "#shared/crypto/utils.ts"; import type { EncryptedAttendeeData, EncryptInput, -} from "#lib/db/attendee-types.ts"; -import { settings } from "#lib/db/settings.ts"; -import { nowIso } from "#lib/now.ts"; -import type { Attendee, ContactInfo, PiiBlob } from "#lib/types.ts"; +} from "#shared/db/attendee-types.ts"; +import { settings } from "#shared/db/settings.ts"; +import { nowIso } from "#shared/now.ts"; +import type { Attendee, ContactInfo, PiiBlob } from "#shared/types.ts"; /** Current PII blob schema version */ export const PII_BLOB_VERSION = 1; diff --git a/src/lib/db/attendees/queries.ts b/src/shared/db/attendees/queries.ts similarity index 93% rename from src/lib/db/attendees/queries.ts rename to src/shared/db/attendees/queries.ts index 6a253b2f6..2f69742d1 100644 --- a/src/lib/db/attendees/queries.ts +++ b/src/shared/db/attendees/queries.ts @@ -3,14 +3,14 @@ */ import { map } from "#fp"; -import { computeTicketTokenIndex } from "#lib/crypto/hashing.ts"; +import { computeTicketTokenIndex } from "#shared/crypto/hashing.ts"; import type { AttendeeWithBookings, EventAttendeeRow, -} from "#lib/db/attendee-types.ts"; -import { decryptAttendeeFields } from "#lib/db/attendees/pii.ts"; -import { inPlaceholders, queryAll, queryOne } from "#lib/db/client.ts"; -import type { Attendee } from "#lib/types.ts"; +} from "#shared/db/attendee-types.ts"; +import { decryptAttendeeFields } from "#shared/db/attendees/pii.ts"; +import { inPlaceholders, queryAll, queryOne } from "#shared/db/client.ts"; +import type { Attendee } from "#shared/types.ts"; /** * Attendee columns for JOIN queries — only the columns actually used at runtime. @@ -23,7 +23,7 @@ const EA_COLS = "ea.event_id, SUBSTR(ea.start_at, 1, 10) as date, ea.quantity, ea.checked_in, ea.refunded, ea.price_paid, ea.attachment_downloads"; /** SELECT clause for attendee + event_attendees JOINs (INNER JOIN context). - * Derives `date` from start_at for backward compatibility with the Attendee type. */ + * Derives `date` from start_at for the Attendee type shape. */ export const ATTENDEE_JOIN_SELECT = `${ATTENDEE_COLS}, ${EA_COLS}`; /** SELECT clause for LEFT JOIN context — COALESCEs nullable join columns so diff --git a/src/lib/db/attendees/stats.ts b/src/shared/db/attendees/stats.ts similarity index 85% rename from src/lib/db/attendees/stats.ts rename to src/shared/db/attendees/stats.ts index 0e534c89a..1e8c592b4 100644 --- a/src/lib/db/attendees/stats.ts +++ b/src/shared/db/attendees/stats.ts @@ -3,9 +3,9 @@ */ import { filter, map, reduce } from "#fp"; -import type { ActiveEventStats } from "#lib/db/attendee-types.ts"; -import { inPlaceholders, queryOne } from "#lib/db/client.ts"; -import type { EventWithCount } from "#lib/types.ts"; +import type { ActiveEventStats } from "#shared/db/attendee-types.ts"; +import { inPlaceholders, queryOne } from "#shared/db/client.ts"; +import type { EventWithCount } from "#shared/types.ts"; /** * Get aggregated statistics for active events. diff --git a/src/lib/db/attendees/update.ts b/src/shared/db/attendees/update.ts similarity index 91% rename from src/lib/db/attendees/update.ts rename to src/shared/db/attendees/update.ts index e305d5113..bf3220c6f 100644 --- a/src/lib/db/attendees/update.ts +++ b/src/shared/db/attendees/update.ts @@ -7,16 +7,16 @@ import type { UpdateAttendeePIIInput, UpdateEventLinkInput, UpdateEventLinkResult, -} from "#lib/db/attendee-types.ts"; +} from "#shared/db/attendee-types.ts"; import { buildCapacityCheckedInsert, checkCapacityResult, dateToStartEnd, -} from "#lib/db/attendees/capacity.ts"; -import { buildPiiBlob, encryptPiiBlob } from "#lib/db/attendees/pii.ts"; -import { buildCapacityCondition } from "#lib/db/capacity.ts"; -import { getDb } from "#lib/db/client.ts"; -import { settings } from "#lib/db/settings.ts"; +} from "#shared/db/attendees/capacity.ts"; +import { buildPiiBlob, encryptPiiBlob } from "#shared/db/attendees/pii.ts"; +import { buildCapacityCondition } from "#shared/db/capacity.ts"; +import { getDb } from "#shared/db/client.ts"; +import { settings } from "#shared/db/settings.ts"; /** Update a per-event status field on event_attendees */ const updateEventAttendeeField = diff --git a/src/lib/db/backup.ts b/src/shared/db/backup.ts similarity index 97% rename from src/lib/db/backup.ts rename to src/shared/db/backup.ts index 68c41a06c..cce47b598 100644 --- a/src/lib/db/backup.ts +++ b/src/shared/db/backup.ts @@ -12,16 +12,16 @@ import { unzipSync, zipSync } from "fflate"; import { map, pipe } from "#fp"; -import { executeBatch, getDb, queryAll } from "#lib/db/client.ts"; +import { executeBatch, getDb, queryAll } from "#shared/db/client.ts"; import { initDb, LATEST_UPDATE, resetDatabase, SCHEMA_HASH, SCHEMA_TABLE_NAMES, -} from "#lib/db/migrations.ts"; -import { requireEnv } from "#lib/env.ts"; -import { uploadRaw } from "#lib/storage.ts"; +} from "#shared/db/migrations.ts"; +import { requireEnv } from "#shared/env.ts"; +import { uploadRaw } from "#shared/storage.ts"; // ─── Types ────────────────────────────────────────────────────── diff --git a/src/lib/db/built-sites.ts b/src/shared/db/built-sites.ts similarity index 95% rename from src/lib/db/built-sites.ts rename to src/shared/db/built-sites.ts index b5045a5ae..d9509f809 100644 --- a/src/lib/db/built-sites.ts +++ b/src/shared/db/built-sites.ts @@ -4,13 +4,13 @@ */ import type { InValue } from "@libsql/client"; -import { registerCache } from "#lib/cache-registry.ts"; -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { queryAll, queryOne } from "#lib/db/client.ts"; -import type { ColumnDef, Table } from "#lib/db/table.ts"; -import { col, defineTable, withCacheInvalidation } from "#lib/db/table.ts"; -import { nowIso } from "#lib/now.ts"; -import { requestCache } from "#lib/request-cache.ts"; +import { registerCache } from "#shared/cache-registry.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { queryAll, queryOne } from "#shared/db/client.ts"; +import type { ColumnDef, Table } from "#shared/db/table.ts"; +import { col, defineTable, withCacheInvalidation } from "#shared/db/table.ts"; +import { nowIso } from "#shared/now.ts"; +import { requestCache } from "#shared/request-cache.ts"; /** Encrypted site data blob shape */ export interface SiteDataBlob { diff --git a/src/lib/db/capacity.ts b/src/shared/db/capacity.ts similarity index 100% rename from src/lib/db/capacity.ts rename to src/shared/db/capacity.ts diff --git a/src/lib/db/client.ts b/src/shared/db/client.ts similarity index 98% rename from src/lib/db/client.ts rename to src/shared/db/client.ts index b414dbfe3..bfa3f1115 100644 --- a/src/lib/db/client.ts +++ b/src/shared/db/client.ts @@ -18,8 +18,8 @@ import { addQueryLogEntry, isQueryLogEnabled, trackQuery, -} from "#lib/db/query-log.ts"; -import { getEnv } from "#lib/env.ts"; +} from "#shared/db/query-log.ts"; +import { getEnv } from "#shared/env.ts"; const createDbClient = (): Client => { const url = getEnv("DB_URL"); diff --git a/src/lib/db/common-schema.ts b/src/shared/db/common-schema.ts similarity index 78% rename from src/lib/db/common-schema.ts rename to src/shared/db/common-schema.ts index 752e258df..8171b9bc7 100644 --- a/src/lib/db/common-schema.ts +++ b/src/shared/db/common-schema.ts @@ -1,7 +1,7 @@ -import { col } from "#lib/db/table.ts"; +import { col } from "#shared/db/table.ts"; -export { registerCache } from "#lib/cache-registry.ts"; -export { defineIdTable } from "#lib/db/define-id-table.ts"; +export { registerCache } from "#shared/cache-registry.ts"; +export { defineIdTable } from "#shared/db/define-id-table.ts"; type EncryptFn = (v: string) => Promise; type DecryptFn = (v: string) => Promise; diff --git a/src/lib/db/define-id-table.ts b/src/shared/db/define-id-table.ts similarity index 79% rename from src/lib/db/define-id-table.ts rename to src/shared/db/define-id-table.ts index 118292c44..29fce435c 100644 --- a/src/lib/db/define-id-table.ts +++ b/src/shared/db/define-id-table.ts @@ -1,4 +1,4 @@ -import { defineTable, type TableSchema } from "#lib/db/table.ts"; +import { defineTable, type TableSchema } from "#shared/db/table.ts"; /** * Helper for tables whose primary key column is `id`. diff --git a/src/lib/db/events.ts b/src/shared/db/events.ts similarity index 96% rename from src/lib/db/events.ts rename to src/shared/db/events.ts index 808676bff..e2ea9164c 100644 --- a/src/lib/db/events.ts +++ b/src/shared/db/events.ts @@ -4,13 +4,13 @@ import type { ResultSet } from "@libsql/client"; import { filter as fpFilter } from "#fp"; -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { hmacHash } from "#lib/crypto/hashing.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; import { ATTENDEE_JOIN_SELECT, ATTENDEE_LEFT_JOIN_SELECT, -} from "#lib/db/attendees.ts"; -import { dateToRange } from "#lib/db/capacity.ts"; +} from "#shared/db/attendees.ts"; +import { dateToRange } from "#shared/db/capacity.ts"; import { executeBatch, getDb, @@ -18,24 +18,24 @@ import { queryAll, queryBatch, resultRows, -} from "#lib/db/client.ts"; +} from "#shared/db/client.ts"; import { defineIdTable, encryptedNameSchema, idAndEncryptedSlugSchema, registerCache, -} from "#lib/db/common-schema.ts"; -import { col, withCacheInvalidation } from "#lib/db/table.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { nowIso } from "#lib/now.ts"; -import { requestCache } from "#lib/request-cache.ts"; +} from "#shared/db/common-schema.ts"; +import { col, withCacheInvalidation } from "#shared/db/table.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { nowIso } from "#shared/now.ts"; +import { requestCache } from "#shared/request-cache.ts"; import type { Attendee, Event, EventFields, EventType, EventWithCount, -} from "#lib/types.ts"; +} from "#shared/types.ts"; import { VALID_DAY_NAMES } from "#templates/fields.ts"; /** Default bookable days (all days of the week) */ diff --git a/src/lib/db/groups.ts b/src/shared/db/groups.ts similarity index 92% rename from src/lib/db/groups.ts rename to src/shared/db/groups.ts index c29250d3d..62b575275 100644 --- a/src/lib/db/groups.ts +++ b/src/shared/db/groups.ts @@ -3,20 +3,20 @@ */ import { mapParallel } from "#fp"; -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { hmacHash } from "#lib/crypto/hashing.ts"; -import { getDb, queryAll } from "#lib/db/client.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; +import { getDb, queryAll } from "#shared/db/client.ts"; import { defineIdTable, encryptedNameSchema, idAndEncryptedSlugSchema, registerCache, -} from "#lib/db/common-schema.ts"; -import { eventsTable, invalidateEventsCache } from "#lib/db/events.ts"; -import { queryAndMap } from "#lib/db/query.ts"; -import { col, withCacheInvalidation } from "#lib/db/table.ts"; -import { requestCache } from "#lib/request-cache.ts"; -import type { Event, EventType, EventWithCount, Group } from "#lib/types.ts"; +} from "#shared/db/common-schema.ts"; +import { eventsTable, invalidateEventsCache } from "#shared/db/events.ts"; +import { queryAndMap } from "#shared/db/query.ts"; +import { col, withCacheInvalidation } from "#shared/db/table.ts"; +import { requestCache } from "#shared/request-cache.ts"; +import type { Event, EventType, EventWithCount, Group } from "#shared/types.ts"; /** Group input fields for create/update (camelCase) */ export type GroupInput = { diff --git a/src/lib/db/holidays.ts b/src/shared/db/holidays.ts similarity index 79% rename from src/lib/db/holidays.ts rename to src/shared/db/holidays.ts index 66570e615..d0c0d0704 100644 --- a/src/lib/db/holidays.ts +++ b/src/shared/db/holidays.ts @@ -3,14 +3,14 @@ */ import { filter } from "#fp"; -import { registerCache } from "#lib/cache-registry.ts"; -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { queryAndMap } from "#lib/db/query.ts"; -import { settings } from "#lib/db/settings.ts"; -import { col, defineTable, withCacheInvalidation } from "#lib/db/table.ts"; -import { requestCache } from "#lib/request-cache.ts"; -import { todayInTz } from "#lib/timezone.ts"; -import type { Holiday } from "#lib/types.ts"; +import { registerCache } from "#shared/cache-registry.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { queryAndMap } from "#shared/db/query.ts"; +import { settings } from "#shared/db/settings.ts"; +import { col, defineTable, withCacheInvalidation } from "#shared/db/table.ts"; +import { requestCache } from "#shared/request-cache.ts"; +import { todayInTz } from "#shared/timezone.ts"; +import type { Holiday } from "#shared/types.ts"; /** Holiday input fields for create/update (camelCase) */ export type HolidayInput = { diff --git a/src/lib/db/login-attempts.ts b/src/shared/db/login-attempts.ts similarity index 90% rename from src/lib/db/login-attempts.ts rename to src/shared/db/login-attempts.ts index adf37ecac..25e6f2b74 100644 --- a/src/lib/db/login-attempts.ts +++ b/src/shared/db/login-attempts.ts @@ -2,10 +2,10 @@ * Login attempts table operations (rate limiting) */ -import { hmacHash } from "#lib/crypto/hashing.ts"; -import { deleteByField, getDb, queryOne } from "#lib/db/client.ts"; -import { LOGIN_LOCKOUT_MS, MAX_LOGIN_ATTEMPTS } from "#lib/limits.ts"; -import { nowMs } from "#lib/now.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; +import { deleteByField, getDb, queryOne } from "#shared/db/client.ts"; +import { LOGIN_LOCKOUT_MS, MAX_LOGIN_ATTEMPTS } from "#shared/limits.ts"; +import { nowMs } from "#shared/now.ts"; type LoginAttemptRow = { attempts: number; locked_until: number | null }; diff --git a/src/lib/db/migrations.ts b/src/shared/db/migrations.ts similarity index 99% rename from src/lib/db/migrations.ts rename to src/shared/db/migrations.ts index b6b504bd5..6277cada9 100644 --- a/src/lib/db/migrations.ts +++ b/src/shared/db/migrations.ts @@ -11,10 +11,10 @@ * LATEST_UPDATE, migrations will still re-run (the hash will differ). */ -import { createAndUploadBackup } from "#lib/db/backup.ts"; -import { getDb } from "#lib/db/client.ts"; -import { logDebug } from "#lib/logger.ts"; -import { isStorageEnabled } from "#lib/storage.ts"; +import { createAndUploadBackup } from "#shared/db/backup.ts"; +import { getDb } from "#shared/db/client.ts"; +import { logDebug } from "#shared/logger.ts"; +import { isStorageEnabled } from "#shared/storage.ts"; // ─── Types ────────────────────────────────────────────────────── diff --git a/src/lib/db/processed-payments.ts b/src/shared/db/processed-payments.ts similarity index 95% rename from src/lib/db/processed-payments.ts rename to src/shared/db/processed-payments.ts index 1cd3b0eb6..ce16c4fd9 100644 --- a/src/lib/db/processed-payments.ts +++ b/src/shared/db/processed-payments.ts @@ -13,10 +13,10 @@ * - Fresh → return conflict error (still being processed) */ -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; -import { getDb, insert, queryOne } from "#lib/db/client.ts"; -import { STALE_RESERVATION_MS } from "#lib/limits.ts"; -import { nowIso, nowMs } from "#lib/now.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; +import { getDb, insert, queryOne } from "#shared/db/client.ts"; +import { STALE_RESERVATION_MS } from "#shared/limits.ts"; +import { nowIso, nowMs } from "#shared/now.ts"; export { STALE_RESERVATION_MS }; diff --git a/src/lib/db/prune.ts b/src/shared/db/prune.ts similarity index 96% rename from src/lib/db/prune.ts rename to src/shared/db/prune.ts index 5e6d4f77e..e01404d20 100644 --- a/src/lib/db/prune.ts +++ b/src/shared/db/prune.ts @@ -16,8 +16,8 @@ * pruned only when PRUNE_INTERVAL_MS has elapsed since its last run. */ -import { getDb } from "#lib/db/client.ts"; -import { settings } from "#lib/db/settings.ts"; +import { getDb } from "#shared/db/client.ts"; +import { settings } from "#shared/db/settings.ts"; import { PRUNE_INTERVAL_MS, PRUNE_LOGINS_RETENTION_MS, @@ -25,9 +25,9 @@ import { PRUNE_SESSIONS_RETENTION_MS, PRUNE_TOKENS_RETENTION_MS, parsePositiveInt, -} from "#lib/limits.ts"; -import { logDebug } from "#lib/logger.ts"; -import { nowMs } from "#lib/now.ts"; +} from "#shared/limits.ts"; +import { logDebug } from "#shared/logger.ts"; +import { nowMs } from "#shared/now.ts"; /** * Delete finalized processed_payments rows older than the retention window. diff --git a/src/lib/db/query-log.ts b/src/shared/db/query-log.ts similarity index 100% rename from src/lib/db/query-log.ts rename to src/shared/db/query-log.ts diff --git a/src/lib/db/query.ts b/src/shared/db/query.ts similarity index 80% rename from src/lib/db/query.ts rename to src/shared/db/query.ts index dddd3e073..9e4ea0457 100644 --- a/src/lib/db/query.ts +++ b/src/shared/db/query.ts @@ -1,6 +1,6 @@ import { mapParallel } from "#fp"; -import { getDb, resultRows } from "#lib/db/client.ts"; -import { trackQuery } from "#lib/db/query-log.ts"; +import { getDb, resultRows } from "#shared/db/client.ts"; +import { trackQuery } from "#shared/db/query-log.ts"; /** * Execute a SQL query and map result rows through an async transformer. diff --git a/src/lib/db/questions.ts b/src/shared/db/questions.ts similarity index 99% rename from src/lib/db/questions.ts rename to src/shared/db/questions.ts index 06bed7298..7719f833d 100644 --- a/src/lib/db/questions.ts +++ b/src/shared/db/questions.ts @@ -7,14 +7,14 @@ import type { InValue } from "@libsql/client"; import { filter, map, reduce } from "#fp"; -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; import { executeBatch, inPlaceholders, insert, queryAll, -} from "#lib/db/client.ts"; -import { col, defineTable } from "#lib/db/table.ts"; +} from "#shared/db/client.ts"; +import { col, defineTable } from "#shared/db/table.ts"; // --------------------------------------------------------------------------- // Types diff --git a/src/lib/db/sessions.ts b/src/shared/db/sessions.ts similarity index 95% rename from src/lib/db/sessions.ts rename to src/shared/db/sessions.ts index 1238a1c37..0d4708caf 100644 --- a/src/lib/db/sessions.ts +++ b/src/shared/db/sessions.ts @@ -2,17 +2,17 @@ * Sessions table operations */ -import { registerCache } from "#lib/cache-registry.ts"; -import { hashSessionToken } from "#lib/crypto/hashing.ts"; +import { registerCache } from "#shared/cache-registry.ts"; +import { hashSessionToken } from "#shared/crypto/hashing.ts"; import { deleteByField, getDb, insert, queryAll, queryOne, -} from "#lib/db/client.ts"; +} from "#shared/db/client.ts"; -import type { Session } from "#lib/types.ts"; +import type { Session } from "#shared/types.ts"; /** * Session cache with TTL (10 seconds) diff --git a/src/lib/db/settings.ts b/src/shared/db/settings.ts similarity index 97% rename from src/lib/db/settings.ts rename to src/shared/db/settings.ts index 5614d9a89..43be3a6d9 100644 --- a/src/lib/db/settings.ts +++ b/src/shared/db/settings.ts @@ -15,33 +15,33 @@ */ import { lazyRef } from "#fp"; -import { registerCache } from "#lib/cache-registry.ts"; -import { DEFAULT_COUNTRY, getCountry } from "#lib/countries.ts"; -import { decrypt, encrypt, encryptWithKey } from "#lib/crypto/encryption.ts"; -import { hashPassword } from "#lib/crypto/hashing.ts"; +import { registerCache } from "#shared/cache-registry.ts"; +import { DEFAULT_COUNTRY, getCountry } from "#shared/countries.ts"; +import { decrypt, encrypt, encryptWithKey } from "#shared/crypto/encryption.ts"; +import { hashPassword } from "#shared/crypto/hashing.ts"; import { deriveKEK, generateDataKey, generateKeyPair, unwrapKey, wrapKey, -} from "#lib/crypto/keys.ts"; -import { getDb, queryAll } from "#lib/db/client.ts"; -import { deleteAllSessions } from "#lib/db/sessions.ts"; -import { createUser, invalidateUsersCache } from "#lib/db/users.ts"; -import { nowMs } from "#lib/now.ts"; -import { DEFAULT_TIMEZONE } from "#lib/timezone.ts"; -import type { PaymentProviderType, Settings, Theme } from "#lib/types.ts"; -import { isPaymentProvider } from "#lib/types.ts"; +} from "#shared/crypto/keys.ts"; +import { getDb, queryAll } from "#shared/db/client.ts"; +import { deleteAllSessions } from "#shared/db/sessions.ts"; +import { createUser, invalidateUsersCache } from "#shared/db/users.ts"; +import { nowMs } from "#shared/now.ts"; +import { DEFAULT_TIMEZONE } from "#shared/timezone.ts"; +import type { PaymentProviderType, Settings, Theme } from "#shared/types.ts"; +import { isPaymentProvider } from "#shared/types.ts"; import { createAppleWalletReadSettings, createAppleWalletUpdateSettings, -} from "#lib/wallets/apple-wallet-settings.ts"; +} from "#shared/wallets/apple-wallet-settings.ts"; import { createGoogleWalletReadSettings, createGoogleWalletUpdateSettings, -} from "#lib/wallets/google-wallet-settings.ts"; -import type { EncryptedUpdateFn } from "#lib/wallets/wallet-settings-types.ts"; +} from "#shared/wallets/google-wallet-settings.ts"; +import type { EncryptedUpdateFn } from "#shared/wallets/wallet-settings-types.ts"; // --------------------------------------------------------------------------- // Setting keys diff --git a/src/lib/db/table.ts b/src/shared/db/table.ts similarity index 99% rename from src/lib/db/table.ts rename to src/shared/db/table.ts index d6d9b2505..5dd58cfaf 100644 --- a/src/lib/db/table.ts +++ b/src/shared/db/table.ts @@ -10,7 +10,7 @@ import type { InValue } from "@libsql/client"; import { compact, filter, mapParallel, reduce } from "#fp"; -import { getDb, queryAll, queryOne } from "#lib/db/client.ts"; +import { getDb, queryAll, queryOne } from "#shared/db/client.ts"; /** * Column definition for a table diff --git a/src/lib/db/token-attempts.ts b/src/shared/db/token-attempts.ts similarity index 95% rename from src/lib/db/token-attempts.ts rename to src/shared/db/token-attempts.ts index 9ee446222..9075c0d62 100644 --- a/src/lib/db/token-attempts.ts +++ b/src/shared/db/token-attempts.ts @@ -20,14 +20,14 @@ * profile (timing of individual invalid-link clicks) small. */ -import { hmacHash } from "#lib/crypto/hashing.ts"; -import { deleteByField, getDb, queryOne } from "#lib/db/client.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; +import { deleteByField, getDb, queryOne } from "#shared/db/client.ts"; import { MAX_TOKEN_404S, TOKEN_LOCKOUT_MS, TOKEN_WINDOW_MS, -} from "#lib/limits.ts"; -import { nowMs } from "#lib/now.ts"; +} from "#shared/limits.ts"; +import { nowMs } from "#shared/now.ts"; type TokenAttemptRow = { recent_tokens: string; diff --git a/src/lib/db/users.ts b/src/shared/db/users.ts similarity index 94% rename from src/lib/db/users.ts rename to src/shared/db/users.ts index 7df7f65b0..d7538c70d 100644 --- a/src/lib/db/users.ts +++ b/src/shared/db/users.ts @@ -2,19 +2,24 @@ * Users table operations */ -import { registerCache } from "#lib/cache-registry.ts"; -import { decrypt, encrypt } from "#lib/crypto/encryption.ts"; +import { registerCache } from "#shared/cache-registry.ts"; +import { decrypt, encrypt } from "#shared/crypto/encryption.ts"; import { hashPassword, hashSessionToken, hmacHash, verifyPassword, -} from "#lib/crypto/hashing.ts"; -import { deriveKEK, wrapKey } from "#lib/crypto/keys.ts"; -import { deleteByFieldBatch, getDb, insert, queryAll } from "#lib/db/client.ts"; -import { now } from "#lib/now.ts"; -import { requestCache } from "#lib/request-cache.ts"; -import { type AdminLevel, isAdminLevel, type User } from "#lib/types.ts"; +} from "#shared/crypto/hashing.ts"; +import { deriveKEK, wrapKey } from "#shared/crypto/keys.ts"; +import { + deleteByFieldBatch, + getDb, + insert, + queryAll, +} from "#shared/db/client.ts"; +import { now } from "#shared/now.ts"; +import { requestCache } from "#shared/request-cache.ts"; +import { type AdminLevel, isAdminLevel, type User } from "#shared/types.ts"; const USER_SELECT = "SELECT id, username_hash, username_index, password_hash, wrapped_data_key, admin_level, invite_code_hash, invite_expiry FROM users ORDER BY id ASC"; diff --git a/src/lib/demo.ts b/src/shared/demo.ts similarity index 98% rename from src/lib/demo.ts rename to src/shared/demo.ts index 9dea6bf89..7c81b1dc9 100644 --- a/src/lib/demo.ts +++ b/src/shared/demo.ts @@ -11,11 +11,11 @@ import { generateBandNames, generateDescriptions, generateVenueNames, -} from "#lib/band-name-generator.ts"; -import { getEnv } from "#lib/env.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { FieldValues } from "#lib/forms.tsx"; -import type { NamedResource } from "#lib/rest/resource.ts"; +} from "#shared/band-name-generator.ts"; +import { getEnv } from "#shared/env.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { FieldValues } from "#shared/forms.tsx"; +import type { NamedResource } from "#shared/rest/resource.ts"; // --------------------------------------------------------------------------- // Demo mode flag diff --git a/src/lib/email-renderer.ts b/src/shared/email-renderer.ts similarity index 94% rename from src/lib/email-renderer.ts rename to src/shared/email-renderer.ts index 452183b85..8794416d6 100644 --- a/src/lib/email-renderer.ts +++ b/src/shared/email-renderer.ts @@ -8,15 +8,15 @@ import { Liquid } from "liquidjs"; import { lazyRef, map } from "#fp"; -import { formatCurrency } from "#lib/currency.ts"; +import { formatCurrency } from "#shared/currency.ts"; import type { EmailTemplateFormat, EmailTemplateType, -} from "#lib/db/settings.ts"; -import { settings } from "#lib/db/settings.ts"; -import type { EmailEntry } from "#lib/email.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { isPaidEvent } from "#lib/types.ts"; +} from "#shared/db/settings.ts"; +import { settings } from "#shared/db/settings.ts"; +import type { EmailEntry } from "#shared/email.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { isPaidEvent } from "#shared/types.ts"; import { DEFAULT_TEMPLATES } from "#templates/email/defaults.ts"; import type { EmailContent } from "#templates/email/shared.ts"; import { eventNames } from "#templates/email/shared.ts"; diff --git a/src/lib/email.ts b/src/shared/email.ts similarity index 95% rename from src/lib/email.ts rename to src/shared/email.ts index e7ada329a..a27aed485 100644 --- a/src/lib/email.ts +++ b/src/shared/email.ts @@ -4,15 +4,18 @@ */ import { lazyRef, map } from "#fp"; -import { toBase64 } from "#lib/crypto/utils.ts"; -import { settings } from "#lib/db/settings.ts"; -import { buildTemplateData, renderEmailContent } from "#lib/email-renderer.ts"; -import { getEnv } from "#lib/env.ts"; -import { fetchText } from "#lib/fetch.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { generateSvgTicket, type SvgTicketData } from "#lib/svg-ticket.ts"; -import { buildCheckinUrl, buildTicketUrl } from "#lib/ticket-url.ts"; -import type { WebhookAttendee, WebhookEvent } from "#lib/webhook.ts"; +import { toBase64 } from "#shared/crypto/utils.ts"; +import { settings } from "#shared/db/settings.ts"; +import { + buildTemplateData, + renderEmailContent, +} from "#shared/email-renderer.ts"; +import { getEnv } from "#shared/env.ts"; +import { fetchText } from "#shared/fetch.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { generateSvgTicket, type SvgTicketData } from "#shared/svg-ticket.ts"; +import { buildCheckinUrl, buildTicketUrl } from "#shared/ticket-url.ts"; +import type { WebhookAttendee, WebhookEvent } from "#shared/webhook.ts"; /** Event data needed for registration pipeline (extends webhook event with display + assignment fields) */ export type EmailEvent = WebhookEvent & { diff --git a/src/lib/embed-hosts.ts b/src/shared/embed-hosts.ts similarity index 100% rename from src/lib/embed-hosts.ts rename to src/shared/embed-hosts.ts diff --git a/src/lib/embed.ts b/src/shared/embed.ts similarity index 95% rename from src/lib/embed.ts rename to src/shared/embed.ts index 21ff3bef6..5b683a6ab 100644 --- a/src/lib/embed.ts +++ b/src/shared/embed.ts @@ -2,7 +2,7 @@ * Embed code generation - shared by single-event and multi-booking views */ -import { EMBED_JS_PATH } from "#lib/asset-paths.ts"; +import { EMBED_JS_PATH } from "#shared/asset-paths.ts"; const DEFAULT_IFRAME_HEIGHT = "600px"; diff --git a/src/lib/env.ts b/src/shared/env.ts similarity index 100% rename from src/lib/env.ts rename to src/shared/env.ts diff --git a/src/lib/event-fields.ts b/src/shared/event-fields.ts similarity index 97% rename from src/lib/event-fields.ts rename to src/shared/event-fields.ts index 05aacd205..6a161030d 100644 --- a/src/lib/event-fields.ts +++ b/src/shared/event-fields.ts @@ -8,7 +8,7 @@ import { type ContactField, type EventFields, isContactField, -} from "#lib/types.ts"; +} from "#shared/types.ts"; /** Parse a comma-separated fields string into individual ContactField names */ export const parseEventFields = (fields: EventFields): ContactField[] => diff --git a/src/lib/events-actions.ts b/src/shared/events-actions.ts similarity index 89% rename from src/lib/events-actions.ts rename to src/shared/events-actions.ts index 4bb41402b..f632f5cae 100644 --- a/src/lib/events-actions.ts +++ b/src/shared/events-actions.ts @@ -5,8 +5,8 @@ * so that the route handlers remain thin response formatters. */ -import { formatCurrency } from "#lib/currency.ts"; -import { logActivity } from "#lib/db/activityLog.ts"; +import { formatCurrency } from "#shared/currency.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; import { computeSlugIndex, deleteEvent, @@ -14,11 +14,11 @@ import { eventsTable, getEventWithCount, isSlugTaken, -} from "#lib/db/events.ts"; -import { groupsTable, validateGroupEventType } from "#lib/db/groups.ts"; -import { generateUniqueSlug } from "#lib/slug.ts"; -import { deleteEventStorageFiles } from "#lib/storage.ts"; -import type { Event, EventWithCount } from "#lib/types.ts"; +} from "#shared/db/events.ts"; +import { groupsTable, validateGroupEventType } from "#shared/db/groups.ts"; +import { generateUniqueSlug } from "#shared/slug.ts"; +import { deleteEventStorageFiles } from "#shared/storage.ts"; +import type { Event, EventWithCount } from "#shared/types.ts"; /** Generate a unique event slug, retrying on collision */ export const generateUniqueEventSlug = (excludeEventId?: number) => diff --git a/src/lib/fetch.ts b/src/shared/fetch.ts similarity index 100% rename from src/lib/fetch.ts rename to src/shared/fetch.ts diff --git a/src/lib/flash-context.ts b/src/shared/flash-context.ts similarity index 100% rename from src/lib/flash-context.ts rename to src/shared/flash-context.ts diff --git a/src/lib/form-data.ts b/src/shared/form-data.ts similarity index 100% rename from src/lib/form-data.ts rename to src/shared/form-data.ts diff --git a/src/lib/forms.tsx b/src/shared/forms.tsx similarity index 99% rename from src/lib/forms.tsx rename to src/shared/forms.tsx index 797072514..d35d854a3 100644 --- a/src/lib/forms.tsx +++ b/src/shared/forms.tsx @@ -4,9 +4,9 @@ import { joinStrings, map, pipe } from "#fp"; import { type Child, Raw } from "#jsx/jsx-runtime.ts"; -import { getCurrentCsrfToken } from "#lib/csrf.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import { appendIframeParam } from "#lib/iframe.ts"; +import { getCurrentCsrfToken } from "#shared/csrf.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import { appendIframeParam } from "#shared/iframe.ts"; const escapeHtml = (str: string): string => str diff --git a/src/lib/google-wallet.ts b/src/shared/google-wallet.ts similarity index 96% rename from src/lib/google-wallet.ts rename to src/shared/google-wallet.ts index b690a5638..e30928240 100644 --- a/src/lib/google-wallet.ts +++ b/src/shared/google-wallet.ts @@ -9,9 +9,9 @@ * The resulting URL format: https://pay.google.com/gp/v/save/{jwt} */ -import { getDecimalPlaces } from "#lib/currency.ts"; -import { startOfHour } from "#lib/dates.ts"; -import type { WalletPassData } from "#routes/token-utils.ts"; +import type { WalletPassData } from "#routes/tickets/token-utils.ts"; +import { getDecimalPlaces } from "#shared/currency.ts"; +import { startOfHour } from "#shared/dates.ts"; /** Google Wallet credentials from service account */ export type GoogleWalletCredentials = { diff --git a/src/lib/iframe.ts b/src/shared/iframe.ts similarity index 100% rename from src/lib/iframe.ts rename to src/shared/iframe.ts diff --git a/src/lib/jsx/jsx-dev-runtime.ts b/src/shared/jsx/jsx-dev-runtime.ts similarity index 57% rename from src/lib/jsx/jsx-dev-runtime.ts rename to src/shared/jsx/jsx-dev-runtime.ts index 645bc794f..5fdabed4e 100644 --- a/src/lib/jsx/jsx-dev-runtime.ts +++ b/src/shared/jsx/jsx-dev-runtime.ts @@ -3,4 +3,4 @@ * Used by TypeScript in development mode */ -export { Fragment, jsx, jsxDEV, jsxs, Raw } from "#lib/jsx/jsx-runtime.ts"; +export { Fragment, jsx, jsxDEV, jsxs, Raw } from "#shared/jsx/jsx-runtime.ts"; diff --git a/src/lib/jsx/jsx-runtime.ts b/src/shared/jsx/jsx-runtime.ts similarity index 100% rename from src/lib/jsx/jsx-runtime.ts rename to src/shared/jsx/jsx-runtime.ts diff --git a/src/lib/limits.ts b/src/shared/limits.ts similarity index 99% rename from src/lib/limits.ts rename to src/shared/limits.ts index a457be2ab..c6921e018 100644 --- a/src/lib/limits.ts +++ b/src/shared/limits.ts @@ -6,7 +6,7 @@ * env vars fall back to the default. */ -import { getEnv } from "#lib/env.ts"; +import { getEnv } from "#shared/env.ts"; /** * Parse a string as a positive integer, falling back to the given default diff --git a/src/lib/logger.ts b/src/shared/logger.ts similarity index 98% rename from src/lib/logger.ts rename to src/shared/logger.ts index 48dbe4ce8..cd5a72775 100644 --- a/src/lib/logger.ts +++ b/src/shared/logger.ts @@ -9,13 +9,13 @@ import { AsyncLocalStorage } from "node:async_hooks"; import { lazyRef } from "#fp"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { sendNtfyError } from "#lib/ntfy.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { sendNtfyError } from "#shared/ntfy.ts"; import { addPendingWork, hasPendingWorkScope, runWithPendingWork, -} from "#lib/pending-work.ts"; +} from "#shared/pending-work.ts"; /** Request-scoped random ID for correlating log entries */ const requestIdStorage = new AsyncLocalStorage(); diff --git a/src/lib/markdown.ts b/src/shared/markdown.ts similarity index 100% rename from src/lib/markdown.ts rename to src/shared/markdown.ts diff --git a/src/lib/merge/attendee-merge-types.ts b/src/shared/merge/attendee-merge-types.ts similarity index 97% rename from src/lib/merge/attendee-merge-types.ts rename to src/shared/merge/attendee-merge-types.ts index 19c601c76..4935cda5e 100644 --- a/src/lib/merge/attendee-merge-types.ts +++ b/src/shared/merge/attendee-merge-types.ts @@ -6,8 +6,8 @@ * 2. Apply: the admin submits explicit decisions for each conflict. */ -import type { EventAttendeeRow } from "#lib/db/attendee-types.ts"; -import type { ContactInfo } from "#lib/types.ts"; +import type { EventAttendeeRow } from "#shared/db/attendee-types.ts"; +import type { ContactInfo } from "#shared/types.ts"; // --------------------------------------------------------------------------- // Choice enums diff --git a/src/lib/merge/attendee-merge.ts b/src/shared/merge/attendee-merge.ts similarity index 98% rename from src/lib/merge/attendee-merge.ts rename to src/shared/merge/attendee-merge.ts index 014c1cee9..2d91d16cd 100644 --- a/src/lib/merge/attendee-merge.ts +++ b/src/shared/merge/attendee-merge.ts @@ -7,14 +7,14 @@ */ import { filter, map, reduce } from "#fp"; -import type { EventAttendeeRow } from "#lib/db/attendee-types.ts"; -import { executeBatch, insert } from "#lib/db/client.ts"; -import { invalidateEventsCache } from "#lib/db/events.ts"; -import type { QuestionWithAnswers } from "#lib/db/questions.ts"; +import type { EventAttendeeRow } from "#shared/db/attendee-types.ts"; +import { executeBatch, insert } from "#shared/db/client.ts"; +import { invalidateEventsCache } from "#shared/db/events.ts"; +import type { QuestionWithAnswers } from "#shared/db/questions.ts"; import { getAttendeeAnswersByQuestion, saveAttendeeAnswersByQuestion, -} from "#lib/db/questions.ts"; +} from "#shared/db/questions.ts"; import type { ApplyAttendeeMergeInput, AttendeeMergeApplyResult, @@ -27,7 +27,7 @@ import type { AttendeeMergeValidationResult, BookingConflictClass, BuildAttendeeMergeDiffInput, -} from "#lib/merge/attendee-merge-types.ts"; +} from "#shared/merge/attendee-merge-types.ts"; // --------------------------------------------------------------------------- // PII field definitions diff --git a/src/lib/now.ts b/src/shared/now.ts similarity index 100% rename from src/lib/now.ts rename to src/shared/now.ts diff --git a/src/lib/ntfy.ts b/src/shared/ntfy.ts similarity index 79% rename from src/lib/ntfy.ts rename to src/shared/ntfy.ts index 8ec2567e3..04c5494a6 100644 --- a/src/lib/ntfy.ts +++ b/src/shared/ntfy.ts @@ -4,10 +4,10 @@ * Only includes domain and error code - no personal or encrypted data */ -import { getEffectiveDomain } from "#lib/config.ts"; -import { getEnv } from "#lib/env.ts"; -import { fetchText } from "#lib/fetch.ts"; -import { ErrorCode, logErrorLocal } from "#lib/logger.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import { getEnv } from "#shared/env.ts"; +import { fetchText } from "#shared/fetch.ts"; +import { ErrorCode, logErrorLocal } from "#shared/logger.ts"; /** * Send an error notification to the configured ntfy URL diff --git a/src/lib/payment-crypto.ts b/src/shared/payment-crypto.ts similarity index 100% rename from src/lib/payment-crypto.ts rename to src/shared/payment-crypto.ts diff --git a/src/lib/payment-helpers.ts b/src/shared/payment-helpers.ts similarity index 96% rename from src/lib/payment-helpers.ts rename to src/shared/payment-helpers.ts index c2a5298af..1b8e49100 100644 --- a/src/lib/payment-helpers.ts +++ b/src/shared/payment-helpers.ts @@ -4,9 +4,9 @@ */ import { map } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; -import type { ErrorCodeType, LogCategory } from "#lib/logger.ts"; -import { logDebug, logError } from "#lib/logger.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; +import type { ErrorCodeType, LogCategory } from "#shared/logger.ts"; +import { logDebug, logError } from "#shared/logger.ts"; import type { BookingIntent, BookingItem, @@ -14,8 +14,8 @@ import type { CheckoutSessionResult, SessionMetadata, ValidatedPaymentSession, -} from "#lib/payments.ts"; -import type { ContactInfo } from "#lib/types.ts"; +} from "#shared/payments.ts"; +import type { ContactInfo } from "#shared/types.ts"; /** Extract a human-readable message from an unknown caught value */ export const errorMessage = (err: unknown): string => diff --git a/src/lib/payments.ts b/src/shared/payments.ts similarity index 95% rename from src/lib/payments.ts rename to src/shared/payments.ts index efe7914fd..2e3e9045d 100644 --- a/src/lib/payments.ts +++ b/src/shared/payments.ts @@ -6,13 +6,13 @@ * this interface so they never depend on a specific provider. */ -import { settings } from "#lib/db/settings.ts"; -import { logDebug } from "#lib/logger.ts"; +import { settings } from "#shared/db/settings.ts"; +import { logDebug } from "#shared/logger.ts"; import { type ContactInfo, createTypeGuard, type PaymentProviderType, -} from "#lib/types.ts"; +} from "#shared/types.ts"; /** Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi) */ export const paymentsApi = { @@ -223,10 +223,14 @@ export const getActivePaymentProvider = logDebug("Payment", `Resolving payment provider: ${providerType}`); if (providerType === "stripe") { - const { stripePaymentProvider } = await import("#lib/stripe-provider.ts"); + const { stripePaymentProvider } = await import( + "#shared/stripe-provider.ts" + ); return stripePaymentProvider; } - const { squarePaymentProvider } = await import("#lib/square-provider.ts"); + const { squarePaymentProvider } = await import( + "#shared/square-provider.ts" + ); return squarePaymentProvider; }; diff --git a/src/lib/pending-work.ts b/src/shared/pending-work.ts similarity index 100% rename from src/lib/pending-work.ts rename to src/shared/pending-work.ts diff --git a/src/lib/phone.ts b/src/shared/phone.ts similarity index 100% rename from src/lib/phone.ts rename to src/shared/phone.ts diff --git a/src/lib/qr-token.ts b/src/shared/qr-token.ts similarity index 97% rename from src/lib/qr-token.ts rename to src/shared/qr-token.ts index a1d39dac0..6e2944e9c 100644 --- a/src/lib/qr-token.ts +++ b/src/shared/qr-token.ts @@ -9,14 +9,14 @@ * HMAC input: "qr-book:{slug}:{payloadB64url}" */ -import { hmacHash } from "#lib/crypto/hashing.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; import { base64ToBase64Url, constantTimeEqual, fromBase64, toBase64, -} from "#lib/crypto/utils.ts"; -import { nowMs } from "#lib/now.ts"; +} from "#shared/crypto/utils.ts"; +import { nowMs } from "#shared/now.ts"; const PREFIX = "qr1."; const DOMAIN = "qr-book:"; diff --git a/src/lib/qr.ts b/src/shared/qr.ts similarity index 80% rename from src/lib/qr.ts rename to src/shared/qr.ts index 76258f3ed..ccc5fbe1d 100644 --- a/src/lib/qr.ts +++ b/src/shared/qr.ts @@ -4,9 +4,9 @@ */ import { renderSVG } from "uqr"; -import { getQuestionsForEvent } from "#lib/db/questions.ts"; -import { parseEventFields } from "#lib/event-fields.ts"; -import type { EventWithCount } from "#lib/types.ts"; +import { getQuestionsForEvent } from "#shared/db/questions.ts"; +import { parseEventFields } from "#shared/event-fields.ts"; +import type { EventWithCount } from "#shared/types.ts"; /** * Generate an SVG string for a QR code encoding the given text. diff --git a/src/lib/request-cache.ts b/src/shared/request-cache.ts similarity index 100% rename from src/lib/request-cache.ts rename to src/shared/request-cache.ts diff --git a/src/lib/response.ts b/src/shared/response.ts similarity index 100% rename from src/lib/response.ts rename to src/shared/response.ts diff --git a/src/lib/rest/crud-api.ts b/src/shared/rest/crud-api.ts similarity index 98% rename from src/lib/rest/crud-api.ts rename to src/shared/rest/crud-api.ts index e24beec51..9ed66b6cf 100644 --- a/src/lib/rest/crud-api.ts +++ b/src/shared/rest/crud-api.ts @@ -18,13 +18,13 @@ */ import type { InValue } from "@libsql/client"; -import { logActivity } from "#lib/db/activityLog.ts"; -import type { Table } from "#lib/db/table.ts"; -import type { AdminSession } from "#lib/types.ts"; import { verifyIdentifierOrJsonError } from "#routes/admin/confirmation.ts"; import { ADMIN_API, withAuth } from "#routes/auth.ts"; import { jsonResponse } from "#routes/response.ts"; import type { RouteHandlerFn } from "#routes/router.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import type { Table } from "#shared/db/table.ts"; +import type { AdminSession } from "#shared/types.ts"; /** JSON body for confirmed delete endpoints */ export type DeleteBody = { confirm_identifier: string }; diff --git a/src/lib/rest/handlers.ts b/src/shared/rest/handlers.ts similarity index 97% rename from src/lib/rest/handlers.ts rename to src/shared/rest/handlers.ts index bd421bbac..701cde3fe 100644 --- a/src/lib/rest/handlers.ts +++ b/src/shared/rest/handlers.ts @@ -8,13 +8,13 @@ */ import type { InValue } from "@libsql/client"; -import type { FormParams } from "#lib/form-data.ts"; +import { AUTH_FORM, type AuthSession, withAuth } from "#routes/auth.ts"; +import type { FormParams } from "#shared/form-data.ts"; import type { CreateResult, DeleteResult, Resource, -} from "#lib/rest/resource.ts"; -import { AUTH_FORM, type AuthSession, withAuth } from "#routes/auth.ts"; +} from "#shared/rest/resource.ts"; /** Async or sync response */ type MaybeAsync = T | Promise; diff --git a/src/lib/rest/resource.ts b/src/shared/rest/resource.ts similarity index 97% rename from src/lib/rest/resource.ts rename to src/shared/rest/resource.ts index 3615a8bf3..051c5d7a5 100644 --- a/src/lib/rest/resource.ts +++ b/src/shared/rest/resource.ts @@ -17,10 +17,10 @@ */ import type { InValue } from "@libsql/client"; -import type { Table } from "#lib/db/table.ts"; -import type { FormParams } from "#lib/form-data.ts"; -import type { Field, FieldValues } from "#lib/forms.tsx"; -import { validateForm } from "#lib/forms.tsx"; +import type { Table } from "#shared/db/table.ts"; +import type { FormParams } from "#shared/form-data.ts"; +import type { Field, FieldValues } from "#shared/forms.tsx"; +import { validateForm } from "#shared/forms.tsx"; /** Success result with data */ type SuccessResult = { ok: true } & T; diff --git a/src/lib/seeds.ts b/src/shared/seeds.ts similarity index 91% rename from src/lib/seeds.ts rename to src/shared/seeds.ts index 21ab9e484..fa9aaa43c 100644 --- a/src/lib/seeds.ts +++ b/src/shared/seeds.ts @@ -4,15 +4,15 @@ */ import { map, reduce } from "#fp"; -import { encrypt } from "#lib/crypto/encryption.ts"; -import { hmacHash } from "#lib/crypto/hashing.ts"; +import { encrypt } from "#shared/crypto/encryption.ts"; +import { hmacHash } from "#shared/crypto/hashing.ts"; import { buildAttendeeInsert, encryptAttendeeFields, -} from "#lib/db/attendees.ts"; -import { executeBatch, insert, queryAll, rawSql } from "#lib/db/client.ts"; -import { invalidateEventsCache } from "#lib/db/events.ts"; -import { settings } from "#lib/db/settings.ts"; +} from "#shared/db/attendees.ts"; +import { executeBatch, insert, queryAll, rawSql } from "#shared/db/client.ts"; +import { invalidateEventsCache } from "#shared/db/events.ts"; +import { settings } from "#shared/db/settings.ts"; import { DEMO_ADDRESSES, DEMO_EMAILS, @@ -23,9 +23,9 @@ import { DEMO_PHONES, DEMO_SPECIAL_INSTRUCTIONS, randomChoice, -} from "#lib/demo.ts"; -import { nowIso } from "#lib/now.ts"; -import { generateUniqueSlug, type SlugWithIndex } from "#lib/slug.ts"; +} from "#shared/demo.ts"; +import { nowIso } from "#shared/now.ts"; +import { generateUniqueSlug, type SlugWithIndex } from "#shared/slug.ts"; /** Max attendees per seeded event */ export const SEED_MAX_ATTENDEES = 100_000; @@ -130,13 +130,13 @@ const prepareAttendee = async ( unitPrice: number, ) => { const pricePaid = unitPrice * quantity; - // createSeeds guards on settings.publicKey before calling prepareAttendee, - // so encryptAttendeeFields cannot return null here. + const paymentId = + unitPrice > 0 ? `seed_${eventId}_${quantity}_${pricePaid}` : ""; const enc = (await encryptAttendeeFields({ address: randomChoice(DEMO_ADDRESSES), email: randomChoice(DEMO_EMAILS), name: randomChoice(DEMO_NAMES), - paymentId: "", + paymentId, phone: randomChoice(DEMO_PHONES), pricePaid, special_instructions: randomChoice(DEMO_SPECIAL_INSTRUCTIONS), diff --git a/src/lib/session-context.ts b/src/shared/session-context.ts similarity index 100% rename from src/lib/session-context.ts rename to src/shared/session-context.ts diff --git a/src/lib/site-assignment.ts b/src/shared/site-assignment.ts similarity index 94% rename from src/lib/site-assignment.ts rename to src/shared/site-assignment.ts index 605e3b192..2b4cce9f2 100644 --- a/src/lib/site-assignment.ts +++ b/src/shared/site-assignment.ts @@ -4,13 +4,17 @@ * All assignment logic is gated behind CAN_BUILD_SITES. */ +import { isBuilderEnabled } from "#routes/admin/builder.ts"; import { assignBuiltSite, getAssignableBuiltSites, -} from "#lib/db/built-sites.ts"; -import { settings } from "#lib/db/settings.ts"; -import { getEmailConfig, getHostEmailConfig, sendEmail } from "#lib/email.ts"; -import { isBuilderEnabled } from "#routes/admin/builder.ts"; +} from "#shared/db/built-sites.ts"; +import { settings } from "#shared/db/settings.ts"; +import { + getEmailConfig, + getHostEmailConfig, + sendEmail, +} from "#shared/email.ts"; /** Entry with the fields needed for site assignment */ type SiteAssignmentEntry = { diff --git a/src/lib/slug.ts b/src/shared/slug.ts similarity index 100% rename from src/lib/slug.ts rename to src/shared/slug.ts diff --git a/src/lib/sort-events.ts b/src/shared/sort-events.ts similarity index 91% rename from src/lib/sort-events.ts rename to src/shared/sort-events.ts index 387bd1d67..f60373dbe 100644 --- a/src/lib/sort-events.ts +++ b/src/shared/sort-events.ts @@ -6,10 +6,10 @@ * Tier 2: Daily events → sorted by next bookable date ASC, then name */ -import { getNextBookableDate } from "#lib/dates.ts"; -import { getAllEvents } from "#lib/db/events.ts"; -import { getActiveHolidays } from "#lib/db/holidays.ts"; -import type { Event, EventWithCount, Holiday } from "#lib/types.ts"; +import { getNextBookableDate } from "#shared/dates.ts"; +import { getAllEvents } from "#shared/db/events.ts"; +import { getActiveHolidays } from "#shared/db/holidays.ts"; +import type { Event, EventWithCount, Holiday } from "#shared/types.ts"; export type { EventWithCount }; diff --git a/src/lib/square-provider.ts b/src/shared/square-provider.ts similarity index 97% rename from src/lib/square-provider.ts rename to src/shared/square-provider.ts index 9038c8a94..569572ed3 100644 --- a/src/lib/square-provider.ts +++ b/src/shared/square-provider.ts @@ -12,27 +12,27 @@ * - Webhook setup is manual (user provides signature key from dashboard) */ -import { logDebug } from "#lib/logger.ts"; +import { logDebug } from "#shared/logger.ts"; import { extractSessionMetadata, hasRequiredSessionMetadata, toCheckoutResult, withCheckoutError, -} from "#lib/payment-helpers.ts"; +} from "#shared/payment-helpers.ts"; import type { CheckoutIntent, PaymentProvider, ValidatedPaymentSession, WebhookEvent, WebhookSetupResult, -} from "#lib/payments.ts"; +} from "#shared/payments.ts"; import { createPaymentLink, refundPayment, retrieveOrder, retrievePayment, verifyWebhookSignature, -} from "#lib/square.ts"; +} from "#shared/square.ts"; /** Square payment provider implementation */ export const squarePaymentProvider: PaymentProvider = { diff --git a/src/lib/square.ts b/src/shared/square.ts similarity index 98% rename from src/lib/square.ts rename to src/shared/square.ts index 9afc2c876..162af82c1 100644 --- a/src/lib/square.ts +++ b/src/shared/square.ts @@ -11,15 +11,15 @@ */ import { lazyRef, map } from "#fp"; -import { getBookingFeeAmount, itemsSubtotal } from "#lib/booking-fee.ts"; -import { settings } from "#lib/db/settings.ts"; -import { fetchText } from "#lib/fetch.ts"; -import { ErrorCode, logDebug, logError } from "#lib/logger.ts"; +import { getBookingFeeAmount, itemsSubtotal } from "#shared/booking-fee.ts"; +import { settings } from "#shared/db/settings.ts"; +import { fetchText } from "#shared/fetch.ts"; +import { ErrorCode, logDebug, logError } from "#shared/logger.ts"; import { computeHmacSha256, hmacToBase64, secureCompare, -} from "#lib/payment-crypto.ts"; +} from "#shared/payment-crypto.ts"; import { buildItemsMetadata, createWithClient, @@ -27,13 +27,13 @@ import { errorMessage, PaymentUserError, SQUARE_METADATA_MAX_VALUE_LENGTH, -} from "#lib/payment-helpers.ts"; +} from "#shared/payment-helpers.ts"; import type { CheckoutIntent, WebhookEvent, WebhookVerifyResult, -} from "#lib/payments.ts"; -import { normalizePhone } from "#lib/phone.ts"; +} from "#shared/payments.ts"; +import { normalizePhone } from "#shared/phone.ts"; /** * Square order metadata constraints (from Square API docs): diff --git a/src/lib/storage.ts b/src/shared/storage.ts similarity index 97% rename from src/lib/storage.ts rename to src/shared/storage.ts index 1ea3782f5..d3e031bf1 100644 --- a/src/lib/storage.ts +++ b/src/shared/storage.ts @@ -7,15 +7,15 @@ import { AsyncLocalStorage } from "node:async_hooks"; import * as BunnyStorageSDK from "@bunny.net/storage-sdk"; -import { decryptBytes, encryptBytes } from "#lib/crypto/encryption.ts"; -import { getEnv } from "#lib/env.ts"; +import { decryptBytes, encryptBytes } from "#shared/crypto/encryption.ts"; +import { getEnv } from "#shared/env.ts"; import { formatBytes, MAX_ATTACHMENT_SIZE, MAX_IMAGE_SIZE, -} from "#lib/limits.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { getDeleteOverride } from "#lib/test-overrides.ts"; +} from "#shared/limits.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { getDeleteOverride } from "#shared/test-overrides.ts"; // --------------------------------------------------------------------------- // Per-context storage config (eliminates env var races in concurrent tests) @@ -367,7 +367,7 @@ export const deleteFile = async (filename: string): Promise => { // Attachment storage (any file type, up to 25MB) // --------------------------------------------------------------------------- -// Re-export for existing consumers (imported from #lib/limits.ts at top) +// Re-export for existing consumers (imported from #shared/limits.ts at top) export { MAX_ATTACHMENT_SIZE }; /** Attachment validation error */ diff --git a/src/lib/stripe-provider.ts b/src/shared/stripe-provider.ts similarity index 97% rename from src/lib/stripe-provider.ts rename to src/shared/stripe-provider.ts index 34a0c11fb..fd704ff8f 100644 --- a/src/lib/stripe-provider.ts +++ b/src/shared/stripe-provider.ts @@ -11,7 +11,7 @@ import { hasRequiredSessionMetadata, toCheckoutResult, withCheckoutError, -} from "#lib/payment-helpers.ts"; +} from "#shared/payment-helpers.ts"; import { type CheckoutIntent, isPaymentStatus, @@ -19,7 +19,7 @@ import { type ValidatedPaymentSession, type WebhookEvent, type WebhookVerifyResult, -} from "#lib/payments.ts"; +} from "#shared/payments.ts"; import { createCheckoutSession, retrieveCheckoutSession, @@ -27,7 +27,7 @@ import { setupWebhookEndpoint, refundPayment as stripeRefund, verifyWebhookSignature, -} from "#lib/stripe.ts"; +} from "#shared/stripe.ts"; /** Stripe payment provider implementation */ export const stripePaymentProvider: PaymentProvider = { diff --git a/src/lib/stripe.ts b/src/shared/stripe.ts similarity index 98% rename from src/lib/stripe.ts rename to src/shared/stripe.ts index 96da13e78..427962c22 100644 --- a/src/lib/stripe.ts +++ b/src/shared/stripe.ts @@ -5,29 +5,29 @@ import type Stripe from "stripe"; import { lazyRef, once } from "#fp"; -import { getBookingFeeAmount, itemsSubtotal } from "#lib/booking-fee.ts"; -import { settings } from "#lib/db/settings.ts"; -import { getEnv } from "#lib/env.ts"; -import { ErrorCode, logDebug, logError } from "#lib/logger.ts"; -import { nowMs } from "#lib/now.ts"; +import { getBookingFeeAmount, itemsSubtotal } from "#shared/booking-fee.ts"; +import { settings } from "#shared/db/settings.ts"; +import { getEnv } from "#shared/env.ts"; +import { ErrorCode, logDebug, logError } from "#shared/logger.ts"; +import { nowMs } from "#shared/now.ts"; import { computeHmacSha256, hmacToHex, secureCompare, -} from "#lib/payment-crypto.ts"; +} from "#shared/payment-crypto.ts"; import { buildItemsMetadata, createWithClient, enforceMetadataLimits, errorMessage, STRIPE_METADATA_MAX_VALUE_LENGTH, -} from "#lib/payment-helpers.ts"; +} from "#shared/payment-helpers.ts"; import type { CheckoutIntent, WebhookEvent, WebhookSetupResult, WebhookVerifyResult, -} from "#lib/payments.ts"; +} from "#shared/payments.ts"; /** Lazy-load Stripe SDK only when needed */ const loadStripe = once(async () => { diff --git a/src/lib/svg-ticket.ts b/src/shared/svg-ticket.ts similarity index 93% rename from src/lib/svg-ticket.ts rename to src/shared/svg-ticket.ts index 51e26cb10..8275e66da 100644 --- a/src/lib/svg-ticket.ts +++ b/src/shared/svg-ticket.ts @@ -4,10 +4,10 @@ * suitable for email attachment. Contains no PII — only event details and booking metadata. */ -import { formatCurrency } from "#lib/currency.ts"; -import { formatDateLabel, formatDatetimeLabel } from "#lib/dates.ts"; -import { generateQrSvg } from "#lib/qr.ts"; -import type { WalletPassData } from "#routes/token-utils.ts"; +import type { WalletPassData } from "#routes/tickets/token-utils.ts"; +import { formatCurrency } from "#shared/currency.ts"; +import { formatDateLabel, formatDatetimeLabel } from "#shared/dates.ts"; +import { generateQrSvg } from "#shared/qr.ts"; import { escapeHtml } from "#templates/layout.tsx"; /** Non-PII ticket data for SVG rendering (extends shared wallet fields with display-formatted values) */ diff --git a/src/lib/test-overrides.ts b/src/shared/test-overrides.ts similarity index 94% rename from src/lib/test-overrides.ts rename to src/shared/test-overrides.ts index 5e6aefa51..ef1f4a83f 100644 --- a/src/lib/test-overrides.ts +++ b/src/shared/test-overrides.ts @@ -1,5 +1,5 @@ import { lazyRef } from "#fp"; -import { getEnv } from "#lib/env.ts"; +import { getEnv } from "#shared/env.ts"; const [getRethrowErrors, setRethrowErrors] = lazyRef( () => null, diff --git a/src/lib/ticket-url.ts b/src/shared/ticket-url.ts similarity index 91% rename from src/lib/ticket-url.ts rename to src/shared/ticket-url.ts index 1e3c01458..8bcf35326 100644 --- a/src/lib/ticket-url.ts +++ b/src/shared/ticket-url.ts @@ -3,7 +3,7 @@ */ import { map, pipe, unique } from "#fp"; -import { getEffectiveDomain } from "#lib/config.ts"; +import { getEffectiveDomain } from "#shared/config.ts"; type TokenEntry = { attendee: { ticket_token: string } }; diff --git a/src/lib/timezone.ts b/src/shared/timezone.ts similarity index 98% rename from src/lib/timezone.ts rename to src/shared/timezone.ts index a6087f50c..1ae0a9867 100644 --- a/src/lib/timezone.ts +++ b/src/shared/timezone.ts @@ -12,7 +12,7 @@ import { parseDateTime, toZoned, } from "@internationalized/date"; -import { formatIsoForPreview } from "#lib/bulk-replace.ts"; +import { formatIsoForPreview } from "#shared/bulk-replace.ts"; /** Default timezone when none is configured */ export const DEFAULT_TIMEZONE = "Europe/London"; diff --git a/src/lib/types.ts b/src/shared/types.ts similarity index 100% rename from src/lib/types.ts rename to src/shared/types.ts diff --git a/src/lib/update.ts b/src/shared/update.ts similarity index 96% rename from src/lib/update.ts rename to src/shared/update.ts index 2a4906856..19863f95f 100644 --- a/src/lib/update.ts +++ b/src/shared/update.ts @@ -7,8 +7,8 @@ */ import { lazyRef } from "#fp"; -import { BUILD_TIMESTAMP } from "#lib/build-info.ts"; -import { deployScriptCode } from "#lib/bunny-cdn.ts"; +import { BUILD_TIMESTAMP } from "#shared/build-info.ts"; +import { deployScriptCode } from "#shared/bunny-cdn.ts"; /** GitHub repo URL — update here if the repo moves */ export const GITHUB_REPO = "chobbledotcom/tickets"; diff --git a/src/lib/wallet-icons.ts b/src/shared/wallet-icons.ts similarity index 100% rename from src/lib/wallet-icons.ts rename to src/shared/wallet-icons.ts diff --git a/src/lib/wallets/apple-wallet-settings.ts b/src/shared/wallets/apple-wallet-settings.ts similarity index 89% rename from src/lib/wallets/apple-wallet-settings.ts rename to src/shared/wallets/apple-wallet-settings.ts index 80db15f6b..d83338883 100644 --- a/src/lib/wallets/apple-wallet-settings.ts +++ b/src/shared/wallets/apple-wallet-settings.ts @@ -4,8 +4,8 @@ * Extracted from settings.ts to keep wallet-specific logic separate. */ -import type { SigningCredentials } from "#lib/apple-wallet.ts"; -import { createWalletSettingsKit } from "#lib/wallets/wallet-settings-types.ts"; +import type { SigningCredentials } from "#shared/apple-wallet.ts"; +import { createWalletSettingsKit } from "#shared/wallets/wallet-settings-types.ts"; const kit = createWalletSettingsKit< SigningCredentials, diff --git a/src/lib/wallets/google-wallet-settings.ts b/src/shared/wallets/google-wallet-settings.ts similarity index 87% rename from src/lib/wallets/google-wallet-settings.ts rename to src/shared/wallets/google-wallet-settings.ts index 3692f8fca..a65c8d93c 100644 --- a/src/lib/wallets/google-wallet-settings.ts +++ b/src/shared/wallets/google-wallet-settings.ts @@ -4,8 +4,8 @@ * Extracted from settings.ts to keep wallet-specific logic separate. */ -import type { GoogleWalletCredentials } from "#lib/google-wallet.ts"; -import { createWalletSettingsKit } from "#lib/wallets/wallet-settings-types.ts"; +import type { GoogleWalletCredentials } from "#shared/google-wallet.ts"; +import { createWalletSettingsKit } from "#shared/wallets/wallet-settings-types.ts"; const kit = createWalletSettingsKit< GoogleWalletCredentials, diff --git a/src/lib/wallets/wallet-settings-types.ts b/src/shared/wallets/wallet-settings-types.ts similarity index 99% rename from src/lib/wallets/wallet-settings-types.ts rename to src/shared/wallets/wallet-settings-types.ts index 6a6495da9..09842ec38 100644 --- a/src/lib/wallets/wallet-settings-types.ts +++ b/src/shared/wallets/wallet-settings-types.ts @@ -1,7 +1,7 @@ /** Shared types and helpers for wallet settings factories. */ import { lazyRef } from "#fp"; -import { getEnv } from "#lib/env.ts"; +import { getEnv } from "#shared/env.ts"; export type SnapFn = (key: string) => string; export type EncryptedUpdateFn = (key: string) => (v: string) => Promise; diff --git a/src/lib/webhook-example.ts b/src/shared/webhook-example.ts similarity index 97% rename from src/lib/webhook-example.ts rename to src/shared/webhook-example.ts index f5e84f396..0372a4715 100644 --- a/src/lib/webhook-example.ts +++ b/src/shared/webhook-example.ts @@ -9,7 +9,7 @@ * an update here. */ -import type { WebhookPayload } from "#lib/webhook.ts"; +import type { WebhookPayload } from "#shared/webhook.ts"; /** Example inputs used by both the fixture and the test */ export const EXAMPLE_EVENT = { diff --git a/src/lib/webhook.ts b/src/shared/webhook.ts similarity index 88% rename from src/lib/webhook.ts rename to src/shared/webhook.ts index 7a4c8064d..69d254905 100644 --- a/src/lib/webhook.ts +++ b/src/shared/webhook.ts @@ -4,16 +4,16 @@ */ import { compact, unique } from "#fp"; -import { logActivity } from "#lib/db/activityLog.ts"; -import { settings } from "#lib/db/settings.ts"; -import { type EmailEntry, sendRegistrationEmails } from "#lib/email.ts"; -import { fetchText } from "#lib/fetch.ts"; -import { ErrorCode, logError } from "#lib/logger.ts"; -import { nowIso } from "#lib/now.ts"; -import { addPendingWork } from "#lib/pending-work.ts"; -import { assignAndNotifyBuiltSites } from "#lib/site-assignment.ts"; -import { buildTicketUrl } from "#lib/ticket-url.ts"; -import { type ContactInfo, isPaidEvent } from "#lib/types.ts"; +import { logActivity } from "#shared/db/activityLog.ts"; +import { settings } from "#shared/db/settings.ts"; +import { type EmailEntry, sendRegistrationEmails } from "#shared/email.ts"; +import { fetchText } from "#shared/fetch.ts"; +import { ErrorCode, logError } from "#shared/logger.ts"; +import { nowIso } from "#shared/now.ts"; +import { addPendingWork } from "#shared/pending-work.ts"; +import { assignAndNotifyBuiltSites } from "#shared/site-assignment.ts"; +import { buildTicketUrl } from "#shared/ticket-url.ts"; +import { type ContactInfo, isPaidEvent } from "#shared/types.ts"; /** Single ticket in the webhook payload */ export type WebhookTicket = { diff --git a/src/test-utils/assertions.ts b/src/test-utils/assertions.ts index 237e4f0a0..aa4c7f176 100644 --- a/src/test-utils/assertions.ts +++ b/src/test-utils/assertions.ts @@ -1,6 +1,6 @@ import { expect } from "@std/expect"; import { it } from "@std/testing/bdd"; -import { parseFlashValue } from "#lib/cookies.ts"; +import { parseFlashValue } from "#shared/cookies.ts"; export const FLASH_TEST_ID = "t001"; diff --git a/src/test-utils/crypto.ts b/src/test-utils/crypto.ts index fc3e66d2e..1f7d61f76 100644 --- a/src/test-utils/crypto.ts +++ b/src/test-utils/crypto.ts @@ -8,8 +8,8 @@ */ import forge from "node-forge"; -import type { SigningCredentials } from "#lib/apple-wallet.ts"; -import type { GoogleWalletCredentials } from "#lib/google-wallet.ts"; +import type { SigningCredentials } from "#shared/apple-wallet.ts"; +import type { GoogleWalletCredentials } from "#shared/google-wallet.ts"; let _testCerts: SigningCredentials | null = null; @@ -87,9 +87,9 @@ export const generateGoogleTestCreds = (): GoogleWalletCredentials => { export const getTestDataKey = async (): Promise => { const { testCookie } = await import("#test-utils/session.ts"); - const { getSessionCookieName } = await import("#lib/cookies.ts"); - const { unwrapKeyWithToken } = await import("#lib/crypto/keys.ts"); - const { getSession } = await import("#lib/db/sessions.ts"); + const { getSessionCookieName } = await import("#shared/cookies.ts"); + const { unwrapKeyWithToken } = await import("#shared/crypto/keys.ts"); + const { getSession } = await import("#shared/db/sessions.ts"); const cookie = await testCookie(); const sessionMatch = cookie.match( new RegExp(`${getSessionCookieName()}=([^;]+)`), @@ -100,14 +100,14 @@ export const getTestDataKey = async (): Promise => { }; export const getTestPrivateKey = async (): Promise => { - const { decryptWithKey } = await import("#lib/crypto/encryption.ts"); + const { decryptWithKey } = await import("#shared/crypto/encryption.ts"); const { deriveKEK, importPrivateKey, unwrapKey } = await import( - "#lib/crypto/keys.ts" + "#shared/crypto/keys.ts" ); const { getUserByUsername, verifyUserPassword } = await import( - "#lib/db/users.ts" + "#shared/db/users.ts" ); - const { settings } = await import("#lib/db/settings.ts"); + const { settings } = await import("#shared/db/settings.ts"); const { TEST_ADMIN_USERNAME, TEST_ADMIN_PASSWORD } = await import( "#test-utils/internal.ts" ); diff --git a/src/test-utils/csrf.ts b/src/test-utils/csrf.ts index 2a59e7fff..4f1860e6f 100644 --- a/src/test-utils/csrf.ts +++ b/src/test-utils/csrf.ts @@ -1,5 +1,5 @@ -import { getSessionCookieName } from "#lib/cookies.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; +import { getSessionCookieName } from "#shared/cookies.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; export const extractCsrfToken = (html: string | null): string | null => { if (!html) return null; @@ -75,7 +75,7 @@ export const getPageCsrfToken = async (path: string): Promise => { export const getCsrfTokenFromCookie = async ( cookie: string, ): Promise => { - const { getSession } = await import("#lib/db/sessions.ts"); + const { getSession } = await import("#shared/db/sessions.ts"); const sessionMatch = cookie.match( new RegExp(`${getSessionCookieName()}=([^;]+)`), ); diff --git a/src/test-utils/db-helpers.ts b/src/test-utils/db-helpers.ts index 204d77ab0..1a57b7f22 100644 --- a/src/test-utils/db-helpers.ts +++ b/src/test-utils/db-helpers.ts @@ -1,19 +1,19 @@ -import { parseFlashValue } from "#lib/cookies.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; -import { toMajorUnits } from "#lib/currency.ts"; -import type { CreateAttendeeResult } from "#lib/db/attendee-types.ts"; -import { getAttendeesRaw } from "#lib/db/attendees.ts"; -import type { BuiltSiteFormInput } from "#lib/db/built-sites.ts"; -import { type EventInput, getEventWithCount } from "#lib/db/events.ts"; -import type { GroupInput } from "#lib/db/groups.ts"; -import type { HolidayInput } from "#lib/db/holidays.ts"; +import { parseFlashValue } from "#shared/cookies.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { toMajorUnits } from "#shared/currency.ts"; +import type { CreateAttendeeResult } from "#shared/db/attendee-types.ts"; +import { getAttendeesRaw } from "#shared/db/attendees.ts"; +import type { BuiltSiteFormInput } from "#shared/db/built-sites.ts"; +import { type EventInput, getEventWithCount } from "#shared/db/events.ts"; +import type { GroupInput } from "#shared/db/groups.ts"; +import type { HolidayInput } from "#shared/db/holidays.ts"; import type { Attendee, Event, EventWithCount, Group, Holiday, -} from "#lib/types.ts"; +} from "#shared/types.ts"; import { testEventInput } from "#test-utils/factories.ts"; import type { BookAttendeeOpts } from "#test-utils/internal.ts"; @@ -202,7 +202,7 @@ export const createTestEvent = ( "/admin/event", buildCreateEventForm(input), async () => { - const { getAllEvents } = await import("#lib/db/events.ts"); + const { getAllEvents } = await import("#shared/db/events.ts"); const events = await getAllEvents(); return events[0] as Event; }, @@ -320,7 +320,7 @@ export const createTestAttendeeDirect = async ( address = "", special_instructions = "", ): Promise<{ attendee: Attendee; token: string }> => { - const { createAttendeeAtomic } = await import("#lib/db/attendees.ts"); + const { createAttendeeAtomic } = await import("#shared/db/attendees.ts"); const result = await createAttendeeAtomic({ address, @@ -379,7 +379,7 @@ export const createPaidTestAttendee = async ( pricePaid = 500, quantity = 1, ): Promise => { - const { createAttendeeAtomic } = await import("#lib/db/attendees.ts"); + const { createAttendeeAtomic } = await import("#shared/db/attendees.ts"); const result = await createAttendeeAtomic({ bookings: [{ eventId, pricePaid, quantity }], email, @@ -393,8 +393,8 @@ export const bookAttendee = async ( event: Pick, opts: BookAttendeeOpts = {}, ): Promise => { - const { createAttendeeAtomic } = await import("#lib/db/attendees.ts"); - const booking: import("#lib/db/attendee-types.ts").EventBooking = { + const { createAttendeeAtomic } = await import("#shared/db/attendees.ts"); + const booking: import("#shared/db/attendee-types.ts").EventBooking = { eventId: event.id, }; if (opts.date !== undefined) booking.date = opts.date; @@ -419,7 +419,7 @@ export const createDailyTestAttendee = async ( date: string, eventOverrides: Partial> = {}, ): Promise<{ event: Event; attendee: Attendee; token: string }> => { - const { createAttendeeAtomic } = await import("#lib/db/attendees.ts"); + const { createAttendeeAtomic } = await import("#shared/db/attendees.ts"); const event = await createDailyTestEvent(eventOverrides); const result = await createAttendeeAtomic({ bookings: [{ date, eventId: event.id }], @@ -452,7 +452,7 @@ export const createTestGroup = async ( ...(input.hidden ? { hidden: "1" } : {}), }, async () => { - const { getAllGroups } = await import("#lib/db/groups.ts"); + const { getAllGroups } = await import("#shared/db/groups.ts"); const groups = await getAllGroups(); return groups[groups.length - 1] as Group; }, @@ -477,7 +477,7 @@ export const updateTestGroup = async ( groupId: number, updates: Partial>, ): Promise => { - const { groupsTable } = await import("#lib/db/groups.ts"); + const { groupsTable } = await import("#shared/db/groups.ts"); const existing = (await groupsTable.findById(groupId)) as Group; const hidden = updates.hidden ?? existing.hidden; @@ -501,7 +501,7 @@ export const updateTestGroup = async ( }; export const deleteTestGroup = async (groupId: number): Promise => { - const { groupsTable } = await import("#lib/db/groups.ts"); + const { groupsTable } = await import("#shared/db/groups.ts"); const existing = (await groupsTable.findById(groupId)) as Group; return doAuthenticatedFormRequest( @@ -529,7 +529,7 @@ export const createTestHoliday = ( start_date: input.startDate, }, async () => { - const { getAllHolidays } = await import("#lib/db/holidays.ts"); + const { getAllHolidays } = await import("#shared/db/holidays.ts"); const holidays = await getAllHolidays(); return holidays[holidays.length - 1] as Holiday; }, @@ -541,7 +541,7 @@ export const updateTestHoliday = async ( holidayId: number, updates: Partial, ): Promise => { - const { holidaysTable } = await import("#lib/db/holidays.ts"); + const { holidaysTable } = await import("#shared/db/holidays.ts"); const existing = (await holidaysTable.findById(holidayId)) as Holiday; return doAuthenticatedFormRequest( @@ -560,7 +560,7 @@ export const updateTestHoliday = async ( }; export const deleteTestHoliday = async (holidayId: number): Promise => { - const { holidaysTable } = await import("#lib/db/holidays.ts"); + const { holidaysTable } = await import("#shared/db/holidays.ts"); const existing = (await holidaysTable.findById(holidayId)) as Holiday; return doAuthenticatedFormRequest( @@ -573,7 +573,7 @@ export const deleteTestHoliday = async (holidayId: number): Promise => { export const createTestBuiltSite = ( overrides: Partial = {}, -): Promise => { +): Promise => { const input: BuiltSiteFormInput = { assignable: overrides.assignable ?? false, bunnyScriptId: overrides.bunnyScriptId ?? "", @@ -594,11 +594,11 @@ export const createTestBuiltSite = ( ...(input.assignable ? { assignable: "1" } : {}), }, async () => { - const { getAllBuiltSites } = await import("#lib/db/built-sites.ts"); + const { getAllBuiltSites } = await import("#shared/db/built-sites.ts"); const sites = await getAllBuiltSites(); return sites[ sites.length - 1 - ] as import("#lib/db/built-sites.ts").BuiltSite; + ] as import("#shared/db/built-sites.ts").BuiltSite; }, "create built site", ); @@ -607,11 +607,11 @@ export const createTestBuiltSite = ( export const updateTestBuiltSite = async ( siteId: number, updates: Partial, -): Promise => { - const { builtSitesCrudTable } = await import("#lib/db/built-sites.ts"); +): Promise => { + const { builtSitesCrudTable } = await import("#shared/db/built-sites.ts"); const existing = (await builtSitesCrudTable.findById( siteId, - )) as import("#lib/db/built-sites.ts").BuiltSite; + )) as import("#shared/db/built-sites.ts").BuiltSite; const assignable = updates.assignable ?? existing.assignable; return doAuthenticatedFormRequest( @@ -626,17 +626,17 @@ export const updateTestBuiltSite = async ( }, async () => { const updated = await builtSitesCrudTable.findById(siteId); - return updated as import("#lib/db/built-sites.ts").BuiltSite; + return updated as import("#shared/db/built-sites.ts").BuiltSite; }, "update built site", ); }; export const deleteTestBuiltSite = async (siteId: number): Promise => { - const { builtSitesCrudTable } = await import("#lib/db/built-sites.ts"); + const { builtSitesCrudTable } = await import("#shared/db/built-sites.ts"); const existing = (await builtSitesCrudTable.findById( siteId, - )) as import("#lib/db/built-sites.ts").BuiltSite; + )) as import("#shared/db/built-sites.ts").BuiltSite; return doAuthenticatedFormRequest( `/admin/built-sites/${siteId}/delete`, diff --git a/src/test-utils/db.ts b/src/test-utils/db.ts index 52d840540..dfc7ed17f 100644 --- a/src/test-utils/db.ts +++ b/src/test-utils/db.ts @@ -1,17 +1,20 @@ import { createClient, type InValue, type Row } from "@libsql/client"; import { afterEach, beforeEach, describe } from "@std/testing/bdd"; -import { resetEffectiveDomain } from "#lib/config.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; -import { getDb, insert, queryOne, setDb } from "#lib/db/client.ts"; -import { invalidateEventsCache } from "#lib/db/events.ts"; -import { invalidateGroupsCache } from "#lib/db/groups.ts"; -import { invalidateHolidaysCache } from "#lib/db/holidays.ts"; -import { initDb } from "#lib/db/migrations.ts"; -import { resetSessionCache } from "#lib/db/sessions.ts"; -import { settings } from "#lib/db/settings.ts"; -import { invalidateUsersCache } from "#lib/db/users.ts"; -import { setDemoModeForTest } from "#lib/demo.ts"; -import { resetHostEmailConfig, setHostEmailConfigForTest } from "#lib/email.ts"; +import { resetEffectiveDomain } from "#shared/config.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { getDb, insert, queryOne, setDb } from "#shared/db/client.ts"; +import { invalidateEventsCache } from "#shared/db/events.ts"; +import { invalidateGroupsCache } from "#shared/db/groups.ts"; +import { invalidateHolidaysCache } from "#shared/db/holidays.ts"; +import { initDb } from "#shared/db/migrations.ts"; +import { resetSessionCache } from "#shared/db/sessions.ts"; +import { settings } from "#shared/db/settings.ts"; +import { invalidateUsersCache } from "#shared/db/users.ts"; +import { setDemoModeForTest } from "#shared/demo.ts"; +import { + resetHostEmailConfig, + setHostEmailConfigForTest, +} from "#shared/email.ts"; import { setTestEnv, setupTestEncryptionKey } from "#test-utils/env.ts"; import { type DescribeEnvOptions, @@ -131,18 +134,18 @@ const createDirectAdminSession = async (): Promise<{ cookie: string; csrfToken: string; }> => { - const { generateSecureToken } = await import("#lib/crypto/utils.ts"); + const { generateSecureToken } = await import("#shared/crypto/utils.ts"); const { deriveKEK, unwrapKey, wrapKeyWithToken } = await import( - "#lib/crypto/keys.ts" + "#shared/crypto/keys.ts" ); const { createSession: createDbSession } = await import( - "#lib/db/sessions.ts" + "#shared/db/sessions.ts" ); - const { buildSessionCookie } = await import("#lib/cookies.ts"); + const { buildSessionCookie } = await import("#shared/cookies.ts"); const { getUserByUsername, verifyUserPassword } = await import( - "#lib/db/users.ts" + "#shared/db/users.ts" ); - const { nowMs } = await import("#lib/now.ts"); + const { nowMs } = await import("#shared/now.ts"); const user = await getUserByUsername(TEST_ADMIN_USERNAME); if (!user?.wrapped_data_key) { diff --git a/src/test-utils/env.ts b/src/test-utils/env.ts index 28fdeabf3..cb1982334 100644 --- a/src/test-utils/env.ts +++ b/src/test-utils/env.ts @@ -1,8 +1,11 @@ -import { setEncryptionKeyForTest } from "#lib/crypto/encryption.ts"; -import { setFastPbkdf2ForTest } from "#lib/crypto/hashing.ts"; -import { setRsaKeySizeForTest } from "#lib/crypto/keys.ts"; -import { setSuppressDebugLogs, setSuppressRequestLogs } from "#lib/logger.ts"; -import { setRethrowErrors, setSkipLoginDelay } from "#lib/test-overrides.ts"; +import { setEncryptionKeyForTest } from "#shared/crypto/encryption.ts"; +import { setFastPbkdf2ForTest } from "#shared/crypto/hashing.ts"; +import { setRsaKeySizeForTest } from "#shared/crypto/keys.ts"; +import { + setSuppressDebugLogs, + setSuppressRequestLogs, +} from "#shared/logger.ts"; +import { setRethrowErrors, setSkipLoginDelay } from "#shared/test-overrides.ts"; import { TEST_ENCRYPTION_KEY } from "#test-utils/internal.ts"; export const setupTestEncryptionKey = (): void => { diff --git a/src/test-utils/factories.ts b/src/test-utils/factories.ts index def3f2334..7663448ee 100644 --- a/src/test-utils/factories.ts +++ b/src/test-utils/factories.ts @@ -1,15 +1,15 @@ -import type { BuiltSite } from "#lib/db/built-sites.ts"; -import type { EventInput } from "#lib/db/events.ts"; -import type { EmailEntry, EmailEvent } from "#lib/email.ts"; -import type { SessionMetadata } from "#lib/payments.ts"; +import type { BuiltSite } from "#shared/db/built-sites.ts"; +import type { EventInput } from "#shared/db/events.ts"; +import type { EmailEntry, EmailEvent } from "#shared/email.ts"; +import type { SessionMetadata } from "#shared/payments.ts"; import type { Attendee, Event, EventWithCount, Group, Holiday, -} from "#lib/types.ts"; -import type { WebhookAttendee } from "#lib/webhook.ts"; +} from "#shared/types.ts"; +import type { WebhookAttendee } from "#shared/webhook.ts"; import { generateTestEventName } from "#test-utils/internal.ts"; export const testEvent = (overrides: Partial = {}): Event => ({ diff --git a/src/test-utils/internal.ts b/src/test-utils/internal.ts index 26c53d95e..eaa4f5848 100644 --- a/src/test-utils/internal.ts +++ b/src/test-utils/internal.ts @@ -81,14 +81,14 @@ export interface FetchCall { } export type AdminTestContext = { - event: import("#lib/types.ts").Event; - attendee: import("#lib/types.ts").Attendee; + event: import("#shared/types.ts").Event; + attendee: import("#shared/types.ts").Attendee; cookie: string; csrfToken: string; }; export type BookAttendeeOpts = Partial< - Omit + Omit > & { name?: string; email?: string; @@ -105,13 +105,13 @@ export type RawEventRange = { }; export type PaymentProviderType = - import("#lib/payments.ts").PaymentProviderType; + import("#shared/payments.ts").PaymentProviderType; -export type SessionMetadata = import("#lib/payments.ts").SessionMetadata; +export type SessionMetadata = import("#shared/payments.ts").SessionMetadata; -export type { BuiltSiteFormInput } from "#lib/db/built-sites.ts"; -export type { EventInput } from "#lib/db/events.ts"; -export type { GroupInput } from "#lib/db/groups.ts"; -export type { HolidayInput } from "#lib/db/holidays.ts"; -export type { EmailEntry, EmailEvent } from "#lib/email.ts"; -export type { WebhookAttendee } from "#lib/webhook.ts"; +export type { BuiltSiteFormInput } from "#shared/db/built-sites.ts"; +export type { EventInput } from "#shared/db/events.ts"; +export type { GroupInput } from "#shared/db/groups.ts"; +export type { HolidayInput } from "#shared/db/holidays.ts"; +export type { EmailEntry, EmailEvent } from "#shared/email.ts"; +export type { WebhookAttendee } from "#shared/webhook.ts"; diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index f1fab75bd..4ede4465f 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach } from "@std/testing/bdd"; import { stub } from "@std/testing/mock"; import { bracket } from "#fp"; -import { bunnyCdnApi } from "#lib/bunny-cdn.ts"; -import { getSessionCookieName } from "#lib/cookies.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; -import { runWithStorageConfig } from "#lib/storage.ts"; +import { bunnyCdnApi } from "#shared/bunny-cdn.ts"; +import { getSessionCookieName } from "#shared/cookies.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { runWithStorageConfig } from "#shared/storage.ts"; import type { TestRequestOptions } from "#test-utils/internal.ts"; export const mockRequestWithHost = ( @@ -311,8 +311,8 @@ export const withLocalStorageEnabled = async ( }; export const mockProviderType = ( - type: import("#lib/payments.ts").PaymentProviderType, -): import("#lib/payments.ts").PaymentProviderType | null => type; + type: import("#shared/payments.ts").PaymentProviderType, +): import("#shared/payments.ts").PaymentProviderType | null => type; export const stubFetchJson = (body: unknown) => stub(globalThis, "fetch", () => diff --git a/src/test-utils/session.ts b/src/test-utils/session.ts index 8285e585b..cd26778f2 100644 --- a/src/test-utils/session.ts +++ b/src/test-utils/session.ts @@ -1,11 +1,11 @@ import type { Row } from "@libsql/client"; -import { getSessionCookieName } from "#lib/cookies.ts"; -import { generateSecureToken } from "#lib/crypto/utils.ts"; -import { signCsrfToken } from "#lib/csrf.ts"; -import { createApiKey } from "#lib/db/api-keys.ts"; -import type { EventInput } from "#lib/db/events.ts"; -import { getSession } from "#lib/db/sessions.ts"; -import type { Event } from "#lib/types.ts"; +import { getSessionCookieName } from "#shared/cookies.ts"; +import { generateSecureToken } from "#shared/crypto/utils.ts"; +import { signCsrfToken } from "#shared/csrf.ts"; +import { createApiKey } from "#shared/db/api-keys.ts"; +import type { EventInput } from "#shared/db/events.ts"; +import { getSession } from "#shared/db/sessions.ts"; +import type { Event } from "#shared/types.ts"; import type { AdminTestContext } from "#test-utils/internal.ts"; import { getCachedAdminSession, @@ -58,8 +58,8 @@ export const getTestSession = async (): Promise<{ const cached = getCachedAdminSession(); if (cached) { - const { getDb } = await import("#lib/db/client.ts"); - const { insert } = await import("#lib/db/client.ts"); + const { getDb } = await import("#shared/db/client.ts"); + const { insert } = await import("#shared/db/client.ts"); await getDb().execute( insert("sessions", { csrf_token: cached.sessionRow.csrf_token, @@ -90,19 +90,19 @@ export const createTestManagerSession = async ( token = "mgr-session", username = "testmanager", ): Promise => { - const { encrypt: enc } = await import("#lib/crypto/encryption.ts"); - const { hmacHash } = await import("#lib/crypto/hashing.ts"); + const { encrypt: enc } = await import("#shared/crypto/encryption.ts"); + const { hmacHash } = await import("#shared/crypto/hashing.ts"); const { deriveKEK, unwrapKey, wrapKeyWithToken } = await import( - "#lib/crypto/keys.ts" + "#shared/crypto/keys.ts" ); - const { getDb } = await import("#lib/db/client.ts"); - const { insert } = await import("#lib/db/client.ts"); - const { createSession } = await import("#lib/db/sessions.ts"); + const { getDb } = await import("#shared/db/client.ts"); + const { insert } = await import("#shared/db/client.ts"); + const { createSession } = await import("#shared/db/sessions.ts"); const { getUserByUsername, verifyUserPassword, invalidateUsersCache: invalidateUsers, - } = await import("#lib/db/users.ts"); + } = await import("#shared/db/users.ts"); const user = await getUserByUsername(TEST_ADMIN_USERNAME); if (!user) throw new Error("Admin user not found"); @@ -172,7 +172,7 @@ export const createTestApiKeyFull = async ( }; const getTestDataKey = async (): Promise => { - const { unwrapKeyWithToken } = await import("#lib/crypto/keys.ts"); + const { unwrapKeyWithToken } = await import("#shared/crypto/keys.ts"); const cookie = await testCookie(); const sessionMatch = cookie.match( new RegExp(`${getSessionCookieName()}=([^;]+)`), diff --git a/src/test-utils/settings.ts b/src/test-utils/settings.ts index 22c1ed565..80c1663a1 100644 --- a/src/test-utils/settings.ts +++ b/src/test-utils/settings.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, it } from "@std/testing/bdd"; import { stub } from "@std/testing/mock"; -import type { SettingsData } from "#lib/db/settings.ts"; -import { settings } from "#lib/db/settings.ts"; +import type { SettingsData } from "#shared/db/settings.ts"; +import { settings } from "#shared/db/settings.ts"; export const withSetting = async ( overrides: Partial, @@ -36,7 +36,7 @@ export const testWithSetting = ( }; export const setupStripe = async (key = "sk_test_mock"): Promise => { - const { settings: s } = await import("#lib/db/settings.ts"); + const { settings: s } = await import("#shared/db/settings.ts"); await s.update.stripe.secretKey(key); await s.update.paymentProvider("stripe"); }; @@ -46,7 +46,7 @@ export const stubWebhookVerify = async (eventData: { type: string; data: { object: Record }; }) => { - const { stripePaymentProvider } = await import("#lib/stripe-provider.ts"); + const { stripePaymentProvider } = await import("#shared/stripe-provider.ts"); return stub(stripePaymentProvider, "verifyWebhookSignature", () => Promise.resolve({ event: eventData, valid: true as const }), ); diff --git a/src/test-utils/validation.ts b/src/test-utils/validation.ts index add87364c..0ec5f5862 100644 --- a/src/test-utils/validation.ts +++ b/src/test-utils/validation.ts @@ -1,7 +1,7 @@ import { expect } from "@std/expect"; -import { FormParams } from "#lib/form-data.ts"; -import type { Field } from "#lib/forms.tsx"; -import { validateForm } from "#lib/forms.tsx"; +import { FormParams } from "#shared/form-data.ts"; +import type { Field } from "#shared/forms.tsx"; +import { validateForm } from "#shared/forms.tsx"; const validateFormData = (fields: Field[], data: Record) => validateForm(new FormParams(data), fields); diff --git a/src/client/admin.ts b/src/ui/client/admin.ts similarity index 100% rename from src/client/admin.ts rename to src/ui/client/admin.ts diff --git a/src/client/admin/char-counter.ts b/src/ui/client/admin/char-counter.ts similarity index 100% rename from src/client/admin/char-counter.ts rename to src/ui/client/admin/char-counter.ts diff --git a/src/client/admin/checkout-popup.ts b/src/ui/client/admin/checkout-popup.ts similarity index 100% rename from src/client/admin/checkout-popup.ts rename to src/ui/client/admin/checkout-popup.ts diff --git a/src/client/admin/closes-at-autofill.ts b/src/ui/client/admin/closes-at-autofill.ts similarity index 100% rename from src/client/admin/closes-at-autofill.ts rename to src/ui/client/admin/closes-at-autofill.ts diff --git a/src/client/admin/csrf.ts b/src/ui/client/admin/csrf.ts similarity index 100% rename from src/client/admin/csrf.ts rename to src/ui/client/admin/csrf.ts diff --git a/src/client/admin/custom-question-visibility.ts b/src/ui/client/admin/custom-question-visibility.ts similarity index 100% rename from src/client/admin/custom-question-visibility.ts rename to src/ui/client/admin/custom-question-visibility.ts diff --git a/src/client/admin/duplicate-preview.ts b/src/ui/client/admin/duplicate-preview.ts similarity index 98% rename from src/client/admin/duplicate-preview.ts rename to src/ui/client/admin/duplicate-preview.ts index c5c2e8089..db1df49bf 100644 --- a/src/client/admin/duplicate-preview.ts +++ b/src/ui/client/admin/duplicate-preview.ts @@ -11,7 +11,7 @@ import { type DuplicateReplacements, formatIsoForPreview, type PreviewableEvent, -} from "#lib/bulk-replace.ts"; +} from "#shared/bulk-replace.ts"; export const initDuplicatePreview = (): void => { const container = document.querySelector( diff --git a/src/client/admin/event-date-picker.ts b/src/ui/client/admin/event-date-picker.ts similarity index 100% rename from src/client/admin/event-date-picker.ts rename to src/ui/client/admin/event-date-picker.ts diff --git a/src/client/admin/fill-default-template.ts b/src/ui/client/admin/fill-default-template.ts similarity index 100% rename from src/client/admin/fill-default-template.ts rename to src/ui/client/admin/fill-default-template.ts diff --git a/src/client/admin/form-submit-disable.ts b/src/ui/client/admin/form-submit-disable.ts similarity index 100% rename from src/client/admin/form-submit-disable.ts rename to src/ui/client/admin/form-submit-disable.ts diff --git a/src/client/admin/iframe-scroll-into-view.ts b/src/ui/client/admin/iframe-scroll-into-view.ts similarity index 100% rename from src/client/admin/iframe-scroll-into-view.ts rename to src/ui/client/admin/iframe-scroll-into-view.ts diff --git a/src/client/admin/manual-checkin.ts b/src/ui/client/admin/manual-checkin.ts similarity index 100% rename from src/client/admin/manual-checkin.ts rename to src/ui/client/admin/manual-checkin.ts diff --git a/src/client/admin/multi-booking.ts b/src/ui/client/admin/multi-booking.ts similarity index 97% rename from src/client/admin/multi-booking.ts rename to src/ui/client/admin/multi-booking.ts index 0d33e35e7..3567d5a20 100644 --- a/src/client/admin/multi-booking.ts +++ b/src/ui/client/admin/multi-booking.ts @@ -1,5 +1,5 @@ /// -import { buildEmbedSnippets } from "#lib/embed.ts"; +import { buildEmbedSnippets } from "#shared/embed.ts"; /** Multi-booking link builder: track checkbox selection order and * render a combined URL + embed snippets once 2+ events are selected. */ diff --git a/src/client/admin/nav-select.ts b/src/ui/client/admin/nav-select.ts similarity index 100% rename from src/client/admin/nav-select.ts rename to src/ui/client/admin/nav-select.ts diff --git a/src/client/admin/payment-result.ts b/src/ui/client/admin/payment-result.ts similarity index 100% rename from src/client/admin/payment-result.ts rename to src/ui/client/admin/payment-result.ts diff --git a/src/client/admin/payment-test-buttons.ts b/src/ui/client/admin/payment-test-buttons.ts similarity index 100% rename from src/client/admin/payment-test-buttons.ts rename to src/ui/client/admin/payment-test-buttons.ts diff --git a/src/client/admin/qr-refresh.ts b/src/ui/client/admin/qr-refresh.ts similarity index 100% rename from src/client/admin/qr-refresh.ts rename to src/ui/client/admin/qr-refresh.ts diff --git a/src/client/admin/scroll-hide-nav.ts b/src/ui/client/admin/scroll-hide-nav.ts similarity index 100% rename from src/client/admin/scroll-hide-nav.ts rename to src/ui/client/admin/scroll-hide-nav.ts diff --git a/src/client/admin/select-on-click.ts b/src/ui/client/admin/select-on-click.ts similarity index 100% rename from src/client/admin/select-on-click.ts rename to src/ui/client/admin/select-on-click.ts diff --git a/src/client/admin/ticket-quantity-required.ts b/src/ui/client/admin/ticket-quantity-required.ts similarity index 100% rename from src/client/admin/ticket-quantity-required.ts rename to src/ui/client/admin/ticket-quantity-required.ts diff --git a/src/client/embed.ts b/src/ui/client/embed.ts similarity index 100% rename from src/client/embed.ts rename to src/ui/client/embed.ts diff --git a/src/client/iframe-resizer-child.ts b/src/ui/client/iframe-resizer-child.ts similarity index 100% rename from src/client/iframe-resizer-child.ts rename to src/ui/client/iframe-resizer-child.ts diff --git a/src/client/iframe-resizer-parent.ts b/src/ui/client/iframe-resizer-parent.ts similarity index 100% rename from src/client/iframe-resizer-parent.ts rename to src/ui/client/iframe-resizer-parent.ts diff --git a/src/client/scanner.js b/src/ui/client/scanner.js similarity index 99% rename from src/client/scanner.js rename to src/ui/client/scanner.js index 8b2a1fbc3..f7392fea1 100644 --- a/src/client/scanner.js +++ b/src/ui/client/scanner.js @@ -242,7 +242,9 @@ const init = () => { const stopCamera = () => { const stream = video.srcObject; if (stream) { - stream.getTracks().forEach((t) => t.stop()); + for (const t of stream.getTracks()) { + t.stop(); + } } }; diff --git a/src/ui/static/admin.js b/src/ui/static/admin.js new file mode 100644 index 000000000..245f0b373 --- /dev/null +++ b/src/ui/static/admin.js @@ -0,0 +1,2 @@ +"use strict";(()=>{var k=()=>{for(let e of document.querySelectorAll("textarea[maxlength]")){let t=Number(e.getAttribute("maxlength"));if(!t)continue;let n=document.createElement("small");n.className="char-counter";let o=()=>{let r=t-e.value.length;n.textContent=`${r} / ${t}`,n.classList.toggle("char-counter-warn",r{let e=document.querySelector("[data-checkout-popup]");if(!e)return;let t=e.dataset.checkoutPopup,n=e.querySelector("[data-checkout-waiting]"),o=e.querySelector("[data-open-checkout]"),r=null,s=()=>{n.hidden=!0,o.parentElement.hidden=!1};window.addEventListener("message",c=>{c.origin===location.origin&&(c.data?.type==="payment-success"?location.href="/ticket/reserved":c.data?.type==="payment-cancel"&&s())});let a=()=>{if(!r||r.closed){s();return}setTimeout(a,500)};o.addEventListener("click",c=>{let d=window.open(t,"_blank");d&&(c.preventDefault(),r=d,o.parentElement.hidden=!0,n.hidden=!1,a())})};var b=()=>{let e=document.querySelector('input[name="date"]'),t=document.querySelector('input[name="closes_at"]');!e||!t||e.addEventListener("change",()=>{e.value&&!t.value&&(t.value=e.value)})};var w=()=>{let e=document.querySelectorAll("fieldset.custom-question[data-event-ids]");if(e.length===0)return;let t=()=>{for(let n of e){let r=(n.dataset.eventIds??"").split(" ").some(s=>{let a=document.querySelector(`[name="quantity_${s}"]`);return a!==null&&Number.parseInt(a.value,10)>0});n.hidden=!r;for(let s of n.querySelectorAll('input[type="radio"]'))s.required=r}};for(let n of document.querySelectorAll('[name^="quantity_"]'))n.addEventListener("change",t);t()};var K=(e,t,n)=>t?e.split(t).join(n):e,Z=(e,t)=>!e||!t?0:Math.round((Date.parse(t)-Date.parse(e))/864e5),G=(e,t)=>!e||t===0?e:new Date(Date.parse(e)+t*864e5).toISOString(),M=(e,t)=>{let n=Z(t.dateFind,t.dateReplace);return e.map(o=>({id:o.id,newDate:G(o.date,n),newName:K(o.name,t.nameFind,t.nameReplace),originalDate:o.date,originalName:o.name}))},L=(e,t)=>e?new Date(e).toLocaleString("sv-SE",{day:"2-digit",hour:"2-digit",hour12:!1,minute:"2-digit",month:"2-digit",timeZone:t,year:"numeric"}).replace(/^(\d{4}-\d{2}-\d{2}) 24:/,"$1 00:"):"";var I=()=>{let e=document.querySelector("[data-duplicate-preview]"),t=document.getElementById("duplicate-preview-events"),n=document.querySelector("[data-duplicate-preview-rows]");if(!e||!t||!n)return;let o=JSON.parse(t.textContent??"[]"),r=e.dataset.timezone??"UTC",s=c=>e.querySelector(`[data-duplicate-field="${c}"]`)?.value??"",a=()=>{let c={dateFind:s("date_find"),dateReplace:s("date_replace"),nameFind:s("name_find"),nameReplace:s("name_replace")},d=M(o,c);n.innerHTML=d.map(u=>{let m=(E,y)=>`${y.replace(/&/g,"&").replace(//g,">")}`;return``+m("original-name",u.originalName)+m("new-name",u.newName)+m("original-date",L(u.originalDate,r))+m("new-date",L(u.newDate,r))+""}).join("")};for(let c of e.querySelectorAll("[data-duplicate-field]"))c.addEventListener("input",a)};var H=()=>{let e=document.getElementById("available-dates-data"),t=document.querySelector('select[name="event_id"], #add_event_id'),n=document.querySelector(".daily-date-field"),o=document.querySelector('select[name="date"], #add_date');if(!e||!t||!n||!o)return;let r=JSON.parse(e.textContent??"{}");t.addEventListener("change",()=>{let s=r[t.value];s&&s.length>0?(n.style.display="",o.innerHTML=''+s.map(a=>``).join(""),o.required=!0):(n.style.display="none",o.required=!1,o.value="")})};var q=()=>{for(let e of document.querySelectorAll("[data-fill-default]"))e.addEventListener("click",t=>{t.preventDefault();let n=document.getElementById(e.dataset.fillDefault);n&&!n.value&&(n.value=n.dataset.defaultTpl??"",n.focus())})};var x=()=>{for(let e of document.querySelectorAll('form[method="POST"]:not([data-manual-checkin])'))e.addEventListener("submit",t=>{requestAnimationFrame(()=>{if(!t.defaultPrevented)for(let n=0;n{if(e.persisted)for(let t of document.querySelectorAll("form[method='POST'] :disabled"))t.disabled=!1})};var R=()=>{if(!document.querySelector("[data-scroll-into-view]"))return;let{parentIframe:e}=window;e?.scrollToOffset(0,0)};var A=()=>{let e=document.querySelector("[data-manual-checkin]");if(!e)return;let t=e.querySelector("#manual-checkin-input"),n=document.getElementById("manual-checkin-token"),o=document.getElementById("ticket-options"),r=document.getElementById("manual-checkin-status"),s=e.dataset.eventId,a=e.querySelector('input[name="csrf_token"]'),c=()=>o.querySelectorAll("[role='option']"),d=()=>{o.classList.remove("hidden"),t.setAttribute("aria-expanded","true")},u=()=>{o.classList.add("hidden"),t.setAttribute("aria-expanded","false")},m=()=>{let i=t.value.toLowerCase(),l=!1;for(let p of c()){let f=(p.textContent??"").toLowerCase().includes(i);p.classList.toggle("hidden",!f),f&&(l=!0)}l&&document.activeElement===t?d():u()},E=i=>{n.value=i.dataset.token,t.value=`${i.dataset.name} (${i.dataset.quantity} ticket${i.dataset.quantity==="1"?"":"s"})`,u()};t.addEventListener("input",()=>{n.value="",m()}),t.addEventListener("focus",()=>{m()}),document.addEventListener("click",i=>{!t.contains(i.target)&&!o.contains(i.target)&&u()});let y=()=>[...o.querySelectorAll("[role='option']:not(.hidden)")],S=()=>o.querySelector("[role='option'].combobox-active"),W=i=>{let l=y();if(l.length===0)return;let p=S(),v=p?l.indexOf(p):-1;p?.classList.remove("combobox-active");let g=l[(v+(i==="down"?1:-1)+l.length)%l.length];g&&(g.classList.add("combobox-active"),g.scrollIntoView({block:"nearest"}))},Q=i=>{if(i.key==="Escape"){u();return}if(i.key==="ArrowDown"||i.key==="ArrowUp"){i.preventDefault(),W(i.key==="ArrowDown"?"down":"up");return}if(i.key==="Enter"){let l=S();l&&(i.preventDefault(),E(l))}};t.addEventListener("keydown",Q),o.addEventListener("click",i=>{let l=i.target.closest("[role='option']");l&&E(l)});let h=(i,l)=>{r.textContent=i,r.classList.remove("hidden","checkin-status-success","checkin-status-warning","checkin-status-error"),r.classList.add("checkin-status",`checkin-status-${l}`)},Y=(i,l,p)=>{let v=Number.isFinite(i.quantity)?i.quantity:1,f=p?" \u2014 verify their ID":"";h(`${i.name} checked in (${v} ticket${v===1?"":"s"})${f}`,"success");for(let g of c())if(g.dataset.token===l){g.remove();break}n.value="",t.value=""},z=(i,l,p)=>{i.status==="checked_in"?Y(i,l,p):i.status==="already_checked_in"?h(`${i.name} already checked in`,"warning"):i.status==="refunded"?h(`${i.name} has been refunded`,"error"):i.status==="not_found"?h("Ticket not found","error"):h(i.message??"Error","error")};e.addEventListener("submit",async i=>{i.preventDefault();let l=n.value.trim();if(!l)return;let p=e.querySelector('button[type="submit"]');p.disabled=!0;let v=async f=>(await fetch(`/admin/event/${s}/scan`,{body:JSON.stringify(f),headers:{"content-type":"application/json","x-csrf-token":a.value},method:"POST"})).json();try{let f=await v({token:l}),g=!1;f.status==="verify_id"&&(g=!0,f=await v({id_verified:!0,token:l})),z(f,l,g)}catch{h("Network error","error")}p.disabled=!1})};var P="/embed.js";var X="600px",ee=e=>(new URL(e).pathname.match(/\/ticket\/([^/]+)/)?.[1]??"").split("+").map(n=>n.trim()).filter(n=>n.length>0),te=e=>{let t=new URL(e);return t.searchParams.set("iframe","true"),t.toString()},_=e=>{let t=new URL(e).origin,n=ee(e).join("+"),o=`