Basic setup for R2 verification presigning
This commit is contained in:
+30
-7
@@ -3,6 +3,8 @@ import {
|
|||||||
ActivityVerificationsSchema,
|
ActivityVerificationsSchema,
|
||||||
ActivityMediaKeysSchema,
|
ActivityMediaKeysSchema,
|
||||||
PlayerSchema,
|
PlayerSchema,
|
||||||
|
minToMs,
|
||||||
|
Activity,
|
||||||
} from "@blade-and-brawn/domain"
|
} from "@blade-and-brawn/domain"
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import { Elysia, redirect, status, t } from "elysia";
|
import { Elysia, redirect, status, t } from "elysia";
|
||||||
@@ -17,7 +19,7 @@ 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, S3Client } from "bun";
|
||||||
import { DatabaseError } from "pg";
|
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";
|
||||||
@@ -47,6 +49,17 @@ const s = (() => {
|
|||||||
return { Standards, Calculator, Commerce, Accounts, Events, Assessments, Verifications };
|
return { Standards, Calculator, Commerce, Accounts, Events, Assessments, Verifications };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// R2 BUCKETS
|
||||||
|
// -----------------------
|
||||||
|
const r2 = {
|
||||||
|
verificationMedia: new S3Client({
|
||||||
|
accessKeyId: env.R2_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
|
||||||
|
bucket: "verification-media",
|
||||||
|
endpoint: env.R2_URL,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// QUEUES
|
// QUEUES
|
||||||
// -----------------------
|
// -----------------------
|
||||||
const queues = [
|
const queues = [
|
||||||
@@ -506,20 +519,30 @@ export const app = new Elysia()
|
|||||||
}, {
|
}, {
|
||||||
params: t.Object({ id: BigIntIdSchema })
|
params: t.Object({ id: BigIntIdSchema })
|
||||||
})
|
})
|
||||||
.post("/verifications", async ({ body: { assessmentId, activityMediaKeys }, accountId }) => {
|
.post("/verifications", async ({ body: { assessmentId }, accountId }) => {
|
||||||
const assessment = await s.Assessments.get(assessmentId, accountId);
|
const assessment = await s.Assessments.get(assessmentId, accountId);
|
||||||
if (!assessment) return status(404, { error: "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
|
|
||||||
|
|
||||||
const created = await s.Verifications.create(assessmentId, activityMediaKeys);
|
const created = await s.Verifications.create(assessmentId);
|
||||||
if (!created) return status(409, { error: "This assessment already has a verification" });
|
if (!created) return status(409, { error: "This assessment already has a verification" });
|
||||||
return created;
|
|
||||||
|
// Generate upload urls
|
||||||
|
const activityMediaUrls: Record<string, string> = {};
|
||||||
|
for (const activity of Object.values(Activity)) {
|
||||||
|
activityMediaUrls[activity] = r2.verificationMedia.presign(
|
||||||
|
`${assessmentId}/${activity}-${crypto.randomUUID()}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
expiresIn: 30 * 60 // 30 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { created, activityMediaUrls };
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
assessmentId: BigIntIdSchema,
|
assessmentId: BigIntIdSchema
|
||||||
activityMediaKeys: ActivityMediaKeysSchema,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.patch("/verifications/:id", async ({ params: { id }, body: { assessmentId, activityMediaKeys }, accountId }) => {
|
.patch("/verifications/:id", async ({ params: { id }, body: { assessmentId, activityMediaKeys }, accountId }) => {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ export const VerificationStatusSchema = t.Union([
|
|||||||
]);
|
]);
|
||||||
export type VerificationStatus = Static<typeof VerificationStatusSchema>;
|
export type VerificationStatus = Static<typeof VerificationStatusSchema>;
|
||||||
|
|
||||||
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",
|
||||||
[Activity.Deadlift]: "media_key_deadlift",
|
[Activity.Deadlift]: "media_key_deadlift",
|
||||||
@@ -33,21 +31,24 @@ const ALLOWED_FROM = {
|
|||||||
} satisfies Record<string, VerificationStatus[]>;
|
} satisfies Record<string, VerificationStatus[]>;
|
||||||
|
|
||||||
export class VerificationsService {
|
export class VerificationsService {
|
||||||
|
static readonly REQUIRED_MEDIA_ACTIVITIES = Object.values(Activity);
|
||||||
|
static readonly ALLOWED_MEDIA_RE = /^(video\/(mp4|quicktime|webm)|image\/(png|jpeg))$/;
|
||||||
|
|
||||||
static missingActivityMediaKeys(verification: Pick<Selectable<DB["verifications"]>, (typeof MEDIA_KEY_COLUMN)[Activity]>) {
|
static missingActivityMediaKeys(verification: Pick<Selectable<DB["verifications"]>, (typeof MEDIA_KEY_COLUMN)[Activity]>) {
|
||||||
return REQUIRED_MEDIA_ACTIVITIES.filter((activity) => !verification[MEDIA_KEY_COLUMN[activity]]);
|
return VerificationsService.REQUIRED_MEDIA_ACTIVITIES.filter((activity) => !verification[MEDIA_KEY_COLUMN[activity]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(assessmentId: string, activityMediaKeys: ActivityMediaKeys) {
|
async create(assessmentId: string, activityMediaKeys?: ActivityMediaKeys) {
|
||||||
return await db.insertInto("verifications")
|
return await db.insertInto("verifications")
|
||||||
.values({
|
.values({
|
||||||
assessment_id: assessmentId,
|
assessment_id: assessmentId,
|
||||||
status: "draft" satisfies VerificationStatus,
|
status: "draft" satisfies VerificationStatus,
|
||||||
media_key_back_squat: activityMediaKeys[Activity.BackSquat] ?? null,
|
media_key_back_squat: activityMediaKeys?.[Activity.BackSquat] ?? null,
|
||||||
media_key_deadlift: activityMediaKeys[Activity.Deadlift] ?? null,
|
media_key_deadlift: activityMediaKeys?.[Activity.Deadlift] ?? null,
|
||||||
media_key_bench_press: activityMediaKeys[Activity.BenchPress] ?? null,
|
media_key_bench_press: activityMediaKeys?.[Activity.BenchPress] ?? null,
|
||||||
media_key_broad_jump: activityMediaKeys[Activity.BroadJump] ?? null,
|
media_key_broad_jump: activityMediaKeys?.[Activity.BroadJump] ?? null,
|
||||||
media_key_run: activityMediaKeys[Activity.Run] ?? null,
|
media_key_run: activityMediaKeys?.[Activity.Run] ?? null,
|
||||||
media_key_cone_drill: activityMediaKeys[Activity.ConeDrill] ?? null,
|
media_key_cone_drill: activityMediaKeys?.[Activity.ConeDrill] ?? null,
|
||||||
})
|
})
|
||||||
.onConflict((oc) => oc.column("assessment_id").doNothing())
|
.onConflict((oc) => oc.column("assessment_id").doNothing())
|
||||||
.returning(["id", "status"])
|
.returning(["id", "status"])
|
||||||
@@ -109,7 +110,7 @@ export class VerificationsService {
|
|||||||
.where("id", "=", id)
|
.where("id", "=", id)
|
||||||
.where("status", "in", ALLOWED_FROM.submit)
|
.where("status", "in", ALLOWED_FROM.submit)
|
||||||
.where((eb) => eb.and(
|
.where((eb) => eb.and(
|
||||||
REQUIRED_MEDIA_ACTIVITIES.map((activity) => eb(MEDIA_KEY_COLUMN[activity], "is not", null))
|
VerificationsService.REQUIRED_MEDIA_ACTIVITIES.map((activity) => eb(MEDIA_KEY_COLUMN[activity], "is not", null))
|
||||||
))
|
))
|
||||||
.set({ status: "submitted" satisfies VerificationStatus, submitted_at: new Date() })
|
.set({ status: "submitted" satisfies VerificationStatus, submitted_at: new Date() })
|
||||||
.returning(["id", "status"])
|
.returning(["id", "status"])
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export const env = {
|
|||||||
BOT_CLIENT_SECRET: requireEnv("BOT_CLIENT_SECRET"),
|
BOT_CLIENT_SECRET: requireEnv("BOT_CLIENT_SECRET"),
|
||||||
BOT_REDIRECT_URL: requireEnv("BOT_REDIRECT_URL"),
|
BOT_REDIRECT_URL: requireEnv("BOT_REDIRECT_URL"),
|
||||||
BOT_LOGIN_REDIRECT_URL: requireEnv("BOT_LOGIN_REDIRECT_URL"),
|
BOT_LOGIN_REDIRECT_URL: requireEnv("BOT_LOGIN_REDIRECT_URL"),
|
||||||
|
CLOUDFLARE_TOKEN: requireEnv("CLOUDFLARE_TOKEN"),
|
||||||
|
R2_ACCESS_KEY_ID: requireEnv("R2_ACCESS_KEY_ID"),
|
||||||
|
R2_SECRET_ACCESS_KEY: requireEnv("R2_SECRET_ACCESS_KEY"),
|
||||||
|
R2_URL: requireEnv("R2_URL")
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT);
|
export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT);
|
||||||
|
|||||||
Reference in New Issue
Block a user