@access-dlsu/leapify 0.260601.1 → 0.260602.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.map +1 -1
- package/dist/auth/auth.d.ts +2 -2
- package/dist/{chunk-2JEY6TSO.js → chunk-WTA2QGY5.js} +4 -2
- package/dist/chunk-WTA2QGY5.js.map +1 -0
- package/dist/{chunk-X4OB4DZ3.cjs → chunk-ZV4TIJXI.cjs} +4 -2
- package/dist/chunk-ZV4TIJXI.cjs.map +1 -0
- package/dist/client/auth.d.ts +41 -41
- package/dist/client/index.cjs +7 -2
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +10 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +4 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/cron/reconcile-slots.d.ts.map +1 -1
- package/dist/db/schema/themes.d.ts +74 -0
- package/dist/db/schema/themes.d.ts.map +1 -1
- package/dist/index.cjs +251 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +251 -179
- package/dist/index.js.map +1 -1
- package/dist/lib/middleware/turnstile-challenge.cjs +6 -6
- package/dist/lib/middleware/turnstile-challenge.d.ts.map +1 -1
- package/dist/lib/middleware/turnstile-challenge.js +1 -1
- package/dist/routes/internal/batch-release.d.ts +4 -0
- package/dist/routes/internal/batch-release.d.ts.map +1 -0
- package/dist/routes/internal/reconcile-slots.d.ts +4 -0
- package/dist/routes/internal/reconcile-slots.d.ts.map +1 -0
- package/dist/routes/internal/reminder-emails.d.ts +4 -0
- package/dist/routes/internal/reminder-emails.d.ts.map +1 -0
- package/dist/routes/internal/renew-watches.d.ts +4 -0
- package/dist/routes/internal/renew-watches.d.ts.map +1 -0
- package/dist/routes/themes.d.ts.map +1 -1
- package/dist/services/gforms.d.ts.map +1 -1
- package/dist/worker.js +249 -175
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-2JEY6TSO.js.map +0 -1
- package/dist/chunk-X4OB4DZ3.cjs.map +0 -1
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkZV4TIJXI_cjs = require('../../chunk-ZV4TIJXI.cjs');
|
|
4
4
|
require('../../chunk-Q7SFCCGT.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Object.defineProperty(exports, "TURNSTILE_COOKIE_NAME", {
|
|
9
9
|
enumerable: true,
|
|
10
|
-
get: function () { return
|
|
10
|
+
get: function () { return chunkZV4TIJXI_cjs.TURNSTILE_COOKIE_NAME; }
|
|
11
11
|
});
|
|
12
12
|
Object.defineProperty(exports, "TURNSTILE_PATH", {
|
|
13
13
|
enumerable: true,
|
|
14
|
-
get: function () { return
|
|
14
|
+
get: function () { return chunkZV4TIJXI_cjs.TURNSTILE_PATH; }
|
|
15
15
|
});
|
|
16
16
|
Object.defineProperty(exports, "TURNSTILE_VERIFY_PATH", {
|
|
17
17
|
enumerable: true,
|
|
18
|
-
get: function () { return
|
|
18
|
+
get: function () { return chunkZV4TIJXI_cjs.TURNSTILE_VERIFY_PATH; }
|
|
19
19
|
});
|
|
20
20
|
Object.defineProperty(exports, "createTurnstileMiddleware", {
|
|
21
21
|
enumerable: true,
|
|
22
|
-
get: function () { return
|
|
22
|
+
get: function () { return chunkZV4TIJXI_cjs.createTurnstileMiddleware; }
|
|
23
23
|
});
|
|
24
24
|
Object.defineProperty(exports, "handleTurnstileVerify", {
|
|
25
25
|
enumerable: true,
|
|
26
|
-
get: function () { return
|
|
26
|
+
get: function () { return chunkZV4TIJXI_cjs.handleTurnstileVerify; }
|
|
27
27
|
});
|
|
28
28
|
//# sourceMappingURL=turnstile-challenge.cjs.map
|
|
29
29
|
//# sourceMappingURL=turnstile-challenge.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turnstile-challenge.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/turnstile-challenge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,eAAO,MAAM,cAAc,mCAAmC,CAAA;AAE9D,eAAO,MAAM,qBAAqB,0CAA6B,CAAA;AAE/D,eAAO,MAAM,qBAAqB,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"turnstile-challenge.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/turnstile-challenge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,eAAO,MAAM,cAAc,mCAAmC,CAAA;AAE9D,eAAO,MAAM,qBAAqB,0CAA6B,CAAA;AAE/D,eAAO,MAAM,qBAAqB,sBAAsB,CAAA;AA2HxD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,eAAe,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;;;oEA6C1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB;cACH,eAAe;yBA2BpD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { TURNSTILE_COOKIE_NAME, TURNSTILE_PATH, TURNSTILE_VERIFY_PATH, createTurnstileMiddleware, handleTurnstileVerify } from '../../chunk-
|
|
1
|
+
export { TURNSTILE_COOKIE_NAME, TURNSTILE_PATH, TURNSTILE_VERIFY_PATH, createTurnstileMiddleware, handleTurnstileVerify } from '../../chunk-WTA2QGY5.js';
|
|
2
2
|
import '../../chunk-PZ5AY32C.js';
|
|
3
3
|
//# sourceMappingURL=turnstile-challenge.js.map
|
|
4
4
|
//# sourceMappingURL=turnstile-challenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch-release.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/batch-release.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,eAAO,MAAM,iBAAiB,yDAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile-slots.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/reconcile-slots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,eAAO,MAAM,mBAAmB,yDAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reminder-emails.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/reminder-emails.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,eAAO,MAAM,mBAAmB,yDAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renew-watches.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/renew-watches.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,eAAO,MAAM,iBAAiB,yDAAyB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"themes.d.ts","sourceRoot":"","sources":["../../src/routes/themes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"themes.d.ts","sourceRoot":"","sources":["../../src/routes/themes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AA+B1C,eAAO,MAAM,WAAW,yDAAyB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gforms.d.ts","sourceRoot":"","sources":["../../src/services/gforms.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAOD,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;gBAE5C,kBAAkB,EAAE,MAAM;YAQxB,cAAc;YAWd,wBAAwB;YA6BxB,SAAS;IA6CvB;;;OAGG;IACG,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvD;;;OAGG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"gforms.d.ts","sourceRoot":"","sources":["../../src/services/gforms.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAOD,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;gBAE5C,kBAAkB,EAAE,MAAM;YAQxB,cAAc;YAWd,wBAAwB;YA6BxB,SAAS;IA6CvB;;;OAGG;IACG,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvD;;;OAGG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IA6B9D;;;OAGG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAO5D;;;OAGG;IACG,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAO5D;;;;;;;OAOG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAiCnD;;OAEG;IACG,UAAU,CACd,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAuBnC"}
|
package/dist/worker.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { cors } from 'hono/cors';
|
|
3
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
4
4
|
import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
|
5
|
-
import { sql, relations, eq, and, count, lte } from 'drizzle-orm';
|
|
5
|
+
import { sql, relations, eq, and, asc, count, isNotNull, lte } from 'drizzle-orm';
|
|
6
6
|
import { createMiddleware } from 'hono/factory';
|
|
7
7
|
import { betterAuth } from 'better-auth';
|
|
8
8
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
@@ -24,8 +24,6 @@ var LeapifyError = class extends Error {
|
|
|
24
24
|
this.code = code;
|
|
25
25
|
this.name = "LeapifyError";
|
|
26
26
|
}
|
|
27
|
-
statusCode;
|
|
28
|
-
code;
|
|
29
27
|
};
|
|
30
28
|
var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
|
|
31
29
|
var domainRestricted = () => new LeapifyError(
|
|
@@ -91,6 +89,10 @@ var themes = sqliteTable("themes", {
|
|
|
91
89
|
name: text("name").notNull().unique(),
|
|
92
90
|
path: text("path").notNull().unique(),
|
|
93
91
|
// e.g. "/pirates-cove"
|
|
92
|
+
imageUrl: text("image_url"),
|
|
93
|
+
descriptionEn: text("description_en"),
|
|
94
|
+
descriptionFil: text("description_fil"),
|
|
95
|
+
sortOrder: integer("sort_order").notNull().default(0),
|
|
94
96
|
createdAt: integer("created_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3)),
|
|
95
97
|
updatedAt: integer("updated_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3))
|
|
96
98
|
});
|
|
@@ -405,6 +407,8 @@ var EXEMPT_PATHS = [
|
|
|
405
407
|
"/api/classes",
|
|
406
408
|
"/api/faqs",
|
|
407
409
|
"/api/config",
|
|
410
|
+
"/api/themes",
|
|
411
|
+
"/api/organizations",
|
|
408
412
|
TURNSTILE_VERIFY_PATH
|
|
409
413
|
];
|
|
410
414
|
function base64urlEncode(bytes) {
|
|
@@ -906,7 +910,6 @@ var CacheService = class {
|
|
|
906
910
|
constructor(kv) {
|
|
907
911
|
this.kv = kv;
|
|
908
912
|
}
|
|
909
|
-
kv;
|
|
910
913
|
async get(key) {
|
|
911
914
|
return this.kv.get(key, "json");
|
|
912
915
|
}
|
|
@@ -952,8 +955,6 @@ var SlotsService = class {
|
|
|
952
955
|
this.db = db;
|
|
953
956
|
this.cache = cache;
|
|
954
957
|
}
|
|
955
|
-
db;
|
|
956
|
-
cache;
|
|
957
958
|
kvKey(slug) {
|
|
958
959
|
return `${SLOT_KV_PREFIX}${slug}`;
|
|
959
960
|
}
|
|
@@ -1108,7 +1109,10 @@ var GFormsService = class {
|
|
|
1108
1109
|
const response = await fetch(url.toString(), {
|
|
1109
1110
|
headers: { Authorization: `Bearer ${token}` }
|
|
1110
1111
|
});
|
|
1111
|
-
if (!response.ok)
|
|
1112
|
+
if (!response.ok) {
|
|
1113
|
+
const err = await response.text();
|
|
1114
|
+
throw new Error(`Forms API error: ${response.status} ${err}`);
|
|
1115
|
+
}
|
|
1112
1116
|
const data = await response.json();
|
|
1113
1117
|
allResponses.push(...data.responses ?? []);
|
|
1114
1118
|
pageToken = data.nextPageToken;
|
|
@@ -1382,6 +1386,27 @@ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
|
1382
1386
|
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
1383
1387
|
return c.json({ data: info });
|
|
1384
1388
|
});
|
|
1389
|
+
classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
|
|
1390
|
+
const { slug } = c.req.param();
|
|
1391
|
+
const db = createDb(c.env.DB);
|
|
1392
|
+
const cache = new CacheService(c.env.KV);
|
|
1393
|
+
const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1394
|
+
const slots = new SlotsService(db, cache);
|
|
1395
|
+
const event = await db.query.events.findFirst({
|
|
1396
|
+
where: eq(events.slug, slug),
|
|
1397
|
+
columns: { gformsId: true }
|
|
1398
|
+
});
|
|
1399
|
+
if (!event) throw notFound("Event");
|
|
1400
|
+
if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
|
|
1401
|
+
try {
|
|
1402
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1403
|
+
await slots.correctCount(slug, googleCount);
|
|
1404
|
+
return c.json({ data: { registeredSlots: googleCount } });
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
const message = err?.message ?? "Failed to fetch from Google Forms API";
|
|
1407
|
+
return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1385
1410
|
classesRoute.post(
|
|
1386
1411
|
"/",
|
|
1387
1412
|
authMiddleware,
|
|
@@ -1587,7 +1612,7 @@ faqsRoute.get("/", async (c) => {
|
|
|
1587
1612
|
const data = await cache.getOrSet(
|
|
1588
1613
|
FAQS_KV_KEY,
|
|
1589
1614
|
() => db.query.faqs.findMany({
|
|
1590
|
-
orderBy: (t, { asc }) => [
|
|
1615
|
+
orderBy: (t, { asc: asc2 }) => [asc2(t.sortOrder), asc2(t.createdAt)]
|
|
1591
1616
|
}),
|
|
1592
1617
|
FAQS_TTL
|
|
1593
1618
|
);
|
|
@@ -1689,6 +1714,196 @@ async function verifyGoogSignature(body, signature, secret) {
|
|
|
1689
1714
|
return false;
|
|
1690
1715
|
}
|
|
1691
1716
|
}
|
|
1717
|
+
var LOCK_KEY = "cron:reconcile-slots:lock";
|
|
1718
|
+
var LOCK_TTL = 300;
|
|
1719
|
+
async function reconcileSlots(env) {
|
|
1720
|
+
const db = createDb(env.DB);
|
|
1721
|
+
const cache = new CacheService(env.KV);
|
|
1722
|
+
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1723
|
+
const slots = new SlotsService(db, cache);
|
|
1724
|
+
const lock = await cache.get(LOCK_KEY);
|
|
1725
|
+
if (lock) {
|
|
1726
|
+
console.log("[reconcile-slots] Lock held, skipping.");
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
await cache.set(LOCK_KEY, "1", LOCK_TTL);
|
|
1730
|
+
try {
|
|
1731
|
+
const publishedEvents = await db.query.events.findMany({
|
|
1732
|
+
where: isNotNull(events.gformsId),
|
|
1733
|
+
columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
|
|
1734
|
+
});
|
|
1735
|
+
const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
|
|
1736
|
+
let corrected = 0;
|
|
1737
|
+
for (const event of eventsWithForms) {
|
|
1738
|
+
try {
|
|
1739
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1740
|
+
const localCount = event.registeredSlots;
|
|
1741
|
+
if (googleCount !== localCount) {
|
|
1742
|
+
console.warn(
|
|
1743
|
+
`[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
|
|
1744
|
+
);
|
|
1745
|
+
await slots.correctCount(event.slug, googleCount);
|
|
1746
|
+
corrected++;
|
|
1747
|
+
}
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
console.log(
|
|
1753
|
+
`[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
|
|
1754
|
+
);
|
|
1755
|
+
} finally {
|
|
1756
|
+
await cache.del(LOCK_KEY);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/routes/internal/reconcile-slots.ts
|
|
1761
|
+
var reconcileSlotsRoute = new Hono();
|
|
1762
|
+
reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
|
|
1763
|
+
await reconcileSlots(c.env);
|
|
1764
|
+
return c.json({ ok: true });
|
|
1765
|
+
});
|
|
1766
|
+
async function batchRelease(env) {
|
|
1767
|
+
const db = createDb(env.DB);
|
|
1768
|
+
const cache = new CacheService(env.KV);
|
|
1769
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1770
|
+
const toPublish = await db.query.events.findMany({
|
|
1771
|
+
where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
|
|
1772
|
+
columns: { id: true, slug: true }
|
|
1773
|
+
});
|
|
1774
|
+
if (toPublish.length === 0) return;
|
|
1775
|
+
const ids = toPublish.map((e) => e.id);
|
|
1776
|
+
await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
|
|
1777
|
+
// Drizzle doesn't have inArray for D1; use raw SQL for batch
|
|
1778
|
+
sql`${events.id} IN (${sql.join(
|
|
1779
|
+
ids.map((id) => sql`${id}`),
|
|
1780
|
+
sql`, `
|
|
1781
|
+
)})`
|
|
1782
|
+
);
|
|
1783
|
+
await cache.del("events:list");
|
|
1784
|
+
await cache.del("events:etag");
|
|
1785
|
+
console.log(
|
|
1786
|
+
`[batch-release] Published ${toPublish.length} events:`,
|
|
1787
|
+
toPublish.map((e) => e.slug).join(", ")
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/routes/internal/batch-release.ts
|
|
1792
|
+
var batchReleaseRoute = new Hono();
|
|
1793
|
+
batchReleaseRoute.post("/", internalMiddleware, async (c) => {
|
|
1794
|
+
await batchRelease(c.env);
|
|
1795
|
+
return c.json({ ok: true });
|
|
1796
|
+
});
|
|
1797
|
+
function parseStartTimestamp(dateTime, startTime) {
|
|
1798
|
+
if (!dateTime) return null;
|
|
1799
|
+
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
1800
|
+
const ms = Date.parse(combined);
|
|
1801
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
1802
|
+
}
|
|
1803
|
+
async function reminderEmails(env) {
|
|
1804
|
+
if (!env.EMAIL_QUEUE) {
|
|
1805
|
+
console.warn(
|
|
1806
|
+
"[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
|
|
1807
|
+
);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
|
|
1811
|
+
const hasResend = !!env.RESEND_API_KEY;
|
|
1812
|
+
if (!hasSes && !hasResend) {
|
|
1813
|
+
console.warn(
|
|
1814
|
+
"[reminder-emails] No email providers configured. Skipping reminders."
|
|
1815
|
+
);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const db = createDb(env.DB);
|
|
1819
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1820
|
+
const candidates24h = await db.query.events.findMany({
|
|
1821
|
+
where: and(
|
|
1822
|
+
eq(events.status, "published"),
|
|
1823
|
+
eq(events.reminder24hSent, false)
|
|
1824
|
+
),
|
|
1825
|
+
columns: {
|
|
1826
|
+
id: true,
|
|
1827
|
+
dateTime: true,
|
|
1828
|
+
startTime: true
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
for (const event of candidates24h) {
|
|
1832
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
1833
|
+
if (!startsAt) continue;
|
|
1834
|
+
const hoursUntil = (startsAt - now) / 3600;
|
|
1835
|
+
if (hoursUntil <= 25 && hoursUntil >= 23) {
|
|
1836
|
+
await env.EMAIL_QUEUE.send({
|
|
1837
|
+
type: "send_reminder_email",
|
|
1838
|
+
payload: { eventId: event.id, hoursBeforeEvent: 24 }
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const candidates1h = await db.query.events.findMany({
|
|
1843
|
+
where: and(
|
|
1844
|
+
eq(events.status, "published"),
|
|
1845
|
+
eq(events.reminder1hSent, false)
|
|
1846
|
+
),
|
|
1847
|
+
columns: {
|
|
1848
|
+
id: true,
|
|
1849
|
+
dateTime: true,
|
|
1850
|
+
startTime: true
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
for (const event of candidates1h) {
|
|
1854
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
1855
|
+
if (!startsAt) continue;
|
|
1856
|
+
const hoursUntil = (startsAt - now) / 3600;
|
|
1857
|
+
if (hoursUntil <= 1.5 && hoursUntil >= 0) {
|
|
1858
|
+
await env.EMAIL_QUEUE.send({
|
|
1859
|
+
type: "send_reminder_email",
|
|
1860
|
+
payload: { eventId: event.id, hoursBeforeEvent: 1 }
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/routes/internal/reminder-emails.ts
|
|
1867
|
+
var reminderEmailsRoute = new Hono();
|
|
1868
|
+
reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
|
|
1869
|
+
await reminderEmails(c.env);
|
|
1870
|
+
return c.json({ ok: true });
|
|
1871
|
+
});
|
|
1872
|
+
var RENEWAL_WINDOW = 86400;
|
|
1873
|
+
async function renewWatches(env) {
|
|
1874
|
+
const db = createDb(env.DB);
|
|
1875
|
+
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1876
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1877
|
+
const threshold = now + RENEWAL_WINDOW;
|
|
1878
|
+
const expiring = await db.query.events.findMany({
|
|
1879
|
+
where: and(
|
|
1880
|
+
eq(events.status, "published"),
|
|
1881
|
+
lte(events.watchExpiresAt, threshold)
|
|
1882
|
+
),
|
|
1883
|
+
columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
|
|
1884
|
+
});
|
|
1885
|
+
const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
|
|
1886
|
+
let renewed = 0;
|
|
1887
|
+
for (const event of watchEvents) {
|
|
1888
|
+
try {
|
|
1889
|
+
const result = await gforms.renewWatch(event.gformsId, event.watchId);
|
|
1890
|
+
const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
|
|
1891
|
+
await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
|
|
1892
|
+
renewed++;
|
|
1893
|
+
console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/routes/internal/renew-watches.ts
|
|
1902
|
+
var renewWatchesRoute = new Hono();
|
|
1903
|
+
renewWatchesRoute.post("/", internalMiddleware, async (c) => {
|
|
1904
|
+
await renewWatches(c.env);
|
|
1905
|
+
return c.json({ ok: true });
|
|
1906
|
+
});
|
|
1692
1907
|
var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1693
1908
|
"image/jpeg",
|
|
1694
1909
|
"image/png",
|
|
@@ -1788,14 +2003,27 @@ function extensionFromMime(mime) {
|
|
|
1788
2003
|
};
|
|
1789
2004
|
return map[mime] ?? "bin";
|
|
1790
2005
|
}
|
|
2006
|
+
function generatePath(name) {
|
|
2007
|
+
return name.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2008
|
+
}
|
|
1791
2009
|
var createThemeSchema = z.object({
|
|
1792
2010
|
name: z.string().min(1),
|
|
1793
|
-
|
|
2011
|
+
imageUrl: z.string().url().nullable().optional(),
|
|
2012
|
+
descriptionEn: z.string().nullable().optional(),
|
|
2013
|
+
descriptionFil: z.string().nullable().optional(),
|
|
2014
|
+
sortOrder: z.number().int().default(0)
|
|
2015
|
+
});
|
|
2016
|
+
z.object({
|
|
2017
|
+
name: z.string().min(1).optional(),
|
|
2018
|
+
imageUrl: z.string().url().nullable().optional(),
|
|
2019
|
+
descriptionEn: z.string().nullable().optional(),
|
|
2020
|
+
descriptionFil: z.string().nullable().optional(),
|
|
2021
|
+
sortOrder: z.number().int().optional()
|
|
1794
2022
|
});
|
|
1795
2023
|
var themesRoute = new Hono();
|
|
1796
2024
|
themesRoute.get("/", async (c) => {
|
|
1797
2025
|
const db = createDb(c.env.DB);
|
|
1798
|
-
const data = await db.select().from(themes);
|
|
2026
|
+
const data = await db.select().from(themes).orderBy(asc(themes.sortOrder), asc(themes.createdAt));
|
|
1799
2027
|
return c.json({ data });
|
|
1800
2028
|
});
|
|
1801
2029
|
themesRoute.post(
|
|
@@ -1806,8 +2034,9 @@ themesRoute.post(
|
|
|
1806
2034
|
async (c) => {
|
|
1807
2035
|
const body = c.req.valid("json");
|
|
1808
2036
|
const db = createDb(c.env.DB);
|
|
2037
|
+
const path = generatePath(body.name);
|
|
1809
2038
|
try {
|
|
1810
|
-
const [created] = await db.insert(themes).values(body).returning();
|
|
2039
|
+
const [created] = await db.insert(themes).values({ ...body, path }).returning();
|
|
1811
2040
|
return c.json({ data: created }, 201);
|
|
1812
2041
|
} catch (err) {
|
|
1813
2042
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -1825,8 +2054,12 @@ themesRoute.patch(
|
|
|
1825
2054
|
const { id } = c.req.param();
|
|
1826
2055
|
const body = await c.req.json();
|
|
1827
2056
|
const db = createDb(c.env.DB);
|
|
2057
|
+
const update = { ...body };
|
|
2058
|
+
if (body.name) {
|
|
2059
|
+
update.path = generatePath(body.name);
|
|
2060
|
+
}
|
|
1828
2061
|
try {
|
|
1829
|
-
const [updated] = await db.update(themes).set(
|
|
2062
|
+
const [updated] = await db.update(themes).set(update).where(eq(themes.id, id)).returning();
|
|
1830
2063
|
if (!updated) throw notFound("Theme");
|
|
1831
2064
|
return c.json({ data: updated });
|
|
1832
2065
|
} catch (err) {
|
|
@@ -1955,6 +2188,10 @@ function createApp(options = {}) {
|
|
|
1955
2188
|
app2.route("/api/faqs", faqsRoute);
|
|
1956
2189
|
app2.route("/api/uploads", uploadsRoute);
|
|
1957
2190
|
app2.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
2191
|
+
app2.route("/internal/reconcile-slots", reconcileSlotsRoute);
|
|
2192
|
+
app2.route("/internal/batch-release", batchReleaseRoute);
|
|
2193
|
+
app2.route("/internal/reminder-emails", reminderEmailsRoute);
|
|
2194
|
+
app2.route("/internal/renew-watches", renewWatchesRoute);
|
|
1958
2195
|
app2.onError(errorHandler);
|
|
1959
2196
|
app2.notFound(
|
|
1960
2197
|
(c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
|
|
@@ -2060,7 +2297,6 @@ var SesError = class extends Error {
|
|
|
2060
2297
|
this.status = status;
|
|
2061
2298
|
this.name = "SesError";
|
|
2062
2299
|
}
|
|
2063
|
-
status;
|
|
2064
2300
|
/**
|
|
2065
2301
|
* True for errors that are permanent (not worth retrying via SES again).
|
|
2066
2302
|
* 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.
|
|
@@ -2385,168 +2621,6 @@ async function processJob(job, services) {
|
|
|
2385
2621
|
}
|
|
2386
2622
|
}
|
|
2387
2623
|
}
|
|
2388
|
-
async function batchRelease(env) {
|
|
2389
|
-
const db = createDb(env.DB);
|
|
2390
|
-
const cache = new CacheService(env.KV);
|
|
2391
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2392
|
-
const toPublish = await db.query.events.findMany({
|
|
2393
|
-
where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
|
|
2394
|
-
columns: { id: true, slug: true }
|
|
2395
|
-
});
|
|
2396
|
-
if (toPublish.length === 0) return;
|
|
2397
|
-
const ids = toPublish.map((e) => e.id);
|
|
2398
|
-
await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
|
|
2399
|
-
// Drizzle doesn't have inArray for D1; use raw SQL for batch
|
|
2400
|
-
sql`${events.id} IN (${sql.join(
|
|
2401
|
-
ids.map((id) => sql`${id}`),
|
|
2402
|
-
sql`, `
|
|
2403
|
-
)})`
|
|
2404
|
-
);
|
|
2405
|
-
await cache.del("events:list");
|
|
2406
|
-
await cache.del("events:etag");
|
|
2407
|
-
console.log(
|
|
2408
|
-
`[batch-release] Published ${toPublish.length} events:`,
|
|
2409
|
-
toPublish.map((e) => e.slug).join(", ")
|
|
2410
|
-
);
|
|
2411
|
-
}
|
|
2412
|
-
var LOCK_KEY = "cron:reconcile-slots:lock";
|
|
2413
|
-
var LOCK_TTL = 300;
|
|
2414
|
-
async function reconcileSlots(env) {
|
|
2415
|
-
const db = createDb(env.DB);
|
|
2416
|
-
const cache = new CacheService(env.KV);
|
|
2417
|
-
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
2418
|
-
const slots = new SlotsService(db, cache);
|
|
2419
|
-
const lock = await cache.get(LOCK_KEY);
|
|
2420
|
-
if (lock) {
|
|
2421
|
-
console.log("[reconcile-slots] Lock held, skipping.");
|
|
2422
|
-
return;
|
|
2423
|
-
}
|
|
2424
|
-
await cache.set(LOCK_KEY, "1", LOCK_TTL);
|
|
2425
|
-
try {
|
|
2426
|
-
const publishedEvents = await db.query.events.findMany({
|
|
2427
|
-
where: eq(events.status, "published"),
|
|
2428
|
-
columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
|
|
2429
|
-
});
|
|
2430
|
-
const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
|
|
2431
|
-
let corrected = 0;
|
|
2432
|
-
for (const event of eventsWithForms) {
|
|
2433
|
-
try {
|
|
2434
|
-
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
2435
|
-
const localCount = event.registeredSlots;
|
|
2436
|
-
if (googleCount !== localCount) {
|
|
2437
|
-
console.warn(
|
|
2438
|
-
`[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
|
|
2439
|
-
);
|
|
2440
|
-
await slots.correctCount(event.slug, googleCount);
|
|
2441
|
-
corrected++;
|
|
2442
|
-
}
|
|
2443
|
-
} catch (err) {
|
|
2444
|
-
console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
console.log(
|
|
2448
|
-
`[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
|
|
2449
|
-
);
|
|
2450
|
-
} finally {
|
|
2451
|
-
await cache.del(LOCK_KEY);
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
function parseStartTimestamp(dateTime, startTime) {
|
|
2455
|
-
if (!dateTime) return null;
|
|
2456
|
-
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
2457
|
-
const ms = Date.parse(combined);
|
|
2458
|
-
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
2459
|
-
}
|
|
2460
|
-
async function reminderEmails(env) {
|
|
2461
|
-
if (!env.EMAIL_QUEUE) {
|
|
2462
|
-
console.warn(
|
|
2463
|
-
"[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
|
|
2464
|
-
);
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
|
-
const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
|
|
2468
|
-
const hasResend = !!env.RESEND_API_KEY;
|
|
2469
|
-
if (!hasSes && !hasResend) {
|
|
2470
|
-
console.warn(
|
|
2471
|
-
"[reminder-emails] No email providers configured. Skipping reminders."
|
|
2472
|
-
);
|
|
2473
|
-
return;
|
|
2474
|
-
}
|
|
2475
|
-
const db = createDb(env.DB);
|
|
2476
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2477
|
-
const candidates24h = await db.query.events.findMany({
|
|
2478
|
-
where: and(
|
|
2479
|
-
eq(events.status, "published"),
|
|
2480
|
-
eq(events.reminder24hSent, false)
|
|
2481
|
-
),
|
|
2482
|
-
columns: {
|
|
2483
|
-
id: true,
|
|
2484
|
-
dateTime: true,
|
|
2485
|
-
startTime: true
|
|
2486
|
-
}
|
|
2487
|
-
});
|
|
2488
|
-
for (const event of candidates24h) {
|
|
2489
|
-
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
2490
|
-
if (!startsAt) continue;
|
|
2491
|
-
const hoursUntil = (startsAt - now) / 3600;
|
|
2492
|
-
if (hoursUntil <= 25 && hoursUntil >= 23) {
|
|
2493
|
-
await env.EMAIL_QUEUE.send({
|
|
2494
|
-
type: "send_reminder_email",
|
|
2495
|
-
payload: { eventId: event.id, hoursBeforeEvent: 24 }
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
const candidates1h = await db.query.events.findMany({
|
|
2500
|
-
where: and(
|
|
2501
|
-
eq(events.status, "published"),
|
|
2502
|
-
eq(events.reminder1hSent, false)
|
|
2503
|
-
),
|
|
2504
|
-
columns: {
|
|
2505
|
-
id: true,
|
|
2506
|
-
dateTime: true,
|
|
2507
|
-
startTime: true
|
|
2508
|
-
}
|
|
2509
|
-
});
|
|
2510
|
-
for (const event of candidates1h) {
|
|
2511
|
-
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
2512
|
-
if (!startsAt) continue;
|
|
2513
|
-
const hoursUntil = (startsAt - now) / 3600;
|
|
2514
|
-
if (hoursUntil <= 1.5 && hoursUntil >= 0) {
|
|
2515
|
-
await env.EMAIL_QUEUE.send({
|
|
2516
|
-
type: "send_reminder_email",
|
|
2517
|
-
payload: { eventId: event.id, hoursBeforeEvent: 1 }
|
|
2518
|
-
});
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
var RENEWAL_WINDOW = 86400;
|
|
2523
|
-
async function renewWatches(env) {
|
|
2524
|
-
const db = createDb(env.DB);
|
|
2525
|
-
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
2526
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2527
|
-
const threshold = now + RENEWAL_WINDOW;
|
|
2528
|
-
const expiring = await db.query.events.findMany({
|
|
2529
|
-
where: and(
|
|
2530
|
-
eq(events.status, "published"),
|
|
2531
|
-
lte(events.watchExpiresAt, threshold)
|
|
2532
|
-
),
|
|
2533
|
-
columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
|
|
2534
|
-
});
|
|
2535
|
-
const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
|
|
2536
|
-
let renewed = 0;
|
|
2537
|
-
for (const event of watchEvents) {
|
|
2538
|
-
try {
|
|
2539
|
-
const result = await gforms.renewWatch(event.gformsId, event.watchId);
|
|
2540
|
-
const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
|
|
2541
|
-
await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
|
|
2542
|
-
renewed++;
|
|
2543
|
-
console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
|
|
2544
|
-
} catch (err) {
|
|
2545
|
-
console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
|
|
2549
|
-
}
|
|
2550
2624
|
|
|
2551
2625
|
// src/db/migrate.ts
|
|
2552
2626
|
var PATCH_STATEMENTS = [
|