@access-dlsu/leapify 0.260505.5 → 0.260507.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +15 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/auth/auth.d.ts +99 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/middleware.d.ts +20 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/types.d.ts +17 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/bun-sqlite-dialect-na--YwnN-NIYANHVJ.cjs +162 -0
- package/dist/bun-sqlite-dialect-na--YwnN-NIYANHVJ.cjs.map +1 -0
- package/dist/bun-sqlite-dialect-na--YwnN-XVQNOKSL.js +160 -0
- package/dist/bun-sqlite-dialect-na--YwnN-XVQNOKSL.js.map +1 -0
- package/dist/chunk-4DPT2KQR.cjs +467 -0
- package/dist/chunk-4DPT2KQR.cjs.map +1 -0
- package/dist/chunk-5JKLV7IE.cjs +2962 -0
- package/dist/chunk-5JKLV7IE.cjs.map +1 -0
- package/dist/chunk-5OQD5ALM.cjs +76 -0
- package/dist/chunk-5OQD5ALM.cjs.map +1 -0
- package/dist/{chunk-FLR7TNLN.js → chunk-63CUZGSZ.js} +4 -12
- package/dist/chunk-63CUZGSZ.js.map +1 -0
- package/dist/chunk-6MMWL46O.cjs +7170 -0
- package/dist/chunk-6MMWL46O.cjs.map +1 -0
- package/dist/chunk-ANNHE3PZ.js +2285 -0
- package/dist/chunk-ANNHE3PZ.js.map +1 -0
- package/dist/chunk-EGRHWZRV.js +3 -0
- package/dist/chunk-EGRHWZRV.js.map +1 -0
- package/dist/chunk-EMMSS5I5.cjs +37 -0
- package/dist/chunk-EMMSS5I5.cjs.map +1 -0
- package/dist/chunk-FUCJEA2S.js +6196 -0
- package/dist/chunk-FUCJEA2S.js.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/chunk-GNRL67OU.js +2949 -0
- package/dist/chunk-GNRL67OU.js.map +1 -0
- package/dist/chunk-HHNEB7YR.js +8 -0
- package/dist/chunk-HHNEB7YR.js.map +1 -0
- package/dist/chunk-IQEWVHLM.js +889 -0
- package/dist/chunk-IQEWVHLM.js.map +1 -0
- package/dist/chunk-JIZPYG6H.js +72 -0
- package/dist/chunk-JIZPYG6H.js.map +1 -0
- package/dist/chunk-JPVIXCF5.cjs +10 -0
- package/dist/chunk-JPVIXCF5.cjs.map +1 -0
- package/dist/chunk-JQSZJWBN.cjs +3075 -0
- package/dist/chunk-JQSZJWBN.cjs.map +1 -0
- package/dist/chunk-MNEW2V4T.js +447 -0
- package/dist/chunk-MNEW2V4T.js.map +1 -0
- package/dist/chunk-MY37YE52.js +3034 -0
- package/dist/chunk-MY37YE52.js.map +1 -0
- package/dist/chunk-NKIQRCOM.cjs +4 -0
- package/dist/chunk-NKIQRCOM.cjs.map +1 -0
- package/dist/chunk-OK6RVPEH.cjs +6200 -0
- package/dist/chunk-OK6RVPEH.cjs.map +1 -0
- package/dist/chunk-QARF2YFF.cjs +2296 -0
- package/dist/chunk-QARF2YFF.cjs.map +1 -0
- package/dist/chunk-RFP2X2FA.cjs +903 -0
- package/dist/chunk-RFP2X2FA.cjs.map +1 -0
- package/dist/chunk-XJSWMHDL.js +7142 -0
- package/dist/chunk-XJSWMHDL.js.map +1 -0
- package/dist/{chunk-3ZWIJNZG.cjs → chunk-YFJBE3AU.cjs} +4 -12
- package/dist/chunk-YFJBE3AU.cjs.map +1 -0
- package/dist/client/{index.d.cts → auth.d.ts} +140 -394
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/index.cjs +909 -48
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +83 -1134
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +908 -47
- package/dist/client/index.js.map +1 -1
- package/dist/client/pow.d.ts +28 -0
- package/dist/client/pow.d.ts.map +1 -0
- package/dist/client/session.d.ts +29 -0
- package/dist/client/session.d.ts.map +1 -0
- package/dist/client/types.d.ts +58 -38
- package/dist/client/types.d.ts.map +1 -0
- package/dist/cron/batch-release.d.ts +9 -0
- package/dist/cron/batch-release.d.ts.map +1 -0
- package/dist/cron/lifecycle-check.d.ts +10 -0
- package/dist/cron/lifecycle-check.d.ts.map +1 -0
- package/dist/cron/reconcile-slots.d.ts +10 -0
- package/dist/cron/reconcile-slots.d.ts.map +1 -0
- package/dist/cron/reminder-emails.d.ts +9 -0
- package/dist/cron/reminder-emails.d.ts.map +1 -0
- package/dist/cron/renew-watches.d.ts +9 -0
- package/dist/cron/renew-watches.d.ts.map +1 -0
- package/dist/d1-sqlite-dialect-C2B7YsIT-6TVV7EJ5.js +122 -0
- package/dist/d1-sqlite-dialect-C2B7YsIT-6TVV7EJ5.js.map +1 -0
- package/dist/d1-sqlite-dialect-C2B7YsIT-PE74FLHQ.cjs +124 -0
- package/dist/d1-sqlite-dialect-C2B7YsIT-PE74FLHQ.cjs.map +1 -0
- package/dist/db/index.d.ts +7 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/migrate.d.ts +23 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/schema/auth.d.ts +649 -0
- package/dist/db/schema/auth.d.ts.map +1 -0
- package/dist/db/schema/bookmarks.d.ts +88 -0
- package/dist/db/schema/bookmarks.d.ts.map +1 -0
- package/dist/db/schema/events.d.ts +546 -0
- package/dist/db/schema/events.d.ts.map +1 -0
- package/dist/db/schema/faqs.d.ts +137 -0
- package/dist/db/schema/faqs.d.ts.map +1 -0
- package/dist/db/schema/index.d.ts +9 -0
- package/dist/db/schema/index.d.ts.map +1 -0
- package/dist/db/schema/organizations.d.ts +125 -0
- package/dist/db/schema/organizations.d.ts.map +1 -0
- package/dist/db/schema/site-config.d.ts +64 -0
- package/dist/db/schema/site-config.d.ts.map +1 -0
- package/dist/db/schema/themes.d.ts +104 -0
- package/dist/db/schema/themes.d.ts.map +1 -0
- package/dist/{types-lTjxCp88.d.cts → db/schema/users.d.ts} +11 -96
- package/dist/db/schema/users.d.ts.map +1 -0
- package/dist/dist-DZHA5VYX.cjs +260 -0
- package/dist/dist-DZHA5VYX.cjs.map +1 -0
- package/dist/dist-RRQUBLLO.js +258 -0
- package/dist/dist-RRQUBLLO.js.map +1 -0
- package/dist/index.cjs +38065 -937
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +23 -1818
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37940 -816
- package/dist/index.js.map +1 -1
- package/dist/kysely-adapter-C76KJVG7.js +10 -0
- package/dist/kysely-adapter-C76KJVG7.js.map +1 -0
- package/dist/kysely-adapter-TGY4UUP5.cjs +27 -0
- package/dist/kysely-adapter-TGY4UUP5.cjs.map +1 -0
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/middleware/cors.d.ts +3 -0
- package/dist/lib/middleware/cors.d.ts.map +1 -0
- package/dist/lib/middleware/error-handler.d.ts +3 -0
- package/dist/lib/middleware/error-handler.d.ts.map +1 -0
- package/dist/lib/middleware/pow-challenge.cjs +7 -7
- package/dist/lib/middleware/pow-challenge.d.ts +29 -18
- package/dist/lib/middleware/pow-challenge.d.ts.map +1 -0
- package/dist/lib/middleware/pow-challenge.js +2 -2
- package/dist/lib/middleware/rate-limit.d.ts +48 -0
- package/dist/lib/middleware/rate-limit.d.ts.map +1 -0
- package/dist/lib/middleware/referer-guard.d.ts +18 -0
- package/dist/lib/middleware/referer-guard.d.ts.map +1 -0
- package/dist/lib/retry.d.ts +12 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/node-sqlite-dialect-B3H37T3R.cjs +162 -0
- package/dist/node-sqlite-dialect-B3H37T3R.cjs.map +1 -0
- package/dist/node-sqlite-dialect-GDP7ZE54.js +160 -0
- package/dist/node-sqlite-dialect-GDP7ZE54.js.map +1 -0
- package/dist/queues/handlers.d.ts +13 -0
- package/dist/queues/handlers.d.ts.map +1 -0
- package/dist/queues/jobs.d.ts +42 -0
- package/dist/queues/jobs.d.ts.map +1 -0
- package/dist/routes/events.d.ts +4 -0
- package/dist/routes/events.d.ts.map +1 -0
- package/dist/routes/faqs.d.ts +4 -0
- package/dist/routes/faqs.d.ts.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/internal/gforms-webhook.d.ts +4 -0
- package/dist/routes/internal/gforms-webhook.d.ts.map +1 -0
- package/dist/routes/organizations.d.ts +4 -0
- package/dist/routes/organizations.d.ts.map +1 -0
- package/dist/routes/site-config.d.ts +4 -0
- package/dist/routes/site-config.d.ts.map +1 -0
- package/dist/routes/themes.d.ts +4 -0
- package/dist/routes/themes.d.ts.map +1 -0
- package/dist/routes/uploads.d.ts +4 -0
- package/dist/routes/uploads.d.ts.map +1 -0
- package/dist/routes/users.d.ts +4 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/services/cache.d.ts +22 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/contentful-management.d.ts +38 -0
- package/dist/services/contentful-management.d.ts.map +1 -0
- package/dist/services/contentful.d.ts +97 -0
- package/dist/services/contentful.d.ts.map +1 -0
- package/dist/services/email.d.ts +75 -0
- package/dist/services/email.d.ts.map +1 -0
- package/dist/services/gforms.d.ts +60 -0
- package/dist/services/gforms.d.ts.map +1 -0
- package/dist/services/resend.d.ts +35 -0
- package/dist/services/resend.d.ts.map +1 -0
- package/dist/services/ses.d.ts +58 -0
- package/dist/services/ses.d.ts.map +1 -0
- package/dist/services/slots.d.ts +48 -0
- package/dist/services/slots.d.ts.map +1 -0
- package/dist/services/snapshot.d.ts +95 -0
- package/dist/services/snapshot.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/worker-handler.d.ts +64 -0
- package/dist/worker-handler.d.ts.map +1 -0
- package/dist/worker.d.ts +36 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +1172 -583
- package/dist/worker.js.map +1 -1
- package/package.json +153 -152
- package/dist/chunk-3ZWIJNZG.cjs.map +0 -1
- package/dist/chunk-FLR7TNLN.js.map +0 -1
- package/dist/client/types.d.cts +0 -192
- package/dist/index.d.cts +0 -1879
- package/dist/lib/middleware/pow-challenge.d.cts +0 -47
- package/dist/types-lTjxCp88.d.ts +0 -208
package/dist/worker.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from 'fs';
|
|
1
|
+
import { createClient } from 'contentful-management';
|
|
3
2
|
|
|
4
3
|
var __create = Object.create;
|
|
5
4
|
var __defProp = Object.defineProperty;
|
|
@@ -25092,14 +25091,11 @@ var errorHandler2 = (err, c) => {
|
|
|
25092
25091
|
|
|
25093
25092
|
// node_modules/hono/dist/middleware/cors/index.js
|
|
25094
25093
|
var cors = (options) => {
|
|
25095
|
-
const
|
|
25094
|
+
const opts = {
|
|
25096
25095
|
origin: "*",
|
|
25097
25096
|
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
25098
25097
|
allowHeaders: [],
|
|
25099
|
-
exposeHeaders: []
|
|
25100
|
-
};
|
|
25101
|
-
const opts = {
|
|
25102
|
-
...defaults,
|
|
25098
|
+
exposeHeaders: [],
|
|
25103
25099
|
...options
|
|
25104
25100
|
};
|
|
25105
25101
|
const findAllowOrigin = ((optsOrigin) => {
|
|
@@ -25332,19 +25328,11 @@ function challengePageHtml(challengeId, difficulty, originalUrl) {
|
|
|
25332
25328
|
const challengeId = ${JSON.stringify(challengeId)};
|
|
25333
25329
|
const difficulty = ${difficulty};
|
|
25334
25330
|
const prefix = '0'.repeat(Math.ceil(difficulty / 4));
|
|
25335
|
-
const data = new TextEncoder().encode(challengeId + ':');
|
|
25336
|
-
const buf = new ArrayBuffer(data.byteLength + 16);
|
|
25337
|
-
new Uint8Array(buf).set(data);
|
|
25338
|
-
const view = new DataView(buf);
|
|
25339
|
-
view.setUint32(data.length, 0);
|
|
25340
|
-
view.setUint32(data.length + 4, 0);
|
|
25341
|
-
view.setUint32(data.length + 8, 0);
|
|
25342
|
-
view.setUint32(data.length + 12, 0);
|
|
25343
25331
|
let nonce = 0;
|
|
25344
25332
|
const t0 = performance.now();
|
|
25345
25333
|
while (true) {
|
|
25346
|
-
|
|
25347
|
-
const hash = await crypto.subtle.digest('SHA-256',
|
|
25334
|
+
const input = challengeId + ':' + nonce;
|
|
25335
|
+
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
|
|
25348
25336
|
const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
25349
25337
|
if (hex.startsWith(prefix)) {
|
|
25350
25338
|
const elapsed = performance.now() - t0;
|
|
@@ -62532,6 +62520,8 @@ __export(schema_exports, {
|
|
|
62532
62520
|
events: () => events,
|
|
62533
62521
|
eventsRelations: () => eventsRelations,
|
|
62534
62522
|
faqs: () => faqs,
|
|
62523
|
+
organizations: () => organizations,
|
|
62524
|
+
organizationsRelations: () => organizationsRelations,
|
|
62535
62525
|
siteConfig: () => siteConfig,
|
|
62536
62526
|
themes: () => themes,
|
|
62537
62527
|
themesRelations: () => themesRelations,
|
|
@@ -62558,13 +62548,26 @@ var themes = sqliteTable("themes", {
|
|
|
62558
62548
|
name: text("name").notNull().unique(),
|
|
62559
62549
|
path: text("path").notNull().unique(),
|
|
62560
62550
|
// e.g. "/pirates-cove"
|
|
62561
|
-
|
|
62562
|
-
|
|
62551
|
+
createdAt: integer2("created_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3)),
|
|
62552
|
+
updatedAt: integer2("updated_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3))
|
|
62563
62553
|
});
|
|
62564
62554
|
var themesRelations = relations(themes, ({ many }) => ({
|
|
62565
62555
|
events: many(events)
|
|
62566
62556
|
}));
|
|
62567
62557
|
|
|
62558
|
+
// src/db/schema/organizations.ts
|
|
62559
|
+
var organizations = sqliteTable("organizations", {
|
|
62560
|
+
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID().replace(/-/g, "")),
|
|
62561
|
+
name: text("name").notNull().unique(),
|
|
62562
|
+
acronym: text("acronym").notNull().unique(),
|
|
62563
|
+
logoUrl: text("logo_url"),
|
|
62564
|
+
link: text("link"),
|
|
62565
|
+
createdAt: integer2("created_at").notNull().default(sql2`(unixepoch())`)
|
|
62566
|
+
});
|
|
62567
|
+
var organizationsRelations = relations(organizations, ({ many }) => ({
|
|
62568
|
+
events: many(events)
|
|
62569
|
+
}));
|
|
62570
|
+
|
|
62568
62571
|
// src/db/schema/events.ts
|
|
62569
62572
|
var events = sqliteTable(
|
|
62570
62573
|
"events",
|
|
@@ -62573,23 +62576,23 @@ var events = sqliteTable(
|
|
|
62573
62576
|
slug: text("slug").notNull().unique(),
|
|
62574
62577
|
// Theme reference
|
|
62575
62578
|
themeId: text("theme_id").references(() => themes.id),
|
|
62579
|
+
// Organization reference
|
|
62580
|
+
organizationId: text("organization_id").references(() => organizations.id),
|
|
62576
62581
|
// Core event fields (maps to LinkData)
|
|
62577
62582
|
title: text("title").notNull(),
|
|
62578
|
-
|
|
62579
|
-
// organizing body / college
|
|
62583
|
+
description: text("description"),
|
|
62580
62584
|
venue: text("venue"),
|
|
62581
62585
|
dateTime: text("date_time"),
|
|
62582
62586
|
// human-readable display string
|
|
62583
|
-
startsAt: integer2("starts_at"),
|
|
62584
|
-
// unix epoch (machine use)
|
|
62585
|
-
endsAt: integer2("ends_at"),
|
|
62586
62587
|
price: text("price"),
|
|
62587
62588
|
// e.g. "Free" or "₱150"
|
|
62588
|
-
backgroundColor: text("background_color"),
|
|
62589
62589
|
backgroundImageUrl: text("background_image_url"),
|
|
62590
|
-
|
|
62591
|
-
|
|
62592
|
-
|
|
62590
|
+
classCode: text("class_code"),
|
|
62591
|
+
// e.g. "CSINTSY"
|
|
62592
|
+
startTime: text("start_time"),
|
|
62593
|
+
// start time string
|
|
62594
|
+
endTime: text("end_time"),
|
|
62595
|
+
// end time string
|
|
62593
62596
|
isMajor: integer2("is_major", { mode: "boolean" }).notNull().default(false),
|
|
62594
62597
|
// Slot tracking (local counter — NOT polled from Google Forms)
|
|
62595
62598
|
maxSlots: integer2("max_slots").notNull().default(0),
|
|
@@ -62598,6 +62601,8 @@ var events = sqliteTable(
|
|
|
62598
62601
|
// Google Form ID for Watch + reconciliation
|
|
62599
62602
|
gformsUrl: text("gforms_url"),
|
|
62600
62603
|
// informational link shown to students
|
|
62604
|
+
gformsEditorUrl: text("gforms_editor_url"),
|
|
62605
|
+
registrationClosesAt: integer2("registration_closes_at"),
|
|
62601
62606
|
watchId: text("watch_id"),
|
|
62602
62607
|
// stored after forms.watches.create
|
|
62603
62608
|
watchExpiresAt: integer2("watch_expires_at"),
|
|
@@ -62608,8 +62613,6 @@ var events = sqliteTable(
|
|
|
62608
62613
|
}).notNull().default("draft"),
|
|
62609
62614
|
releaseAt: integer2("release_at"),
|
|
62610
62615
|
// scheduled publish epoch
|
|
62611
|
-
registrationOpensAt: integer2("registration_opens_at"),
|
|
62612
|
-
registrationClosesAt: integer2("registration_closes_at"),
|
|
62613
62616
|
// Reminder tracking
|
|
62614
62617
|
reminder24hSent: integer2("reminder_24h_sent", { mode: "boolean" }).notNull().default(false),
|
|
62615
62618
|
reminder1hSent: integer2("reminder_1h_sent", { mode: "boolean" }).notNull().default(false),
|
|
@@ -62625,6 +62628,7 @@ var events = sqliteTable(
|
|
|
62625
62628
|
table.releaseAt
|
|
62626
62629
|
),
|
|
62627
62630
|
themeIdx: index("idx_events_theme_id").on(table.themeId),
|
|
62631
|
+
organizationIdx: index("idx_events_organization_id").on(table.organizationId),
|
|
62628
62632
|
slugIdx: index("idx_events_slug").on(table.slug)
|
|
62629
62633
|
})
|
|
62630
62634
|
);
|
|
@@ -62632,6 +62636,10 @@ var eventsRelations = relations(events, ({ one }) => ({
|
|
|
62632
62636
|
theme: one(themes, {
|
|
62633
62637
|
fields: [events.themeId],
|
|
62634
62638
|
references: [themes.id]
|
|
62639
|
+
}),
|
|
62640
|
+
organization: one(organizations, {
|
|
62641
|
+
fields: [events.organizationId],
|
|
62642
|
+
references: [organizations.id]
|
|
62635
62643
|
})
|
|
62636
62644
|
}));
|
|
62637
62645
|
|
|
@@ -62652,7 +62660,6 @@ var faqs = sqliteTable("faqs", {
|
|
|
62652
62660
|
category: text("category"),
|
|
62653
62661
|
// optional grouping, e.g. "Registration"
|
|
62654
62662
|
sortOrder: integer2("sort_order").notNull().default(0),
|
|
62655
|
-
isActive: integer2("is_active", { mode: "boolean" }).notNull().default(true),
|
|
62656
62663
|
createdAt: integer2("created_at").notNull().default(sql2`(unixepoch())`),
|
|
62657
62664
|
updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
|
|
62658
62665
|
});
|
|
@@ -62803,12 +62810,21 @@ function createAuth(env2) {
|
|
|
62803
62810
|
*/
|
|
62804
62811
|
after: async (user) => {
|
|
62805
62812
|
const [{ total }] = await db.select({ total: count() }).from(users);
|
|
62806
|
-
|
|
62813
|
+
const isFirstUser = total === 0;
|
|
62814
|
+
const base = {
|
|
62807
62815
|
betterAuthId: user.id,
|
|
62808
62816
|
email: user.email,
|
|
62809
62817
|
name: user.name ?? user.email.split("@")[0],
|
|
62810
|
-
role:
|
|
62811
|
-
}
|
|
62818
|
+
role: isFirstUser ? "super_admin" : "student"
|
|
62819
|
+
};
|
|
62820
|
+
if (isFirstUser) {
|
|
62821
|
+
await db.insert(users).values(base).onConflictDoUpdate({
|
|
62822
|
+
target: users.betterAuthId,
|
|
62823
|
+
set: { role: "super_admin" }
|
|
62824
|
+
});
|
|
62825
|
+
} else {
|
|
62826
|
+
await db.insert(users).values(base).onConflictDoNothing({ target: users.betterAuthId });
|
|
62827
|
+
}
|
|
62812
62828
|
}
|
|
62813
62829
|
}
|
|
62814
62830
|
}
|
|
@@ -62816,21 +62832,280 @@ function createAuth(env2) {
|
|
|
62816
62832
|
});
|
|
62817
62833
|
}
|
|
62818
62834
|
|
|
62835
|
+
// src/auth/middleware.ts
|
|
62836
|
+
var SESSION_KV_PREFIX = "auth:session:";
|
|
62837
|
+
var SESSION_KV_TTL = 3600;
|
|
62838
|
+
function extractRawToken(c) {
|
|
62839
|
+
const authHeader = c.req.header("Authorization");
|
|
62840
|
+
if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7);
|
|
62841
|
+
const cookie = c.req.header("Cookie") ?? "";
|
|
62842
|
+
const match2 = cookie.match(/(?:^|;\s*)better-auth\.session_token=([^;]+)/);
|
|
62843
|
+
return match2?.[1] ? decodeURIComponent(match2[1]) : void 0;
|
|
62844
|
+
}
|
|
62845
|
+
async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAuthUserName, betterAuthEmailVerified) {
|
|
62846
|
+
const db = createDb(env2.DB);
|
|
62847
|
+
let dbUser = await db.query.users.findFirst({
|
|
62848
|
+
where: eq(users.betterAuthId, betterAuthUserId)
|
|
62849
|
+
});
|
|
62850
|
+
if (!dbUser) {
|
|
62851
|
+
const [created] = await db.insert(users).values({
|
|
62852
|
+
betterAuthId: betterAuthUserId,
|
|
62853
|
+
email: betterAuthUserEmail,
|
|
62854
|
+
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
|
|
62855
|
+
}).onConflictDoNothing({ target: users.betterAuthId }).returning();
|
|
62856
|
+
dbUser = created;
|
|
62857
|
+
}
|
|
62858
|
+
if (!dbUser) throw unauthorized("Failed to resolve user record");
|
|
62859
|
+
return {
|
|
62860
|
+
uid: betterAuthUserId,
|
|
62861
|
+
dbId: dbUser.id,
|
|
62862
|
+
role: dbUser.role,
|
|
62863
|
+
email: betterAuthUserEmail,
|
|
62864
|
+
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0],
|
|
62865
|
+
emailVerified: betterAuthEmailVerified
|
|
62866
|
+
};
|
|
62867
|
+
}
|
|
62868
|
+
var authMiddleware = createMiddleware(
|
|
62869
|
+
async (c, next) => {
|
|
62870
|
+
const rawToken = extractRawToken(c);
|
|
62871
|
+
if (rawToken) {
|
|
62872
|
+
const cached2 = await c.env.KV.get(
|
|
62873
|
+
`${SESSION_KV_PREFIX}${rawToken}`,
|
|
62874
|
+
"json"
|
|
62875
|
+
);
|
|
62876
|
+
if (cached2) {
|
|
62877
|
+
c.set("user", cached2);
|
|
62878
|
+
return next();
|
|
62879
|
+
}
|
|
62880
|
+
}
|
|
62881
|
+
const auth = createAuth(c.env);
|
|
62882
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
62883
|
+
if (!session?.user) {
|
|
62884
|
+
throw unauthorized("No valid session found");
|
|
62885
|
+
}
|
|
62886
|
+
if (!session.user.email.endsWith("@dlsu.edu.ph")) {
|
|
62887
|
+
throw domainRestricted();
|
|
62888
|
+
}
|
|
62889
|
+
const leapifyUser = await resolveUser(
|
|
62890
|
+
c.env,
|
|
62891
|
+
session.user.id,
|
|
62892
|
+
session.user.email,
|
|
62893
|
+
session.user.name,
|
|
62894
|
+
session.user.emailVerified
|
|
62895
|
+
);
|
|
62896
|
+
if (rawToken) {
|
|
62897
|
+
const sessionExpiresAt = new Date(session.session.expiresAt).getTime();
|
|
62898
|
+
const secondsRemaining = Math.floor((sessionExpiresAt - Date.now()) / 1e3);
|
|
62899
|
+
const kvTtl = Math.max(1, Math.min(secondsRemaining, SESSION_KV_TTL));
|
|
62900
|
+
await c.env.KV.put(
|
|
62901
|
+
`${SESSION_KV_PREFIX}${rawToken}`,
|
|
62902
|
+
JSON.stringify(leapifyUser),
|
|
62903
|
+
{ expirationTtl: kvTtl }
|
|
62904
|
+
);
|
|
62905
|
+
}
|
|
62906
|
+
c.set("user", leapifyUser);
|
|
62907
|
+
return next();
|
|
62908
|
+
}
|
|
62909
|
+
);
|
|
62910
|
+
var optionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
62911
|
+
const rawToken = extractRawToken(c);
|
|
62912
|
+
if (!rawToken) {
|
|
62913
|
+
c.set("user", null);
|
|
62914
|
+
return next();
|
|
62915
|
+
}
|
|
62916
|
+
return authMiddleware(c, next);
|
|
62917
|
+
});
|
|
62918
|
+
var adminMiddleware = createMiddleware(
|
|
62919
|
+
async (c, next) => {
|
|
62920
|
+
const user = c.get("user");
|
|
62921
|
+
if (!user || !["admin", "super_admin"].includes(user.role)) {
|
|
62922
|
+
throw forbidden("Admin access required");
|
|
62923
|
+
}
|
|
62924
|
+
return next();
|
|
62925
|
+
}
|
|
62926
|
+
);
|
|
62927
|
+
var internalMiddleware = createMiddleware(async (c, next) => {
|
|
62928
|
+
const secret = c.req.header("X-Internal-Secret");
|
|
62929
|
+
if (!secret || secret !== c.env.INTERNAL_API_SECRET) {
|
|
62930
|
+
throw forbidden("Invalid internal secret");
|
|
62931
|
+
}
|
|
62932
|
+
return next();
|
|
62933
|
+
});
|
|
62934
|
+
|
|
62819
62935
|
// src/routes/health.ts
|
|
62820
62936
|
var healthRoute = new Hono2();
|
|
62821
|
-
|
|
62822
|
-
const
|
|
62823
|
-
|
|
62937
|
+
async function probeResend(apiKey) {
|
|
62938
|
+
const start = Date.now();
|
|
62939
|
+
try {
|
|
62940
|
+
const res = await fetch("https://api.resend.com/domains", {
|
|
62941
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
62942
|
+
});
|
|
62943
|
+
return {
|
|
62944
|
+
configured: true,
|
|
62945
|
+
ok: res.ok,
|
|
62946
|
+
latencyMs: Date.now() - start,
|
|
62947
|
+
...res.ok ? {} : { error: `HTTP ${res.status}` }
|
|
62948
|
+
};
|
|
62949
|
+
} catch (e) {
|
|
62950
|
+
return {
|
|
62951
|
+
configured: true,
|
|
62952
|
+
ok: false,
|
|
62953
|
+
latencyMs: Date.now() - start,
|
|
62954
|
+
error: String(e)
|
|
62955
|
+
};
|
|
62956
|
+
}
|
|
62957
|
+
}
|
|
62958
|
+
async function probeSes(region, accessKeyId, secretAccessKey) {
|
|
62959
|
+
const start = Date.now();
|
|
62960
|
+
try {
|
|
62961
|
+
if (!region || !accessKeyId || !secretAccessKey) {
|
|
62962
|
+
return {
|
|
62963
|
+
configured: false,
|
|
62964
|
+
ok: false,
|
|
62965
|
+
latencyMs: Date.now() - start,
|
|
62966
|
+
error: "Missing SES credentials"
|
|
62967
|
+
};
|
|
62968
|
+
}
|
|
62969
|
+
return {
|
|
62970
|
+
configured: true,
|
|
62971
|
+
ok: true,
|
|
62972
|
+
latencyMs: Date.now() - start
|
|
62973
|
+
};
|
|
62974
|
+
} catch (e) {
|
|
62975
|
+
return {
|
|
62976
|
+
configured: true,
|
|
62977
|
+
ok: false,
|
|
62978
|
+
latencyMs: Date.now() - start,
|
|
62979
|
+
error: String(e)
|
|
62980
|
+
};
|
|
62981
|
+
}
|
|
62982
|
+
}
|
|
62983
|
+
async function probeContentful(spaceId, accessToken, environment) {
|
|
62984
|
+
const start = Date.now();
|
|
62985
|
+
try {
|
|
62986
|
+
const res = await fetch(
|
|
62987
|
+
`https://cdn.contentful.com/spaces/${spaceId}/environments/${environment}/entries?limit=0`,
|
|
62988
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
62989
|
+
);
|
|
62990
|
+
return {
|
|
62991
|
+
configured: true,
|
|
62992
|
+
ok: res.ok,
|
|
62993
|
+
latencyMs: Date.now() - start,
|
|
62994
|
+
...res.ok ? {} : { error: `HTTP ${res.status}` }
|
|
62995
|
+
};
|
|
62996
|
+
} catch (e) {
|
|
62997
|
+
return {
|
|
62998
|
+
configured: true,
|
|
62999
|
+
ok: false,
|
|
63000
|
+
latencyMs: Date.now() - start,
|
|
63001
|
+
error: String(e)
|
|
63002
|
+
};
|
|
63003
|
+
}
|
|
63004
|
+
}
|
|
63005
|
+
async function probeGForms(serviceAccountJson) {
|
|
63006
|
+
const start = Date.now();
|
|
63007
|
+
try {
|
|
63008
|
+
const creds = JSON.parse(serviceAccountJson);
|
|
63009
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
63010
|
+
const claims = {
|
|
63011
|
+
iss: creds.client_email,
|
|
63012
|
+
scope: "https://www.googleapis.com/auth/forms.responses.readonly",
|
|
63013
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
63014
|
+
iat: now2,
|
|
63015
|
+
exp: now2 + 3600
|
|
63016
|
+
};
|
|
63017
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
63018
|
+
const encode5 = (obj) => btoa(JSON.stringify(obj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
63019
|
+
const signingInput = `${encode5(header)}.${encode5(claims)}`;
|
|
63020
|
+
const pemBody = creds.private_key.replace(/-----BEGIN PRIVATE KEY-----/, "").replace(/-----END PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
63021
|
+
const keyBytes = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
|
|
63022
|
+
const privateKey = await crypto.subtle.importKey(
|
|
63023
|
+
"pkcs8",
|
|
63024
|
+
keyBytes,
|
|
63025
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
63026
|
+
false,
|
|
63027
|
+
["sign"]
|
|
63028
|
+
);
|
|
63029
|
+
const signature = await crypto.subtle.sign(
|
|
63030
|
+
"RSASSA-PKCS1-v1_5",
|
|
63031
|
+
privateKey,
|
|
63032
|
+
new TextEncoder().encode(signingInput)
|
|
63033
|
+
);
|
|
63034
|
+
const sigB64 = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
63035
|
+
const jwt2 = `${signingInput}.${sigB64}`;
|
|
63036
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
63037
|
+
method: "POST",
|
|
63038
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
63039
|
+
body: new URLSearchParams({
|
|
63040
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
63041
|
+
assertion: jwt2
|
|
63042
|
+
})
|
|
63043
|
+
});
|
|
63044
|
+
return {
|
|
63045
|
+
configured: true,
|
|
63046
|
+
ok: res.ok,
|
|
63047
|
+
latencyMs: Date.now() - start,
|
|
63048
|
+
...res.ok ? {} : { error: `HTTP ${res.status}` }
|
|
63049
|
+
};
|
|
63050
|
+
} catch (e) {
|
|
63051
|
+
return {
|
|
63052
|
+
configured: true,
|
|
63053
|
+
ok: false,
|
|
63054
|
+
latencyMs: Date.now() - start,
|
|
63055
|
+
error: String(e)
|
|
63056
|
+
};
|
|
63057
|
+
}
|
|
63058
|
+
}
|
|
63059
|
+
healthRoute.get("/", async (c) => {
|
|
63060
|
+
const env2 = c.env;
|
|
63061
|
+
const hasSes = Boolean(env2.SES_REGION) && Boolean(env2.SES_ACCESS_KEY_ID) && Boolean(env2.SES_SECRET_ACCESS_KEY);
|
|
63062
|
+
const hasResend = Boolean(env2.RESEND_API_KEY);
|
|
63063
|
+
const hasContentful = Boolean(env2.CONTENTFUL_SPACE_ID) && Boolean(env2.CONTENTFUL_ACCESS_TOKEN);
|
|
63064
|
+
const hasGForms = Boolean(env2.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
63065
|
+
const probes = [];
|
|
63066
|
+
if (hasSes) {
|
|
63067
|
+
probes.push(
|
|
63068
|
+
probeSes(env2.SES_REGION, env2.SES_ACCESS_KEY_ID, env2.SES_SECRET_ACCESS_KEY).then(
|
|
63069
|
+
(h) => ["ses", h]
|
|
63070
|
+
)
|
|
63071
|
+
);
|
|
63072
|
+
}
|
|
63073
|
+
if (hasResend) {
|
|
63074
|
+
probes.push(
|
|
63075
|
+
probeResend(env2.RESEND_API_KEY).then((h) => ["resend", h])
|
|
63076
|
+
);
|
|
63077
|
+
}
|
|
63078
|
+
if (hasContentful) {
|
|
63079
|
+
probes.push(
|
|
63080
|
+
probeContentful(
|
|
63081
|
+
env2.CONTENTFUL_SPACE_ID,
|
|
63082
|
+
env2.CONTENTFUL_ACCESS_TOKEN,
|
|
63083
|
+
env2.CONTENTFUL_ENVIRONMENT || "master"
|
|
63084
|
+
).then((h) => ["contentful", h])
|
|
63085
|
+
);
|
|
63086
|
+
}
|
|
63087
|
+
if (hasGForms) {
|
|
63088
|
+
probes.push(
|
|
63089
|
+
probeGForms(env2.GFORMS_SERVICE_ACCOUNT_JSON).then(
|
|
63090
|
+
(h) => ["gforms", h]
|
|
63091
|
+
)
|
|
63092
|
+
);
|
|
63093
|
+
}
|
|
63094
|
+
const results = await Promise.all(probes);
|
|
63095
|
+
const services = {};
|
|
63096
|
+
for (const [name, health] of results) {
|
|
63097
|
+
services[name] = health;
|
|
63098
|
+
}
|
|
63099
|
+
const allOk = results.length === 0 || results.every(([, h]) => h.ok);
|
|
62824
63100
|
return c.json({
|
|
62825
|
-
|
|
62826
|
-
|
|
62827
|
-
|
|
62828
|
-
|
|
62829
|
-
resend: hasResend
|
|
63101
|
+
data: {
|
|
63102
|
+
status: allOk ? "OK" : "DEGRADED",
|
|
63103
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
63104
|
+
services
|
|
62830
63105
|
}
|
|
62831
63106
|
});
|
|
62832
63107
|
});
|
|
62833
|
-
healthRoute.post("/queue-burst", async (c) => {
|
|
63108
|
+
healthRoute.post("/queue-burst", authMiddleware, adminMiddleware, async (c) => {
|
|
62834
63109
|
if (!c.env.EMAIL_QUEUE) {
|
|
62835
63110
|
return c.json({ error: "Queue binding missing" }, 400);
|
|
62836
63111
|
}
|
|
@@ -63310,106 +63585,189 @@ var GFormsService = class {
|
|
|
63310
63585
|
return { expireTime: data.expireTime };
|
|
63311
63586
|
}
|
|
63312
63587
|
};
|
|
63313
|
-
|
|
63314
|
-
|
|
63315
|
-
|
|
63316
|
-
|
|
63317
|
-
|
|
63318
|
-
|
|
63319
|
-
|
|
63320
|
-
|
|
63321
|
-
const match2 = cookie.match(/(?:^|;\s*)better-auth\.session_token=([^;]+)/);
|
|
63322
|
-
return match2?.[1] ? decodeURIComponent(match2[1]) : void 0;
|
|
63323
|
-
}
|
|
63324
|
-
async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAuthUserName, betterAuthEmailVerified) {
|
|
63325
|
-
const db = createDb(env2.DB);
|
|
63326
|
-
let dbUser = await db.query.users.findFirst({
|
|
63327
|
-
where: eq(users.betterAuthId, betterAuthUserId)
|
|
63328
|
-
});
|
|
63329
|
-
if (!dbUser) {
|
|
63330
|
-
const [created] = await db.insert(users).values({
|
|
63331
|
-
betterAuthId: betterAuthUserId,
|
|
63332
|
-
email: betterAuthUserEmail,
|
|
63333
|
-
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
|
|
63334
|
-
}).onConflictDoNothing({ target: users.betterAuthId }).returning();
|
|
63335
|
-
dbUser = created;
|
|
63336
|
-
}
|
|
63337
|
-
if (!dbUser) throw unauthorized("Failed to resolve user record");
|
|
63338
|
-
return {
|
|
63339
|
-
uid: betterAuthUserId,
|
|
63340
|
-
dbId: dbUser.id,
|
|
63341
|
-
role: dbUser.role,
|
|
63342
|
-
email: betterAuthUserEmail,
|
|
63343
|
-
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0],
|
|
63344
|
-
emailVerified: betterAuthEmailVerified
|
|
63345
|
-
};
|
|
63346
|
-
}
|
|
63347
|
-
var authMiddleware = createMiddleware(
|
|
63348
|
-
async (c, next) => {
|
|
63349
|
-
const rawToken = extractRawToken(c);
|
|
63350
|
-
if (rawToken) {
|
|
63351
|
-
const cached2 = await c.env.KV.get(
|
|
63352
|
-
`${SESSION_KV_PREFIX}${rawToken}`,
|
|
63353
|
-
"json"
|
|
63354
|
-
);
|
|
63355
|
-
if (cached2) {
|
|
63356
|
-
c.set("user", cached2);
|
|
63357
|
-
return next();
|
|
63588
|
+
var ContentfulManagement = class {
|
|
63589
|
+
client;
|
|
63590
|
+
constructor(spaceId, token, environment = "master") {
|
|
63591
|
+
this.client = createClient(
|
|
63592
|
+
{ accessToken: token },
|
|
63593
|
+
{
|
|
63594
|
+
type: "plain",
|
|
63595
|
+
defaults: { spaceId, environmentId: environment }
|
|
63358
63596
|
}
|
|
63597
|
+
);
|
|
63598
|
+
}
|
|
63599
|
+
static isConfigured(spaceId, token) {
|
|
63600
|
+
return !!(spaceId && token);
|
|
63601
|
+
}
|
|
63602
|
+
// ─── Content Types ───────────────────────────────────────────────────────
|
|
63603
|
+
async getContentType(contentTypeId) {
|
|
63604
|
+
try {
|
|
63605
|
+
return await this.client.contentType.get({ contentTypeId });
|
|
63606
|
+
} catch (err) {
|
|
63607
|
+
if (err?.status === 404) return null;
|
|
63608
|
+
console.warn(`[Contentful] getContentType(${contentTypeId}) error:`, err?.message ?? err);
|
|
63609
|
+
return null;
|
|
63359
63610
|
}
|
|
63360
|
-
|
|
63361
|
-
|
|
63362
|
-
|
|
63363
|
-
|
|
63611
|
+
}
|
|
63612
|
+
async ensureContentType(contentTypeId, name, fields) {
|
|
63613
|
+
const existing = await this.getContentType(contentTypeId);
|
|
63614
|
+
if (!existing) {
|
|
63615
|
+
console.log(`[Contentful] Creating content type ${contentTypeId}...`);
|
|
63616
|
+
try {
|
|
63617
|
+
const created = await this.client.contentType.createWithId(
|
|
63618
|
+
{ contentTypeId },
|
|
63619
|
+
{ name, fields, displayField: fields[0]?.id }
|
|
63620
|
+
);
|
|
63621
|
+
console.log(`[Contentful] Created ${contentTypeId} v${created.sys.version}`);
|
|
63622
|
+
let version4 = created.sys.version;
|
|
63623
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
63624
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
63625
|
+
try {
|
|
63626
|
+
const saved = await this.client.contentType.update(
|
|
63627
|
+
{ contentTypeId },
|
|
63628
|
+
{ ...created, name, fields, displayField: fields[0]?.id, sys: { ...created.sys, version: version4 } }
|
|
63629
|
+
);
|
|
63630
|
+
console.log(`[Contentful] Saved ${contentTypeId} \u2192 v${saved.sys.version}`);
|
|
63631
|
+
await this.client.contentType.publish(
|
|
63632
|
+
{ contentTypeId },
|
|
63633
|
+
saved
|
|
63634
|
+
);
|
|
63635
|
+
console.log(`[Contentful] Published ${contentTypeId}`);
|
|
63636
|
+
return;
|
|
63637
|
+
} catch (err) {
|
|
63638
|
+
console.warn(`[Contentful] Attempt ${attempt + 1} to publish ${contentTypeId} failed:`, err?.message ?? err);
|
|
63639
|
+
const refetched = await this.getContentType(contentTypeId);
|
|
63640
|
+
if (refetched) version4 = refetched.sys.version ?? version4;
|
|
63641
|
+
}
|
|
63642
|
+
}
|
|
63643
|
+
console.warn(`[Contentful] Could not publish ${contentTypeId} after retries`);
|
|
63644
|
+
} catch (err) {
|
|
63645
|
+
if (err?.status === 409) {
|
|
63646
|
+
console.log(`[Contentful] ${contentTypeId} already exists (409), attempting to publish...`);
|
|
63647
|
+
await this.tryPublishExisting(contentTypeId, name, fields);
|
|
63648
|
+
return;
|
|
63649
|
+
}
|
|
63650
|
+
console.warn(`[Contentful] Failed to create ${contentTypeId}:`, err?.message ?? err);
|
|
63651
|
+
}
|
|
63652
|
+
return;
|
|
63364
63653
|
}
|
|
63365
|
-
if (!
|
|
63366
|
-
|
|
63654
|
+
if (!existing.sys.publishedVersion || existing.sys.publishedVersion < (existing.sys.version ?? 0)) {
|
|
63655
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
63656
|
+
try {
|
|
63657
|
+
await this.client.contentType.publish({ contentTypeId }, existing);
|
|
63658
|
+
console.log(`[Contentful] Published ${contentTypeId}`);
|
|
63659
|
+
return;
|
|
63660
|
+
} catch {
|
|
63661
|
+
await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
|
|
63662
|
+
}
|
|
63663
|
+
}
|
|
63367
63664
|
}
|
|
63368
|
-
const
|
|
63369
|
-
|
|
63370
|
-
|
|
63371
|
-
|
|
63372
|
-
|
|
63373
|
-
|
|
63374
|
-
);
|
|
63375
|
-
if (rawToken) {
|
|
63376
|
-
const sessionExpiresAt = new Date(session.session.expiresAt).getTime();
|
|
63377
|
-
const secondsRemaining = Math.floor((sessionExpiresAt - Date.now()) / 1e3);
|
|
63378
|
-
const kvTtl = Math.max(1, Math.min(secondsRemaining, SESSION_KV_TTL));
|
|
63379
|
-
await c.env.KV.put(
|
|
63380
|
-
`${SESSION_KV_PREFIX}${rawToken}`,
|
|
63381
|
-
JSON.stringify(leapifyUser),
|
|
63382
|
-
{ expirationTtl: kvTtl }
|
|
63665
|
+
const existingFieldIds = existing.fields.map((f) => f.id).sort().join(",");
|
|
63666
|
+
const newFieldIds = fields.map((f) => f.id).sort().join(",");
|
|
63667
|
+
if (existingFieldIds !== newFieldIds || !existing.displayField) {
|
|
63668
|
+
const updated = await this.client.contentType.update(
|
|
63669
|
+
{ contentTypeId },
|
|
63670
|
+
{ ...existing, name, fields, displayField: fields[0]?.id }
|
|
63383
63671
|
);
|
|
63672
|
+
await this.client.contentType.publish({ contentTypeId }, updated);
|
|
63673
|
+
console.log(`[Contentful] Updated + published ${contentTypeId}`);
|
|
63384
63674
|
}
|
|
63385
|
-
c.set("user", leapifyUser);
|
|
63386
|
-
return next();
|
|
63387
63675
|
}
|
|
63388
|
-
)
|
|
63389
|
-
|
|
63390
|
-
|
|
63391
|
-
|
|
63392
|
-
|
|
63393
|
-
|
|
63676
|
+
async tryPublishExisting(contentTypeId, name, fields) {
|
|
63677
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
63678
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
63679
|
+
const existing = await this.getContentType(contentTypeId);
|
|
63680
|
+
if (existing) {
|
|
63681
|
+
if (!existing.sys.publishedVersion || existing.sys.publishedVersion < (existing.sys.version ?? 0)) {
|
|
63682
|
+
try {
|
|
63683
|
+
const saved = await this.client.contentType.update(
|
|
63684
|
+
{ contentTypeId },
|
|
63685
|
+
{ ...existing, name, fields, displayField: fields[0]?.id }
|
|
63686
|
+
);
|
|
63687
|
+
await this.client.contentType.publish({ contentTypeId }, saved);
|
|
63688
|
+
console.log(`[Contentful] Published existing ${contentTypeId}`);
|
|
63689
|
+
return;
|
|
63690
|
+
} catch (err) {
|
|
63691
|
+
console.warn(`[Contentful] Attempt ${attempt + 1} to publish existing ${contentTypeId}:`, err?.message ?? err);
|
|
63692
|
+
}
|
|
63693
|
+
} else {
|
|
63694
|
+
console.log(`[Contentful] ${contentTypeId} already published`);
|
|
63695
|
+
return;
|
|
63696
|
+
}
|
|
63697
|
+
}
|
|
63698
|
+
}
|
|
63699
|
+
console.warn(`[Contentful] Could not publish existing ${contentTypeId} after retries`);
|
|
63394
63700
|
}
|
|
63395
|
-
|
|
63396
|
-
|
|
63397
|
-
|
|
63398
|
-
|
|
63399
|
-
|
|
63400
|
-
|
|
63401
|
-
|
|
63701
|
+
// ─── Entries ─────────────────────────────────────────────────────────────
|
|
63702
|
+
async getEntry(entryId) {
|
|
63703
|
+
try {
|
|
63704
|
+
return await this.client.entry.get({ entryId });
|
|
63705
|
+
} catch (err) {
|
|
63706
|
+
if (err?.status === 404) return null;
|
|
63707
|
+
return null;
|
|
63402
63708
|
}
|
|
63403
|
-
return next();
|
|
63404
63709
|
}
|
|
63405
|
-
)
|
|
63406
|
-
|
|
63407
|
-
|
|
63408
|
-
|
|
63409
|
-
|
|
63710
|
+
async upsertEntry(contentTypeId, entryId, fields) {
|
|
63711
|
+
const existing = await this.getEntry(entryId);
|
|
63712
|
+
let entry;
|
|
63713
|
+
if (existing) {
|
|
63714
|
+
entry = await this.client.entry.update(
|
|
63715
|
+
{ entryId },
|
|
63716
|
+
{ ...existing, fields }
|
|
63717
|
+
);
|
|
63718
|
+
} else {
|
|
63719
|
+
entry = await this.client.entry.createWithId(
|
|
63720
|
+
{ entryId, contentTypeId },
|
|
63721
|
+
{ fields }
|
|
63722
|
+
);
|
|
63723
|
+
}
|
|
63724
|
+
return this.client.entry.publish({ entryId }, entry);
|
|
63725
|
+
}
|
|
63726
|
+
async unpublishEntry(entryId) {
|
|
63727
|
+
const entry = await this.getEntry(entryId);
|
|
63728
|
+
if (!entry) return null;
|
|
63729
|
+
return this.client.entry.unpublish({ entryId }, entry);
|
|
63730
|
+
}
|
|
63731
|
+
async deleteEntry(entryId) {
|
|
63732
|
+
await this.client.entry.delete({ entryId });
|
|
63733
|
+
}
|
|
63734
|
+
// ─── Asset Uploads ────────────────────────────────────────────────────────
|
|
63735
|
+
async uploadFile(_fileName, data, _contentType) {
|
|
63736
|
+
const upload = await this.client.upload.create({}, { file: data });
|
|
63737
|
+
return upload.sys.id;
|
|
63738
|
+
}
|
|
63739
|
+
async createAssetFromUpload(uploadId, title, fileName, contentType) {
|
|
63740
|
+
const asset = await this.client.asset.create({}, {
|
|
63741
|
+
fields: {
|
|
63742
|
+
title: { "en-US": title },
|
|
63743
|
+
file: {
|
|
63744
|
+
"en-US": {
|
|
63745
|
+
contentType,
|
|
63746
|
+
fileName,
|
|
63747
|
+
uploadFrom: { sys: { type: "Link", linkType: "Upload", id: uploadId } }
|
|
63748
|
+
}
|
|
63749
|
+
}
|
|
63750
|
+
}
|
|
63751
|
+
});
|
|
63752
|
+
const processed = await this.client.asset.processForLocale({}, asset, "en-US");
|
|
63753
|
+
const published = await this.client.asset.publish(
|
|
63754
|
+
{ assetId: processed.sys.id },
|
|
63755
|
+
processed
|
|
63756
|
+
);
|
|
63757
|
+
const url2 = published.fields?.file?.["en-US"]?.url ?? "";
|
|
63758
|
+
return { id: published.sys.id, url: url2 };
|
|
63410
63759
|
}
|
|
63411
|
-
|
|
63412
|
-
|
|
63760
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
63761
|
+
static locale(value) {
|
|
63762
|
+
return { "en-US": value };
|
|
63763
|
+
}
|
|
63764
|
+
static entryRef(entryId) {
|
|
63765
|
+
return { "en-US": { sys: { type: "Link", linkType: "Entry", id: entryId } } };
|
|
63766
|
+
}
|
|
63767
|
+
static assetRef(assetId) {
|
|
63768
|
+
return { "en-US": { sys: { type: "Link", linkType: "Asset", id: assetId } } };
|
|
63769
|
+
}
|
|
63770
|
+
};
|
|
63413
63771
|
|
|
63414
63772
|
// src/lib/middleware/rate-limit.ts
|
|
63415
63773
|
function createRateLimitMiddleware(config3) {
|
|
@@ -63465,25 +63823,87 @@ var adminEventsRateLimit = createRateLimitMiddleware({
|
|
|
63465
63823
|
var EVENTS_LIST_KV_KEY = "events:list";
|
|
63466
63824
|
var EVENTS_ETAG_KV_KEY = "events:etag";
|
|
63467
63825
|
var EVENTS_LIST_TTL = 300;
|
|
63826
|
+
var CF_EVENT_CT = "event";
|
|
63827
|
+
async function pushEventToContentful(env2, event) {
|
|
63828
|
+
if (!ContentfulManagement.isConfigured(env2.CONTENTFUL_SPACE_ID, env2.CONTENTFUL_MANAGEMENT_TOKEN)) return;
|
|
63829
|
+
const mgmt = new ContentfulManagement(
|
|
63830
|
+
env2.CONTENTFUL_SPACE_ID,
|
|
63831
|
+
env2.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
63832
|
+
env2.CONTENTFUL_ENVIRONMENT
|
|
63833
|
+
);
|
|
63834
|
+
try {
|
|
63835
|
+
const fields = {
|
|
63836
|
+
title: ContentfulManagement.locale(event.title),
|
|
63837
|
+
slug: ContentfulManagement.locale(event.slug),
|
|
63838
|
+
isMajor: ContentfulManagement.locale(event.isMajor),
|
|
63839
|
+
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
63840
|
+
};
|
|
63841
|
+
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
63842
|
+
if (event.organizationId) fields.organization = ContentfulManagement.entryRef(event.organizationId);
|
|
63843
|
+
if (event.venue) fields.venue = ContentfulManagement.locale(event.venue);
|
|
63844
|
+
if (event.dateTime) {
|
|
63845
|
+
const parsed = new Date(event.dateTime);
|
|
63846
|
+
fields.date = ContentfulManagement.locale(
|
|
63847
|
+
Number.isNaN(parsed.getTime()) ? event.dateTime : parsed.toISOString()
|
|
63848
|
+
);
|
|
63849
|
+
}
|
|
63850
|
+
if (event.price) fields.price = ContentfulManagement.locale(event.price);
|
|
63851
|
+
if (event.classCode) fields.classCode = ContentfulManagement.locale(event.classCode);
|
|
63852
|
+
if (event.startTime) fields.startTime = ContentfulManagement.locale(event.startTime);
|
|
63853
|
+
if (event.endTime) fields.endTime = ContentfulManagement.locale(event.endTime);
|
|
63854
|
+
if (event.gformsUrl) fields.gformsUrl = ContentfulManagement.locale(event.gformsUrl);
|
|
63855
|
+
if (event.gformsEditorUrl) fields.gformsEditorUrl = ContentfulManagement.locale(event.gformsEditorUrl);
|
|
63856
|
+
if (event.registrationClosesAt) {
|
|
63857
|
+
fields.registrationClosesAt = ContentfulManagement.locale(
|
|
63858
|
+
new Date(event.registrationClosesAt * 1e3).toISOString()
|
|
63859
|
+
);
|
|
63860
|
+
}
|
|
63861
|
+
if (event.backgroundImageUrl && env2.FILES) {
|
|
63862
|
+
try {
|
|
63863
|
+
const imageKey = new URL(event.backgroundImageUrl).pathname.replace(/^\/api\/uploads\/images\//, "");
|
|
63864
|
+
console.log(`[Contentful] Attempting image sync for event ${event.id}, key: ${imageKey}`);
|
|
63865
|
+
const object2 = await env2.FILES.get(imageKey);
|
|
63866
|
+
if (object2) {
|
|
63867
|
+
const data = await object2.arrayBuffer();
|
|
63868
|
+
const contentType = object2.httpMetadata?.contentType || "image/jpeg";
|
|
63869
|
+
const fileName = imageKey.split("/").pop() || "image.jpg";
|
|
63870
|
+
const uploadId = await mgmt.uploadFile(fileName, data, contentType);
|
|
63871
|
+
const asset = await mgmt.createAssetFromUpload(uploadId, event.title, fileName, contentType);
|
|
63872
|
+
fields.image = ContentfulManagement.assetRef(asset.id);
|
|
63873
|
+
console.log(`[Contentful] Image synced for event ${event.id}, asset: ${asset.id}`);
|
|
63874
|
+
} else {
|
|
63875
|
+
console.warn(`[Contentful] Image not found in R2 for event ${event.id}, key: ${imageKey}`);
|
|
63876
|
+
}
|
|
63877
|
+
} catch (err) {
|
|
63878
|
+
console.warn(`[Contentful] Failed to upload image for event ${event.id}:`, err);
|
|
63879
|
+
}
|
|
63880
|
+
} else if (event.backgroundImageUrl && !env2.FILES) {
|
|
63881
|
+
console.warn(`[Contentful] FILES binding not available, skipping image sync for event ${event.id}`);
|
|
63882
|
+
}
|
|
63883
|
+
await mgmt.upsertEntry(CF_EVENT_CT, event.id, fields);
|
|
63884
|
+
} catch (err) {
|
|
63885
|
+
console.warn(`[Contentful] Failed to sync event ${event.id}:`, err);
|
|
63886
|
+
}
|
|
63887
|
+
}
|
|
63468
63888
|
var createEventSchema = external_exports.object({
|
|
63469
63889
|
themeId: external_exports.string().min(1),
|
|
63890
|
+
organizationId: external_exports.string().optional(),
|
|
63470
63891
|
title: external_exports.string().min(1),
|
|
63471
|
-
|
|
63892
|
+
description: external_exports.string().optional(),
|
|
63472
63893
|
venue: external_exports.string().optional(),
|
|
63473
63894
|
dateTime: external_exports.string().optional(),
|
|
63474
|
-
startsAt: external_exports.number().optional(),
|
|
63475
|
-
endsAt: external_exports.number().optional(),
|
|
63476
63895
|
price: external_exports.string().optional(),
|
|
63477
|
-
backgroundColor: external_exports.string().optional(),
|
|
63478
63896
|
backgroundImageUrl: external_exports.string().url().optional(),
|
|
63479
|
-
|
|
63897
|
+
classCode: external_exports.string().optional(),
|
|
63898
|
+
startTime: external_exports.string().optional(),
|
|
63899
|
+
endTime: external_exports.string().optional(),
|
|
63900
|
+
registrationClosesAt: external_exports.number().optional(),
|
|
63480
63901
|
isMajor: external_exports.boolean().default(false),
|
|
63481
63902
|
maxSlots: external_exports.number().int().min(0).default(0),
|
|
63482
63903
|
gformsId: external_exports.string().optional(),
|
|
63483
63904
|
gformsUrl: external_exports.string().url().optional(),
|
|
63905
|
+
gformsEditorUrl: external_exports.string().url().optional(),
|
|
63484
63906
|
releaseAt: external_exports.number().optional(),
|
|
63485
|
-
registrationOpensAt: external_exports.number().optional(),
|
|
63486
|
-
registrationClosesAt: external_exports.number().optional(),
|
|
63487
63907
|
contentfulEntryId: external_exports.string().optional(),
|
|
63488
63908
|
status: external_exports.enum(["draft", "queued", "published"]).default("draft")
|
|
63489
63909
|
});
|
|
@@ -63491,6 +63911,42 @@ var eventsRoute = new Hono2();
|
|
|
63491
63911
|
function generateSlug(title) {
|
|
63492
63912
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
63493
63913
|
}
|
|
63914
|
+
eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
63915
|
+
const db = createDb(c.env.DB);
|
|
63916
|
+
const data = await db.query.events.findMany({
|
|
63917
|
+
with: { theme: true, organization: true },
|
|
63918
|
+
orderBy: (e, { desc: desc2 }) => [desc2(e.createdAt)]
|
|
63919
|
+
});
|
|
63920
|
+
return c.json({ data });
|
|
63921
|
+
});
|
|
63922
|
+
eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
|
|
63923
|
+
const body = await c.req.json();
|
|
63924
|
+
const db = createDb(c.env.DB);
|
|
63925
|
+
const cache3 = new CacheService(c.env.KV);
|
|
63926
|
+
if (!body.ids?.length) {
|
|
63927
|
+
return c.json({ error: "ids are required" }, 400);
|
|
63928
|
+
}
|
|
63929
|
+
if (body.releaseAt) {
|
|
63930
|
+
await db.update(events).set({ releaseAt: body.releaseAt, status: "queued" }).where(
|
|
63931
|
+
sql2`${events.id} IN (${sql2.join(
|
|
63932
|
+
body.ids.map((id) => sql2`${id}`),
|
|
63933
|
+
sql2`, `
|
|
63934
|
+
)})`
|
|
63935
|
+
);
|
|
63936
|
+
} else {
|
|
63937
|
+
await db.update(events).set({ status: "published", publishedAt: sql2`(unixepoch())` }).where(
|
|
63938
|
+
sql2`${events.id} IN (${sql2.join(
|
|
63939
|
+
body.ids.map((id) => sql2`${id}`),
|
|
63940
|
+
sql2`, `
|
|
63941
|
+
)})`
|
|
63942
|
+
);
|
|
63943
|
+
}
|
|
63944
|
+
await Promise.all([
|
|
63945
|
+
cache3.del(EVENTS_LIST_KV_KEY),
|
|
63946
|
+
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
63947
|
+
]);
|
|
63948
|
+
return c.json({ data: { updated: body.ids.length } });
|
|
63949
|
+
});
|
|
63494
63950
|
eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
63495
63951
|
const db = createDb(c.env.DB);
|
|
63496
63952
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -63509,28 +63965,28 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63509
63965
|
() => db.query.events.findMany({
|
|
63510
63966
|
where: eq(events.status, "published"),
|
|
63511
63967
|
with: {
|
|
63512
|
-
theme: true
|
|
63968
|
+
theme: true,
|
|
63969
|
+
organization: true
|
|
63513
63970
|
},
|
|
63514
63971
|
columns: {
|
|
63515
63972
|
id: true,
|
|
63516
63973
|
slug: true,
|
|
63517
63974
|
themeId: true,
|
|
63975
|
+
organizationId: true,
|
|
63518
63976
|
title: true,
|
|
63519
|
-
org: true,
|
|
63520
63977
|
venue: true,
|
|
63521
63978
|
dateTime: true,
|
|
63522
|
-
startsAt: true,
|
|
63523
|
-
endsAt: true,
|
|
63524
63979
|
price: true,
|
|
63525
|
-
backgroundColor: true,
|
|
63526
63980
|
backgroundImageUrl: true,
|
|
63527
|
-
|
|
63981
|
+
classCode: true,
|
|
63982
|
+
startTime: true,
|
|
63983
|
+
endTime: true,
|
|
63984
|
+
registrationClosesAt: true,
|
|
63528
63985
|
isMajor: true,
|
|
63529
63986
|
maxSlots: true,
|
|
63530
63987
|
registeredSlots: true,
|
|
63531
63988
|
gformsUrl: true,
|
|
63532
|
-
|
|
63533
|
-
registrationClosesAt: true,
|
|
63989
|
+
gformsEditorUrl: true,
|
|
63534
63990
|
publishedAt: true
|
|
63535
63991
|
}
|
|
63536
63992
|
}),
|
|
@@ -63600,6 +64056,7 @@ eventsRoute.post(
|
|
|
63600
64056
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
63601
64057
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
63602
64058
|
]);
|
|
64059
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
|
|
63603
64060
|
return c.json({ data: created }, 201);
|
|
63604
64061
|
}
|
|
63605
64062
|
);
|
|
@@ -63618,11 +64075,57 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
|
63618
64075
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
63619
64076
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
63620
64077
|
]);
|
|
64078
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
|
|
63621
64079
|
return c.json({ data: updated });
|
|
63622
64080
|
});
|
|
64081
|
+
eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
64082
|
+
const { slug } = c.req.param();
|
|
64083
|
+
const db = createDb(c.env.DB);
|
|
64084
|
+
const cache3 = new CacheService(c.env.KV);
|
|
64085
|
+
const [deleted] = await db.delete(events).where(eq(events.slug, slug)).returning();
|
|
64086
|
+
if (!deleted) throw notFound("Event");
|
|
64087
|
+
await Promise.all([
|
|
64088
|
+
cache3.del(EVENTS_LIST_KV_KEY),
|
|
64089
|
+
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
64090
|
+
]);
|
|
64091
|
+
return c.body(null, 204);
|
|
64092
|
+
});
|
|
63623
64093
|
|
|
63624
64094
|
// src/routes/users.ts
|
|
64095
|
+
var VALID_ROLES = ["student", "admin", "super_admin"];
|
|
63625
64096
|
var usersRoute = new Hono2();
|
|
64097
|
+
usersRoute.get("/", authMiddleware, adminMiddleware, async (c) => {
|
|
64098
|
+
const db = createDb(c.env.DB);
|
|
64099
|
+
const data = await db.select().from(users);
|
|
64100
|
+
return c.json({ data });
|
|
64101
|
+
});
|
|
64102
|
+
usersRoute.patch("/:id/role", authMiddleware, adminMiddleware, async (c) => {
|
|
64103
|
+
const { id } = c.req.param();
|
|
64104
|
+
const { role } = await c.req.json();
|
|
64105
|
+
if (!role || !VALID_ROLES.includes(role)) {
|
|
64106
|
+
throw badRequest("Role must be 'student', 'admin', or 'super_admin'.");
|
|
64107
|
+
}
|
|
64108
|
+
const db = createDb(c.env.DB);
|
|
64109
|
+
const [updated] = await db.update(users).set({ role }).where(eq(users.id, id)).returning();
|
|
64110
|
+
if (!updated) throw notFound("User");
|
|
64111
|
+
return c.json({ data: updated });
|
|
64112
|
+
});
|
|
64113
|
+
usersRoute.post("/by-email", authMiddleware, adminMiddleware, async (c) => {
|
|
64114
|
+
const { email: email3, role } = await c.req.json();
|
|
64115
|
+
if (!email3 || !role || !VALID_ROLES.includes(role)) {
|
|
64116
|
+
throw badRequest("Email and valid role ('student', 'admin', 'super_admin') are required.");
|
|
64117
|
+
}
|
|
64118
|
+
const db = createDb(c.env.DB);
|
|
64119
|
+
const existing = await db.query.users.findFirst({
|
|
64120
|
+
where: eq(users.email, email3)
|
|
64121
|
+
});
|
|
64122
|
+
if (existing) {
|
|
64123
|
+
const [updated] = await db.update(users).set({ role }).where(eq(users.email, email3)).returning();
|
|
64124
|
+
return c.json({ data: updated });
|
|
64125
|
+
}
|
|
64126
|
+
const [created] = await db.insert(users).values({ betterAuthId: `pending:${email3}`, email: email3, name: email3.split("@")[0], role }).returning();
|
|
64127
|
+
return c.json({ data: created }, 201);
|
|
64128
|
+
});
|
|
63626
64129
|
usersRoute.get("/me", optionalAuthMiddleware, async (c) => {
|
|
63627
64130
|
const user = c.get("user");
|
|
63628
64131
|
if (!user) return c.json({ data: null });
|
|
@@ -63631,7 +64134,11 @@ usersRoute.get("/me", optionalAuthMiddleware, async (c) => {
|
|
|
63631
64134
|
where: eq(users.id, user.dbId)
|
|
63632
64135
|
});
|
|
63633
64136
|
if (!profile) return c.json({ data: null });
|
|
63634
|
-
|
|
64137
|
+
const auth = await db.query.authUser.findFirst({
|
|
64138
|
+
where: eq(authUser.id, profile.betterAuthId),
|
|
64139
|
+
columns: { image: true }
|
|
64140
|
+
});
|
|
64141
|
+
return c.json({ data: { ...profile, image: auth?.image ?? null } });
|
|
63635
64142
|
});
|
|
63636
64143
|
usersRoute.get("/me/bookmarks", optionalAuthMiddleware, async (c) => {
|
|
63637
64144
|
const user = c.get("user");
|
|
@@ -63670,297 +64177,83 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
|
|
|
63670
64177
|
return c.json({ data: { bookmarked: false } });
|
|
63671
64178
|
});
|
|
63672
64179
|
|
|
63673
|
-
// src/services/contentful-management.ts
|
|
63674
|
-
var CONTENTFUL_MGMT = "https://api.contentful.com";
|
|
63675
|
-
var ContentfulManagement = class {
|
|
63676
|
-
spaceId;
|
|
63677
|
-
token;
|
|
63678
|
-
environment;
|
|
63679
|
-
constructor(spaceId, token, environment = "master") {
|
|
63680
|
-
this.spaceId = spaceId;
|
|
63681
|
-
this.token = token;
|
|
63682
|
-
this.environment = environment;
|
|
63683
|
-
}
|
|
63684
|
-
static isConfigured(spaceId, token) {
|
|
63685
|
-
return !!(spaceId && token);
|
|
63686
|
-
}
|
|
63687
|
-
// ─── Content Types ───────────────────────────────────────────────────────
|
|
63688
|
-
/**
|
|
63689
|
-
* Get a content type by ID. Returns null if not found.
|
|
63690
|
-
*/
|
|
63691
|
-
async getContentType(contentTypeId) {
|
|
63692
|
-
const res = await this.fetch(`/content_types/${contentTypeId}`);
|
|
63693
|
-
if (res.status === 404) return null;
|
|
63694
|
-
if (!res.ok) throw new Error(`Failed to get content type: ${res.status} ${await res.text()}`);
|
|
63695
|
-
return res.json();
|
|
63696
|
-
}
|
|
63697
|
-
/**
|
|
63698
|
-
* Create a content type. Does NOT publish it — call publishContentType() after.
|
|
63699
|
-
*/
|
|
63700
|
-
async createContentType(contentTypeId, name, fields) {
|
|
63701
|
-
const res = await this.fetch(`/content_types`, {
|
|
63702
|
-
method: "POST",
|
|
63703
|
-
headers: { "Content-Type": "application/json", ...this.authHeader() },
|
|
63704
|
-
body: JSON.stringify({
|
|
63705
|
-
sys: { id: contentTypeId, type: "ContentType" },
|
|
63706
|
-
name,
|
|
63707
|
-
fields
|
|
63708
|
-
})
|
|
63709
|
-
});
|
|
63710
|
-
if (!res.ok) throw new Error(`Failed to create content type: ${res.status} ${await res.text()}`);
|
|
63711
|
-
const ct = await res.json();
|
|
63712
|
-
console.log(`[Contentful] Created content type ${contentTypeId}: version=${ct.sys.version}`);
|
|
63713
|
-
return ct;
|
|
63714
|
-
}
|
|
63715
|
-
/**
|
|
63716
|
-
* Update a content type. Must republish after update.
|
|
63717
|
-
*/
|
|
63718
|
-
async updateContentType(contentTypeId, name, fields, version4) {
|
|
63719
|
-
const res = await this.fetch(`/content_types/${contentTypeId}`, {
|
|
63720
|
-
method: "PUT",
|
|
63721
|
-
headers: {
|
|
63722
|
-
"Content-Type": "application/json",
|
|
63723
|
-
...this.authHeader(),
|
|
63724
|
-
"X-Contentful-Version": String(version4)
|
|
63725
|
-
},
|
|
63726
|
-
body: JSON.stringify({
|
|
63727
|
-
sys: { id: contentTypeId, type: "ContentType" },
|
|
63728
|
-
name,
|
|
63729
|
-
fields
|
|
63730
|
-
})
|
|
63731
|
-
});
|
|
63732
|
-
if (!res.ok) throw new Error(`Failed to update content type: ${res.status} ${await res.text()}`);
|
|
63733
|
-
return res.json();
|
|
63734
|
-
}
|
|
63735
|
-
/**
|
|
63736
|
-
* Publish a content type (makes it available for entries).
|
|
63737
|
-
*/
|
|
63738
|
-
async publishContentType(contentTypeId, version4) {
|
|
63739
|
-
console.log(`[Contentful] Publishing content type ${contentTypeId} with version ${version4}`);
|
|
63740
|
-
const res = await this.fetch(`/content_types/${contentTypeId}/published`, {
|
|
63741
|
-
method: "PUT",
|
|
63742
|
-
headers: {
|
|
63743
|
-
"Content-Type": "application/vnd.contentful.management.v1+json",
|
|
63744
|
-
...this.authHeader(),
|
|
63745
|
-
"X-Contentful-Version": String(version4)
|
|
63746
|
-
}
|
|
63747
|
-
});
|
|
63748
|
-
if (!res.ok) {
|
|
63749
|
-
const body = await res.text();
|
|
63750
|
-
console.error(`[Contentful] Publish failed: ${res.status} ${body}`);
|
|
63751
|
-
throw new Error(`Failed to publish content type: ${res.status} ${body}`);
|
|
63752
|
-
}
|
|
63753
|
-
return res.json();
|
|
63754
|
-
}
|
|
63755
|
-
// ─── Entries ─────────────────────────────────────────────────────────────
|
|
63756
|
-
/**
|
|
63757
|
-
* Get an entry by ID. Returns null if not found.
|
|
63758
|
-
*/
|
|
63759
|
-
async getEntry(entryId) {
|
|
63760
|
-
const res = await this.fetch(`/entries/${entryId}`);
|
|
63761
|
-
if (res.status === 404) return null;
|
|
63762
|
-
if (!res.ok) throw new Error(`Failed to get entry: ${res.status} ${await res.text()}`);
|
|
63763
|
-
return res.json();
|
|
63764
|
-
}
|
|
63765
|
-
/**
|
|
63766
|
-
* Create a new entry. Does NOT publish — call publishEntry() after.
|
|
63767
|
-
* Fields must be locale-wrapped: { "en-US": value }
|
|
63768
|
-
*/
|
|
63769
|
-
async createEntry(contentTypeId, entryId, fields) {
|
|
63770
|
-
const res = await this.fetch(`/entries?content_type=${contentTypeId}`, {
|
|
63771
|
-
method: "PUT",
|
|
63772
|
-
headers: {
|
|
63773
|
-
"Content-Type": "application/vnd.contentful.management.v1+json",
|
|
63774
|
-
...this.authHeader(),
|
|
63775
|
-
"X-Contentful-Content-Type": contentTypeId
|
|
63776
|
-
},
|
|
63777
|
-
body: JSON.stringify({
|
|
63778
|
-
sys: { id: entryId, type: "Entry" },
|
|
63779
|
-
fields
|
|
63780
|
-
})
|
|
63781
|
-
});
|
|
63782
|
-
if (!res.ok) throw new Error(`Failed to create entry: ${res.status} ${await res.text()}`);
|
|
63783
|
-
return res.json();
|
|
63784
|
-
}
|
|
63785
|
-
/**
|
|
63786
|
-
* Update an existing entry. Must republish after update.
|
|
63787
|
-
*/
|
|
63788
|
-
async updateEntry(entryId, fields, version4) {
|
|
63789
|
-
const res = await this.fetch(`/entries/${entryId}`, {
|
|
63790
|
-
method: "PUT",
|
|
63791
|
-
headers: {
|
|
63792
|
-
"Content-Type": "application/vnd.contentful.management.v1+json",
|
|
63793
|
-
...this.authHeader(),
|
|
63794
|
-
"X-Contentful-Version": String(version4)
|
|
63795
|
-
},
|
|
63796
|
-
body: JSON.stringify({ fields })
|
|
63797
|
-
});
|
|
63798
|
-
if (!res.ok) throw new Error(`Failed to update entry: ${res.status} ${await res.text()}`);
|
|
63799
|
-
return res.json();
|
|
63800
|
-
}
|
|
63801
|
-
/**
|
|
63802
|
-
* Publish an entry (makes it available via Delivery API).
|
|
63803
|
-
*/
|
|
63804
|
-
async publishEntry(entryId, version4) {
|
|
63805
|
-
const res = await this.fetch(`/entries/${entryId}/published`, {
|
|
63806
|
-
method: "PUT",
|
|
63807
|
-
headers: {
|
|
63808
|
-
...this.authHeader(),
|
|
63809
|
-
"X-Contentful-Version": String(version4)
|
|
63810
|
-
}
|
|
63811
|
-
});
|
|
63812
|
-
if (!res.ok) throw new Error(`Failed to publish entry: ${res.status} ${await res.text()}`);
|
|
63813
|
-
return res.json();
|
|
63814
|
-
}
|
|
63815
|
-
/**
|
|
63816
|
-
* Upsert an entry: create if missing, update if exists, then publish.
|
|
63817
|
-
*/
|
|
63818
|
-
async upsertEntry(contentTypeId, entryId, fields) {
|
|
63819
|
-
let existing = null;
|
|
63820
|
-
try {
|
|
63821
|
-
existing = await Promise.race([
|
|
63822
|
-
this.getEntry(entryId),
|
|
63823
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("getEntry timeout")), 5e3))
|
|
63824
|
-
]);
|
|
63825
|
-
} catch (err) {
|
|
63826
|
-
console.warn(`[Contentful] getEntry failed (will try create): ${err}`);
|
|
63827
|
-
}
|
|
63828
|
-
console.log(`[Contentful] upsertEntry: existing=${existing ? "yes" : "no"}`);
|
|
63829
|
-
let entry;
|
|
63830
|
-
if (existing) {
|
|
63831
|
-
entry = await this.updateEntry(entryId, fields, existing.sys.version ?? 1);
|
|
63832
|
-
} else {
|
|
63833
|
-
console.log(`[Contentful] upsertEntry: creating new entry...`);
|
|
63834
|
-
entry = await this.createEntry(contentTypeId, entryId, fields);
|
|
63835
|
-
}
|
|
63836
|
-
console.log(`[Contentful] upsertEntry: publishing (version=${entry.sys.version})`);
|
|
63837
|
-
const published = await this.publishEntry(entry.sys.id, entry.sys.version ?? 1);
|
|
63838
|
-
console.log(`[Contentful] upsertEntry: published successfully`);
|
|
63839
|
-
return published;
|
|
63840
|
-
}
|
|
63841
|
-
// ─── Content type setup ──────────────────────────────────────────────────
|
|
63842
|
-
/**
|
|
63843
|
-
* Ensure a content type exists and is published with the given fields.
|
|
63844
|
-
* Creates it if missing, updates if fields changed.
|
|
63845
|
-
*/
|
|
63846
|
-
async ensureContentType(contentTypeId, name, fields) {
|
|
63847
|
-
const existing = await this.getContentType(contentTypeId);
|
|
63848
|
-
if (!existing) {
|
|
63849
|
-
const created = await this.createContentType(contentTypeId, name, fields);
|
|
63850
|
-
const version4 = created.sys.version ?? 1;
|
|
63851
|
-
console.log(`[Contentful] Created content type ${contentTypeId}, attempting publish with version ${version4}`);
|
|
63852
|
-
try {
|
|
63853
|
-
await this.publishContentType(contentTypeId, version4);
|
|
63854
|
-
console.log(`[Contentful] Created and published content type: ${contentTypeId}`);
|
|
63855
|
-
} catch (err) {
|
|
63856
|
-
console.warn(`[Contentful] Created content type ${contentTypeId} but publish failed (entries will still work): ${err}`);
|
|
63857
|
-
}
|
|
63858
|
-
return;
|
|
63859
|
-
}
|
|
63860
|
-
const existingFieldIds = existing.fields.map((f) => f.id).sort().join(",");
|
|
63861
|
-
const newFieldIds = fields.map((f) => f.id).sort().join(",");
|
|
63862
|
-
if (existingFieldIds !== newFieldIds) {
|
|
63863
|
-
const updated = await this.updateContentType(contentTypeId, name, fields, existing.sys.version ?? 1);
|
|
63864
|
-
const fetched = await this.getContentType(contentTypeId);
|
|
63865
|
-
const version4 = fetched?.sys.version ?? updated.sys.version ?? 1;
|
|
63866
|
-
try {
|
|
63867
|
-
await this.publishContentType(contentTypeId, version4);
|
|
63868
|
-
console.log(`[Contentful] Updated and published content type: ${contentTypeId}`);
|
|
63869
|
-
} catch (err) {
|
|
63870
|
-
console.warn(`[Contentful] Updated content type ${contentTypeId} but publish failed: ${err}`);
|
|
63871
|
-
}
|
|
63872
|
-
} else if (!existing.sys.publishedVersion || existing.sys.publishedVersion < (existing.sys.version ?? 0)) {
|
|
63873
|
-
try {
|
|
63874
|
-
await this.publishContentType(contentTypeId, existing.sys.version ?? 1);
|
|
63875
|
-
console.log(`[Contentful] Published content type: ${contentTypeId}`);
|
|
63876
|
-
} catch (err) {
|
|
63877
|
-
console.warn(`[Contentful] Publish failed for ${contentTypeId}: ${err}`);
|
|
63878
|
-
}
|
|
63879
|
-
}
|
|
63880
|
-
}
|
|
63881
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
63882
|
-
/**
|
|
63883
|
-
* Locale-wrap a value for Contentful fields.
|
|
63884
|
-
*/
|
|
63885
|
-
static locale(value) {
|
|
63886
|
-
return { "en-US": value };
|
|
63887
|
-
}
|
|
63888
|
-
/**
|
|
63889
|
-
* Locale-wrap a reference (Link to Entry).
|
|
63890
|
-
*/
|
|
63891
|
-
static entryRef(entryId) {
|
|
63892
|
-
return { "en-US": { sys: { type: "Link", linkType: "Entry", id: entryId } } };
|
|
63893
|
-
}
|
|
63894
|
-
// ─── Private ─────────────────────────────────────────────────────────────
|
|
63895
|
-
authHeader() {
|
|
63896
|
-
return { Authorization: `Bearer ${this.token}` };
|
|
63897
|
-
}
|
|
63898
|
-
async fetch(path, init2) {
|
|
63899
|
-
const url2 = `${CONTENTFUL_MGMT}/spaces/${this.spaceId}/environments/${this.environment}${path}`;
|
|
63900
|
-
return globalThis.fetch(url2, {
|
|
63901
|
-
...init2,
|
|
63902
|
-
headers: { ...this.authHeader(), ...init2?.headers }
|
|
63903
|
-
});
|
|
63904
|
-
}
|
|
63905
|
-
};
|
|
63906
|
-
|
|
63907
64180
|
// src/services/snapshot.ts
|
|
64181
|
+
async function batchRun(items, fn, concurrency = 5) {
|
|
64182
|
+
const results = [];
|
|
64183
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
64184
|
+
const batch = items.slice(i, i + concurrency);
|
|
64185
|
+
const settled = await Promise.allSettled(batch.map(fn));
|
|
64186
|
+
results.push(...settled);
|
|
64187
|
+
}
|
|
64188
|
+
return results;
|
|
64189
|
+
}
|
|
63908
64190
|
async function ensureContentTypes(mgmt, config3 = {}) {
|
|
63909
64191
|
const eventTypeId = config3.eventTypeId ?? "event";
|
|
63910
64192
|
const themeTypeId = config3.themeTypeId ?? "theme";
|
|
63911
64193
|
const faqTypeId = config3.faqTypeId ?? "faq";
|
|
64194
|
+
const organizationTypeId = config3.organizationTypeId ?? "organization";
|
|
63912
64195
|
await mgmt.ensureContentType(themeTypeId, "Theme", [
|
|
63913
64196
|
{ id: "name", name: "Name", type: "Symbol", required: true },
|
|
63914
|
-
{ id: "path", name: "Path", type: "Symbol", required: true }
|
|
63915
|
-
{ id: "color", name: "Color", type: "Symbol" }
|
|
64197
|
+
{ id: "path", name: "Path", type: "Symbol", required: true }
|
|
63916
64198
|
]);
|
|
63917
64199
|
await mgmt.ensureContentType(eventTypeId, "Event", [
|
|
63918
64200
|
{ id: "title", name: "Title", type: "Symbol", required: true },
|
|
63919
64201
|
{ id: "slug", name: "Slug", type: "Symbol", required: true },
|
|
63920
64202
|
{ id: "theme", name: "Theme", type: "Link", linkType: "Entry" },
|
|
63921
|
-
{ id: "
|
|
64203
|
+
{ id: "organization", name: "Organization", type: "Link", linkType: "Entry" },
|
|
63922
64204
|
{ id: "venue", name: "Venue", type: "Symbol" },
|
|
63923
|
-
{ id: "
|
|
63924
|
-
{ id: "
|
|
63925
|
-
{ id: "
|
|
64205
|
+
{ id: "date", name: "Date", type: "Date" },
|
|
64206
|
+
{ id: "startTime", name: "Start Time", type: "Symbol" },
|
|
64207
|
+
{ id: "endTime", name: "End Time", type: "Symbol" },
|
|
63926
64208
|
{ id: "price", name: "Price", type: "Symbol" },
|
|
63927
|
-
{ id: "backgroundColor", name: "Background Color", type: "Symbol" },
|
|
63928
64209
|
{ id: "image", name: "Image", type: "Link", linkType: "Asset" },
|
|
63929
|
-
{ id: "subtheme", name: "Subtheme", type: "Symbol" },
|
|
63930
64210
|
{ id: "isMajor", name: "Major Event", type: "Boolean" },
|
|
63931
64211
|
{ id: "maxSlots", name: "Max Slots", type: "Integer" },
|
|
63932
64212
|
{ id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
|
|
63933
|
-
{ id: "
|
|
63934
|
-
{ id: "registrationClosesAt", name: "Registration Closes", type: "Date" }
|
|
64213
|
+
{ id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
|
|
64214
|
+
{ id: "registrationClosesAt", name: "Registration Closes", type: "Date" },
|
|
64215
|
+
{ id: "classCode", name: "Class Code", type: "Symbol" }
|
|
64216
|
+
]);
|
|
64217
|
+
await mgmt.ensureContentType(organizationTypeId, "Organization", [
|
|
64218
|
+
{ id: "name", name: "Name", type: "Symbol", required: true },
|
|
64219
|
+
{ id: "acronym", name: "Acronym", type: "Symbol", required: true },
|
|
64220
|
+
{ id: "logo", name: "Logo", type: "Link", linkType: "Asset" },
|
|
64221
|
+
{ id: "link", name: "Link", type: "Symbol" }
|
|
63935
64222
|
]);
|
|
63936
64223
|
await mgmt.ensureContentType(faqTypeId, "FAQ", [
|
|
63937
64224
|
{ id: "question", name: "Question", type: "Symbol", required: true },
|
|
63938
64225
|
{ id: "answer", name: "Answer", type: "Text", required: true },
|
|
63939
64226
|
{ id: "category", name: "Category", type: "Symbol" },
|
|
63940
|
-
{ id: "sortOrder", name: "Sort Order", type: "Integer" }
|
|
63941
|
-
{ id: "isActive", name: "Active", type: "Boolean" }
|
|
64227
|
+
{ id: "sortOrder", name: "Sort Order", type: "Integer" }
|
|
63942
64228
|
]);
|
|
63943
64229
|
}
|
|
63944
|
-
|
|
64230
|
+
var LAST_PUSH_KV_KEY = "contentful:last_push";
|
|
64231
|
+
async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
|
|
63945
64232
|
const eventTypeId = config3.eventTypeId ?? "event";
|
|
63946
64233
|
const themeTypeId = config3.themeTypeId ?? "theme";
|
|
63947
64234
|
const faqTypeId = config3.faqTypeId ?? "faq";
|
|
64235
|
+
const organizationTypeId = config3.organizationTypeId ?? "organization";
|
|
63948
64236
|
const result = {
|
|
63949
64237
|
themesSynced: 0,
|
|
63950
64238
|
eventsSynced: 0,
|
|
63951
64239
|
faqsSynced: 0,
|
|
64240
|
+
organizationsSynced: 0,
|
|
63952
64241
|
imagesUploaded: 0,
|
|
63953
64242
|
imagesSkipped: 0,
|
|
63954
64243
|
errors: []
|
|
63955
64244
|
};
|
|
64245
|
+
let lastPushTs = 0;
|
|
64246
|
+
if (kv && !forceFull) {
|
|
64247
|
+
const stored = await kv.get(LAST_PUSH_KV_KEY);
|
|
64248
|
+
if (stored) lastPushTs = Number(stored) || 0;
|
|
64249
|
+
}
|
|
63956
64250
|
try {
|
|
63957
64251
|
const dbThemes = await db.query.themes.findMany();
|
|
63958
64252
|
for (const theme of dbThemes) {
|
|
63959
64253
|
try {
|
|
63960
64254
|
await mgmt.upsertEntry(themeTypeId, theme.id, {
|
|
63961
64255
|
name: ContentfulManagement.locale(theme.name),
|
|
63962
|
-
path: ContentfulManagement.locale(theme.path)
|
|
63963
|
-
color: ContentfulManagement.locale(theme.color)
|
|
64256
|
+
path: ContentfulManagement.locale(theme.path)
|
|
63964
64257
|
});
|
|
63965
64258
|
result.themesSynced++;
|
|
63966
64259
|
} catch (err) {
|
|
@@ -63971,57 +64264,86 @@ async function pushToContentful(db, mgmt, config3 = {}) {
|
|
|
63971
64264
|
result.errors.push(`Themes fetch failed: ${err}`);
|
|
63972
64265
|
}
|
|
63973
64266
|
try {
|
|
63974
|
-
const
|
|
63975
|
-
for (const
|
|
64267
|
+
const dbOrgs = await db.query.organizations.findMany();
|
|
64268
|
+
for (const org of dbOrgs) {
|
|
63976
64269
|
try {
|
|
63977
64270
|
const fields = {
|
|
63978
|
-
|
|
63979
|
-
|
|
63980
|
-
|
|
63981
|
-
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
64271
|
+
name: ContentfulManagement.locale(org.name),
|
|
64272
|
+
acronym: ContentfulManagement.locale(org.acronym),
|
|
64273
|
+
link: ContentfulManagement.locale(org.link)
|
|
63982
64274
|
};
|
|
63983
|
-
|
|
63984
|
-
|
|
63985
|
-
if (event.venue) fields.venue = ContentfulManagement.locale(event.venue);
|
|
63986
|
-
if (event.dateTime) fields.dateTime = ContentfulManagement.locale(event.dateTime);
|
|
63987
|
-
if (event.price) fields.price = ContentfulManagement.locale(event.price);
|
|
63988
|
-
if (event.backgroundColor) fields.backgroundColor = ContentfulManagement.locale(event.backgroundColor);
|
|
63989
|
-
if (event.subtheme) fields.subtheme = ContentfulManagement.locale(event.subtheme);
|
|
63990
|
-
if (event.gformsUrl) fields.gformsUrl = ContentfulManagement.locale(event.gformsUrl);
|
|
63991
|
-
if (event.startsAt) fields.startsAt = ContentfulManagement.locale(new Date(event.startsAt * 1e3).toISOString());
|
|
63992
|
-
if (event.endsAt) fields.endsAt = ContentfulManagement.locale(new Date(event.endsAt * 1e3).toISOString());
|
|
63993
|
-
if (event.registrationOpensAt) fields.registrationOpensAt = ContentfulManagement.locale(new Date(event.registrationOpensAt * 1e3).toISOString());
|
|
63994
|
-
if (event.registrationClosesAt) fields.registrationClosesAt = ContentfulManagement.locale(new Date(event.registrationClosesAt * 1e3).toISOString());
|
|
63995
|
-
await mgmt.upsertEntry(eventTypeId, event.id, fields);
|
|
63996
|
-
result.eventsSynced++;
|
|
64275
|
+
await mgmt.upsertEntry(organizationTypeId, org.id, fields);
|
|
64276
|
+
result.organizationsSynced++;
|
|
63997
64277
|
} catch (err) {
|
|
63998
|
-
result.errors.push(`
|
|
64278
|
+
result.errors.push(`Organization ${org.id}: ${err}`);
|
|
63999
64279
|
}
|
|
64000
64280
|
}
|
|
64001
64281
|
} catch (err) {
|
|
64002
|
-
result.errors.push(`
|
|
64282
|
+
result.errors.push(`Organizations fetch failed: ${err}`);
|
|
64003
64283
|
}
|
|
64004
64284
|
try {
|
|
64005
|
-
const
|
|
64006
|
-
|
|
64007
|
-
|
|
64008
|
-
|
|
64009
|
-
|
|
64010
|
-
|
|
64011
|
-
|
|
64012
|
-
|
|
64013
|
-
|
|
64014
|
-
|
|
64015
|
-
|
|
64016
|
-
|
|
64017
|
-
|
|
64285
|
+
const dbEvents = lastPushTs > 0 ? await db.query.events.findMany().then((all) => all.filter((e) => {
|
|
64286
|
+
if (e.createdAt >= lastPushTs) return true;
|
|
64287
|
+
if (e.publishedAt && e.publishedAt >= lastPushTs) return true;
|
|
64288
|
+
if (!e.contentfulEntryId) return true;
|
|
64289
|
+
return false;
|
|
64290
|
+
})) : await db.query.events.findMany();
|
|
64291
|
+
await batchRun(dbEvents, async (event) => {
|
|
64292
|
+
const fields = {
|
|
64293
|
+
title: ContentfulManagement.locale(event.title),
|
|
64294
|
+
slug: ContentfulManagement.locale(event.slug),
|
|
64295
|
+
isMajor: ContentfulManagement.locale(event.isMajor),
|
|
64296
|
+
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
64297
|
+
};
|
|
64298
|
+
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
64299
|
+
if (event.organizationId) fields.organization = ContentfulManagement.entryRef(event.organizationId);
|
|
64300
|
+
if (event.venue) fields.venue = ContentfulManagement.locale(event.venue);
|
|
64301
|
+
if (event.dateTime) {
|
|
64302
|
+
const parsed = new Date(event.dateTime);
|
|
64303
|
+
fields.date = ContentfulManagement.locale(
|
|
64304
|
+
Number.isNaN(parsed.getTime()) ? event.dateTime : parsed.toISOString()
|
|
64305
|
+
);
|
|
64018
64306
|
}
|
|
64019
|
-
|
|
64307
|
+
if (event.price) fields.price = ContentfulManagement.locale(event.price);
|
|
64308
|
+
if (event.classCode) fields.classCode = ContentfulManagement.locale(event.classCode);
|
|
64309
|
+
if (event.startTime) fields.startTime = ContentfulManagement.locale(event.startTime);
|
|
64310
|
+
if (event.endTime) fields.endTime = ContentfulManagement.locale(event.endTime);
|
|
64311
|
+
if (event.gformsUrl) fields.gformsUrl = ContentfulManagement.locale(event.gformsUrl);
|
|
64312
|
+
if (event.gformsEditorUrl) fields.gformsEditorUrl = ContentfulManagement.locale(event.gformsEditorUrl);
|
|
64313
|
+
if (event.registrationClosesAt) fields.registrationClosesAt = ContentfulManagement.locale(new Date(event.registrationClosesAt * 1e3).toISOString());
|
|
64314
|
+
await mgmt.upsertEntry(eventTypeId, event.id, fields);
|
|
64315
|
+
if (!event.contentfulEntryId) {
|
|
64316
|
+
await db.update(events).set({ contentfulEntryId: event.id }).where(eq(events.id, event.id));
|
|
64317
|
+
}
|
|
64318
|
+
result.eventsSynced++;
|
|
64319
|
+
});
|
|
64320
|
+
} catch (err) {
|
|
64321
|
+
result.errors.push(`Events fetch failed: ${err}`);
|
|
64322
|
+
}
|
|
64323
|
+
try {
|
|
64324
|
+
const dbFaqs = lastPushTs > 0 ? await db.query.faqs.findMany().then((all) => all.filter((faq) => {
|
|
64325
|
+
if (faq.updatedAt >= lastPushTs) return true;
|
|
64326
|
+
if (faq.createdAt >= lastPushTs) return true;
|
|
64327
|
+
return false;
|
|
64328
|
+
})) : await db.query.faqs.findMany();
|
|
64329
|
+
await batchRun(dbFaqs, async (faq) => {
|
|
64330
|
+
await mgmt.upsertEntry(faqTypeId, faq.id, {
|
|
64331
|
+
question: ContentfulManagement.locale(faq.question),
|
|
64332
|
+
answer: ContentfulManagement.locale(faq.answer),
|
|
64333
|
+
category: ContentfulManagement.locale(faq.category),
|
|
64334
|
+
sortOrder: ContentfulManagement.locale(faq.sortOrder)
|
|
64335
|
+
});
|
|
64336
|
+
result.faqsSynced++;
|
|
64337
|
+
});
|
|
64020
64338
|
} catch (err) {
|
|
64021
64339
|
result.errors.push(`FAQs fetch failed: ${err}`);
|
|
64022
64340
|
}
|
|
64341
|
+
if (kv) {
|
|
64342
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
64343
|
+
await kv.put(LAST_PUSH_KV_KEY, String(now2));
|
|
64344
|
+
}
|
|
64023
64345
|
console.log(
|
|
64024
|
-
`[Push] Complete: ${result.themesSynced} themes, ${result.eventsSynced} events, ${result.faqsSynced} FAQs pushed to Contentful, ${result.errors.length} errors`
|
|
64346
|
+
`[Push] Complete: ${result.themesSynced} themes, ${result.organizationsSynced} organizations, ${result.eventsSynced} events, ${result.faqsSynced} FAQs pushed to Contentful, ${result.errors.length} errors`
|
|
64025
64347
|
);
|
|
64026
64348
|
return result;
|
|
64027
64349
|
}
|
|
@@ -64059,19 +64381,30 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64059
64381
|
});
|
|
64060
64382
|
return c.json({ data: { key, value } });
|
|
64061
64383
|
});
|
|
64384
|
+
var SYNC_LOCK_KEY = "contentful:sync:lock";
|
|
64385
|
+
var SYNC_LOCK_TTL = 60;
|
|
64062
64386
|
siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
|
|
64063
64387
|
if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
|
|
64064
64388
|
throw serviceUnavailable("Contentful Management API credentials not configured.");
|
|
64065
64389
|
}
|
|
64066
|
-
const
|
|
64067
|
-
|
|
64068
|
-
|
|
64069
|
-
|
|
64070
|
-
);
|
|
64071
|
-
|
|
64072
|
-
|
|
64073
|
-
|
|
64074
|
-
|
|
64390
|
+
const existingLock = await c.env.KV.get(SYNC_LOCK_KEY);
|
|
64391
|
+
if (existingLock) {
|
|
64392
|
+
throw conflict("A sync is already in progress. Please wait and try again.");
|
|
64393
|
+
}
|
|
64394
|
+
await c.env.KV.put(SYNC_LOCK_KEY, "1", { expirationTtl: SYNC_LOCK_TTL });
|
|
64395
|
+
try {
|
|
64396
|
+
const mgmt = new ContentfulManagement(
|
|
64397
|
+
c.env.CONTENTFUL_SPACE_ID,
|
|
64398
|
+
c.env.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
64399
|
+
c.env.CONTENTFUL_ENVIRONMENT
|
|
64400
|
+
);
|
|
64401
|
+
const db = createDb(c.env.DB);
|
|
64402
|
+
await ensureContentTypes(mgmt, {});
|
|
64403
|
+
const result = await pushToContentful(db, mgmt, {}, c.env.KV);
|
|
64404
|
+
return c.json({ data: result });
|
|
64405
|
+
} finally {
|
|
64406
|
+
await c.env.KV.delete(SYNC_LOCK_KEY);
|
|
64407
|
+
}
|
|
64075
64408
|
});
|
|
64076
64409
|
|
|
64077
64410
|
// src/routes/faqs.ts
|
|
@@ -64107,8 +64440,7 @@ async function pushFaqToContentful(env2, faq) {
|
|
|
64107
64440
|
"en-US": { question: faq.question },
|
|
64108
64441
|
answer: { "en-US": faq.answer },
|
|
64109
64442
|
category: { "en-US": faq.category },
|
|
64110
|
-
sortOrder: { "en-US": faq.sortOrder }
|
|
64111
|
-
isActive: { "en-US": faq.isActive }
|
|
64443
|
+
sortOrder: { "en-US": faq.sortOrder }
|
|
64112
64444
|
});
|
|
64113
64445
|
console.log(`[Contentful] Synced FAQ ${faq.id} successfully`);
|
|
64114
64446
|
} catch (err) {
|
|
@@ -64121,7 +64453,6 @@ faqsRoute.get("/", async (c) => {
|
|
|
64121
64453
|
const data = await cache3.getOrSet(
|
|
64122
64454
|
FAQS_KV_KEY,
|
|
64123
64455
|
() => db.query.faqs.findMany({
|
|
64124
|
-
where: eq(faqs.isActive, true),
|
|
64125
64456
|
orderBy: (t, { asc: asc2 }) => [asc2(t.sortOrder), asc2(t.createdAt)]
|
|
64126
64457
|
}),
|
|
64127
64458
|
FAQS_TTL
|
|
@@ -64159,11 +64490,9 @@ faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64159
64490
|
const { id } = c.req.param();
|
|
64160
64491
|
const db = createDb(c.env.DB);
|
|
64161
64492
|
const cache3 = new CacheService(c.env.KV);
|
|
64162
|
-
const
|
|
64163
|
-
const [deleted] = await db.update(faqs).set({ isActive: false, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
|
|
64493
|
+
const [deleted] = await db.delete(faqs).where(eq(faqs.id, id)).returning();
|
|
64164
64494
|
if (!deleted) throw notFound("FAQ");
|
|
64165
64495
|
await cache3.del(FAQS_KV_KEY);
|
|
64166
|
-
c.executionCtx.waitUntil(pushFaqToContentful(c.env, deleted));
|
|
64167
64496
|
return c.json({ data: { deleted: true } });
|
|
64168
64497
|
});
|
|
64169
64498
|
|
|
@@ -64317,6 +64646,49 @@ uploadsRoute.post(
|
|
|
64317
64646
|
);
|
|
64318
64647
|
}
|
|
64319
64648
|
);
|
|
64649
|
+
uploadsRoute.post(
|
|
64650
|
+
"/contentful",
|
|
64651
|
+
authMiddleware,
|
|
64652
|
+
adminMiddleware,
|
|
64653
|
+
async (c) => {
|
|
64654
|
+
if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
|
|
64655
|
+
throw serviceUnavailable("Contentful Management API credentials not configured.");
|
|
64656
|
+
}
|
|
64657
|
+
let formData;
|
|
64658
|
+
try {
|
|
64659
|
+
formData = await c.req.formData();
|
|
64660
|
+
} catch {
|
|
64661
|
+
throw badRequest("Request body must be multipart/form-data.");
|
|
64662
|
+
}
|
|
64663
|
+
const file2 = formData.get("file");
|
|
64664
|
+
if (!(file2 instanceof File)) {
|
|
64665
|
+
throw badRequest('A "file" field is required.');
|
|
64666
|
+
}
|
|
64667
|
+
const contentType = file2.type || "application/octet-stream";
|
|
64668
|
+
if (!ALLOWED_MIME_TYPES.has(contentType)) {
|
|
64669
|
+
throw badRequest(`Unsupported file type "${contentType}".`);
|
|
64670
|
+
}
|
|
64671
|
+
if (file2.size > MAX_FILE_SIZE) {
|
|
64672
|
+
throw badRequest("File exceeds 10MB limit.");
|
|
64673
|
+
}
|
|
64674
|
+
const mgmt = new ContentfulManagement(
|
|
64675
|
+
c.env.CONTENTFUL_SPACE_ID,
|
|
64676
|
+
c.env.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
64677
|
+
c.env.CONTENTFUL_ENVIRONMENT
|
|
64678
|
+
);
|
|
64679
|
+
const arrayBuffer = await file2.arrayBuffer();
|
|
64680
|
+
const uploadId = await mgmt.uploadFile(file2.name, arrayBuffer, contentType);
|
|
64681
|
+
const asset = await mgmt.createAssetFromUpload(uploadId, file2.name, file2.name, contentType);
|
|
64682
|
+
return c.json({
|
|
64683
|
+
data: {
|
|
64684
|
+
assetId: asset.id,
|
|
64685
|
+
url: asset.url,
|
|
64686
|
+
size: file2.size,
|
|
64687
|
+
contentType
|
|
64688
|
+
}
|
|
64689
|
+
}, 201);
|
|
64690
|
+
}
|
|
64691
|
+
);
|
|
64320
64692
|
function sanitizeFolder(raw2) {
|
|
64321
64693
|
if (typeof raw2 !== "string" || !raw2.trim()) return "images";
|
|
64322
64694
|
return raw2.trim().replace(/[^a-zA-Z0-9\-_/]/g, "").replace(/\/+/g, "/").replace(/^\/|\/$/g, "") || "images";
|
|
@@ -64333,10 +64705,43 @@ function extensionFromMime(mime) {
|
|
|
64333
64705
|
}
|
|
64334
64706
|
|
|
64335
64707
|
// src/routes/themes.ts
|
|
64708
|
+
var CF_THEME_CT = "theme";
|
|
64709
|
+
async function pushThemeToContentful(env2, theme) {
|
|
64710
|
+
if (!ContentfulManagement.isConfigured(env2.CONTENTFUL_SPACE_ID, env2.CONTENTFUL_MANAGEMENT_TOKEN)) return;
|
|
64711
|
+
const mgmt = new ContentfulManagement(
|
|
64712
|
+
env2.CONTENTFUL_SPACE_ID,
|
|
64713
|
+
env2.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
64714
|
+
env2.CONTENTFUL_ENVIRONMENT
|
|
64715
|
+
);
|
|
64716
|
+
try {
|
|
64717
|
+
await mgmt.upsertEntry(CF_THEME_CT, theme.id, {
|
|
64718
|
+
name: ContentfulManagement.locale(theme.name),
|
|
64719
|
+
path: ContentfulManagement.locale(theme.path)
|
|
64720
|
+
});
|
|
64721
|
+
} catch (err) {
|
|
64722
|
+
console.warn(`[Contentful] Failed to sync theme ${theme.id}:`, err);
|
|
64723
|
+
}
|
|
64724
|
+
}
|
|
64725
|
+
async function deleteThemeFromContentful(env2, themeId) {
|
|
64726
|
+
if (!ContentfulManagement.isConfigured(env2.CONTENTFUL_SPACE_ID, env2.CONTENTFUL_MANAGEMENT_TOKEN)) return;
|
|
64727
|
+
const mgmt = new ContentfulManagement(
|
|
64728
|
+
env2.CONTENTFUL_SPACE_ID,
|
|
64729
|
+
env2.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
64730
|
+
env2.CONTENTFUL_ENVIRONMENT
|
|
64731
|
+
);
|
|
64732
|
+
try {
|
|
64733
|
+
const entry = await mgmt.getEntry(themeId);
|
|
64734
|
+
if (entry) {
|
|
64735
|
+
await mgmt.unpublishEntry(themeId);
|
|
64736
|
+
await mgmt.deleteEntry(themeId);
|
|
64737
|
+
}
|
|
64738
|
+
} catch (err) {
|
|
64739
|
+
console.warn(`[Contentful] Failed to delete theme ${themeId} from Contentful:`, err);
|
|
64740
|
+
}
|
|
64741
|
+
}
|
|
64336
64742
|
var createThemeSchema = external_exports.object({
|
|
64337
64743
|
name: external_exports.string().min(1),
|
|
64338
|
-
path: external_exports.string().min(1)
|
|
64339
|
-
color: external_exports.string().optional()
|
|
64744
|
+
path: external_exports.string().min(1)
|
|
64340
64745
|
});
|
|
64341
64746
|
var themesRoute = new Hono2();
|
|
64342
64747
|
themesRoute.get("/", async (c) => {
|
|
@@ -64354,6 +64759,7 @@ themesRoute.post(
|
|
|
64354
64759
|
const db = createDb(c.env.DB);
|
|
64355
64760
|
try {
|
|
64356
64761
|
const [created] = await db.insert(themes).values(body).returning();
|
|
64762
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
|
|
64357
64763
|
return c.json({ data: created }, 201);
|
|
64358
64764
|
} catch (err) {
|
|
64359
64765
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -64374,6 +64780,7 @@ themesRoute.patch(
|
|
|
64374
64780
|
try {
|
|
64375
64781
|
const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
|
|
64376
64782
|
if (!updated) throw notFound("Theme");
|
|
64783
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
|
|
64377
64784
|
return c.json({ data: updated });
|
|
64378
64785
|
} catch (err) {
|
|
64379
64786
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -64388,6 +64795,67 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64388
64795
|
const db = createDb(c.env.DB);
|
|
64389
64796
|
const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
|
|
64390
64797
|
if (!deleted) throw notFound("Theme");
|
|
64798
|
+
c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
|
|
64799
|
+
return c.body(null, 204);
|
|
64800
|
+
});
|
|
64801
|
+
|
|
64802
|
+
// src/routes/organizations.ts
|
|
64803
|
+
var createOrganizationSchema = external_exports.object({
|
|
64804
|
+
name: external_exports.string().min(1),
|
|
64805
|
+
acronym: external_exports.string().min(1),
|
|
64806
|
+
logoUrl: external_exports.string().url().nullable().optional(),
|
|
64807
|
+
link: external_exports.string().url().nullable().optional()
|
|
64808
|
+
});
|
|
64809
|
+
var organizationsRoute = new Hono2();
|
|
64810
|
+
organizationsRoute.get("/", async (c) => {
|
|
64811
|
+
const db = createDb(c.env.DB);
|
|
64812
|
+
const data = await db.select().from(organizations);
|
|
64813
|
+
return c.json({ data });
|
|
64814
|
+
});
|
|
64815
|
+
organizationsRoute.post(
|
|
64816
|
+
"/",
|
|
64817
|
+
authMiddleware,
|
|
64818
|
+
adminMiddleware,
|
|
64819
|
+
zValidator("json", createOrganizationSchema),
|
|
64820
|
+
async (c) => {
|
|
64821
|
+
const body = c.req.valid("json");
|
|
64822
|
+
const db = createDb(c.env.DB);
|
|
64823
|
+
try {
|
|
64824
|
+
const [created] = await db.insert(organizations).values(body).returning();
|
|
64825
|
+
return c.json({ data: created }, 201);
|
|
64826
|
+
} catch (err) {
|
|
64827
|
+
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
64828
|
+
throw conflict("An organization with this name or acronym already exists.");
|
|
64829
|
+
}
|
|
64830
|
+
throw err;
|
|
64831
|
+
}
|
|
64832
|
+
}
|
|
64833
|
+
);
|
|
64834
|
+
organizationsRoute.patch(
|
|
64835
|
+
"/:id",
|
|
64836
|
+
authMiddleware,
|
|
64837
|
+
adminMiddleware,
|
|
64838
|
+
async (c) => {
|
|
64839
|
+
const { id } = c.req.param();
|
|
64840
|
+
const body = await c.req.json();
|
|
64841
|
+
const db = createDb(c.env.DB);
|
|
64842
|
+
try {
|
|
64843
|
+
const [updated] = await db.update(organizations).set(body).where(eq(organizations.id, id)).returning();
|
|
64844
|
+
if (!updated) throw notFound("Organization");
|
|
64845
|
+
return c.json({ data: updated });
|
|
64846
|
+
} catch (err) {
|
|
64847
|
+
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
64848
|
+
throw conflict("An organization with this name or acronym already exists.");
|
|
64849
|
+
}
|
|
64850
|
+
throw err;
|
|
64851
|
+
}
|
|
64852
|
+
}
|
|
64853
|
+
);
|
|
64854
|
+
organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
64855
|
+
const { id } = c.req.param();
|
|
64856
|
+
const db = createDb(c.env.DB);
|
|
64857
|
+
const [deleted] = await db.delete(organizations).where(eq(organizations.id, id)).returning();
|
|
64858
|
+
if (!deleted) throw notFound("Organization");
|
|
64391
64859
|
return c.body(null, 204);
|
|
64392
64860
|
});
|
|
64393
64861
|
|
|
@@ -64435,12 +64903,13 @@ function createApp(options = {}) {
|
|
|
64435
64903
|
});
|
|
64436
64904
|
app2.post(POW_VERIFY_PATH, handlePowVerify);
|
|
64437
64905
|
app2.route("/health", healthRoute);
|
|
64438
|
-
app2.route("/config", siteConfigRoute);
|
|
64439
|
-
app2.route("/events", eventsRoute);
|
|
64440
|
-
app2.route("/themes", themesRoute);
|
|
64441
|
-
app2.route("/users", usersRoute);
|
|
64442
|
-
app2.route("/
|
|
64443
|
-
app2.route("/
|
|
64906
|
+
app2.route("/api/config", siteConfigRoute);
|
|
64907
|
+
app2.route("/api/events", eventsRoute);
|
|
64908
|
+
app2.route("/api/themes", themesRoute);
|
|
64909
|
+
app2.route("/api/users", usersRoute);
|
|
64910
|
+
app2.route("/api/organizations", organizationsRoute);
|
|
64911
|
+
app2.route("/api/faqs", faqsRoute);
|
|
64912
|
+
app2.route("/api/uploads", uploadsRoute);
|
|
64444
64913
|
app2.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
64445
64914
|
app2.onError(errorHandler2);
|
|
64446
64915
|
app2.notFound(
|
|
@@ -64655,11 +65124,12 @@ var ResendService = class {
|
|
|
64655
65124
|
}
|
|
64656
65125
|
};
|
|
64657
65126
|
function buildReminderEmail(event) {
|
|
65127
|
+
const timeDisplay = [event.dateTime, event.startTime].filter(Boolean).join(" at ");
|
|
64658
65128
|
return `
|
|
64659
65129
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 24px;">
|
|
64660
65130
|
<h2 style="color: #1a1a2e;">\u{1F4C5} Reminder: ${event.title}</h2>
|
|
64661
|
-
${event.
|
|
64662
|
-
${
|
|
65131
|
+
${event.organization ? `<p style="color: #666;">Organized by: <strong>${event.organization}</strong></p>` : ""}
|
|
65132
|
+
${timeDisplay ? `<p>\u{1F550} <strong>${timeDisplay}</strong></p>` : ""}
|
|
64663
65133
|
${event.venue ? `<p>\u{1F4CD} <strong>${event.venue}</strong></p>` : ""}
|
|
64664
65134
|
<hr style="margin: 24px 0; border: none; border-top: 1px solid #eee;" />
|
|
64665
65135
|
${event.gformsUrl ? `<p>You registered for this event. See you there!</p>
|
|
@@ -64802,72 +65272,74 @@ function createQueueHandler(env2) {
|
|
|
64802
65272
|
};
|
|
64803
65273
|
}
|
|
64804
65274
|
async function processJob(job, services) {
|
|
64805
|
-
const { db, email: email3, gforms
|
|
65275
|
+
const { db, email: email3, gforms } = services;
|
|
64806
65276
|
switch (job.type) {
|
|
64807
65277
|
case "send_email": {
|
|
64808
|
-
if (!email3) throw new Error("Email provider not configured
|
|
65278
|
+
if (!email3) throw new Error("Email provider not configured");
|
|
64809
65279
|
const result = await email3.sendEmail(job.payload);
|
|
64810
65280
|
console.log(`[Queue] send_email dispatched via ${result.provider} (id=${result.id})`);
|
|
64811
65281
|
break;
|
|
64812
65282
|
}
|
|
64813
65283
|
case "send_reminder_email": {
|
|
64814
|
-
if (!email3) throw new Error("Email provider not configured
|
|
65284
|
+
if (!email3) throw new Error("Email provider not configured");
|
|
64815
65285
|
const event = await db.query.events.findFirst({
|
|
64816
|
-
where: eq(events.id, job.payload.eventId)
|
|
65286
|
+
where: eq(events.id, job.payload.eventId),
|
|
65287
|
+
with: { organization: true }
|
|
64817
65288
|
});
|
|
64818
65289
|
if (!event?.gformsId) break;
|
|
64819
65290
|
const emails = await gforms.getRespondentEmails(event.gformsId);
|
|
64820
65291
|
if (emails.length === 0) break;
|
|
64821
65292
|
const isDay = job.payload.hoursBeforeEvent === 24;
|
|
64822
|
-
const subject = isDay ? `Reminder: "${event.title}" is tomorrow!` : `Reminder: "${event.title}"
|
|
64823
|
-
const html2 = buildReminderEmail(
|
|
64824
|
-
|
|
65293
|
+
const subject = isDay ? `Reminder: "${event.title}" is tomorrow!` : `Reminder: "${event.title}" is in 1 hour!`;
|
|
65294
|
+
const html2 = buildReminderEmail({
|
|
65295
|
+
title: event.title,
|
|
65296
|
+
organization: event.organization?.name ?? null,
|
|
65297
|
+
dateTime: event.dateTime,
|
|
65298
|
+
startTime: event.startTime,
|
|
65299
|
+
venue: event.venue,
|
|
65300
|
+
gformsUrl: event.gformsUrl
|
|
65301
|
+
});
|
|
65302
|
+
const BATCH_SIZE = 50;
|
|
65303
|
+
const chunks = [];
|
|
64825
65304
|
for (let i = 0; i < emails.length; i += BATCH_SIZE) {
|
|
64826
|
-
|
|
64827
|
-
|
|
64828
|
-
|
|
64829
|
-
|
|
65305
|
+
chunks.push(emails.slice(i, i + BATCH_SIZE));
|
|
65306
|
+
}
|
|
65307
|
+
const failures = [];
|
|
65308
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
65309
|
+
const chunk = chunks[i];
|
|
65310
|
+
try {
|
|
65311
|
+
await email3.sendEmail({ to: chunk, subject, html: html2 });
|
|
65312
|
+
} catch (err) {
|
|
65313
|
+
failures.push(...chunk);
|
|
64830
65314
|
console.error(
|
|
64831
65315
|
`[Queue] send_reminder_email: ${failures.length}/${chunk.length} messages failed in batch ${i / BATCH_SIZE + 1}`,
|
|
64832
|
-
|
|
65316
|
+
err
|
|
64833
65317
|
);
|
|
64834
65318
|
}
|
|
64835
65319
|
}
|
|
64836
|
-
|
|
64837
|
-
|
|
64838
|
-
} else {
|
|
64839
|
-
await db.update(events).set({ reminder1hSent: true }).where(eq(events.id, job.payload.eventId));
|
|
64840
|
-
}
|
|
65320
|
+
const field = isDay ? "reminder24hSent" : "reminder1hSent";
|
|
65321
|
+
await db.update(events).set({ [field]: true }).where(eq(events.id, event.id));
|
|
64841
65322
|
break;
|
|
64842
65323
|
}
|
|
64843
65324
|
case "audit_log": {
|
|
64844
|
-
console.log(
|
|
65325
|
+
console.log(`[Queue] audit_log: ${job.payload.action} by ${job.payload.userId}`);
|
|
64845
65326
|
break;
|
|
64846
65327
|
}
|
|
64847
|
-
case "
|
|
64848
|
-
console.log(
|
|
65328
|
+
case "snapshot_content": {
|
|
65329
|
+
console.log(`[Queue] snapshot_content triggered at ${job.payload.triggeredAt}`);
|
|
64849
65330
|
break;
|
|
64850
65331
|
}
|
|
64851
|
-
case "
|
|
64852
|
-
|
|
64853
|
-
const newExpiry = Math.floor(new Date(renewed.expireTime).getTime() / 1e3);
|
|
64854
|
-
await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.gformsId, job.payload.formId));
|
|
65332
|
+
case "notify_batch_release": {
|
|
65333
|
+
console.log(`[Queue] notify_batch_release: ${job.payload.eventIds.length} events released`);
|
|
64855
65334
|
break;
|
|
64856
65335
|
}
|
|
64857
|
-
case "
|
|
64858
|
-
|
|
64859
|
-
|
|
64860
|
-
console.
|
|
64861
|
-
|
|
65336
|
+
case "renew_forms_watch": {
|
|
65337
|
+
try {
|
|
65338
|
+
await gforms.renewWatch(job.payload.formId, job.payload.watchId);
|
|
65339
|
+
console.log(`[Queue] renew_forms_watch: renewed watch for form ${job.payload.formId}`);
|
|
65340
|
+
} catch (err) {
|
|
65341
|
+
console.error(`[Queue] renew_forms_watch failed for form ${job.payload.formId}:`, err);
|
|
64862
65342
|
}
|
|
64863
|
-
const mgmt = new ContentfulManagement(
|
|
64864
|
-
env2.CONTENTFUL_SPACE_ID,
|
|
64865
|
-
env2.CONTENTFUL_MANAGEMENT_TOKEN,
|
|
64866
|
-
env2.CONTENTFUL_ENVIRONMENT
|
|
64867
|
-
);
|
|
64868
|
-
await ensureContentTypes(mgmt, {});
|
|
64869
|
-
const result = await pushToContentful(db, mgmt, {});
|
|
64870
|
-
console.log("[Snapshot] Result:", JSON.stringify(result));
|
|
64871
65343
|
break;
|
|
64872
65344
|
}
|
|
64873
65345
|
}
|
|
@@ -64944,9 +65416,12 @@ async function reconcileSlots(env2) {
|
|
|
64944
65416
|
}
|
|
64945
65417
|
|
|
64946
65418
|
// src/cron/reminder-emails.ts
|
|
64947
|
-
|
|
64948
|
-
|
|
64949
|
-
|
|
65419
|
+
function parseStartTimestamp(dateTime, startTime) {
|
|
65420
|
+
if (!dateTime) return null;
|
|
65421
|
+
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
65422
|
+
const ms = Date.parse(combined);
|
|
65423
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
65424
|
+
}
|
|
64950
65425
|
async function reminderEmails(env2) {
|
|
64951
65426
|
if (!env2.EMAIL_QUEUE) {
|
|
64952
65427
|
console.warn(
|
|
@@ -64964,17 +65439,21 @@ async function reminderEmails(env2) {
|
|
|
64964
65439
|
}
|
|
64965
65440
|
const db = createDb(env2.DB);
|
|
64966
65441
|
const now2 = Math.floor(Date.now() / 1e3);
|
|
64967
|
-
const
|
|
65442
|
+
const candidates24h = await db.query.events.findMany({
|
|
64968
65443
|
where: and(
|
|
64969
65444
|
eq(events.status, "published"),
|
|
64970
|
-
eq(events.reminder24hSent, false)
|
|
64971
|
-
lte(events.startsAt, now2 + SECONDS_IN_24H + WINDOW)
|
|
65445
|
+
eq(events.reminder24hSent, false)
|
|
64972
65446
|
),
|
|
64973
|
-
columns: {
|
|
65447
|
+
columns: {
|
|
65448
|
+
id: true,
|
|
65449
|
+
dateTime: true,
|
|
65450
|
+
startTime: true
|
|
65451
|
+
}
|
|
64974
65452
|
});
|
|
64975
|
-
for (const event of
|
|
64976
|
-
|
|
64977
|
-
|
|
65453
|
+
for (const event of candidates24h) {
|
|
65454
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
65455
|
+
if (!startsAt) continue;
|
|
65456
|
+
const hoursUntil = (startsAt - now2) / 3600;
|
|
64978
65457
|
if (hoursUntil <= 25 && hoursUntil >= 23) {
|
|
64979
65458
|
await env2.EMAIL_QUEUE.send({
|
|
64980
65459
|
type: "send_reminder_email",
|
|
@@ -64982,27 +65461,28 @@ async function reminderEmails(env2) {
|
|
|
64982
65461
|
});
|
|
64983
65462
|
}
|
|
64984
65463
|
}
|
|
64985
|
-
const
|
|
65464
|
+
const candidates1h = await db.query.events.findMany({
|
|
64986
65465
|
where: and(
|
|
64987
65466
|
eq(events.status, "published"),
|
|
64988
|
-
eq(events.reminder1hSent, false)
|
|
64989
|
-
lte(events.startsAt, now2 + SECONDS_IN_1H + WINDOW)
|
|
65467
|
+
eq(events.reminder1hSent, false)
|
|
64990
65468
|
),
|
|
64991
|
-
columns: {
|
|
65469
|
+
columns: {
|
|
65470
|
+
id: true,
|
|
65471
|
+
dateTime: true,
|
|
65472
|
+
startTime: true
|
|
65473
|
+
}
|
|
64992
65474
|
});
|
|
64993
|
-
for (const event of
|
|
64994
|
-
|
|
64995
|
-
|
|
64996
|
-
|
|
65475
|
+
for (const event of candidates1h) {
|
|
65476
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
65477
|
+
if (!startsAt) continue;
|
|
65478
|
+
const hoursUntil = (startsAt - now2) / 3600;
|
|
65479
|
+
if (hoursUntil <= 1.5 && hoursUntil >= 0) {
|
|
64997
65480
|
await env2.EMAIL_QUEUE.send({
|
|
64998
65481
|
type: "send_reminder_email",
|
|
64999
65482
|
payload: { eventId: event.id, hoursBeforeEvent: 1 }
|
|
65000
65483
|
});
|
|
65001
65484
|
}
|
|
65002
65485
|
}
|
|
65003
|
-
console.log(
|
|
65004
|
-
`[reminder-emails] Queued ${events24h.length} 24h reminders, ${events1h.length} 1h reminders.`
|
|
65005
|
-
);
|
|
65006
65486
|
}
|
|
65007
65487
|
|
|
65008
65488
|
// src/cron/lifecycle-check.ts
|
|
@@ -65018,7 +65498,6 @@ async function lifecycleCheck(env2, ctx) {
|
|
|
65018
65498
|
if (!siteEndsAt || snapshotCompleted) return;
|
|
65019
65499
|
if (now2 >= siteEndsAt) {
|
|
65020
65500
|
console.log("[lifecycle-check] siteEndsAt passed \u2014 triggering content snapshot.");
|
|
65021
|
-
await db.update(siteConfig).set({ value: "true", updatedAt: now2 }).where(eq(siteConfig.key, "snapshot_completed"));
|
|
65022
65501
|
if (env2.EMAIL_QUEUE) {
|
|
65023
65502
|
ctx.waitUntil(
|
|
65024
65503
|
env2.EMAIL_QUEUE.send({ type: "snapshot_content", payload: { triggeredAt: now2 } })
|
|
@@ -65056,73 +65535,183 @@ async function renewWatches(env2) {
|
|
|
65056
65535
|
}
|
|
65057
65536
|
console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
|
|
65058
65537
|
}
|
|
65059
|
-
|
|
65060
|
-
|
|
65061
|
-
|
|
65062
|
-
|
|
65063
|
-
|
|
65064
|
-
|
|
65065
|
-
|
|
65066
|
-
|
|
65067
|
-
|
|
65068
|
-
|
|
65069
|
-
|
|
65538
|
+
|
|
65539
|
+
// src/db/migrate.ts
|
|
65540
|
+
var PATCH_STATEMENTS = [
|
|
65541
|
+
`ALTER TABLE "themes" ADD COLUMN "updated_at" integer NOT NULL DEFAULT (unixepoch())`,
|
|
65542
|
+
`ALTER TABLE "events" ADD COLUMN "organization_id" text`,
|
|
65543
|
+
`ALTER TABLE "events" ADD COLUMN "class_code" text`,
|
|
65544
|
+
`ALTER TABLE "events" ADD COLUMN "start_time" text`,
|
|
65545
|
+
`ALTER TABLE "events" ADD COLUMN "end_time" text`,
|
|
65546
|
+
`CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
|
|
65547
|
+
];
|
|
65548
|
+
var CREATE_STATEMENTS = [
|
|
65549
|
+
// Better Auth: user
|
|
65550
|
+
`CREATE TABLE IF NOT EXISTS "user" (
|
|
65551
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65552
|
+
"name" text NOT NULL,
|
|
65553
|
+
"email" text NOT NULL,
|
|
65554
|
+
"email_verified" integer DEFAULT false NOT NULL,
|
|
65555
|
+
"image" text,
|
|
65556
|
+
"created_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
65557
|
+
"updated_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
|
65558
|
+
)`,
|
|
65559
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "user_email_unique" ON "user" ("email")`,
|
|
65560
|
+
// Better Auth: session
|
|
65561
|
+
`CREATE TABLE IF NOT EXISTS "session" (
|
|
65562
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65563
|
+
"expires_at" integer NOT NULL,
|
|
65564
|
+
"token" text NOT NULL,
|
|
65565
|
+
"created_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
65566
|
+
"updated_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
65567
|
+
"ip_address" text,
|
|
65568
|
+
"user_agent" text,
|
|
65569
|
+
"user_id" text NOT NULL,
|
|
65570
|
+
FOREIGN KEY ("user_id") REFERENCES "user"("id") ON UPDATE no action ON DELETE cascade
|
|
65571
|
+
)`,
|
|
65572
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "session_token_unique" ON "session" ("token")`,
|
|
65573
|
+
`CREATE INDEX IF NOT EXISTS "session_userId_idx" ON "session" ("user_id")`,
|
|
65574
|
+
// Better Auth: account
|
|
65575
|
+
`CREATE TABLE IF NOT EXISTS "account" (
|
|
65576
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65577
|
+
"account_id" text NOT NULL,
|
|
65578
|
+
"provider_id" text NOT NULL,
|
|
65579
|
+
"user_id" text NOT NULL,
|
|
65580
|
+
"access_token" text,
|
|
65581
|
+
"refresh_token" text,
|
|
65582
|
+
"id_token" text,
|
|
65583
|
+
"access_token_expires_at" integer,
|
|
65584
|
+
"refresh_token_expires_at" integer,
|
|
65585
|
+
"scope" text,
|
|
65586
|
+
"password" text,
|
|
65587
|
+
"created_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
65588
|
+
"updated_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
|
65589
|
+
FOREIGN KEY ("user_id") REFERENCES "user"("id") ON UPDATE no action ON DELETE cascade
|
|
65590
|
+
)`,
|
|
65591
|
+
`CREATE INDEX IF NOT EXISTS "account_userId_idx" ON "account" ("user_id")`,
|
|
65592
|
+
// Better Auth: verification
|
|
65593
|
+
`CREATE TABLE IF NOT EXISTS "verification" (
|
|
65594
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65595
|
+
"identifier" text NOT NULL,
|
|
65596
|
+
"value" text NOT NULL,
|
|
65597
|
+
"expires_at" integer NOT NULL,
|
|
65598
|
+
"created_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)),
|
|
65599
|
+
"updated_at" integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer))
|
|
65600
|
+
)`,
|
|
65601
|
+
`CREATE INDEX IF NOT EXISTS "verification_identifier_idx" ON "verification" ("identifier")`,
|
|
65602
|
+
// App: users
|
|
65603
|
+
`CREATE TABLE IF NOT EXISTS "users" (
|
|
65604
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65605
|
+
"better_auth_id" text NOT NULL,
|
|
65606
|
+
"email" text NOT NULL,
|
|
65607
|
+
"name" text NOT NULL,
|
|
65608
|
+
"role" text DEFAULT 'student' NOT NULL,
|
|
65609
|
+
"created_at" integer DEFAULT (unixepoch()) NOT NULL
|
|
65610
|
+
)`,
|
|
65611
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "users_better_auth_id_unique" ON "users" ("better_auth_id")`,
|
|
65612
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "users_email_unique" ON "users" ("email")`,
|
|
65613
|
+
// App: organizations (before events, due to FK)
|
|
65614
|
+
`CREATE TABLE IF NOT EXISTS "organizations" (
|
|
65615
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65616
|
+
"name" text NOT NULL,
|
|
65617
|
+
"acronym" text NOT NULL,
|
|
65618
|
+
"logo_url" text,
|
|
65619
|
+
"link" text,
|
|
65620
|
+
"created_at" integer DEFAULT (unixepoch()) NOT NULL
|
|
65621
|
+
)`,
|
|
65622
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "organizations_name_unique" ON "organizations" ("name")`,
|
|
65623
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "organizations_acronym_unique" ON "organizations" ("acronym")`,
|
|
65624
|
+
// App: themes (before events, due to FK)
|
|
65625
|
+
`CREATE TABLE IF NOT EXISTS "themes" (
|
|
65626
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65627
|
+
"name" text NOT NULL,
|
|
65628
|
+
"path" text NOT NULL,
|
|
65629
|
+
"created_at" integer NOT NULL,
|
|
65630
|
+
"updated_at" integer NOT NULL DEFAULT (unixepoch())
|
|
65631
|
+
)`,
|
|
65632
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "themes_name_unique" ON "themes" ("name")`,
|
|
65633
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "themes_path_unique" ON "themes" ("path")`,
|
|
65634
|
+
// App: events
|
|
65635
|
+
`CREATE TABLE IF NOT EXISTS "events" (
|
|
65636
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65637
|
+
"slug" text NOT NULL,
|
|
65638
|
+
"theme_id" text,
|
|
65639
|
+
"organization_id" text,
|
|
65640
|
+
"title" text NOT NULL,
|
|
65641
|
+
"description" text,
|
|
65642
|
+
"venue" text,
|
|
65643
|
+
"date_time" text,
|
|
65644
|
+
"price" text,
|
|
65645
|
+
"background_image_url" text,
|
|
65646
|
+
"class_code" text,
|
|
65647
|
+
"start_time" text,
|
|
65648
|
+
"end_time" text,
|
|
65649
|
+
"is_major" integer DEFAULT false NOT NULL,
|
|
65650
|
+
"max_slots" integer DEFAULT 0 NOT NULL,
|
|
65651
|
+
"registered_slots" integer DEFAULT 0 NOT NULL,
|
|
65652
|
+
"gforms_id" text,
|
|
65653
|
+
"gforms_url" text,
|
|
65654
|
+
"gforms_editor_url" text,
|
|
65655
|
+
"registration_closes_at" integer,
|
|
65656
|
+
"watch_id" text,
|
|
65657
|
+
"watch_expires_at" integer,
|
|
65658
|
+
"status" text DEFAULT 'draft' NOT NULL,
|
|
65659
|
+
"release_at" integer,
|
|
65660
|
+
"reminder_24h_sent" integer DEFAULT false NOT NULL,
|
|
65661
|
+
"reminder_1h_sent" integer DEFAULT false NOT NULL,
|
|
65662
|
+
"contentful_entry_id" text,
|
|
65663
|
+
"created_at" integer DEFAULT (unixepoch()) NOT NULL,
|
|
65664
|
+
"published_at" integer,
|
|
65665
|
+
FOREIGN KEY ("theme_id") REFERENCES "themes"("id") ON UPDATE no action ON DELETE set null,
|
|
65666
|
+
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE no action ON DELETE set null
|
|
65667
|
+
)`,
|
|
65668
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "events_slug_unique" ON "events" ("slug")`,
|
|
65669
|
+
`CREATE INDEX IF NOT EXISTS "idx_events_status_release" ON "events" ("status", "release_at")`,
|
|
65670
|
+
`CREATE INDEX IF NOT EXISTS "idx_events_theme_id" ON "events" ("theme_id")`,
|
|
65671
|
+
`CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`,
|
|
65672
|
+
`CREATE INDEX IF NOT EXISTS "idx_events_slug" ON "events" ("slug")`,
|
|
65673
|
+
// App: faqs
|
|
65674
|
+
`CREATE TABLE IF NOT EXISTS "faqs" (
|
|
65675
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65676
|
+
"question" text NOT NULL,
|
|
65677
|
+
"answer" text NOT NULL,
|
|
65678
|
+
"category" text,
|
|
65679
|
+
"sort_order" integer DEFAULT 0 NOT NULL,
|
|
65680
|
+
"created_at" integer DEFAULT (unixepoch()) NOT NULL,
|
|
65681
|
+
"updated_at" integer DEFAULT (unixepoch()) NOT NULL
|
|
65682
|
+
)`,
|
|
65683
|
+
// App: site_config
|
|
65684
|
+
`CREATE TABLE IF NOT EXISTS "site_config" (
|
|
65685
|
+
"key" text PRIMARY KEY NOT NULL,
|
|
65686
|
+
"value" text NOT NULL,
|
|
65687
|
+
"updated_at" integer DEFAULT (unixepoch()) NOT NULL
|
|
65688
|
+
)`,
|
|
65689
|
+
// App: bookmarks
|
|
65690
|
+
`CREATE TABLE IF NOT EXISTS "bookmarks" (
|
|
65691
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
65692
|
+
"user_id" text NOT NULL,
|
|
65693
|
+
"event_id" text NOT NULL,
|
|
65694
|
+
"created_at" integer DEFAULT (unixepoch()) NOT NULL,
|
|
65695
|
+
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE no action ON DELETE cascade,
|
|
65696
|
+
FOREIGN KEY ("event_id") REFERENCES "events"("id") ON UPDATE no action ON DELETE cascade
|
|
65697
|
+
)`,
|
|
65698
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "idx_bookmarks_user_event" ON "bookmarks" ("user_id", "event_id")`
|
|
65699
|
+
];
|
|
65700
|
+
async function ensureDatabase(d1) {
|
|
65701
|
+
const { results } = await d1.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='user'").all();
|
|
65702
|
+
if (results.length === 0) {
|
|
65703
|
+
for (const sql3 of CREATE_STATEMENTS) {
|
|
65704
|
+
await d1.prepare(sql3).run();
|
|
65705
|
+
}
|
|
65706
|
+
}
|
|
65707
|
+
for (const sql3 of PATCH_STATEMENTS) {
|
|
65070
65708
|
try {
|
|
65071
|
-
|
|
65072
|
-
|
|
65073
|
-
|
|
65074
|
-
|
|
65075
|
-
migrationQueries.push({
|
|
65076
|
-
sql: result,
|
|
65077
|
-
bps: journalEntry.breakpoints,
|
|
65078
|
-
folderMillis: journalEntry.when,
|
|
65079
|
-
hash: crypto2.createHash("sha256").update(query).digest("hex")
|
|
65080
|
-
});
|
|
65081
|
-
} catch {
|
|
65082
|
-
throw new Error(`No file ${migrationPath} found in ${migrationFolderTo} folder`);
|
|
65083
|
-
}
|
|
65084
|
-
}
|
|
65085
|
-
return migrationQueries;
|
|
65086
|
-
}
|
|
65087
|
-
|
|
65088
|
-
// node_modules/drizzle-orm/d1/migrator.js
|
|
65089
|
-
async function migrate(db, config3) {
|
|
65090
|
-
const migrations = readMigrationFiles(config3);
|
|
65091
|
-
const migrationsTable = config3.migrationsTable ?? "__drizzle_migrations";
|
|
65092
|
-
const migrationTableCreate = sql2`
|
|
65093
|
-
CREATE TABLE IF NOT EXISTS ${sql2.identifier(migrationsTable)} (
|
|
65094
|
-
id SERIAL PRIMARY KEY,
|
|
65095
|
-
hash text NOT NULL,
|
|
65096
|
-
created_at numeric
|
|
65097
|
-
)
|
|
65098
|
-
`;
|
|
65099
|
-
await db.session.run(migrationTableCreate);
|
|
65100
|
-
const dbMigrations = await db.values(
|
|
65101
|
-
sql2`SELECT id, hash, created_at FROM ${sql2.identifier(migrationsTable)} ORDER BY created_at DESC LIMIT 1`
|
|
65102
|
-
);
|
|
65103
|
-
const lastDbMigration = dbMigrations[0] ?? void 0;
|
|
65104
|
-
const statementToBatch = [];
|
|
65105
|
-
for (const migration of migrations) {
|
|
65106
|
-
if (!lastDbMigration || Number(lastDbMigration[2]) < migration.folderMillis) {
|
|
65107
|
-
for (const stmt of migration.sql) {
|
|
65108
|
-
statementToBatch.push(db.run(sql2.raw(stmt)));
|
|
65109
|
-
}
|
|
65110
|
-
statementToBatch.push(
|
|
65111
|
-
db.run(
|
|
65112
|
-
sql2`INSERT INTO ${sql2.identifier(migrationsTable)} ("hash", "created_at") VALUES(${sql2.raw(`'${migration.hash}'`)}, ${sql2.raw(`${migration.folderMillis}`)})`
|
|
65113
|
-
)
|
|
65114
|
-
);
|
|
65709
|
+
await d1.prepare(sql3).run();
|
|
65710
|
+
} catch (err) {
|
|
65711
|
+
if (err?.message?.includes("duplicate column")) continue;
|
|
65712
|
+
throw err;
|
|
65115
65713
|
}
|
|
65116
65714
|
}
|
|
65117
|
-
if (statementToBatch.length > 0) {
|
|
65118
|
-
await db.session.batch(statementToBatch);
|
|
65119
|
-
}
|
|
65120
|
-
}
|
|
65121
|
-
|
|
65122
|
-
// src/db/migrate.ts
|
|
65123
|
-
async function ensureDatabase(d1, migrationsFolder = "./drizzle") {
|
|
65124
|
-
const db = createDb(d1);
|
|
65125
|
-
await migrate(db, { migrationsFolder });
|
|
65126
65715
|
}
|
|
65127
65716
|
|
|
65128
65717
|
// src/index.ts
|
|
@@ -65154,7 +65743,7 @@ function createLeapify(options = {}) {
|
|
|
65154
65743
|
if (options.autoMigrate && !migrated) {
|
|
65155
65744
|
migrated = true;
|
|
65156
65745
|
try {
|
|
65157
|
-
await ensureDatabase(env2.DB
|
|
65746
|
+
await ensureDatabase(env2.DB);
|
|
65158
65747
|
} catch (err) {
|
|
65159
65748
|
console.error("[leapify] Auto-migration failed:", err);
|
|
65160
65749
|
}
|