Cleanup
This commit is contained in:
+77
-75
@@ -5,19 +5,20 @@ import {
|
|||||||
PlayerSchema,
|
PlayerSchema,
|
||||||
} from "@blade-and-brawn/domain"
|
} from "@blade-and-brawn/domain"
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import { Elysia, NotFoundError, redirect, status, t } from "elysia";
|
import { Elysia, redirect, status, t } from "elysia";
|
||||||
import {
|
import {
|
||||||
PrintfulError,
|
PrintfulError,
|
||||||
WebflowError,
|
WebflowError,
|
||||||
Printful,
|
Printful,
|
||||||
Webflow,
|
Webflow,
|
||||||
} from "@blade-and-brawn/commerce";
|
} from "@blade-and-brawn/commerce";
|
||||||
import { DEFAULT_NAME, DUMMY_PASSWORD_HASH, env, log } from "./util";
|
import { BigIntIdSchema, DEFAULT_NAME, DUMMY_PASSWORD_HASH, env, log } from "./util";
|
||||||
import serverTiming from "@elysia/server-timing";
|
import serverTiming from "@elysia/server-timing";
|
||||||
import jwt from "@elysia/jwt";
|
import jwt from "@elysia/jwt";
|
||||||
import { CommerceService, WOrderStatusSchema } from "./services/commerce";
|
import { CommerceService, WOrderStatusSchema } from "./services/commerce";
|
||||||
import cluster from "node:cluster";
|
import cluster from "node:cluster";
|
||||||
import { randomUUIDv7, sleep } from "bun";
|
import { randomUUIDv7, sleep } from "bun";
|
||||||
|
import { DatabaseError } from "pg";
|
||||||
import { CalculatorService, CalculatorUnavailableError } from "./services/calculator";
|
import { CalculatorService, CalculatorUnavailableError } from "./services/calculator";
|
||||||
import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
|
import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
|
||||||
import { StandardsService } from "./services/standards";
|
import { StandardsService } from "./services/standards";
|
||||||
@@ -62,14 +63,14 @@ const authPlugin = new Elysia({ name: "auth" })
|
|||||||
authAdmin: {
|
authAdmin: {
|
||||||
async resolve({ jwt, cookie: { auth } }) {
|
async resolve({ jwt, cookie: { auth } }) {
|
||||||
const token = auth.value && await jwt.verify(auth.value);
|
const token = auth.value && await jwt.verify(auth.value);
|
||||||
if (!token || token.role !== "admin" || !token.accountId || !token.sessionId) throw status(401, "Unauthorized");
|
if (!token || token.role !== "admin" || !token.accountId || !token.sessionId) return status(401, { error: "Unauthorized" });
|
||||||
return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() };
|
return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
async resolve({ jwt, cookie: { auth } }) {
|
async resolve({ jwt, cookie: { auth } }) {
|
||||||
const token = auth.value && await jwt.verify(auth.value);
|
const token = auth.value && await jwt.verify(auth.value);
|
||||||
if (!token || !token.role || !token.accountId || !token.sessionId) throw status(401, "Unauthorized");
|
if (!token || !token.role || !token.accountId || !token.sessionId) return status(401, { error: "Unauthorized" });
|
||||||
return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() };
|
return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,11 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
|
|
||||||
.onError(({ code, error }) => {
|
.onError(({ code, error }) => {
|
||||||
|
// Database Errors
|
||||||
|
if (error instanceof DatabaseError && error.code === "23505")
|
||||||
|
return status(409, { error: "Conflicts with an existing record" });
|
||||||
|
|
||||||
|
// Custom Errors
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case "PrintfulError":
|
case "PrintfulError":
|
||||||
case "WebflowError":
|
case "WebflowError":
|
||||||
@@ -126,16 +132,18 @@ export const app = new Elysia()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.onAfterResponse(({ request, status, path }) => {
|
.onAfterResponse(({ request, set, path, responseValue }) => {
|
||||||
if (env.NODE_ENV === "development") {
|
if (env.NODE_ENV === "development") {
|
||||||
const skip: Record<string, string[]> = { "/commerce/products/sync/": ["GET"] };
|
const skip: Record<string, string[]> = { "/commerce/products/sync/": ["GET"] };
|
||||||
if (skip[path]?.includes(request.method)) return;
|
if (skip[path]?.includes(request.method)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info({
|
const failed = Number(set.status) >= 400;
|
||||||
|
log[failed ? "warn" : "info"]({
|
||||||
method: request.method,
|
method: request.method,
|
||||||
path,
|
path,
|
||||||
status
|
status: set.status,
|
||||||
|
...(failed && { response: responseValue }),
|
||||||
}, "request");
|
}, "request");
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -146,7 +154,7 @@ export const app = new Elysia()
|
|||||||
.post("/auth/login", async ({ jwt, body: { password, email }, cookie: { auth } }) => {
|
.post("/auth/login", async ({ jwt, body: { password, email }, cookie: { auth } }) => {
|
||||||
const account = await s.Accounts.getByEmail(email);
|
const account = await s.Accounts.getByEmail(email);
|
||||||
const password_match = await Bun.password.verify(password, account?.password_hash ?? DUMMY_PASSWORD_HASH);
|
const password_match = await Bun.password.verify(password, account?.password_hash ?? DUMMY_PASSWORD_HASH);
|
||||||
if (!account || !password_match) throw status(401, "Invalid credentials");
|
if (!account || !password_match) return status(401, { error: "Invalid credentials" });
|
||||||
|
|
||||||
auth.set({
|
auth.set({
|
||||||
value: await jwt.sign({ role: account.role, sessionId: randomUUIDv7(), accountId: account.id, exp: JWT_EXP }),
|
value: await jwt.sign({ role: account.role, sessionId: randomUUIDv7(), accountId: account.id, exp: JWT_EXP }),
|
||||||
@@ -184,10 +192,10 @@ export const app = new Elysia()
|
|||||||
return redirect(url.toString(), 302);
|
return redirect(url.toString(), 302);
|
||||||
})
|
})
|
||||||
.get("/callback", async ({ query, cookie: { authDiscord, auth }, jwt }) => {
|
.get("/callback", async ({ query, cookie: { authDiscord, auth }, jwt }) => {
|
||||||
if (query.error) throw status(400, { error: query.error_description ? `${query.error}: ${query.error_description}` : query.error });
|
if (query.error) return status(400, { error: query.error_description ? `${query.error}: ${query.error_description}` : query.error });
|
||||||
|
|
||||||
if (!query.state) throw status(400, "No Discord OAuth2 state query parameter provided");
|
if (!query.state) return status(400, { error: "No Discord OAuth2 state query parameter provided" });
|
||||||
if (query.state !== authDiscord.value) throw status(400, "Invalid Discord OAuth state");
|
if (query.state !== authDiscord.value) return status(400, { error: "Invalid Discord OAuth state" });
|
||||||
|
|
||||||
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
|
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -204,7 +212,7 @@ export const app = new Elysia()
|
|||||||
});
|
});
|
||||||
if (!tokenRes.ok) {
|
if (!tokenRes.ok) {
|
||||||
const errorBody = await tokenRes.json().catch(() => null);
|
const errorBody = await tokenRes.json().catch(() => null);
|
||||||
throw status(502, { error: errorBody ?? "Discord token exchange failed" });
|
return status(502, { error: errorBody ?? "Discord token exchange failed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenResPayload = await tokenRes.json();
|
const tokenResPayload = await tokenRes.json();
|
||||||
@@ -215,7 +223,7 @@ export const app = new Elysia()
|
|||||||
});
|
});
|
||||||
if (!identityRes.ok) {
|
if (!identityRes.ok) {
|
||||||
const errorBody = await identityRes.json().catch(() => null);
|
const errorBody = await identityRes.json().catch(() => null);
|
||||||
throw status(502, { error: errorBody ?? "Failed to fetch Discord identity" });
|
return status(502, { error: errorBody ?? "Failed to fetch Discord identity" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const identityResPayload = await identityRes.json();
|
const identityResPayload = await identityRes.json();
|
||||||
@@ -274,7 +282,7 @@ export const app = new Elysia()
|
|||||||
.post("/standards/config/switch", async ({ body: { standardsConfigId } }) => {
|
.post("/standards/config/switch", async ({ body: { standardsConfigId } }) => {
|
||||||
await s.Calculator.Standards.Config.switch(standardsConfigId);
|
await s.Calculator.Standards.Config.switch(standardsConfigId);
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({ standardsConfigId: t.String() })
|
body: t.Object({ standardsConfigId: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -282,7 +290,7 @@ export const app = new Elysia()
|
|||||||
.post("/configs", async ({ body: { name, datasetId, params } }) => {
|
.post("/configs", async ({ body: { name, datasetId, params } }) => {
|
||||||
return await s.Standards.Configs.create(name, datasetId, params);
|
return await s.Standards.Configs.create(name, datasetId, params);
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({ name: t.String(), datasetId: t.String(), params: StandardsParamsSchema })
|
body: t.Object({ name: t.String(), datasetId: BigIntIdSchema, params: StandardsParamsSchema })
|
||||||
})
|
})
|
||||||
.get("/configs", async () => {
|
.get("/configs", async () => {
|
||||||
return await s.Standards.Configs.list();
|
return await s.Standards.Configs.list();
|
||||||
@@ -290,29 +298,29 @@ export const app = new Elysia()
|
|||||||
.get("/configs/:id", async ({ params: { id } }) => {
|
.get("/configs/:id", async ({ params: { id } }) => {
|
||||||
return await s.Standards.Configs.get(id);
|
return await s.Standards.Configs.get(id);
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.put("/configs/:id", async ({ params: { id }, body: { name, datasetId, params: parameters } }) => {
|
.put("/configs/:id", async ({ params: { id }, body: { name, datasetId, params: parameters } }) => {
|
||||||
await s.Standards.Configs.update(id, name, datasetId, parameters);
|
await s.Standards.Configs.update(id, name, datasetId, parameters);
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: BigIntIdSchema }),
|
||||||
body: t.Object({ name: t.String(), datasetId: t.String(), params: StandardsParamsSchema })
|
body: t.Object({ name: t.String(), datasetId: BigIntIdSchema, params: StandardsParamsSchema })
|
||||||
})
|
})
|
||||||
.delete("/configs/:id", async ({ params: { id } }) => {
|
.delete("/configs/:id", async ({ params: { id } }) => {
|
||||||
await s.Standards.Configs.delete(id);
|
await s.Standards.Configs.delete(id);
|
||||||
}, { params: t.Object({ id: t.String() }) })
|
}, { params: t.Object({ id: BigIntIdSchema }) })
|
||||||
.get("/datasets", async () => {
|
.get("/datasets", async () => {
|
||||||
return await s.Standards.Datasets.list();
|
return await s.Standards.Datasets.list();
|
||||||
})
|
})
|
||||||
.get("/datasets/:id", async ({ params: { id } }) => {
|
.get("/datasets/:id", async ({ params: { id } }) => {
|
||||||
return await s.Standards.Datasets.get(id);
|
return await s.Standards.Datasets.get(id);
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.patch("/datasets/:id", async ({ params: { id }, body: { name } }) => {
|
.patch("/datasets/:id", async ({ params: { id }, body: { name } }) => {
|
||||||
await s.Standards.Datasets.update(id, name);
|
await s.Standards.Datasets.update(id, name);
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: BigIntIdSchema }),
|
||||||
body: t.Object({ name: t.String() })
|
body: t.Object({ name: t.String() })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -343,7 +351,7 @@ export const app = new Elysia()
|
|||||||
// Sync status
|
// Sync status
|
||||||
.get("/", async ({ sessionId }) => {
|
.get("/", async ({ sessionId }) => {
|
||||||
const latestSyncState = await s.Commerce.Apparel.Syncs.getLatestSyncState(sessionId);
|
const latestSyncState = await s.Commerce.Apparel.Syncs.getLatestSyncState(sessionId);
|
||||||
if (!latestSyncState) throw new NotFoundError("No product sync found for the provided session");
|
if (!latestSyncState) return status(404, { error: "No product sync found for the provided session" });
|
||||||
return latestSyncState;
|
return latestSyncState;
|
||||||
})
|
})
|
||||||
// Run sync
|
// Run sync
|
||||||
@@ -362,10 +370,10 @@ export const app = new Elysia()
|
|||||||
)
|
)
|
||||||
.get("/:pProductId", async ({ params: { pProductId } }) => {
|
.get("/:pProductId", async ({ params: { pProductId } }) => {
|
||||||
const pProduct = await s.Commerce.Printful.Products.get(pProductId);
|
const pProduct = await s.Commerce.Printful.Products.get(pProductId);
|
||||||
if (!pProduct) throw new NotFoundError("Missing printful product");
|
if (!pProduct) return status(404, { error: "Missing printful product" });
|
||||||
|
|
||||||
const wProductId = pProduct.sync_product.external_id.split("-")[0];
|
const wProductId = pProduct.sync_product.external_id.split("-")[0];
|
||||||
if (!wProductId) throw new NotFoundError("Missing webflow product ID");
|
if (!wProductId) return status(404, { error: "Missing webflow product ID" });
|
||||||
|
|
||||||
const wProduct = await s.Commerce.Webflow.Products.get(wProductId);
|
const wProduct = await s.Commerce.Webflow.Products.get(wProductId);
|
||||||
|
|
||||||
@@ -398,7 +406,7 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
.get("/:wOrderId", async ({ params: { wOrderId } }) => {
|
.get("/:wOrderId", async ({ params: { wOrderId } }) => {
|
||||||
const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId);
|
const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId);
|
||||||
if (!wOrder) throw new NotFoundError("Missing webflow order");
|
if (!wOrder) return status(404, { error: "Missing webflow order" });
|
||||||
|
|
||||||
const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`);
|
const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`);
|
||||||
|
|
||||||
@@ -406,10 +414,10 @@ export const app = new Elysia()
|
|||||||
}, { params: t.Object({ wOrderId: t.String() }) })
|
}, { params: t.Object({ wOrderId: t.String() }) })
|
||||||
.post("/sync/:wOrderId", async ({ params: { wOrderId } }) => {
|
.post("/sync/:wOrderId", async ({ params: { wOrderId } }) => {
|
||||||
const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId);
|
const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId);
|
||||||
if (!wOrder) throw new NotFoundError("Missing webflow order");
|
if (!wOrder) return status(404, { error: "Missing webflow order" });
|
||||||
|
|
||||||
const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`);
|
const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`);
|
||||||
if (pOrder) throw status(409, { error: "Cannot sync an already synced webflow order" });
|
if (pOrder) return status(409, { error: "Cannot sync an already synced webflow order" });
|
||||||
|
|
||||||
await s.Commerce.Apparel.Orders.Queue.enqueue({
|
await s.Commerce.Apparel.Orders.Queue.enqueue({
|
||||||
type: "apparel_order_create",
|
type: "apparel_order_create",
|
||||||
@@ -441,16 +449,16 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
.get("/:id", async ({ params: { id } }) => {
|
.get("/:id", async ({ params: { id } }) => {
|
||||||
const event = await s.Events.get(id);
|
const event = await s.Events.get(id);
|
||||||
if (!event) throw new NotFoundError("Event not found");
|
if (!event) return status(404, { error: "Event not found" });
|
||||||
return { ...event, status: EventsService.status(event) };
|
return { ...event, status: EventsService.status(event) };
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.post("/:id/retry", async ({ params: { id } }) => {
|
.post("/:id/retry", async ({ params: { id } }) => {
|
||||||
const retried = await s.Events.retry(id);
|
const retried = await s.Events.retry(id);
|
||||||
if (!retried) throw new NotFoundError("Event not found or not in a failed state");
|
if (!retried) return status(404, { error: "Event not found or not in a failed state" });
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.get("/groups", async ({ }) => queues.map((q) => q.group))
|
.get("/groups", async ({ }) => queues.map((q) => q.group))
|
||||||
)
|
)
|
||||||
@@ -460,7 +468,7 @@ export const app = new Elysia()
|
|||||||
// TODO: for added security, could enforce bot or admin only access
|
// TODO: for added security, could enforce bot or admin only access
|
||||||
.get("/:id/stats", async ({ params: { id } }) => {
|
.get("/:id/stats", async ({ params: { id } }) => {
|
||||||
const stats = await s.Accounts.stats(id);
|
const stats = await s.Accounts.stats(id);
|
||||||
if (!stats) throw new NotFoundError("Account stats not found");
|
if (!stats) return status(404, { error: "Account stats not found" });
|
||||||
return stats;
|
return stats;
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: t.String() })
|
||||||
@@ -468,7 +476,7 @@ export const app = new Elysia()
|
|||||||
.guard({ authAdmin: true }, (app) => app
|
.guard({ authAdmin: true }, (app) => app
|
||||||
.get("/:id", async ({ params: { id } }) => {
|
.get("/:id", async ({ params: { id } }) => {
|
||||||
const account = await s.Accounts.get(id);
|
const account = await s.Accounts.get(id);
|
||||||
if (!account) throw new NotFoundError("Account not found");
|
if (!account) return status(404, { error: "Account not found" });
|
||||||
return account;
|
return account;
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: t.String() })
|
||||||
@@ -477,7 +485,7 @@ export const app = new Elysia()
|
|||||||
.group("/me", { auth: true }, (app) => app
|
.group("/me", { auth: true }, (app) => app
|
||||||
.get("/stats", async ({ accountId }) => {
|
.get("/stats", async ({ accountId }) => {
|
||||||
const stats = await s.Accounts.stats(accountId);
|
const stats = await s.Accounts.stats(accountId);
|
||||||
if (!stats) throw new NotFoundError("Account stats not found");
|
if (!stats) return status(404, { error: "Account stats not found" });
|
||||||
return stats;
|
return stats;
|
||||||
})
|
})
|
||||||
.post("/assessments", async ({ body: { player, activityPerformances }, accountId }) => {
|
.post("/assessments", async ({ body: { player, activityPerformances }, accountId }) => {
|
||||||
@@ -494,55 +502,48 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
.delete("/assessments/:id", async ({ params: { id }, accountId }) => {
|
.delete("/assessments/:id", async ({ params: { id }, accountId }) => {
|
||||||
const deleted = await s.Assessments.delete(id, accountId);
|
const deleted = await s.Assessments.delete(id, accountId);
|
||||||
if (!deleted) throw new NotFoundError("Assessment not found");
|
if (!deleted) return status(404, { error: "Assessment not found" });
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.post("/verifications", async ({ body: { assessmentId, activityMediaKeys }, accountId }) => {
|
.post("/verifications", async ({ body: { assessmentId, activityMediaKeys }, accountId }) => {
|
||||||
const assessment = await s.Assessments.get(assessmentId);
|
const assessment = await s.Assessments.get(assessmentId, accountId);
|
||||||
if (!assessment || assessment.account_id !== accountId) throw new NotFoundError("Assessment not found");
|
if (!assessment) return status(404, { error: "Assessment not found" });
|
||||||
|
|
||||||
// TODO: only allow a single active non-completed verification at a time
|
// TODO: only allow a single active non-completed verification at a time
|
||||||
// TODO: return R2 POST urls for the media
|
// TODO: return R2 POST urls for the media
|
||||||
|
|
||||||
const created = await s.Verifications.create(assessmentId, activityMediaKeys);
|
const created = await s.Verifications.create(assessmentId, activityMediaKeys);
|
||||||
if (!created) throw status(409, { error: "This assessment already has a verification" });
|
if (!created) return status(409, { error: "This assessment already has a verification" });
|
||||||
return created;
|
return created;
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
assessmentId: t.String(),
|
assessmentId: BigIntIdSchema,
|
||||||
activityMediaKeys: ActivityMediaKeysSchema,
|
activityMediaKeys: ActivityMediaKeysSchema,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.patch("/verifications/:id", async ({ params: { id }, body: { assessmentId, activityMediaKeys }, accountId }) => {
|
.patch("/verifications/:id", async ({ params: { id }, body: { assessmentId, activityMediaKeys }, accountId }) => {
|
||||||
const verification = await s.Verifications.get(id);
|
const verification = await s.Verifications.get(id, accountId);
|
||||||
if (!verification || verification.account_id !== accountId) throw new NotFoundError("Verification not found");
|
if (!verification) return status(404, { error: "Verification not found" });
|
||||||
|
|
||||||
if (assessmentId === undefined && Object.keys(activityMediaKeys ?? {}).length === 0)
|
if (assessmentId === undefined && Object.keys(activityMediaKeys ?? {}).length === 0)
|
||||||
throw status(400, { error: "Nothing to update" });
|
return status(400, { error: "Nothing to update" });
|
||||||
|
|
||||||
if (assessmentId !== undefined) {
|
if (assessmentId !== undefined && !await s.Assessments.get(assessmentId, accountId))
|
||||||
const assessment = await s.Assessments.get(assessmentId);
|
return status(404, { error: "Assessment not found" });
|
||||||
if (!assessment || assessment.account_id !== accountId) throw new NotFoundError("Assessment not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await s.Verifications.update(id, { assessmentId, activityMediaKeys })
|
const updated = await s.Verifications.update(id, { assessmentId, activityMediaKeys });
|
||||||
.catch((err) => {
|
if (!updated) return status(409, { error: `Cannot update a ${verification.status} verification` });
|
||||||
if (err?.code === "23505") throw status(409, { error: "That assessment already has a verification" });
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
if (!updated) throw status(409, { error: `Cannot update a ${verification.status} verification` });
|
|
||||||
return updated;
|
return updated;
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: BigIntIdSchema }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
assessmentId: t.Optional(t.String()),
|
assessmentId: t.Optional(BigIntIdSchema),
|
||||||
activityMediaKeys: t.Optional(ActivityMediaKeysSchema),
|
activityMediaKeys: t.Optional(ActivityMediaKeysSchema),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.post("/verifications/:id/submit", async ({ params: { id }, accountId }) => {
|
.post("/verifications/:id/submit", async ({ params: { id }, accountId }) => {
|
||||||
const verification = await s.Verifications.get(id);
|
if (!await s.Verifications.get(id, accountId)) return status(404, { error: "Verification not found" });
|
||||||
if (!verification || verification.account_id !== accountId) throw new NotFoundError("Verification not found");
|
|
||||||
|
|
||||||
const submitted = await s.Verifications.submit(id);
|
const submitted = await s.Verifications.submit(id);
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
@@ -550,18 +551,18 @@ export const app = new Elysia()
|
|||||||
return submitted;
|
return submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = await s.Verifications.get(id);
|
const current = await s.Verifications.get(id, accountId);
|
||||||
if (!current) throw new NotFoundError("Verification not found");
|
if (!current) return status(404, { error: "Verification not found" });
|
||||||
if (current.status === "submitted") return { id: current.id, status: current.status };
|
if (current.status === "submitted") return { id: current.id, status: current.status };
|
||||||
|
|
||||||
const missingActivityMediaKeys = VerificationsService.missingActivityMediaKeys(current);
|
const missingActivityMediaKeys = VerificationsService.missingActivityMediaKeys(current);
|
||||||
throw status(409, {
|
return status(409, {
|
||||||
error: current.status === "draft" && missingActivityMediaKeys.length > 0
|
error: current.status === "draft" && missingActivityMediaKeys.length > 0
|
||||||
? `Missing media for: ${missingActivityMediaKeys.join(", ")}`
|
? `Missing media for: ${missingActivityMediaKeys.join(", ")}`
|
||||||
: `Cannot submit a ${current.status} verification`
|
: `Cannot submit a ${current.status} verification`
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -569,7 +570,7 @@ export const app = new Elysia()
|
|||||||
// ASSESSMENTS
|
// ASSESSMENTS
|
||||||
.group("/assessments", { authAdmin: true }, (app) => app
|
.group("/assessments", { authAdmin: true }, (app) => app
|
||||||
.post("/", async ({ body: { player, activityPerformances, id } }) => {
|
.post("/", async ({ body: { player, activityPerformances, id } }) => {
|
||||||
await s.Assessments.create(player, activityPerformances, id);
|
return await s.Assessments.create(player, activityPerformances, id);
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
player: PlayerSchema,
|
player: PlayerSchema,
|
||||||
@@ -579,9 +580,10 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
.put("/:id", async ({ body: { player, activityPerformances }, params: { id } }) => {
|
.put("/:id", async ({ body: { player, activityPerformances }, params: { id } }) => {
|
||||||
const updated = await s.Assessments.update(id, player, activityPerformances);
|
const updated = await s.Assessments.update(id, player, activityPerformances);
|
||||||
if (!updated) throw new NotFoundError("Assessment not found");
|
if (!updated) return status(404, { error: "Assessment not found" });
|
||||||
|
return updated;
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: BigIntIdSchema }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
player: PlayerSchema,
|
player: PlayerSchema,
|
||||||
activityPerformances: t.Array(ActivityPerformanceSchema),
|
activityPerformances: t.Array(ActivityPerformanceSchema),
|
||||||
@@ -606,20 +608,20 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
.get("/:id", async ({ params: { id } }) => {
|
.get("/:id", async ({ params: { id } }) => {
|
||||||
const verification = await s.Verifications.get(id);
|
const verification = await s.Verifications.get(id);
|
||||||
if (!verification) throw new NotFoundError("Verification not found");
|
if (!verification) return status(404, { error: "Verification not found" });
|
||||||
return verification;
|
return verification;
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.post("/:id/request-action", async ({ params: { id }, body: { reviewerNotes, activityVerifications } }) => {
|
.post("/:id/request-action", async ({ params: { id }, body: { reviewerNotes, activityVerifications } }) => {
|
||||||
const updated = await s.Verifications.requestAction(id, reviewerNotes ?? null, activityVerifications);
|
const updated = await s.Verifications.requestAction(id, reviewerNotes ?? null, activityVerifications);
|
||||||
if (updated) return updated;
|
if (updated) return updated;
|
||||||
|
|
||||||
const verification = await s.Verifications.get(id);
|
const verification = await s.Verifications.get(id);
|
||||||
if (!verification) throw new NotFoundError("Verification not found");
|
if (!verification) return status(404, { error: "Verification not found" });
|
||||||
throw status(409, { error: `Cannot request action on a ${verification.status} verification` });
|
return status(409, { error: `Cannot request action on a ${verification.status} verification` });
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: BigIntIdSchema }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
reviewerNotes: t.Optional(t.String()),
|
reviewerNotes: t.Optional(t.String()),
|
||||||
activityVerifications: ActivityVerificationsSchema,
|
activityVerifications: ActivityVerificationsSchema,
|
||||||
@@ -630,10 +632,10 @@ export const app = new Elysia()
|
|||||||
if (updated) return updated;
|
if (updated) return updated;
|
||||||
|
|
||||||
const verification = await s.Verifications.get(id);
|
const verification = await s.Verifications.get(id);
|
||||||
if (!verification) throw new NotFoundError("Verification not found");
|
if (!verification) return status(404, { error: "Verification not found" });
|
||||||
throw status(409, { error: `Cannot complete a ${verification.status} verification` });
|
return status(409, { error: `Cannot complete a ${verification.status} verification` });
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: BigIntIdSchema }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
activityVerifications: ActivityVerificationsSchema,
|
activityVerifications: ActivityVerificationsSchema,
|
||||||
})
|
})
|
||||||
@@ -644,7 +646,7 @@ export const app = new Elysia()
|
|||||||
.post("/webhooks/printful", async ({ body, query }) => {
|
.post("/webhooks/printful", async ({ body, query }) => {
|
||||||
// https://webflow.com/integrations/printful
|
// https://webflow.com/integrations/printful
|
||||||
if (!s.Commerce.Printful.Util.verifySecret(query.secret))
|
if (!s.Commerce.Printful.Util.verifySecret(query.secret))
|
||||||
throw status(400, "Invalid secret");
|
return status(400, { error: "Invalid secret" });
|
||||||
|
|
||||||
const payload = body as Printful.Webhook.EventPayload;
|
const payload = body as Printful.Webhook.EventPayload;
|
||||||
|
|
||||||
@@ -667,7 +669,7 @@ export const app = new Elysia()
|
|||||||
const pProduct = payload.data.sync_product;
|
const pProduct = payload.data.sync_product;
|
||||||
const wProductId = pProduct.external_id.split("-")[0];
|
const wProductId = pProduct.external_id.split("-")[0];
|
||||||
log.info({ externalId: payload.data.sync_product.external_id, wProductId }, "printful webhook: product deleted");
|
log.info({ externalId: payload.data.sync_product.external_id, wProductId }, "printful webhook: product deleted");
|
||||||
if (!wProductId) throw new NotFoundError("Missing webflow product ID");
|
if (!wProductId) return status(404, { error: "Missing webflow product ID" });
|
||||||
|
|
||||||
await s.Commerce.Apparel.Syncs.Queue.enqueue({
|
await s.Commerce.Apparel.Syncs.Queue.enqueue({
|
||||||
type: "apparel_sync_delete",
|
type: "apparel_sync_delete",
|
||||||
@@ -699,7 +701,7 @@ export const app = new Elysia()
|
|||||||
}, { query: t.Object({ secret: t.String() }) })
|
}, { query: t.Object({ secret: t.String() }) })
|
||||||
.post("/webhooks/webflow", async ({ request, body }) => {
|
.post("/webhooks/webflow", async ({ request, body }) => {
|
||||||
if (!s.Commerce.Webflow.Util.verifySecret(request, body))
|
if (!s.Commerce.Webflow.Util.verifySecret(request, body))
|
||||||
throw status(400, "Invalid signature");
|
return status(400, { error: "Invalid signature" });
|
||||||
|
|
||||||
const payload = body as Webflow.Webhook.EventPayload;
|
const payload = body as Webflow.Webhook.EventPayload;
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ export class AssessmentsService {
|
|||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(id: string, accountId?: string) {
|
||||||
return await db.selectFrom("assessments")
|
return await db.selectFrom("assessments")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where("id", "=", id)
|
.where("id", "=", id)
|
||||||
|
.$if(accountId !== undefined, (qb) => qb.where("account_id", "=", accountId!))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ export class AssessmentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, player: Player, activityPerformances: ActivityPerformance[]) {
|
async update(id: string, player: Player, activityPerformances: ActivityPerformance[]) {
|
||||||
const result = await db.updateTable("assessments")
|
return await db.updateTable("assessments")
|
||||||
.where("id", "=", id)
|
.where("id", "=", id)
|
||||||
.set({
|
.set({
|
||||||
name: player.name ?? "Anonymous",
|
name: player.name ?? "Anonymous",
|
||||||
@@ -62,8 +63,8 @@ export class AssessmentsService {
|
|||||||
perf_run: AssessmentsService.performanceFor(activityPerformances, Activity.Run),
|
perf_run: AssessmentsService.performanceFor(activityPerformances, Activity.Run),
|
||||||
perf_cone_drill: AssessmentsService.performanceFor(activityPerformances, Activity.ConeDrill),
|
perf_cone_drill: AssessmentsService.performanceFor(activityPerformances, Activity.ConeDrill),
|
||||||
})
|
})
|
||||||
|
.returning("id")
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
return result.numUpdatedRows > 0n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, accountId: string): Promise<boolean> {
|
async delete(id: string, accountId: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const VerificationStatusSchema = t.Union([
|
|||||||
]);
|
]);
|
||||||
export type VerificationStatus = Static<typeof VerificationStatusSchema>;
|
export type VerificationStatus = Static<typeof VerificationStatusSchema>;
|
||||||
|
|
||||||
export const REQUIRED_MEDIA_ACTIVITIES = Object.values(Activity);
|
const REQUIRED_MEDIA_ACTIVITIES = Object.values(Activity);
|
||||||
|
|
||||||
const MEDIA_KEY_COLUMN = {
|
const MEDIA_KEY_COLUMN = {
|
||||||
[Activity.BackSquat]: "media_key_back_squat",
|
[Activity.BackSquat]: "media_key_back_squat",
|
||||||
@@ -80,9 +80,10 @@ export class VerificationsService {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(id: string, accountId?: string) {
|
||||||
return await VerificationsService.baseQuery()
|
return await VerificationsService.baseQuery()
|
||||||
.where("verifications.id", "=", id)
|
.where("verifications.id", "=", id)
|
||||||
|
.$if(accountId !== undefined, (qb) => qb.where("assessments.account_id", "=", accountId!))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +174,6 @@ export class VerificationsService {
|
|||||||
"verifications.verf_broad_jump",
|
"verifications.verf_broad_jump",
|
||||||
"verifications.verf_run",
|
"verifications.verf_run",
|
||||||
"verifications.verf_cone_drill",
|
"verifications.verf_cone_drill",
|
||||||
"assessments.account_id",
|
|
||||||
"assessments.name",
|
"assessments.name",
|
||||||
"assessments.gender",
|
"assessments.gender",
|
||||||
"assessments.age",
|
"assessments.age",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import cluster from 'node:cluster';
|
|||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { pino } from 'pino';
|
import { pino } from 'pino';
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
|
import { t } from 'elysia';
|
||||||
|
|
||||||
export const log = pino({
|
export const log = pino({
|
||||||
level: Bun.env.LOG_LEVEL ?? "info",
|
level: Bun.env.LOG_LEVEL ?? "info",
|
||||||
@@ -37,6 +38,8 @@ export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_
|
|||||||
export const DEFAULT_NAME = "Default";
|
export const DEFAULT_NAME = "Default";
|
||||||
export const DUMMY_PASSWORD_HASH = await Bun.password.hash("Dummy");
|
export const DUMMY_PASSWORD_HASH = await Bun.password.hash("Dummy");
|
||||||
|
|
||||||
|
export const BigIntIdSchema = t.String({ pattern: "^\\d+$" });
|
||||||
|
|
||||||
function requireEnv(key: string): string {
|
function requireEnv(key: string): string {
|
||||||
const val = Bun.env[key];
|
const val = Bun.env[key];
|
||||||
if (!val) {
|
if (!val) {
|
||||||
|
|||||||
Reference in New Issue
Block a user