@access-dlsu/leapify 0.260602.1 → 0.260605.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/{chunk-ZV4TIJXI.cjs → chunk-NYEPGZMP.cjs} +4 -2
- package/dist/chunk-NYEPGZMP.cjs.map +1 -0
- package/dist/{chunk-WTA2QGY5.js → chunk-WEW5LGZC.js} +4 -2
- package/dist/chunk-WEW5LGZC.js.map +1 -0
- package/dist/client/types.d.ts +1 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.cjs +911 -465
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +909 -463
- 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/health.d.ts.map +1 -1
- package/dist/routes/internal/batch-release.d.ts.map +1 -1
- package/dist/routes/internal/gforms-webhook.d.ts.map +1 -1
- package/dist/routes/internal/reconcile-slots.d.ts.map +1 -1
- package/dist/routes/internal/reminder-emails.d.ts.map +1 -1
- package/dist/routes/internal/renew-watches.d.ts.map +1 -1
- package/dist/routes/site-config.d.ts.map +1 -1
- package/dist/routes/uploads.d.ts.map +1 -1
- package/dist/routes/users.d.ts.map +1 -1
- package/dist/worker-handler.d.ts.map +1 -1
- package/dist/worker.js +908 -462
- package/dist/worker.js.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-WTA2QGY5.js.map +0 -1
- package/dist/chunk-ZV4TIJXI.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkNYEPGZMP_cjs = require('./chunk-NYEPGZMP.cjs');
|
|
4
4
|
var chunkQ7SFCCGT_cjs = require('./chunk-Q7SFCCGT.cjs');
|
|
5
5
|
var hono = require('hono');
|
|
6
|
+
var honoOpenapi = require('hono-openapi');
|
|
7
|
+
var swaggerUi = require('@hono/swagger-ui');
|
|
6
8
|
var cors = require('hono/cors');
|
|
7
9
|
var d1 = require('drizzle-orm/d1');
|
|
8
10
|
var sqliteCore = require('drizzle-orm/sqlite-core');
|
|
@@ -11,7 +13,6 @@ var factory = require('hono/factory');
|
|
|
11
13
|
var betterAuth = require('better-auth');
|
|
12
14
|
var drizzle = require('better-auth/adapters/drizzle');
|
|
13
15
|
var plugins = require('better-auth/plugins');
|
|
14
|
-
var zodValidator = require('@hono/zod-validator');
|
|
15
16
|
var zod = require('zod');
|
|
16
17
|
|
|
17
18
|
// src/lib/errors.ts
|
|
@@ -580,8 +581,6 @@ var internalMiddleware = factory.createMiddleware(async (c, next) => {
|
|
|
580
581
|
}
|
|
581
582
|
return next();
|
|
582
583
|
});
|
|
583
|
-
|
|
584
|
-
// src/routes/health.ts
|
|
585
584
|
var healthRoute = new hono.Hono();
|
|
586
585
|
async function probeResend(apiKey) {
|
|
587
586
|
const start = Date.now();
|
|
@@ -683,69 +682,87 @@ async function probeGForms(serviceAccountJson) {
|
|
|
683
682
|
};
|
|
684
683
|
}
|
|
685
684
|
}
|
|
686
|
-
healthRoute.get(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
685
|
+
healthRoute.get(
|
|
686
|
+
"/",
|
|
687
|
+
honoOpenapi.describeRoute({
|
|
688
|
+
tags: ["Health"],
|
|
689
|
+
summary: "Service health check",
|
|
690
|
+
responses: { 200: { description: "Health status of configured services" } }
|
|
691
|
+
}),
|
|
692
|
+
async (c) => {
|
|
693
|
+
const env = c.env;
|
|
694
|
+
const hasSes = Boolean(env.SES_REGION) && Boolean(env.SES_ACCESS_KEY_ID) && Boolean(env.SES_SECRET_ACCESS_KEY);
|
|
695
|
+
const hasResend = Boolean(env.RESEND_API_KEY);
|
|
696
|
+
let hasGForms = false;
|
|
697
|
+
if (env.GFORMS_SERVICE_ACCOUNT_JSON) {
|
|
698
|
+
try {
|
|
699
|
+
const parsed = JSON.parse(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
700
|
+
hasGForms = Boolean(parsed.client_email && parsed.private_key);
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
696
703
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
)
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
if (hasResend) {
|
|
707
|
-
probes.push(
|
|
708
|
-
probeResend(env.RESEND_API_KEY).then((h) => ["resend", h])
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
if (hasGForms) {
|
|
712
|
-
probes.push(
|
|
713
|
-
probeGForms(env.GFORMS_SERVICE_ACCOUNT_JSON).then(
|
|
714
|
-
(h) => ["gforms", h]
|
|
715
|
-
)
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
const results = await Promise.all(probes);
|
|
719
|
-
const services = {};
|
|
720
|
-
for (const [name, health] of results) {
|
|
721
|
-
services[name] = health;
|
|
722
|
-
}
|
|
723
|
-
const allOk = results.length === 0 || results.every(([, h]) => h.ok);
|
|
724
|
-
return c.json({
|
|
725
|
-
data: {
|
|
726
|
-
status: allOk ? "OK" : "DEGRADED",
|
|
727
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
728
|
-
services
|
|
704
|
+
const probes = [];
|
|
705
|
+
if (hasSes) {
|
|
706
|
+
probes.push(
|
|
707
|
+
probeSes(env.SES_REGION, env.SES_ACCESS_KEY_ID, env.SES_SECRET_ACCESS_KEY).then(
|
|
708
|
+
(h) => ["ses", h]
|
|
709
|
+
)
|
|
710
|
+
);
|
|
729
711
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
712
|
+
if (hasResend) {
|
|
713
|
+
probes.push(
|
|
714
|
+
probeResend(env.RESEND_API_KEY).then((h) => ["resend", h])
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (hasGForms) {
|
|
718
|
+
probes.push(
|
|
719
|
+
probeGForms(env.GFORMS_SERVICE_ACCOUNT_JSON).then(
|
|
720
|
+
(h) => ["gforms", h]
|
|
721
|
+
)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const results = await Promise.all(probes);
|
|
725
|
+
const services = {};
|
|
726
|
+
for (const [name, health] of results) {
|
|
727
|
+
services[name] = health;
|
|
728
|
+
}
|
|
729
|
+
const allOk = results.length === 0 || results.every(([, h]) => h.ok);
|
|
730
|
+
return c.json({
|
|
731
|
+
data: {
|
|
732
|
+
status: allOk ? "OK" : "DEGRADED",
|
|
733
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
734
|
+
services
|
|
743
735
|
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
healthRoute.post(
|
|
740
|
+
"/queue-burst",
|
|
741
|
+
honoOpenapi.describeRoute({
|
|
742
|
+
tags: ["Health"],
|
|
743
|
+
summary: "Queue load test (admin)",
|
|
744
|
+
responses: { 200: { description: "Items queued" } }
|
|
745
|
+
}),
|
|
746
|
+
authMiddleware,
|
|
747
|
+
adminMiddleware,
|
|
748
|
+
async (c) => {
|
|
749
|
+
if (!c.env.EMAIL_QUEUE) {
|
|
750
|
+
return c.json({ error: "Queue binding missing" }, 400);
|
|
744
751
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
752
|
+
const batch = Array.from({ length: 100 }, (_, i) => ({
|
|
753
|
+
body: {
|
|
754
|
+
type: "audit_log",
|
|
755
|
+
payload: {
|
|
756
|
+
action: "queue_load_test",
|
|
757
|
+
userId: "system",
|
|
758
|
+
meta: { index: i, time: Date.now() }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}));
|
|
762
|
+
await c.env.EMAIL_QUEUE.sendBatch(batch);
|
|
763
|
+
return c.json({ status: "queued", count: 100 });
|
|
764
|
+
}
|
|
765
|
+
);
|
|
749
766
|
|
|
750
767
|
// src/services/cache.ts
|
|
751
768
|
var CacheService = class {
|
|
@@ -1118,143 +1135,229 @@ var classesRoute = new hono.Hono();
|
|
|
1118
1135
|
function generateSlug(title) {
|
|
1119
1136
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1120
1137
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
return
|
|
1128
|
-
}
|
|
1129
|
-
classesRoute.
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1138
|
+
function serializeEvent(event) {
|
|
1139
|
+
if (!event) return event;
|
|
1140
|
+
const { dateTime, ...rest } = event;
|
|
1141
|
+
return { ...rest, date: dateTime };
|
|
1142
|
+
}
|
|
1143
|
+
function serializeEvents(events2) {
|
|
1144
|
+
return events2.map(serializeEvent);
|
|
1145
|
+
}
|
|
1146
|
+
classesRoute.get(
|
|
1147
|
+
"/admin",
|
|
1148
|
+
honoOpenapi.describeRoute({
|
|
1149
|
+
tags: ["Events"],
|
|
1150
|
+
summary: "List all events (admin)",
|
|
1151
|
+
responses: { 200: { description: "List of all events" } }
|
|
1152
|
+
}),
|
|
1153
|
+
authMiddleware,
|
|
1154
|
+
adminMiddleware,
|
|
1155
|
+
async (c) => {
|
|
1156
|
+
const db = createDb(c.env.DB);
|
|
1157
|
+
const data = await db.query.events.findMany({
|
|
1158
|
+
with: { theme: true, organization: true },
|
|
1159
|
+
orderBy: (e, { desc }) => [desc(e.createdAt)]
|
|
1160
|
+
});
|
|
1161
|
+
return c.json({ data: serializeEvents(data) });
|
|
1162
|
+
}
|
|
1163
|
+
);
|
|
1164
|
+
classesRoute.post(
|
|
1165
|
+
"/admin/publish",
|
|
1166
|
+
honoOpenapi.describeRoute({
|
|
1167
|
+
tags: ["Events"],
|
|
1168
|
+
summary: "Batch publish queued events",
|
|
1169
|
+
responses: {
|
|
1170
|
+
200: { description: "Events published successfully" },
|
|
1171
|
+
400: { description: "Missing event IDs" }
|
|
1172
|
+
}
|
|
1173
|
+
}),
|
|
1174
|
+
authMiddleware,
|
|
1175
|
+
adminMiddleware,
|
|
1176
|
+
async (c) => {
|
|
1177
|
+
const body = await c.req.json();
|
|
1178
|
+
const db = createDb(c.env.DB);
|
|
1179
|
+
const cache = new CacheService(c.env.KV);
|
|
1180
|
+
if (!body.ids?.length) {
|
|
1181
|
+
return c.json({ error: "ids are required" }, 400);
|
|
1182
|
+
}
|
|
1183
|
+
if (body.releaseAt) {
|
|
1184
|
+
await db.update(events).set({ releaseAt: body.releaseAt, status: "queued" }).where(
|
|
1185
|
+
drizzleOrm.sql`${events.id} IN (${drizzleOrm.sql.join(
|
|
1186
|
+
body.ids.map((id) => drizzleOrm.sql`${id}`),
|
|
1187
|
+
drizzleOrm.sql`, `
|
|
1188
|
+
)})`
|
|
1189
|
+
);
|
|
1190
|
+
} else {
|
|
1191
|
+
await db.update(events).set({ status: "published", publishedAt: drizzleOrm.sql`(unixepoch())` }).where(
|
|
1192
|
+
drizzleOrm.sql`${events.id} IN (${drizzleOrm.sql.join(
|
|
1193
|
+
body.ids.map((id) => drizzleOrm.sql`${id}`),
|
|
1194
|
+
drizzleOrm.sql`, `
|
|
1195
|
+
)})`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
await Promise.all([
|
|
1199
|
+
cache.del(EVENTS_LIST_KV_KEY),
|
|
1200
|
+
cache.del(EVENTS_ETAG_KV_KEY)
|
|
1201
|
+
]);
|
|
1202
|
+
return c.json({ data: { updated: body.ids.length } });
|
|
1203
|
+
}
|
|
1204
|
+
);
|
|
1205
|
+
classesRoute.get(
|
|
1206
|
+
"/",
|
|
1207
|
+
honoOpenapi.describeRoute({
|
|
1208
|
+
tags: ["Events"],
|
|
1209
|
+
summary: "List published events",
|
|
1210
|
+
responses: { 200: { description: "List of published events with themes" } }
|
|
1211
|
+
}),
|
|
1212
|
+
eventsListRateLimit,
|
|
1213
|
+
async (c) => {
|
|
1214
|
+
const db = createDb(c.env.DB);
|
|
1215
|
+
const cache = new CacheService(c.env.KV);
|
|
1216
|
+
const [latest] = await db.select({ max: events.publishedAt }).from(events).where(drizzleOrm.eq(events.status, "published")).limit(1);
|
|
1217
|
+
const etag = await cache.getOrSet(
|
|
1218
|
+
EVENTS_ETAG_KV_KEY,
|
|
1219
|
+
() => cache.generateETag(String(latest?.max ?? "0")),
|
|
1220
|
+
300
|
|
1221
|
+
);
|
|
1222
|
+
const ifNoneMatch = c.req.header("If-None-Match");
|
|
1223
|
+
if (ifNoneMatch === etag) {
|
|
1224
|
+
return c.body(null, 304);
|
|
1225
|
+
}
|
|
1226
|
+
const data = await cache.getOrSet(
|
|
1227
|
+
EVENTS_LIST_KV_KEY,
|
|
1228
|
+
() => db.query.events.findMany({
|
|
1229
|
+
where: drizzleOrm.eq(events.status, "published"),
|
|
1230
|
+
with: {
|
|
1231
|
+
theme: true,
|
|
1232
|
+
organization: true
|
|
1233
|
+
},
|
|
1234
|
+
columns: {
|
|
1235
|
+
id: true,
|
|
1236
|
+
slug: true,
|
|
1237
|
+
themeId: true,
|
|
1238
|
+
organizationId: true,
|
|
1239
|
+
title: true,
|
|
1240
|
+
venue: true,
|
|
1241
|
+
dateTime: true,
|
|
1242
|
+
price: true,
|
|
1243
|
+
backgroundImageUrl: true,
|
|
1244
|
+
classCode: true,
|
|
1245
|
+
startTime: true,
|
|
1246
|
+
endTime: true,
|
|
1247
|
+
registrationClosesAt: true,
|
|
1248
|
+
isSpotlight: true,
|
|
1249
|
+
maxSlots: true,
|
|
1250
|
+
registeredSlots: true,
|
|
1251
|
+
gformsUrl: true,
|
|
1252
|
+
gformsEditorUrl: true,
|
|
1253
|
+
publishedAt: true
|
|
1254
|
+
}
|
|
1255
|
+
}),
|
|
1256
|
+
EVENTS_LIST_TTL
|
|
1142
1257
|
);
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
drizzleOrm.sql`, `
|
|
1148
|
-
)})`
|
|
1258
|
+
c.header("ETag", etag);
|
|
1259
|
+
c.header(
|
|
1260
|
+
"Cache-Control",
|
|
1261
|
+
"public, max-age=604800, stale-while-revalidate=86400"
|
|
1149
1262
|
);
|
|
1263
|
+
return c.json({ data: serializeEvents(data) });
|
|
1150
1264
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if (ifNoneMatch === etag) {
|
|
1168
|
-
return c.body(null, 304);
|
|
1169
|
-
}
|
|
1170
|
-
const data = await cache.getOrSet(
|
|
1171
|
-
EVENTS_LIST_KV_KEY,
|
|
1172
|
-
() => db.query.events.findMany({
|
|
1173
|
-
where: drizzleOrm.eq(events.status, "published"),
|
|
1265
|
+
);
|
|
1266
|
+
classesRoute.get(
|
|
1267
|
+
"/:slug",
|
|
1268
|
+
honoOpenapi.describeRoute({
|
|
1269
|
+
tags: ["Events"],
|
|
1270
|
+
summary: "Get event by slug",
|
|
1271
|
+
responses: {
|
|
1272
|
+
200: { description: "Event details" },
|
|
1273
|
+
404: { description: "Event not found" }
|
|
1274
|
+
}
|
|
1275
|
+
}),
|
|
1276
|
+
async (c) => {
|
|
1277
|
+
const { slug } = c.req.param();
|
|
1278
|
+
const db = createDb(c.env.DB);
|
|
1279
|
+
const event = await db.query.events.findFirst({
|
|
1280
|
+
where: drizzleOrm.and(drizzleOrm.eq(events.slug, slug), drizzleOrm.eq(events.status, "published")),
|
|
1174
1281
|
with: {
|
|
1175
|
-
theme: true
|
|
1176
|
-
organization: true
|
|
1177
|
-
},
|
|
1178
|
-
columns: {
|
|
1179
|
-
id: true,
|
|
1180
|
-
slug: true,
|
|
1181
|
-
themeId: true,
|
|
1182
|
-
organizationId: true,
|
|
1183
|
-
title: true,
|
|
1184
|
-
venue: true,
|
|
1185
|
-
dateTime: true,
|
|
1186
|
-
price: true,
|
|
1187
|
-
backgroundImageUrl: true,
|
|
1188
|
-
classCode: true,
|
|
1189
|
-
startTime: true,
|
|
1190
|
-
endTime: true,
|
|
1191
|
-
registrationClosesAt: true,
|
|
1192
|
-
isSpotlight: true,
|
|
1193
|
-
maxSlots: true,
|
|
1194
|
-
registeredSlots: true,
|
|
1195
|
-
gformsUrl: true,
|
|
1196
|
-
gformsEditorUrl: true,
|
|
1197
|
-
publishedAt: true
|
|
1282
|
+
theme: true
|
|
1198
1283
|
}
|
|
1199
|
-
})
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
const event = await db.query.events.findFirst({
|
|
1213
|
-
where: drizzleOrm.and(drizzleOrm.eq(events.slug, slug), drizzleOrm.eq(events.status, "published")),
|
|
1214
|
-
with: {
|
|
1215
|
-
theme: true
|
|
1284
|
+
});
|
|
1285
|
+
if (!event) throw notFound("Event");
|
|
1286
|
+
return c.json({ data: serializeEvent(event) });
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
classesRoute.get(
|
|
1290
|
+
"/:slug/slots",
|
|
1291
|
+
honoOpenapi.describeRoute({
|
|
1292
|
+
tags: ["Events"],
|
|
1293
|
+
summary: "Get event slot availability",
|
|
1294
|
+
responses: {
|
|
1295
|
+
200: { description: "Slot availability info" },
|
|
1296
|
+
404: { description: "Event not found" }
|
|
1216
1297
|
}
|
|
1217
|
-
})
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
1229
|
-
return c.json({ data: info });
|
|
1230
|
-
});
|
|
1231
|
-
classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
|
|
1232
|
-
const { slug } = c.req.param();
|
|
1233
|
-
const db = createDb(c.env.DB);
|
|
1234
|
-
const cache = new CacheService(c.env.KV);
|
|
1235
|
-
const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1236
|
-
const slots = new SlotsService(db, cache);
|
|
1237
|
-
const event = await db.query.events.findFirst({
|
|
1238
|
-
where: drizzleOrm.eq(events.slug, slug),
|
|
1239
|
-
columns: { gformsId: true }
|
|
1240
|
-
});
|
|
1241
|
-
if (!event) throw notFound("Event");
|
|
1242
|
-
if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
|
|
1243
|
-
try {
|
|
1244
|
-
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1245
|
-
await slots.correctCount(slug, googleCount);
|
|
1246
|
-
return c.json({ data: { registeredSlots: googleCount } });
|
|
1247
|
-
} catch (err) {
|
|
1248
|
-
const message = err?.message ?? "Failed to fetch from Google Forms API";
|
|
1249
|
-
return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
|
|
1298
|
+
}),
|
|
1299
|
+
eventsSlotsRateLimit,
|
|
1300
|
+
async (c) => {
|
|
1301
|
+
const { slug } = c.req.param();
|
|
1302
|
+
const db = createDb(c.env.DB);
|
|
1303
|
+
const cache = new CacheService(c.env.KV);
|
|
1304
|
+
const slotsService = new SlotsService(db, cache);
|
|
1305
|
+
const info = await slotsService.getSlots(slug);
|
|
1306
|
+
if (!info) throw notFound("Event");
|
|
1307
|
+
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
1308
|
+
return c.json({ data: info });
|
|
1250
1309
|
}
|
|
1251
|
-
|
|
1310
|
+
);
|
|
1311
|
+
classesRoute.post(
|
|
1312
|
+
"/:slug/reconcile",
|
|
1313
|
+
honoOpenapi.describeRoute({
|
|
1314
|
+
tags: ["Events"],
|
|
1315
|
+
summary: "Reconcile event slot count with Google Forms",
|
|
1316
|
+
responses: {
|
|
1317
|
+
200: { description: "Slot count reconciled" },
|
|
1318
|
+
400: { description: "No gformsId set" },
|
|
1319
|
+
404: { description: "Event not found" },
|
|
1320
|
+
502: { description: "Google Forms API error" }
|
|
1321
|
+
}
|
|
1322
|
+
}),
|
|
1323
|
+
authMiddleware,
|
|
1324
|
+
adminMiddleware,
|
|
1325
|
+
async (c) => {
|
|
1326
|
+
const { slug } = c.req.param();
|
|
1327
|
+
const db = createDb(c.env.DB);
|
|
1328
|
+
const cache = new CacheService(c.env.KV);
|
|
1329
|
+
const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1330
|
+
const slots = new SlotsService(db, cache);
|
|
1331
|
+
const event = await db.query.events.findFirst({
|
|
1332
|
+
where: drizzleOrm.eq(events.slug, slug),
|
|
1333
|
+
columns: { gformsId: true }
|
|
1334
|
+
});
|
|
1335
|
+
if (!event) throw notFound("Event");
|
|
1336
|
+
if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
|
|
1337
|
+
try {
|
|
1338
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1339
|
+
await slots.correctCount(slug, googleCount);
|
|
1340
|
+
return c.json({ data: { registeredSlots: googleCount } });
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
const message = err?.message ?? "Failed to fetch from Google Forms API";
|
|
1343
|
+
return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1252
1347
|
classesRoute.post(
|
|
1253
1348
|
"/",
|
|
1349
|
+
honoOpenapi.describeRoute({
|
|
1350
|
+
tags: ["Events"],
|
|
1351
|
+
summary: "Create a new event",
|
|
1352
|
+
responses: {
|
|
1353
|
+
201: { description: "Event created successfully" },
|
|
1354
|
+
422: { description: "Validation error" }
|
|
1355
|
+
}
|
|
1356
|
+
}),
|
|
1254
1357
|
authMiddleware,
|
|
1255
1358
|
adminMiddleware,
|
|
1256
1359
|
adminEventsRateLimit,
|
|
1257
|
-
|
|
1360
|
+
honoOpenapi.validator("json", createEventSchema),
|
|
1258
1361
|
async (c) => {
|
|
1259
1362
|
const body = c.req.valid("json");
|
|
1260
1363
|
const db = createDb(c.env.DB);
|
|
@@ -1284,161 +1387,287 @@ classesRoute.post(
|
|
|
1284
1387
|
cache.del(EVENTS_LIST_KV_KEY),
|
|
1285
1388
|
cache.del(EVENTS_ETAG_KV_KEY)
|
|
1286
1389
|
]);
|
|
1287
|
-
return c.json({ data: created }, 201);
|
|
1390
|
+
return c.json({ data: serializeEvent(created) }, 201);
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
classesRoute.patch(
|
|
1394
|
+
"/:slug",
|
|
1395
|
+
honoOpenapi.describeRoute({
|
|
1396
|
+
tags: ["Events"],
|
|
1397
|
+
summary: "Update an event",
|
|
1398
|
+
responses: {
|
|
1399
|
+
200: { description: "Event updated successfully" },
|
|
1400
|
+
404: { description: "Event not found" }
|
|
1401
|
+
}
|
|
1402
|
+
}),
|
|
1403
|
+
authMiddleware,
|
|
1404
|
+
adminMiddleware,
|
|
1405
|
+
async (c) => {
|
|
1406
|
+
const { slug } = c.req.param();
|
|
1407
|
+
const body = await c.req.json();
|
|
1408
|
+
const db = createDb(c.env.DB);
|
|
1409
|
+
const cache = new CacheService(c.env.KV);
|
|
1410
|
+
let newSlug;
|
|
1411
|
+
if (body.title) {
|
|
1412
|
+
newSlug = generateSlug(body.title);
|
|
1413
|
+
}
|
|
1414
|
+
const [updated] = await db.update(events).set(newSlug ? { ...body, slug: newSlug } : body).where(drizzleOrm.eq(events.slug, slug)).returning();
|
|
1415
|
+
if (!updated) throw notFound("Event");
|
|
1416
|
+
await Promise.all([
|
|
1417
|
+
cache.del(EVENTS_LIST_KV_KEY),
|
|
1418
|
+
cache.del(EVENTS_ETAG_KV_KEY)
|
|
1419
|
+
]);
|
|
1420
|
+
return c.json({ data: serializeEvent(updated) });
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
1423
|
+
classesRoute.delete(
|
|
1424
|
+
"/:slug",
|
|
1425
|
+
honoOpenapi.describeRoute({
|
|
1426
|
+
tags: ["Events"],
|
|
1427
|
+
summary: "Delete an event",
|
|
1428
|
+
responses: {
|
|
1429
|
+
204: { description: "Event deleted" },
|
|
1430
|
+
404: { description: "Event not found" }
|
|
1431
|
+
}
|
|
1432
|
+
}),
|
|
1433
|
+
authMiddleware,
|
|
1434
|
+
adminMiddleware,
|
|
1435
|
+
async (c) => {
|
|
1436
|
+
const { slug } = c.req.param();
|
|
1437
|
+
const db = createDb(c.env.DB);
|
|
1438
|
+
const cache = new CacheService(c.env.KV);
|
|
1439
|
+
const [deleted] = await db.delete(events).where(drizzleOrm.eq(events.slug, slug)).returning();
|
|
1440
|
+
if (!deleted) throw notFound("Event");
|
|
1441
|
+
await Promise.all([
|
|
1442
|
+
cache.del(EVENTS_LIST_KV_KEY),
|
|
1443
|
+
cache.del(EVENTS_ETAG_KV_KEY)
|
|
1444
|
+
]);
|
|
1445
|
+
return c.body(null, 204);
|
|
1288
1446
|
}
|
|
1289
1447
|
);
|
|
1290
|
-
classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
1291
|
-
const { slug } = c.req.param();
|
|
1292
|
-
const body = await c.req.json();
|
|
1293
|
-
const db = createDb(c.env.DB);
|
|
1294
|
-
const cache = new CacheService(c.env.KV);
|
|
1295
|
-
let newSlug;
|
|
1296
|
-
if (body.title) {
|
|
1297
|
-
newSlug = generateSlug(body.title);
|
|
1298
|
-
}
|
|
1299
|
-
const [updated] = await db.update(events).set(newSlug ? { ...body, slug: newSlug } : body).where(drizzleOrm.eq(events.slug, slug)).returning();
|
|
1300
|
-
if (!updated) throw notFound("Event");
|
|
1301
|
-
await Promise.all([
|
|
1302
|
-
cache.del(EVENTS_LIST_KV_KEY),
|
|
1303
|
-
cache.del(EVENTS_ETAG_KV_KEY)
|
|
1304
|
-
]);
|
|
1305
|
-
return c.json({ data: updated });
|
|
1306
|
-
});
|
|
1307
|
-
classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
1308
|
-
const { slug } = c.req.param();
|
|
1309
|
-
const db = createDb(c.env.DB);
|
|
1310
|
-
const cache = new CacheService(c.env.KV);
|
|
1311
|
-
const [deleted] = await db.delete(events).where(drizzleOrm.eq(events.slug, slug)).returning();
|
|
1312
|
-
if (!deleted) throw notFound("Event");
|
|
1313
|
-
await Promise.all([
|
|
1314
|
-
cache.del(EVENTS_LIST_KV_KEY),
|
|
1315
|
-
cache.del(EVENTS_ETAG_KV_KEY)
|
|
1316
|
-
]);
|
|
1317
|
-
return c.body(null, 204);
|
|
1318
|
-
});
|
|
1319
1448
|
var VALID_ROLES = ["student", "admin", "super_admin"];
|
|
1320
1449
|
var usersRoute = new hono.Hono();
|
|
1321
|
-
usersRoute.get(
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1450
|
+
usersRoute.get(
|
|
1451
|
+
"/",
|
|
1452
|
+
honoOpenapi.describeRoute({
|
|
1453
|
+
tags: ["Users"],
|
|
1454
|
+
summary: "List all users (admin)",
|
|
1455
|
+
responses: { 200: { description: "List of users" } }
|
|
1456
|
+
}),
|
|
1457
|
+
authMiddleware,
|
|
1458
|
+
adminMiddleware,
|
|
1459
|
+
async (c) => {
|
|
1460
|
+
const db = createDb(c.env.DB);
|
|
1461
|
+
const data = await db.select().from(users);
|
|
1462
|
+
return c.json({ data });
|
|
1463
|
+
}
|
|
1464
|
+
);
|
|
1465
|
+
usersRoute.patch(
|
|
1466
|
+
"/:id/role",
|
|
1467
|
+
honoOpenapi.describeRoute({
|
|
1468
|
+
tags: ["Users"],
|
|
1469
|
+
summary: "Change user role (admin)",
|
|
1470
|
+
responses: {
|
|
1471
|
+
200: { description: "Role updated" },
|
|
1472
|
+
400: { description: "Invalid role" },
|
|
1473
|
+
404: { description: "User not found" }
|
|
1474
|
+
}
|
|
1475
|
+
}),
|
|
1476
|
+
authMiddleware,
|
|
1477
|
+
adminMiddleware,
|
|
1478
|
+
async (c) => {
|
|
1479
|
+
const { id } = c.req.param();
|
|
1480
|
+
const { role } = await c.req.json();
|
|
1481
|
+
if (!role || !VALID_ROLES.includes(role)) {
|
|
1482
|
+
throw badRequest("Role must be 'student', 'admin', or 'super_admin'.");
|
|
1483
|
+
}
|
|
1484
|
+
const db = createDb(c.env.DB);
|
|
1485
|
+
const [updated] = await db.update(users).set({ role }).where(drizzleOrm.eq(users.id, id)).returning();
|
|
1486
|
+
if (!updated) throw notFound("User");
|
|
1348
1487
|
return c.json({ data: updated });
|
|
1349
1488
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
const { eventId } = c.req.param();
|
|
1380
|
-
const user = c.get("user");
|
|
1381
|
-
const db = createDb(c.env.DB);
|
|
1382
|
-
const event = await db.query.events.findFirst({
|
|
1383
|
-
where: drizzleOrm.eq(events.id, eventId),
|
|
1384
|
-
columns: { id: true }
|
|
1385
|
-
});
|
|
1386
|
-
if (!event) throw notFound("Event");
|
|
1387
|
-
const inserted = await db.insert(bookmarks).values({ userId: user.dbId, eventId }).onConflictDoNothing({ target: [bookmarks.userId, bookmarks.eventId] }).returning();
|
|
1388
|
-
if (inserted.length > 0) {
|
|
1389
|
-
return c.json({ data: { bookmarked: true } }, 201);
|
|
1489
|
+
);
|
|
1490
|
+
usersRoute.post(
|
|
1491
|
+
"/by-email",
|
|
1492
|
+
honoOpenapi.describeRoute({
|
|
1493
|
+
tags: ["Users"],
|
|
1494
|
+
summary: "Find or create user by email (admin)",
|
|
1495
|
+
responses: {
|
|
1496
|
+
200: { description: "User updated" },
|
|
1497
|
+
201: { description: "User created" },
|
|
1498
|
+
400: { description: "Invalid email or role" }
|
|
1499
|
+
}
|
|
1500
|
+
}),
|
|
1501
|
+
authMiddleware,
|
|
1502
|
+
adminMiddleware,
|
|
1503
|
+
async (c) => {
|
|
1504
|
+
const { email, role } = await c.req.json();
|
|
1505
|
+
if (!email || !role || !VALID_ROLES.includes(role)) {
|
|
1506
|
+
throw badRequest("Email and valid role ('student', 'admin', 'super_admin') are required.");
|
|
1507
|
+
}
|
|
1508
|
+
const db = createDb(c.env.DB);
|
|
1509
|
+
const existing = await db.query.users.findFirst({
|
|
1510
|
+
where: drizzleOrm.eq(users.email, email)
|
|
1511
|
+
});
|
|
1512
|
+
if (existing) {
|
|
1513
|
+
const [updated] = await db.update(users).set({ role }).where(drizzleOrm.eq(users.email, email)).returning();
|
|
1514
|
+
return c.json({ data: updated });
|
|
1515
|
+
}
|
|
1516
|
+
const [created] = await db.insert(users).values({ betterAuthId: `pending:${email}`, email, name: email.split("@")[0], role }).returning();
|
|
1517
|
+
return c.json({ data: created }, 201);
|
|
1390
1518
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
)
|
|
1401
|
-
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1519
|
+
);
|
|
1520
|
+
usersRoute.get(
|
|
1521
|
+
"/me",
|
|
1522
|
+
honoOpenapi.describeRoute({
|
|
1523
|
+
tags: ["Users"],
|
|
1524
|
+
summary: "Get current user profile",
|
|
1525
|
+
responses: { 200: { description: "Current user profile or null" } }
|
|
1526
|
+
}),
|
|
1527
|
+
optionalAuthMiddleware,
|
|
1528
|
+
async (c) => {
|
|
1529
|
+
const user = c.get("user");
|
|
1530
|
+
if (!user) return c.json({ data: null });
|
|
1531
|
+
const db = createDb(c.env.DB);
|
|
1532
|
+
const profile = await db.query.users.findFirst({
|
|
1533
|
+
where: drizzleOrm.eq(users.id, user.dbId)
|
|
1534
|
+
});
|
|
1535
|
+
if (!profile) return c.json({ data: null });
|
|
1536
|
+
const auth = await db.query.authUser.findFirst({
|
|
1537
|
+
where: drizzleOrm.eq(authUser.id, profile.betterAuthId),
|
|
1538
|
+
columns: { image: true }
|
|
1539
|
+
});
|
|
1540
|
+
return c.json({ data: { ...profile, image: auth?.image ?? null } });
|
|
1541
|
+
}
|
|
1542
|
+
);
|
|
1543
|
+
usersRoute.get(
|
|
1544
|
+
"/me/bookmarks",
|
|
1545
|
+
honoOpenapi.describeRoute({
|
|
1546
|
+
tags: ["Users"],
|
|
1547
|
+
summary: "Get current user's bookmarks",
|
|
1548
|
+
responses: { 200: { description: "List of bookmarked events" } }
|
|
1549
|
+
}),
|
|
1550
|
+
optionalAuthMiddleware,
|
|
1551
|
+
async (c) => {
|
|
1552
|
+
const user = c.get("user");
|
|
1553
|
+
if (!user) return c.json({ data: [] });
|
|
1554
|
+
const db = createDb(c.env.DB);
|
|
1555
|
+
const rows = await db.query.bookmarks.findMany({
|
|
1556
|
+
where: drizzleOrm.eq(bookmarks.userId, user.dbId),
|
|
1557
|
+
with: { event: true }
|
|
1558
|
+
});
|
|
1559
|
+
const data = rows.map((r) => ({ bookmarkedAt: r.createdAt, event: r.event }));
|
|
1560
|
+
return c.json({ data });
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
usersRoute.post(
|
|
1564
|
+
"/me/bookmarks/:eventId",
|
|
1565
|
+
honoOpenapi.describeRoute({
|
|
1566
|
+
tags: ["Users"],
|
|
1567
|
+
summary: "Toggle bookmark for an event",
|
|
1568
|
+
responses: {
|
|
1569
|
+
201: { description: "Bookmark created" },
|
|
1570
|
+
200: { description: "Bookmark removed" },
|
|
1571
|
+
404: { description: "Event not found" }
|
|
1419
1572
|
}
|
|
1420
|
-
})
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
if (key === "allowed_origins") {
|
|
1573
|
+
}),
|
|
1574
|
+
authMiddleware,
|
|
1575
|
+
bookmarksRateLimit,
|
|
1576
|
+
async (c) => {
|
|
1577
|
+
const { eventId } = c.req.param();
|
|
1426
1578
|
const user = c.get("user");
|
|
1427
|
-
|
|
1428
|
-
|
|
1579
|
+
const db = createDb(c.env.DB);
|
|
1580
|
+
const event = await db.query.events.findFirst({
|
|
1581
|
+
where: drizzleOrm.eq(events.id, eventId),
|
|
1582
|
+
columns: { id: true }
|
|
1583
|
+
});
|
|
1584
|
+
if (!event) throw notFound("Event");
|
|
1585
|
+
const inserted = await db.insert(bookmarks).values({ userId: user.dbId, eventId }).onConflictDoNothing({ target: [bookmarks.userId, bookmarks.eventId] }).returning();
|
|
1586
|
+
if (inserted.length > 0) {
|
|
1587
|
+
return c.json({ data: { bookmarked: true } }, 201);
|
|
1429
1588
|
}
|
|
1589
|
+
await db.delete(bookmarks).where(drizzleOrm.and(drizzleOrm.eq(bookmarks.userId, user.dbId), drizzleOrm.eq(bookmarks.eventId, eventId)));
|
|
1590
|
+
return c.json({ data: { bookmarked: false } }, 200);
|
|
1430
1591
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
});
|
|
1592
|
+
);
|
|
1593
|
+
usersRoute.delete(
|
|
1594
|
+
"/me/bookmarks/:eventId",
|
|
1595
|
+
honoOpenapi.describeRoute({
|
|
1596
|
+
tags: ["Users"],
|
|
1597
|
+
summary: "Remove bookmark for an event",
|
|
1598
|
+
responses: { 200: { description: "Bookmark removed" } }
|
|
1599
|
+
}),
|
|
1600
|
+
authMiddleware,
|
|
1601
|
+
async (c) => {
|
|
1602
|
+
const { eventId } = c.req.param();
|
|
1603
|
+
const user = c.get("user");
|
|
1604
|
+
const db = createDb(c.env.DB);
|
|
1605
|
+
await db.delete(bookmarks).where(
|
|
1606
|
+
drizzleOrm.and(drizzleOrm.eq(bookmarks.userId, user.dbId), drizzleOrm.eq(bookmarks.eventId, eventId))
|
|
1607
|
+
);
|
|
1608
|
+
return c.json({ data: { bookmarked: false } });
|
|
1609
|
+
}
|
|
1610
|
+
);
|
|
1611
|
+
var siteConfigRoute = new hono.Hono();
|
|
1612
|
+
siteConfigRoute.get(
|
|
1613
|
+
"/",
|
|
1614
|
+
honoOpenapi.describeRoute({
|
|
1615
|
+
tags: ["Site Config"],
|
|
1616
|
+
summary: "Get public site configuration",
|
|
1617
|
+
responses: { 200: { description: "Site configuration values" } }
|
|
1618
|
+
}),
|
|
1619
|
+
async (c) => {
|
|
1620
|
+
const db = createDb(c.env.DB);
|
|
1621
|
+
const rows = await db.query.siteConfig.findMany();
|
|
1622
|
+
const config = Object.fromEntries(
|
|
1623
|
+
rows.map((r) => [r.key, JSON.parse(r.value)])
|
|
1624
|
+
);
|
|
1625
|
+
return c.json({
|
|
1626
|
+
data: {
|
|
1627
|
+
comingSoonUntil: config.coming_soon_until ?? null,
|
|
1628
|
+
siteEndsAt: config.site_ends_at ?? null,
|
|
1629
|
+
siteName: config.site_name ?? null,
|
|
1630
|
+
registrationGloballyOpen: config.registration_globally_open ?? true,
|
|
1631
|
+
maintenanceMode: config.maintenance_mode ?? false,
|
|
1632
|
+
allowedOrigins: config.allowed_origins ?? null,
|
|
1633
|
+
now: Math.floor(Date.now() / 1e3)
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
);
|
|
1638
|
+
siteConfigRoute.patch(
|
|
1639
|
+
"/:key",
|
|
1640
|
+
honoOpenapi.describeRoute({
|
|
1641
|
+
tags: ["Site Config"],
|
|
1642
|
+
summary: "Update a site configuration key (admin)",
|
|
1643
|
+
responses: {
|
|
1644
|
+
200: { description: "Config updated" },
|
|
1645
|
+
403: { description: "Super admin required for this key" }
|
|
1646
|
+
}
|
|
1647
|
+
}),
|
|
1648
|
+
authMiddleware,
|
|
1649
|
+
adminMiddleware,
|
|
1650
|
+
async (c) => {
|
|
1651
|
+
const key = c.req.param("key");
|
|
1652
|
+
const { value } = await c.req.json();
|
|
1653
|
+
if (key === "allowed_origins") {
|
|
1654
|
+
const user = c.get("user");
|
|
1655
|
+
if (!user || user.role !== "super_admin") {
|
|
1656
|
+
throw forbidden("Super Admin access required to change allowed origins");
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const db = createDb(c.env.DB);
|
|
1660
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1661
|
+
await db.insert(siteConfig).values({ key, value: JSON.stringify(value), updatedAt: now }).onConflictDoUpdate({
|
|
1662
|
+
target: siteConfig.key,
|
|
1663
|
+
set: { value: JSON.stringify(value), updatedAt: now }
|
|
1664
|
+
});
|
|
1665
|
+
await c.env.KV.put(`config:${key}`, JSON.stringify(value), {
|
|
1666
|
+
expirationTtl: 86400
|
|
1667
|
+
});
|
|
1668
|
+
return c.json({ data: { key, value } });
|
|
1669
|
+
}
|
|
1670
|
+
);
|
|
1442
1671
|
var FAQS_KV_KEY = "faqs:active";
|
|
1443
1672
|
var FAQS_TTL = 600;
|
|
1444
1673
|
var faqSchema = zod.z.object({
|
|
@@ -1448,23 +1677,39 @@ var faqSchema = zod.z.object({
|
|
|
1448
1677
|
sortOrder: zod.z.number().int().default(0)
|
|
1449
1678
|
});
|
|
1450
1679
|
var faqsRoute = new hono.Hono();
|
|
1451
|
-
faqsRoute.get(
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1680
|
+
faqsRoute.get(
|
|
1681
|
+
"/",
|
|
1682
|
+
honoOpenapi.describeRoute({
|
|
1683
|
+
tags: ["FAQs"],
|
|
1684
|
+
summary: "List all active FAQs",
|
|
1685
|
+
responses: { 200: { description: "List of FAQs" } }
|
|
1686
|
+
}),
|
|
1687
|
+
async (c) => {
|
|
1688
|
+
const db = createDb(c.env.DB);
|
|
1689
|
+
const cache = new CacheService(c.env.KV);
|
|
1690
|
+
const data = await cache.getOrSet(
|
|
1691
|
+
FAQS_KV_KEY,
|
|
1692
|
+
() => db.query.faqs.findMany({
|
|
1693
|
+
orderBy: (t, { asc: asc2 }) => [asc2(t.sortOrder), asc2(t.createdAt)]
|
|
1694
|
+
}),
|
|
1695
|
+
FAQS_TTL
|
|
1696
|
+
);
|
|
1697
|
+
return c.json({ data });
|
|
1698
|
+
}
|
|
1699
|
+
);
|
|
1463
1700
|
faqsRoute.post(
|
|
1464
1701
|
"/",
|
|
1702
|
+
honoOpenapi.describeRoute({
|
|
1703
|
+
tags: ["FAQs"],
|
|
1704
|
+
summary: "Create a new FAQ (admin)",
|
|
1705
|
+
responses: {
|
|
1706
|
+
201: { description: "FAQ created" },
|
|
1707
|
+
422: { description: "Validation error" }
|
|
1708
|
+
}
|
|
1709
|
+
}),
|
|
1465
1710
|
authMiddleware,
|
|
1466
1711
|
adminMiddleware,
|
|
1467
|
-
|
|
1712
|
+
honoOpenapi.validator("json", faqSchema),
|
|
1468
1713
|
async (c) => {
|
|
1469
1714
|
const body = c.req.valid("json");
|
|
1470
1715
|
const db = createDb(c.env.DB);
|
|
@@ -1474,65 +1719,105 @@ faqsRoute.post(
|
|
|
1474
1719
|
return c.json({ data: created }, 201);
|
|
1475
1720
|
}
|
|
1476
1721
|
);
|
|
1477
|
-
faqsRoute.patch(
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1722
|
+
faqsRoute.patch(
|
|
1723
|
+
"/:id",
|
|
1724
|
+
honoOpenapi.describeRoute({
|
|
1725
|
+
tags: ["FAQs"],
|
|
1726
|
+
summary: "Update an FAQ (admin)",
|
|
1727
|
+
responses: {
|
|
1728
|
+
200: { description: "FAQ updated" },
|
|
1729
|
+
404: { description: "FAQ not found" }
|
|
1730
|
+
}
|
|
1731
|
+
}),
|
|
1732
|
+
authMiddleware,
|
|
1733
|
+
adminMiddleware,
|
|
1734
|
+
async (c) => {
|
|
1735
|
+
const { id } = c.req.param();
|
|
1736
|
+
const body = await c.req.json();
|
|
1737
|
+
const db = createDb(c.env.DB);
|
|
1738
|
+
const cache = new CacheService(c.env.KV);
|
|
1739
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1740
|
+
const [updated] = await db.update(faqs).set({ ...body, updatedAt: now }).where(drizzleOrm.eq(faqs.id, id)).returning();
|
|
1741
|
+
if (!updated) throw notFound("FAQ");
|
|
1742
|
+
await cache.del(FAQS_KV_KEY);
|
|
1743
|
+
return c.json({ data: updated });
|
|
1744
|
+
}
|
|
1745
|
+
);
|
|
1746
|
+
faqsRoute.delete(
|
|
1747
|
+
"/:id",
|
|
1748
|
+
honoOpenapi.describeRoute({
|
|
1749
|
+
tags: ["FAQs"],
|
|
1750
|
+
summary: "Delete an FAQ (admin)",
|
|
1751
|
+
responses: {
|
|
1752
|
+
200: { description: "FAQ deleted" },
|
|
1753
|
+
404: { description: "FAQ not found" }
|
|
1509
1754
|
}
|
|
1755
|
+
}),
|
|
1756
|
+
authMiddleware,
|
|
1757
|
+
adminMiddleware,
|
|
1758
|
+
async (c) => {
|
|
1759
|
+
const { id } = c.req.param();
|
|
1760
|
+
const db = createDb(c.env.DB);
|
|
1761
|
+
const cache = new CacheService(c.env.KV);
|
|
1762
|
+
const [deleted] = await db.delete(faqs).where(drizzleOrm.eq(faqs.id, id)).returning();
|
|
1763
|
+
if (!deleted) throw notFound("FAQ");
|
|
1764
|
+
await cache.del(FAQS_KV_KEY);
|
|
1765
|
+
return c.json({ data: { deleted: true } });
|
|
1510
1766
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
})
|
|
1525
|
-
|
|
1526
|
-
|
|
1767
|
+
);
|
|
1768
|
+
var gformsWebhookRoute = new hono.Hono();
|
|
1769
|
+
gformsWebhookRoute.post(
|
|
1770
|
+
"/",
|
|
1771
|
+
honoOpenapi.describeRoute({
|
|
1772
|
+
tags: ["Internal"],
|
|
1773
|
+
summary: "Google Forms webhook receiver",
|
|
1774
|
+
description: "Receives Google Forms Watch push notifications and increments slot counters.",
|
|
1775
|
+
responses: {
|
|
1776
|
+
200: { description: "Notification processed" },
|
|
1777
|
+
400: { description: "Invalid payload" },
|
|
1778
|
+
403: { description: "Invalid HMAC signature" }
|
|
1779
|
+
}
|
|
1780
|
+
}),
|
|
1781
|
+
internalMiddleware,
|
|
1782
|
+
async (c) => {
|
|
1783
|
+
const rawBody = await c.req.text();
|
|
1784
|
+
const signature = c.req.header("X-Goog-Signature");
|
|
1785
|
+
if (signature) {
|
|
1786
|
+
const isValid = await verifyGoogSignature(
|
|
1787
|
+
rawBody,
|
|
1788
|
+
signature,
|
|
1789
|
+
c.env.GFORMS_WEBHOOK_SECRET
|
|
1790
|
+
);
|
|
1791
|
+
if (!isValid) {
|
|
1792
|
+
return c.json({ error: "Invalid signature" }, 403);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
let payload;
|
|
1796
|
+
try {
|
|
1797
|
+
payload = JSON.parse(rawBody);
|
|
1798
|
+
} catch {
|
|
1799
|
+
return c.json({ error: "Invalid payload" }, 400);
|
|
1800
|
+
}
|
|
1801
|
+
const { formId } = payload;
|
|
1802
|
+
if (!formId) return c.json({ error: "Missing formId" }, 400);
|
|
1803
|
+
const db = createDb(c.env.DB);
|
|
1804
|
+
const cache = new CacheService(c.env.KV);
|
|
1805
|
+
const event = await db.query.events.findFirst({
|
|
1806
|
+
where: drizzleOrm.eq(events.gformsId, formId),
|
|
1807
|
+
columns: { slug: true, maxSlots: true, registeredSlots: true }
|
|
1808
|
+
});
|
|
1809
|
+
if (!event) {
|
|
1810
|
+
console.warn(`[gforms-webhook] Unknown formId: ${formId}`);
|
|
1811
|
+
return c.json({ ok: true });
|
|
1812
|
+
}
|
|
1813
|
+
const slotsService = new SlotsService(db, cache);
|
|
1814
|
+
const updated = await slotsService.increment(event.slug);
|
|
1815
|
+
console.log(
|
|
1816
|
+
`[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
|
|
1817
|
+
);
|
|
1527
1818
|
return c.json({ ok: true });
|
|
1528
1819
|
}
|
|
1529
|
-
|
|
1530
|
-
const updated = await slotsService.increment(event.slug);
|
|
1531
|
-
console.log(
|
|
1532
|
-
`[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
|
|
1533
|
-
);
|
|
1534
|
-
return c.json({ ok: true });
|
|
1535
|
-
});
|
|
1820
|
+
);
|
|
1536
1821
|
async function verifyGoogSignature(body, signature, secret) {
|
|
1537
1822
|
try {
|
|
1538
1823
|
const key = await crypto.subtle.importKey(
|
|
@@ -1601,10 +1886,20 @@ async function reconcileSlots(env) {
|
|
|
1601
1886
|
|
|
1602
1887
|
// src/routes/internal/reconcile-slots.ts
|
|
1603
1888
|
var reconcileSlotsRoute = new hono.Hono();
|
|
1604
|
-
reconcileSlotsRoute.post(
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1889
|
+
reconcileSlotsRoute.post(
|
|
1890
|
+
"/",
|
|
1891
|
+
honoOpenapi.describeRoute({
|
|
1892
|
+
tags: ["Internal"],
|
|
1893
|
+
summary: "Reconcile event slot counts",
|
|
1894
|
+
description: "Triggers slot count reconciliation for all events with Google Forms.",
|
|
1895
|
+
responses: { 200: { description: "Reconciliation complete" } }
|
|
1896
|
+
}),
|
|
1897
|
+
internalMiddleware,
|
|
1898
|
+
async (c) => {
|
|
1899
|
+
await reconcileSlots(c.env);
|
|
1900
|
+
return c.json({ ok: true });
|
|
1901
|
+
}
|
|
1902
|
+
);
|
|
1608
1903
|
async function batchRelease(env) {
|
|
1609
1904
|
const db = createDb(env.DB);
|
|
1610
1905
|
const cache = new CacheService(env.KV);
|
|
@@ -1632,10 +1927,20 @@ async function batchRelease(env) {
|
|
|
1632
1927
|
|
|
1633
1928
|
// src/routes/internal/batch-release.ts
|
|
1634
1929
|
var batchReleaseRoute = new hono.Hono();
|
|
1635
|
-
batchReleaseRoute.post(
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1930
|
+
batchReleaseRoute.post(
|
|
1931
|
+
"/",
|
|
1932
|
+
honoOpenapi.describeRoute({
|
|
1933
|
+
tags: ["Internal"],
|
|
1934
|
+
summary: "Batch release queued events",
|
|
1935
|
+
description: "Triggers batch release of queued events whose releaseAt time has passed.",
|
|
1936
|
+
responses: { 200: { description: "Batch release complete" } }
|
|
1937
|
+
}),
|
|
1938
|
+
internalMiddleware,
|
|
1939
|
+
async (c) => {
|
|
1940
|
+
await batchRelease(c.env);
|
|
1941
|
+
return c.json({ ok: true });
|
|
1942
|
+
}
|
|
1943
|
+
);
|
|
1639
1944
|
function parseStartTimestamp(dateTime, startTime) {
|
|
1640
1945
|
if (!dateTime) return null;
|
|
1641
1946
|
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
@@ -1707,10 +2012,20 @@ async function reminderEmails(env) {
|
|
|
1707
2012
|
|
|
1708
2013
|
// src/routes/internal/reminder-emails.ts
|
|
1709
2014
|
var reminderEmailsRoute = new hono.Hono();
|
|
1710
|
-
reminderEmailsRoute.post(
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
2015
|
+
reminderEmailsRoute.post(
|
|
2016
|
+
"/",
|
|
2017
|
+
honoOpenapi.describeRoute({
|
|
2018
|
+
tags: ["Internal"],
|
|
2019
|
+
summary: "Send reminder emails",
|
|
2020
|
+
description: "Triggers reminder email processing for upcoming events.",
|
|
2021
|
+
responses: { 200: { description: "Reminder emails processed" } }
|
|
2022
|
+
}),
|
|
2023
|
+
internalMiddleware,
|
|
2024
|
+
async (c) => {
|
|
2025
|
+
await reminderEmails(c.env);
|
|
2026
|
+
return c.json({ ok: true });
|
|
2027
|
+
}
|
|
2028
|
+
);
|
|
1714
2029
|
var RENEWAL_WINDOW = 86400;
|
|
1715
2030
|
async function renewWatches(env) {
|
|
1716
2031
|
const db = createDb(env.DB);
|
|
@@ -1742,10 +2057,20 @@ async function renewWatches(env) {
|
|
|
1742
2057
|
|
|
1743
2058
|
// src/routes/internal/renew-watches.ts
|
|
1744
2059
|
var renewWatchesRoute = new hono.Hono();
|
|
1745
|
-
renewWatchesRoute.post(
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2060
|
+
renewWatchesRoute.post(
|
|
2061
|
+
"/",
|
|
2062
|
+
honoOpenapi.describeRoute({
|
|
2063
|
+
tags: ["Internal"],
|
|
2064
|
+
summary: "Renew Google Forms watches",
|
|
2065
|
+
description: "Triggers renewal of expiring Google Forms Watch subscriptions.",
|
|
2066
|
+
responses: { 200: { description: "Watch renewal complete" } }
|
|
2067
|
+
}),
|
|
2068
|
+
internalMiddleware,
|
|
2069
|
+
async (c) => {
|
|
2070
|
+
await renewWatches(c.env);
|
|
2071
|
+
return c.json({ ok: true });
|
|
2072
|
+
}
|
|
2073
|
+
);
|
|
1749
2074
|
var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1750
2075
|
"image/jpeg",
|
|
1751
2076
|
"image/png",
|
|
@@ -1755,30 +2080,50 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
1755
2080
|
]);
|
|
1756
2081
|
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1757
2082
|
var uploadsRoute = new hono.Hono();
|
|
1758
|
-
uploadsRoute.get(
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2083
|
+
uploadsRoute.get(
|
|
2084
|
+
"/images/*",
|
|
2085
|
+
honoOpenapi.describeRoute({
|
|
2086
|
+
tags: ["Uploads"],
|
|
2087
|
+
summary: "Serve an image from R2 storage",
|
|
2088
|
+
responses: {
|
|
2089
|
+
200: { description: "Image file" },
|
|
2090
|
+
404: { description: "Image not found" }
|
|
2091
|
+
}
|
|
2092
|
+
}),
|
|
2093
|
+
async (c) => {
|
|
2094
|
+
const bucket = c.env.FILES;
|
|
2095
|
+
if (!bucket) {
|
|
2096
|
+
throw serviceUnavailable("File storage (R2) is not configured.");
|
|
2097
|
+
}
|
|
2098
|
+
const path = c.req.path.split("/uploads/images/")[1];
|
|
2099
|
+
if (!path) throw notFound("Image");
|
|
2100
|
+
const object = await bucket.get(path);
|
|
2101
|
+
if (!object) throw notFound("Image");
|
|
2102
|
+
const headers = {
|
|
2103
|
+
etag: object.httpEtag,
|
|
2104
|
+
// Cache at the edge/browser for 1 month
|
|
2105
|
+
"Cache-Control": "public, max-age=2592000, immutable"
|
|
2106
|
+
};
|
|
2107
|
+
if (object.httpMetadata?.contentType) {
|
|
2108
|
+
headers["Content-Type"] = object.httpMetadata.contentType;
|
|
2109
|
+
}
|
|
2110
|
+
if (object.httpMetadata?.cacheControl) {
|
|
2111
|
+
headers["Cache-Control"] = object.httpMetadata.cacheControl;
|
|
2112
|
+
}
|
|
2113
|
+
return c.body(object.body, 200, headers);
|
|
1777
2114
|
}
|
|
1778
|
-
|
|
1779
|
-
});
|
|
2115
|
+
);
|
|
1780
2116
|
uploadsRoute.post(
|
|
1781
2117
|
"/images",
|
|
2118
|
+
honoOpenapi.describeRoute({
|
|
2119
|
+
tags: ["Uploads"],
|
|
2120
|
+
summary: "Upload an image to R2 storage (admin)",
|
|
2121
|
+
responses: {
|
|
2122
|
+
201: { description: "Image uploaded successfully" },
|
|
2123
|
+
400: { description: "Invalid file or MIME type" },
|
|
2124
|
+
503: { description: "R2 storage not configured" }
|
|
2125
|
+
}
|
|
2126
|
+
}),
|
|
1782
2127
|
authMiddleware,
|
|
1783
2128
|
adminMiddleware,
|
|
1784
2129
|
async (c) => {
|
|
@@ -1863,16 +2208,33 @@ zod.z.object({
|
|
|
1863
2208
|
sortOrder: zod.z.number().int().optional()
|
|
1864
2209
|
});
|
|
1865
2210
|
var themesRoute = new hono.Hono();
|
|
1866
|
-
themesRoute.get(
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2211
|
+
themesRoute.get(
|
|
2212
|
+
"/",
|
|
2213
|
+
honoOpenapi.describeRoute({
|
|
2214
|
+
tags: ["Themes"],
|
|
2215
|
+
summary: "List all themes",
|
|
2216
|
+
responses: { 200: { description: "List of themes" } }
|
|
2217
|
+
}),
|
|
2218
|
+
async (c) => {
|
|
2219
|
+
const db = createDb(c.env.DB);
|
|
2220
|
+
const data = await db.select().from(themes).orderBy(drizzleOrm.asc(themes.sortOrder), drizzleOrm.asc(themes.createdAt));
|
|
2221
|
+
return c.json({ data });
|
|
2222
|
+
}
|
|
2223
|
+
);
|
|
1871
2224
|
themesRoute.post(
|
|
1872
2225
|
"/",
|
|
2226
|
+
honoOpenapi.describeRoute({
|
|
2227
|
+
tags: ["Themes"],
|
|
2228
|
+
summary: "Create a new theme (admin)",
|
|
2229
|
+
responses: {
|
|
2230
|
+
201: { description: "Theme created" },
|
|
2231
|
+
409: { description: "Theme already exists" },
|
|
2232
|
+
422: { description: "Validation error" }
|
|
2233
|
+
}
|
|
2234
|
+
}),
|
|
1873
2235
|
authMiddleware,
|
|
1874
2236
|
adminMiddleware,
|
|
1875
|
-
|
|
2237
|
+
honoOpenapi.validator("json", createThemeSchema),
|
|
1876
2238
|
async (c) => {
|
|
1877
2239
|
const body = c.req.valid("json");
|
|
1878
2240
|
const db = createDb(c.env.DB);
|
|
@@ -1890,6 +2252,15 @@ themesRoute.post(
|
|
|
1890
2252
|
);
|
|
1891
2253
|
themesRoute.patch(
|
|
1892
2254
|
"/:id",
|
|
2255
|
+
honoOpenapi.describeRoute({
|
|
2256
|
+
tags: ["Themes"],
|
|
2257
|
+
summary: "Update a theme (admin)",
|
|
2258
|
+
responses: {
|
|
2259
|
+
200: { description: "Theme updated" },
|
|
2260
|
+
404: { description: "Theme not found" },
|
|
2261
|
+
409: { description: "Theme already exists" }
|
|
2262
|
+
}
|
|
2263
|
+
}),
|
|
1893
2264
|
authMiddleware,
|
|
1894
2265
|
adminMiddleware,
|
|
1895
2266
|
async (c) => {
|
|
@@ -1912,13 +2283,26 @@ themesRoute.patch(
|
|
|
1912
2283
|
}
|
|
1913
2284
|
}
|
|
1914
2285
|
);
|
|
1915
|
-
themesRoute.delete(
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
2286
|
+
themesRoute.delete(
|
|
2287
|
+
"/:id",
|
|
2288
|
+
honoOpenapi.describeRoute({
|
|
2289
|
+
tags: ["Themes"],
|
|
2290
|
+
summary: "Delete a theme (admin)",
|
|
2291
|
+
responses: {
|
|
2292
|
+
204: { description: "Theme deleted" },
|
|
2293
|
+
404: { description: "Theme not found" }
|
|
2294
|
+
}
|
|
2295
|
+
}),
|
|
2296
|
+
authMiddleware,
|
|
2297
|
+
adminMiddleware,
|
|
2298
|
+
async (c) => {
|
|
2299
|
+
const { id } = c.req.param();
|
|
2300
|
+
const db = createDb(c.env.DB);
|
|
2301
|
+
const [deleted] = await db.delete(themes).where(drizzleOrm.eq(themes.id, id)).returning();
|
|
2302
|
+
if (!deleted) throw notFound("Theme");
|
|
2303
|
+
return c.body(null, 204);
|
|
2304
|
+
}
|
|
2305
|
+
);
|
|
1922
2306
|
var createOrganizationSchema = zod.z.object({
|
|
1923
2307
|
name: zod.z.string().min(1),
|
|
1924
2308
|
acronym: zod.z.string().min(1),
|
|
@@ -1926,16 +2310,33 @@ var createOrganizationSchema = zod.z.object({
|
|
|
1926
2310
|
link: zod.z.string().url().nullable().optional()
|
|
1927
2311
|
});
|
|
1928
2312
|
var organizationsRoute = new hono.Hono();
|
|
1929
|
-
organizationsRoute.get(
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2313
|
+
organizationsRoute.get(
|
|
2314
|
+
"/",
|
|
2315
|
+
honoOpenapi.describeRoute({
|
|
2316
|
+
tags: ["Organizations"],
|
|
2317
|
+
summary: "List all organizations",
|
|
2318
|
+
responses: { 200: { description: "List of organizations" } }
|
|
2319
|
+
}),
|
|
2320
|
+
async (c) => {
|
|
2321
|
+
const db = createDb(c.env.DB);
|
|
2322
|
+
const data = await db.select().from(organizations);
|
|
2323
|
+
return c.json({ data });
|
|
2324
|
+
}
|
|
2325
|
+
);
|
|
1934
2326
|
organizationsRoute.post(
|
|
1935
2327
|
"/",
|
|
2328
|
+
honoOpenapi.describeRoute({
|
|
2329
|
+
tags: ["Organizations"],
|
|
2330
|
+
summary: "Create a new organization (admin)",
|
|
2331
|
+
responses: {
|
|
2332
|
+
201: { description: "Organization created" },
|
|
2333
|
+
409: { description: "Organization already exists" },
|
|
2334
|
+
422: { description: "Validation error" }
|
|
2335
|
+
}
|
|
2336
|
+
}),
|
|
1936
2337
|
authMiddleware,
|
|
1937
2338
|
adminMiddleware,
|
|
1938
|
-
|
|
2339
|
+
honoOpenapi.validator("json", createOrganizationSchema),
|
|
1939
2340
|
async (c) => {
|
|
1940
2341
|
const body = c.req.valid("json");
|
|
1941
2342
|
const db = createDb(c.env.DB);
|
|
@@ -1952,6 +2353,15 @@ organizationsRoute.post(
|
|
|
1952
2353
|
);
|
|
1953
2354
|
organizationsRoute.patch(
|
|
1954
2355
|
"/:id",
|
|
2356
|
+
honoOpenapi.describeRoute({
|
|
2357
|
+
tags: ["Organizations"],
|
|
2358
|
+
summary: "Update an organization (admin)",
|
|
2359
|
+
responses: {
|
|
2360
|
+
200: { description: "Organization updated" },
|
|
2361
|
+
404: { description: "Organization not found" },
|
|
2362
|
+
409: { description: "Organization already exists" }
|
|
2363
|
+
}
|
|
2364
|
+
}),
|
|
1955
2365
|
authMiddleware,
|
|
1956
2366
|
adminMiddleware,
|
|
1957
2367
|
async (c) => {
|
|
@@ -1970,13 +2380,26 @@ organizationsRoute.patch(
|
|
|
1970
2380
|
}
|
|
1971
2381
|
}
|
|
1972
2382
|
);
|
|
1973
|
-
organizationsRoute.delete(
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
}
|
|
2383
|
+
organizationsRoute.delete(
|
|
2384
|
+
"/:id",
|
|
2385
|
+
honoOpenapi.describeRoute({
|
|
2386
|
+
tags: ["Organizations"],
|
|
2387
|
+
summary: "Delete an organization (admin)",
|
|
2388
|
+
responses: {
|
|
2389
|
+
204: { description: "Organization deleted" },
|
|
2390
|
+
404: { description: "Organization not found" }
|
|
2391
|
+
}
|
|
2392
|
+
}),
|
|
2393
|
+
authMiddleware,
|
|
2394
|
+
adminMiddleware,
|
|
2395
|
+
async (c) => {
|
|
2396
|
+
const { id } = c.req.param();
|
|
2397
|
+
const db = createDb(c.env.DB);
|
|
2398
|
+
const [deleted] = await db.delete(organizations).where(drizzleOrm.eq(organizations.id, id)).returning();
|
|
2399
|
+
if (!deleted) throw notFound("Organization");
|
|
2400
|
+
return c.body(null, 204);
|
|
2401
|
+
}
|
|
2402
|
+
);
|
|
1980
2403
|
|
|
1981
2404
|
// src/app.ts
|
|
1982
2405
|
function createApp(options = {}) {
|
|
@@ -1989,7 +2412,7 @@ function createApp(options = {}) {
|
|
|
1989
2412
|
});
|
|
1990
2413
|
}
|
|
1991
2414
|
app.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
|
|
1992
|
-
app.use("*",
|
|
2415
|
+
app.use("*", chunkNYEPGZMP_cjs.createTurnstileMiddleware());
|
|
1993
2416
|
app.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
|
|
1994
2417
|
app.on(["POST", "GET"], "/api/auth/*", (c) => {
|
|
1995
2418
|
const auth = createAuth(c.env);
|
|
@@ -2020,7 +2443,7 @@ function createApp(options = {}) {
|
|
|
2020
2443
|
}
|
|
2021
2444
|
return next();
|
|
2022
2445
|
});
|
|
2023
|
-
app.post(
|
|
2446
|
+
app.post(chunkNYEPGZMP_cjs.TURNSTILE_VERIFY_PATH, chunkNYEPGZMP_cjs.handleTurnstileVerify);
|
|
2024
2447
|
app.route("/health", healthRoute);
|
|
2025
2448
|
app.route("/api/config", siteConfigRoute);
|
|
2026
2449
|
app.route("/api/classes", classesRoute);
|
|
@@ -2034,6 +2457,27 @@ function createApp(options = {}) {
|
|
|
2034
2457
|
app.route("/internal/batch-release", batchReleaseRoute);
|
|
2035
2458
|
app.route("/internal/reminder-emails", reminderEmailsRoute);
|
|
2036
2459
|
app.route("/internal/renew-watches", renewWatchesRoute);
|
|
2460
|
+
app.get(
|
|
2461
|
+
"/api/openapi.json",
|
|
2462
|
+
authMiddleware,
|
|
2463
|
+
adminMiddleware,
|
|
2464
|
+
honoOpenapi.openAPIRouteHandler(app, {
|
|
2465
|
+
documentation: {
|
|
2466
|
+
info: {
|
|
2467
|
+
title: "Leapify API",
|
|
2468
|
+
version: "0.260605.1" ,
|
|
2469
|
+
description: "DLSU CSO LEAP backend API"
|
|
2470
|
+
},
|
|
2471
|
+
openapi: "3.1.0"
|
|
2472
|
+
}
|
|
2473
|
+
})
|
|
2474
|
+
);
|
|
2475
|
+
app.get(
|
|
2476
|
+
"/api/docs",
|
|
2477
|
+
authMiddleware,
|
|
2478
|
+
adminMiddleware,
|
|
2479
|
+
swaggerUi.swaggerUI({ url: "/api/openapi.json" })
|
|
2480
|
+
);
|
|
2037
2481
|
app.onError(errorHandler);
|
|
2038
2482
|
app.notFound(
|
|
2039
2483
|
(c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
|
|
@@ -2654,6 +3098,8 @@ var API_PREFIXES = [
|
|
|
2654
3098
|
"/api/themes",
|
|
2655
3099
|
"/api/config",
|
|
2656
3100
|
"/api/uploads",
|
|
3101
|
+
"/api/docs",
|
|
3102
|
+
"/api/openapi.json",
|
|
2657
3103
|
"/health",
|
|
2658
3104
|
"/internal/",
|
|
2659
3105
|
"/.well-known/"
|