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