diff --git a/.env.example b/.env.example index 56ea400b58..7c9c142812 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,9 @@ NEXTAUTH_URL="http://localhost:3000" # --- echogram (can be found in the nano repository) #NEXT_PUBLIC_ECHOGRAM_URL="http://localhost:8001" +# --- ars +NEXT_PUBLIC_ARS_URL="http://localhost:4444" + # --- Testing # Set to true to enable testing mode #TESTING=true diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5572a6e19f..fc877cceb9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,7 @@ env: ADMIN_KEY: foobar DATABASE_URL: postgres://postgres:postgres@localhost:5432/echo-web NEXT_PUBLIC_SANITY_DATASET: "testing" + NEXT_PUBLIC_ARS_URL: "http://localhost:4444" FEIDE_CLIENT_ID: ${{ secrets.FEIDE_CLIENT_ID }} FEIDE_CLIENT_SECRET: ${{ secrets.FEIDE_CLIENT_SECRET }} NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} diff --git a/apps/ars/.dockerignore b/apps/ars/.dockerignore new file mode 100644 index 0000000000..f178b5c04d --- /dev/null +++ b/apps/ars/.dockerignore @@ -0,0 +1,30 @@ +# dev +.yarn/ +!.yarn/releases +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf + +# deps +node_modules/ + +# env +.env +.env.production + +.turbo/ + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# misc +.DS_Store diff --git a/apps/ars/.gitignore b/apps/ars/.gitignore new file mode 100644 index 0000000000..36fabb6cb2 --- /dev/null +++ b/apps/ars/.gitignore @@ -0,0 +1,28 @@ +# dev +.yarn/ +!.yarn/releases +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf + +# deps +node_modules/ + +# env +.env +.env.production + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# misc +.DS_Store diff --git a/apps/ars/Dockerfile b/apps/ars/Dockerfile new file mode 100644 index 0000000000..18dfa0356d --- /dev/null +++ b/apps/ars/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1 + +# Stage 1: Base with Node.js and PNPM installed +ARG NODE_VERSION=20.12.1 +FROM node:${NODE_VERSION}-slim as base + +LABEL fly_launch_runtime="Node.js" + +WORKDIR /app + +ENV NODE_ENV=production + +ARG PNPM_VERSION=9.0.4 +RUN npm install -g pnpm@$PNPM_VERSION + +# Stage 2: Build Stage +FROM base as build + +ENV NODE_ENV=development + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 && \ + rm -rf /var/lib/apt/lists/* + +COPY --link . . + +RUN pnpm --filter=ars deploy pruned-ars && \ + cd pruned-ars && \ + pnpm install && \ + pnpm run build && \ + pnpm prune --prod + +ENV NODE_ENV=production + +# Stage 3: Production Image +FROM base + +COPY --from=build /app/pruned-ars /app + +EXPOSE 4444 + +CMD ["pnpm", "run", "start"] diff --git a/apps/ars/README.md b/apps/ars/README.md new file mode 100644 index 0000000000..ec90ebe5c5 --- /dev/null +++ b/apps/ars/README.md @@ -0,0 +1 @@ +# ARS (Advanced Registration System) diff --git a/apps/ars/fly.toml b/apps/ars/fly.toml new file mode 100644 index 0000000000..b81df3f6c9 --- /dev/null +++ b/apps/ars/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for ragger-snowy-sound-6162 on 2024-08-30T01:38:21+02:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'ragger-snowy-sound-6162' +primary_region = 'arn' + +[build] + +[http_service] + internal_port = 4444 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 diff --git a/apps/ars/package.json b/apps/ars/package.json new file mode 100644 index 0000000000..189588f855 --- /dev/null +++ b/apps/ars/package.json @@ -0,0 +1,30 @@ +{ + "name": "@echo-webkom/ars", + "scripts": { + "start": "pnpm with-env node dist/index.js", + "build": "pnpm with-env esbuild src/**/*.ts --bundle --platform=node --outdir=dist", + "dev": "pnpm with-env tsx watch src/index.ts", + "test:unit": "vitest", + "clean": "rm -rf .turbo node_modules", + "lint": "eslint", + "typecheck": "tsc --noEmit", + "with-env": "dotenv -e ../../.env --" + }, + "dependencies": { + "@echo-webkom/db": "workspace:*", + "@hono/node-server": "1.12.2", + "date-fns": "3.6.0", + "drizzle-orm": "0.33.0", + "hono": "4.5.9", + "zod": "3.23.8" + }, + "devDependencies": { + "@flydotio/dockerfile": "0.5.8", + "@types/node": "20.16.1", + "dotenv-cli": "7.4.2", + "esbuild": "0.23.1", + "tsx": "4.17.0", + "typescript": "5.5.4", + "vitest": "2.0.5" + } +} diff --git a/apps/ars/src/app.ts b/apps/ars/src/app.ts new file mode 100644 index 0000000000..85860b4fc0 --- /dev/null +++ b/apps/ars/src/app.ts @@ -0,0 +1,170 @@ +import { isFuture, isPast } from "date-fns"; + +import { getCorrectSpotrange } from "./correct-spot-range"; +import { createApp } from "./create-app"; +import { isUserBanned } from "./is-user-banned"; +import { isUserComplete } from "./is-user-complete"; +import { doesIntersect } from "./list"; +import { logger } from "./logger"; +import { auth } from "./middleware"; +import { + getExisitingRegistration, + getHappening, + getHostGroups, + getUser, + registerUserToHappening, +} from "./queries"; +import { registerJsonSchema } from "./schema"; + +const app = createApp(); + +app.get("/", (c) => { + return c.json({ + message: "OK", + }); +}); + +app.post("/", auth, async (c) => { + const json = await c.req.json().catch(() => {}); + const { success, data } = registerJsonSchema.safeParse(json); + + if (!success) { + logger.error("Invalid JSON", json); + return c.json({ success: false, message: "Ugyldig JSON" }); + } + + const { userId, happeningId, questions } = data; + + const user = await getUser(userId); + + if (!user) { + logger.error("User not found", { + userId, + }); + return c.json({ success: false, message: "Brukeren finnes ikke" }); + } + + const happening = await getHappening(happeningId); + + if (!happening) { + logger.error("Happening not found", { + happeningId, + }); + return c.json({ success: false, message: "Arrangementet finnes ikke" }); + } + + if (!isUserComplete(user)) { + logger.error("User is not complete", { + userId, + }); + return c.json({ success: false, message: "Brukeren er ikke fullført" }); + } + + const isBanned = await isUserBanned(user, happening); + if (isBanned) { + logger.error("User is banned", { + userId, + }); + return c.json({ success: false, message: "Brukeren er utestengt" }); + } + + const exisitingRegistration = await getExisitingRegistration(userId, happeningId); + + if (exisitingRegistration) { + logger.error("Registration already exists", { + userId, + happeningId, + }); + + const status = + exisitingRegistration.status === "registered" + ? "Du er allerede påmeldt dette arrangementet" + : "Du er allerede på venteliste til dette arrangementet"; + + return c.json({ + success: false, + message: status, + }); + } + + const canEarlyRegister = doesIntersect( + happening.registrationGroups ?? [], + user.memberships.map((membership) => membership.groupId), + ); + + if (!canEarlyRegister && happening.registrationStart && isFuture(happening.registrationStart)) { + logger.error("Registration is not open", { + userId, + happeningId, + }); + return c.json({ success: false, message: "Påmeldingen har ikke startet" }); + } + + if (!canEarlyRegister && !happening.registrationStart) { + logger.error("Registration is not open", { + userId, + happeningId, + }); + return c.json({ success: false, message: "Påmelding er bare for inviterte undergrupper" }); + } + + if (happening.registrationEnd && isPast(happening.registrationEnd)) { + logger.error("Registration is closed", { + userId, + happeningId, + }); + return c.json({ success: false, message: "Påmeldingen har allerede stengt" }); + } + + const hostGroups = await getHostGroups(happeningId); + + const canSkipSpotRange = doesIntersect( + hostGroups, + user.memberships.map((membership) => membership.groupId), + ); + + const userSpotRange = getCorrectSpotrange(user.year, happening.spotRanges, canSkipSpotRange); + + if (!userSpotRange) { + logger.error("User is not in any spot range", { + userId, + happeningId, + }); + return c.json({ success: false, message: "Brukeren er ikke i en plassering" }); + } + + const allQuestionsAnswered = happening.questions.every((question) => { + const questionExists = questions.find((q) => q.questionId === question.id); + const questionAnswer = questionExists?.answer; + + return question.required ? !!questionAnswer : true; + }); + + if (!allQuestionsAnswered) { + logger.error("Not all questions are answered", { + userId, + happeningId, + }); + return c.json({ success: false, message: "Du må svare på alle spørsmålene" }); + } + + const status = await registerUserToHappening(userId, happeningId, userSpotRange); + + if (status === "error") { + logger.error("Failed to update registration", { + userId, + happeningId, + }); + return c.json({ success: false, message: "Noe gikk galt" }); + } + + const message = + status === "waitlisted" ? "Du er nå på venteliste" : "Du er nå påmeldt arrangementet"; + + return c.json({ + success: true, + message, + }); +}); + +export default app; diff --git a/apps/ars/src/correct-spot-range.ts b/apps/ars/src/correct-spot-range.ts new file mode 100644 index 0000000000..3aca7ac210 --- /dev/null +++ b/apps/ars/src/correct-spot-range.ts @@ -0,0 +1,17 @@ +import { type SpotRange } from "@echo-webkom/db/schemas"; + +export const getCorrectSpotrange = ( + year: number, + spotRanges: Array, + canSkipSpotRange: boolean, +) => { + return ( + spotRanges.find((spotRange) => { + if (canSkipSpotRange) { + return true; + } + + return year >= spotRange.minYear && year <= spotRange.maxYear; + }) ?? null + ); +}; diff --git a/apps/ars/src/create-app.ts b/apps/ars/src/create-app.ts new file mode 100644 index 0000000000..df32896c74 --- /dev/null +++ b/apps/ars/src/create-app.ts @@ -0,0 +1,13 @@ +import { Hono } from "hono"; + +export type Bindings = { + ADMIN_KEY: string; +}; + +export const createApp = () => { + const app = new Hono<{ + Bindings: Bindings; + }>(); + + return app; +}; diff --git a/apps/ars/src/index.ts b/apps/ars/src/index.ts new file mode 100644 index 0000000000..5d698598dc --- /dev/null +++ b/apps/ars/src/index.ts @@ -0,0 +1,11 @@ +import { serve } from "@hono/node-server"; + +import app from "./app"; + +const PORT = 4444; +console.log(`Server is running on port ${PORT}`); + +serve({ + fetch: app.fetch, + port: PORT, +}); diff --git a/apps/ars/src/is-user-banned.ts b/apps/ars/src/is-user-banned.ts new file mode 100644 index 0000000000..b6c897746d --- /dev/null +++ b/apps/ars/src/is-user-banned.ts @@ -0,0 +1,39 @@ +import { getHappeningsFromDateToDate, getStrike, type DBHappening, type DBUser } from "./queries"; + +export const BAN_LENGTH = 3; + +export const isUserBanned = async (user: DBUser, bedpres: DBHappening) => { + if (bedpres.type !== "bedpres") { + return false; + } + + if (!bedpres.date) { + return false; + } + + if (!user.year) { + return false; + } + + if (!user.bannedFromStrike || !user.isBanned) { + return false; + } + + const strike = await getStrike(user.bannedFromStrike); + const dateBanned = strike?.strikeInfo.createdAt; + if (!dateBanned) { + return false; + } + + const happenings = await getHappeningsFromDateToDate(dateBanned, bedpres.date); + + const available = happenings + .filter((happening) => + happening.spotRanges.some( + (spotRange) => user.year! >= spotRange.minYear && user.year! <= spotRange.maxYear, + ), + ) + .filter((happening) => happening.type === "bedpres"); + + return available.length < BAN_LENGTH; +}; diff --git a/apps/ars/src/is-user-complete.ts b/apps/ars/src/is-user-complete.ts new file mode 100644 index 0000000000..38ece92a9f --- /dev/null +++ b/apps/ars/src/is-user-complete.ts @@ -0,0 +1,15 @@ +import type { DBUser } from "./queries"; + +type CompleteUser = DBUser & { + degreeId: string; + year: number; + hasReadTerms: boolean; +}; + +export const isUserComplete = (user: DBUser): user is CompleteUser => { + if (!user.degreeId || !user.year || !user.hasReadTerms) { + return false; + } + + return true; +}; diff --git a/apps/ars/src/list.ts b/apps/ars/src/list.ts new file mode 100644 index 0000000000..7d9165e5f2 --- /dev/null +++ b/apps/ars/src/list.ts @@ -0,0 +1,26 @@ +/** + * Returns the intersection of two lists. + * + * @example + * ```ts + * intersection([1, 2, 3], [2, 3, 4]); // [2, 3] + * ``` + * + * @param a list a + * @param b list b + * @returns the elements that are in both lists + */ +export const intersection = (a: Array, b: Array): Array => { + return a.filter((value) => b.includes(value)); +}; + +/** + * Checks if two lists have any elements in common. + * + * @param a list a + * @param b list b + * @returns true if the lists have any elements in common, false otherwise + */ +export const doesIntersect = (a: Array, b: Array): boolean => { + return intersection(a, b).length > 0; +}; diff --git a/apps/ars/src/logger.ts b/apps/ars/src/logger.ts new file mode 100644 index 0000000000..e2d9c088f0 --- /dev/null +++ b/apps/ars/src/logger.ts @@ -0,0 +1,8 @@ +export const logger = { + info: (message: string, meta?: Record) => { + console.log("ℹ️ [INFO]", message, JSON.stringify(meta)); + }, + error: (message: string, meta?: Record) => { + console.error("❌ [ERROR]", message, JSON.stringify(meta)); + }, +}; diff --git a/apps/ars/src/middleware.ts b/apps/ars/src/middleware.ts new file mode 100644 index 0000000000..485b89cf12 --- /dev/null +++ b/apps/ars/src/middleware.ts @@ -0,0 +1,23 @@ +import type { MiddlewareHandler } from "hono"; + +import type { Bindings } from "./create-app"; + +/** + * Check if the request authorization header matches the secret + * + * @param c hono context + * @param next next handler + * @returns the next handler + */ +export const auth: MiddlewareHandler<{ Bindings: Bindings }> = async (c, next) => { + if (!c.env.ADMIN_KEY) { + return next(); + } + + const token = c.req.header("Authorization"); + if (token !== `Bearer ${c.env.ADMIN_KEY}`) { + return c.text("Unauthorized", { status: 401 }); + } + + return next(); +}; diff --git a/apps/ars/src/queries.ts b/apps/ars/src/queries.ts new file mode 100644 index 0000000000..46a6f629ed --- /dev/null +++ b/apps/ars/src/queries.ts @@ -0,0 +1,175 @@ +import { and, eq, gte, lte, or, sql } from "drizzle-orm"; + +import { db } from "@echo-webkom/db"; +import { + answers, + registrations, + users, + type AnswerInsert, + type SpotRange, +} from "@echo-webkom/db/schemas"; + +import { type RegisterJson } from "./schema"; + +export type DBUser = Exclude>, undefined>; + +export const getUser = async (userId: string) => { + return await db.query.users.findFirst({ + where: (row, { eq }) => eq(row.id, userId), + columns: { + id: true, + degreeId: true, + year: true, + hasReadTerms: true, + bannedFromStrike: true, + isBanned: true, + }, + with: { + memberships: { + columns: { + groupId: true, + }, + }, + }, + }); +}; + +export type DBHappening = Exclude>, undefined>; + +export const getHappening = async (happeningId: string) => { + return await db.query.happenings.findFirst({ + where: (row, { eq }) => eq(row.id, happeningId), + columns: { + id: true, + type: true, + date: true, + registrationStart: true, + registrationEnd: true, + registrationGroups: true, + }, + with: { + questions: true, + spotRanges: true, + }, + }); +}; + +export const getStrike = async (strikeId: number) => { + return await db.query.strikes.findFirst({ + where: (row, { eq }) => eq(row.id, strikeId), + with: { + strikeInfo: true, + }, + }); +}; + +export const getHappeningsFromDateToDate = async (fromDate: Date, toDate: Date) => { + return await db.query.happenings.findMany({ + where: (happening, { and, eq, gt, lt }) => + and(eq(happening.type, "bedpres"), gt(happening.date, fromDate), lt(happening.date, toDate)), + with: { + spotRanges: true, + }, + orderBy: (happening, { asc }) => [asc(happening.date)], + }); +}; + +export const getExisitingRegistration = async (userId: string, happeningId: string) => { + return await db.query.registrations.findFirst({ + where: (registration, { and, eq, or }) => + and( + eq(registration.userId, userId), + eq(registration.happeningId, happeningId), + or(eq(registration.status, "registered"), eq(registration.status, "waiting")), + ), + columns: { + status: true, + }, + }); +}; + +export const getHostGroups = async (id: string) => { + return await db.query.happeningsToGroups + .findMany({ + where: (happeningToGroup, { eq }) => eq(happeningToGroup.happeningId, id), + }) + .then((groups) => groups.map((group) => group.groupId)); +}; + +export const registerUserToHappening = async ( + userId: string, + happeningId: string, + spotRange: SpotRange, +) => { + return await db.transaction( + async (tx) => { + await tx.execute(sql`LOCK TABLE ${registrations} IN EXCLUSIVE MODE`); + + const regs = await tx + .select() + .from(registrations) + .where( + and( + eq(registrations.happeningId, happeningId), + lte(users.year, spotRange.maxYear), + gte(users.year, spotRange.minYear), + or(eq(registrations.status, "registered"), eq(registrations.status, "waiting")), + ), + ) + .leftJoin(users, eq(registrations.userId, users.id)); + + const isInfiniteSpots = spotRange.spots === 0; + const isWaitlisted = !isInfiniteSpots && regs.length >= spotRange.spots; + + const registration = await tx + .insert(registrations) + .values({ + status: isWaitlisted ? "waiting" : "registered", + happeningId, + userId, + changedBy: null, + }) + .returning() + .onConflictDoUpdate({ + target: [registrations.happeningId, registrations.userId], + set: { + status: isWaitlisted ? "waiting" : "registered", + }, + }) + .then((res) => res[0] ?? null); + + if (!registration) { + return "error" as const; + } + + return isWaitlisted ? "waitlisted" : ("registered" as const); + }, + { + isolationLevel: "read committed", + }, + ); +}; + +export const insertAnswers = async ( + happeningId: string, + userId: string, + questions: RegisterJson["questions"], +) => { + const answersToInsert = questions.map( + (question) => + ({ + happeningId: happeningId, + userId: userId, + questionId: question.questionId, + answer: question.answer + ? { + answer: question.answer, + } + : null, + }) satisfies AnswerInsert, + ); + + if (answersToInsert.length > 0) { + await db.insert(answers).values(answersToInsert).onConflictDoNothing(); + } +}; diff --git a/apps/ars/src/schema.ts b/apps/ars/src/schema.ts new file mode 100644 index 0000000000..8b4e9121bf --- /dev/null +++ b/apps/ars/src/schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const registerJsonSchema = z.object({ + userId: z.string(), + happeningId: z.string(), + questions: z.array( + z.object({ + questionId: z.string(), + answer: z.string().or(z.string().array()).optional(), + }), + ), +}); + +export type RegisterJson = z.infer; diff --git a/apps/ars/tests/fixtures/env.ts b/apps/ars/tests/fixtures/env.ts new file mode 100644 index 0000000000..b7565caac6 --- /dev/null +++ b/apps/ars/tests/fixtures/env.ts @@ -0,0 +1,5 @@ +import { type Bindings } from "../../src/create-app"; + +export const env = { + ADMIN_KEY: "foobar", +} satisfies Bindings; diff --git a/apps/ars/tests/fixtures/happenings.ts b/apps/ars/tests/fixtures/happenings.ts new file mode 100644 index 0000000000..ff7a711820 --- /dev/null +++ b/apps/ars/tests/fixtures/happenings.ts @@ -0,0 +1,20 @@ +import { type DBHappening } from "../../src/queries"; + +export const bedpres = { + id: "bedpres", + type: "bedpres", + date: new Date("2023-01-04"), + registrationStart: new Date("2023-01-01"), + registrationEnd: new Date("2023-01-03"), + registrationGroups: ["webkom"], + spotRanges: [ + { + happeningId: "bedpres", + spots: 1, + minYear: 1, + maxYear: 5, + id: "1", + }, + ], + questions: [], +} satisfies DBHappening; diff --git a/apps/ars/tests/fixtures/request.ts b/apps/ars/tests/fixtures/request.ts new file mode 100644 index 0000000000..28fe3400d5 --- /dev/null +++ b/apps/ars/tests/fixtures/request.ts @@ -0,0 +1,13 @@ +import { env } from "./env"; + +export const defaultRequest = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${env.ADMIN_KEY}`, + }, + body: JSON.stringify({ + userId: "foo", + happeningId: "bedpres", + questions: [], + }), +}; diff --git a/apps/ars/tests/fixtures/users.ts b/apps/ars/tests/fixtures/users.ts new file mode 100644 index 0000000000..1e2a8ca44e --- /dev/null +++ b/apps/ars/tests/fixtures/users.ts @@ -0,0 +1,25 @@ +import { DBUser } from "../../src/queries"; + +export const bo = { + id: "bo", + year: 1, + degreeId: "dsik", + hasReadTerms: true, + bannedFromStrike: null, + isBanned: false, + memberships: [], +} satisfies DBUser; + +export const andreas = { + id: "andreas", + year: 1, + degreeId: "dsik", + hasReadTerms: true, + bannedFromStrike: null, + isBanned: false, + memberships: [ + { + groupId: "webkom", + }, + ], +} satisfies DBUser; diff --git a/apps/ars/tests/index.test.ts b/apps/ars/tests/index.test.ts new file mode 100644 index 0000000000..cc7b642879 --- /dev/null +++ b/apps/ars/tests/index.test.ts @@ -0,0 +1,133 @@ +import { afterEach } from "node:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { isUserBanned } from "../src/is-user-banned"; +import { + getExisitingRegistration, + getHappening, + getHostGroups, + getUser, + registerUserToHappening, +} from "../src/queries"; +import { bedpres } from "./fixtures/happenings"; +import { defaultRequest } from "./fixtures/request"; +import { bo } from "./fixtures/users"; +import { runRequest } from "./lib/run-request"; + +const getUserMock = vi.mocked(getUser); +const getHappeningMock = vi.mocked(getHappening); +const isUserBannedMock = vi.mocked(isUserBanned); +const getExisitingRegistrationMock = vi.mocked(getExisitingRegistration); +const getHostGroupsMock = vi.mocked(getHostGroups); +const registerUserToHappeningMock = vi.mocked(registerUserToHappening); + +vi.mock("../src/queries", () => { + return { + getUser: vi.fn(), + getHappening: vi.fn(), + getExisitingRegistration: vi.fn(), + getHostGroups: vi.fn(), + registerUserToHappening: vi.fn(), + }; +}); + +vi.mock("../src/is-user-banned", () => { + return { + isUserBanned: vi.fn(), + }; +}); + +describe("ragger", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should be unauthorized", async () => { + const res = await runRequest("POST", "/", { + headers: { + Authorization: "Bearer invalid", + }, + }); + + expect(res.status).toBe(401); + }); + + it("should be invalid json", async () => { + const res = await runRequest("POST", "/"); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: false, message: "Ugyldig JSON" }); + }); + + it("should be user not found", async () => { + getUserMock.mockReturnValue(Promise.resolve(undefined)); + + const res = await runRequest("POST", "/", defaultRequest); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: false, message: "Brukeren finnes ikke" }); + }); + + it("should be incomplete user", async () => { + getUserMock.mockReturnValue(Promise.resolve({ ...bo, hasReadTerms: false })); + getHappeningMock.mockReturnValue(Promise.resolve(bedpres)); + + const res = await runRequest("POST", "/", defaultRequest); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: false, message: "Brukeren er ikke fullført" }); + }); + + it("should be banned", async () => { + getUserMock.mockReturnValue(Promise.resolve({ ...bo, isBanned: true })); + getHappeningMock.mockReturnValue(Promise.resolve(bedpres)); + isUserBannedMock.mockReturnValue(Promise.resolve(true)); + + const res = await runRequest("POST", "/", defaultRequest); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: false, message: "Brukeren er utestengt" }); + }); + + it("should be already registered", async () => { + getUserMock.mockReturnValue(Promise.resolve(bo)); + getHappeningMock.mockReturnValue(Promise.resolve(bedpres)); + isUserBannedMock.mockReturnValue(Promise.resolve(false)); + getExisitingRegistrationMock.mockReturnValue(Promise.resolve({ status: "registered" })); + + const res = await runRequest("POST", "/", defaultRequest); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ + success: false, + message: "Du er allerede påmeldt dette arrangementet", + }); + expect(getUserMock).toHaveBeenCalledWith("foo"); + expect(getHappeningMock).toHaveBeenCalledWith("bedpres"); + expect(getExisitingRegistrationMock).toHaveBeenCalledWith("foo", "bedpres"); + }); + + it("should register user", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2023-01-02")); + + getUserMock.mockReturnValue(Promise.resolve(bo)); + getHappeningMock.mockReturnValue(Promise.resolve(bedpres)); + isUserBannedMock.mockReturnValue(Promise.resolve(false)); + getHostGroupsMock.mockReturnValue(Promise.resolve(["webkom"])); + getExisitingRegistrationMock.mockReturnValue(Promise.resolve(undefined)); + registerUserToHappeningMock.mockReturnValue(Promise.resolve("registered")); + + const res = await runRequest("POST", "/", defaultRequest); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: true, message: "Du er nå påmeldt arrangementet" }); + expect(getUserMock).toHaveBeenCalledWith("foo"); + expect(getHappeningMock).toHaveBeenCalledWith("bedpres"); + expect(getExisitingRegistrationMock).toHaveBeenCalledWith("foo", "bedpres"); + }); +}); diff --git a/apps/ars/tests/lib/run-request.ts b/apps/ars/tests/lib/run-request.ts new file mode 100644 index 0000000000..d5ba89635f --- /dev/null +++ b/apps/ars/tests/lib/run-request.ts @@ -0,0 +1,18 @@ +import app from "../../src/app"; +import { env } from "../fixtures/env"; + +export const runRequest = async (method: string, path: string, requestInit?: RequestInit) => { + return await app.request( + path, + { + method, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${env.ADMIN_KEY}`, + ...requestInit?.headers, + }, + ...requestInit, + }, + env, + ); +}; diff --git a/apps/ars/tsconfig.json b/apps/ars/tsconfig.json new file mode 100644 index 0000000000..c8d16141dd --- /dev/null +++ b/apps/ars/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowJs": true, + + "types": ["node"], + + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} diff --git a/apps/ars/vitest.config.ts b/apps/ars/vitest.config.ts new file mode 100644 index 0000000000..f624398e8d --- /dev/null +++ b/apps/ars/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +}); diff --git a/apps/web/package.json b/apps/web/package.json index 3e142dd2c5..02c609ddc9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ "@echo-webkom/db": "workspace:*", "@echo-webkom/email": "workspace:*", "@echo-webkom/lib": "workspace:*", - "@echo-webkom/sanity": "workspace:^", + "@echo-webkom/sanity": "workspace:*", "@hookform/resolvers": "3.9.0", "@json2csv/plainjs": "^7.0.6", "@radix-ui/react-alert-dialog": "1.1.1", diff --git a/apps/web/src/actions/register.ts b/apps/web/src/actions/register.ts index 2f57321fb9..67b00ca942 100644 --- a/apps/web/src/actions/register.ts +++ b/apps/web/src/actions/register.ts @@ -1,350 +1,82 @@ "use server"; -import { isFuture, isPast } from "date-fns"; -import { and, eq, gte, lte, or, sql } from "drizzle-orm"; -import { z } from "zod"; - -import { db } from "@echo-webkom/db"; -import { - answers, - registrations, - users, - type AnswerInsert, - type SpotRange, -} from "@echo-webkom/db/schemas"; +import { type z } from "zod"; import { pingBoomtown } from "@/api/boomtown"; import { revalidateRegistrations } from "@/data/registrations/revalidate"; -import { isUserBannedFromBedpres } from "@/lib/ban-info"; import { getUser } from "@/lib/get-user"; -import { registrationFormSchema } from "@/lib/schemas/registration"; -import { isErrorMessage } from "@/utils/error"; -import { doesIntersect } from "@/utils/list"; - -export const register = async (id: string, payload: z.infer) => { - /** - * Check if user is signed in - */ - const user = await getUser(); - - if (!user) { - console.error("User not found", { - happeningId: id, - }); +import { type registrationFormSchema } from "@/lib/schemas/registration"; + +const ARS_URL = process.env.NEXT_PUBLIC_ARS_URL; + +const registerUser = async ({ + userId, + happeningId, + questions, +}: { + userId: string; + happeningId: string; + questions: Array<{ + questionId: string; + answer?: string | Array; + }>; +}): Promise<{ + success: boolean; + message: string; +}> => { + if (!ARS_URL) { return { success: false, - message: "Du er ikke logget inn", + message: "Ikke koblet til ARS", }; } - try { - /** - * Check if user has filled out necessary information - */ - if (!user.degreeId || !user.year || !user.hasReadTerms) { - console.error("User has not filled out necessary information", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Du må ha fylt ut studieinformasjon for å kunne registrere deg", - }; - } - - /** - * Get happening, and check if it exists - */ - const happening = await db.query.happenings.findFirst({ - where: (happening) => eq(happening.id, id), - with: { - questions: true, - }, - }); - - if (!happening) { - console.error("Happening not found", { - happeningId: id, - }); - return { - success: false, - message: "Arrangementet finnes ikke", - }; - } - - /** - * Check if user is banned - */ - const isBanned = - user.isBanned && happening.type === "bedpres" - ? await isUserBannedFromBedpres(user, happening) - : false; - - if (isBanned) { - console.error("User is banned", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Du er utestengt fra denne bedriftspresentasjonen", - }; - } - - /** - * Check if user is already registered - */ - const exisitingRegistration = await db.query.registrations.findFirst({ - where: (registration) => - and( - eq(registration.happeningId, id), - eq(registration.userId, user.id), - or(eq(registration.status, "registered"), eq(registration.status, "waiting")), - ), - }); - - if (exisitingRegistration) { - console.error("Registration already exists", { - userId: user.id, - happeningId: id, - }); - const status = - exisitingRegistration.status === "registered" - ? "Du er allerede påmeldt dette arrangementet" - : "Du er allerede på venteliste til dette arrangementet"; - return { - success: false, - message: status, - }; - } - - const canEarlyRegister = doesIntersect( - happening.registrationGroups ?? [], - user.memberships.map((membership) => membership.group.id), - ); - - /** - * Check if registration is open for user that can not early register - */ - if (!canEarlyRegister && happening.registrationStart && isFuture(happening.registrationStart)) { - console.error("Registration is not open", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Påmeldingen har ikke startet", - }; - } - - if (!canEarlyRegister && !happening.registrationStart) { - console.error("Registration is not open", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Påmelding er bare for inviterte undergrupper", - }; - } - - /** - * Check if registration is closed for user that can not early register - */ - if (happening.registrationEnd && isPast(happening.registrationEnd)) { - console.error("Registration is closed", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Påmeldingen har allerede stengt", - }; - } - - /** - * Get spot ranges for happening - */ - const spotRanges = await db.query.spotRanges.findMany({ - where: (spotRange) => eq(spotRange.happeningId, id), - }); - - /** - * Get groups that host the happening - */ - const hostGroups = await db.query.happeningsToGroups - .findMany({ - where: (happeningToGroup) => eq(happeningToGroup.happeningId, id), - }) - .then((groups) => groups.map((group) => group.groupId)); - - const canSkipSpotRange = doesIntersect( - hostGroups, - user.memberships.map((membership) => membership.group.id), - ); - - /** - * Get correct spot range for user - * - * If user is not in any spot range, return error - */ - const userSpotRange = getCorrectSpotrange(user.year, spotRanges, canSkipSpotRange); - - if (!userSpotRange) { - console.error("User is not in any spot range", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Du kan ikke melde deg på dette arrangementet", - }; - } - - const data = await registrationFormSchema.parseAsync(payload); - - /** - * Check if all questions are answered - */ - const allQuestionsAnswered = happening.questions.every((question) => { - const questionExists = data.questions.find((q) => q.questionId === question.id); - const questionAnswer = questionExists?.answer; - - return question.required ? !!questionAnswer : true; - }); - - if (!allQuestionsAnswered) { - console.error("Not all questions are answered", { - userId: user.id, - happeningId: id, - }); - return { - success: false, - message: "Du må svare på alle spørsmålene", - }; - } - - const { registration, isWaitlisted } = await db.transaction( - async (tx) => { - await tx.execute(sql`LOCK TABLE ${registrations} IN EXCLUSIVE MODE`); - - const regs = await tx - .select() - .from(registrations) - .where( - and( - eq(registrations.happeningId, id), - lte(users.year, userSpotRange.maxYear), - gte(users.year, userSpotRange.minYear), - or(eq(registrations.status, "registered"), eq(registrations.status, "waiting")), - ), - ) - .leftJoin(users, eq(registrations.userId, users.id)); - - const isInfiniteSpots = userSpotRange.spots === 0; - const isWaitlisted = !isInfiniteSpots && regs.length >= userSpotRange.spots; - - const registration = await tx - .insert(registrations) - .values({ - status: isWaitlisted ? "waiting" : "registered", - happeningId: id, - userId: user.id, - changedBy: null, - }) - .returning() - .onConflictDoUpdate({ - target: [registrations.happeningId, registrations.userId], - set: { - status: isWaitlisted ? "waiting" : "registered", - }, - }) - .then((res) => res[0] ?? null); - - return { - registration, - isWaitlisted, - }; - }, - { - isolationLevel: "read committed", - }, - ); - - revalidateRegistrations(id, user.id); - - if (!registration) { - throw new Error("Failed to update registration"); - } - - /** - * Insert answers - */ - const answersToInsert = data.questions.map( - (question) => - ({ - happeningId: happening.id, - userId: user.id, - questionId: question.questionId, - answer: question.answer - ? { - answer: question.answer, - } - : null, - }) satisfies AnswerInsert, - ); - - if (answersToInsert.length > 0) { - await db.insert(answers).values(answersToInsert).onConflictDoNothing(); - } - - console.info("Successful registration", { - userId: user.id, - happeningId: happening.id, - isWaitlisted, - }); - - void (async () => { - await pingBoomtown(id); - })(); - + const response = await fetch(ARS_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId, + happeningId, + questions, + }), + }); + + if (!response.ok) { return { - success: true, - message: isWaitlisted ? "Du er nå på venteliste" : "Du er nå påmeldt arrangementet", + success: false, + message: "Noe gikk galt", }; - } catch (error) { - console.error("Failed to register", { - userId: user?.id, - happeningId: id, - error: isErrorMessage(error) ? error.message : "En ukjent feil har oppstått", - }); + } - if (error instanceof z.ZodError) { - return { - success: false, - message: "Skjemaet er ikke i riktig format", - }; - } + return (await response.json()) as { + success: boolean; + message: string; + }; +}; + +export const register = async (id: string, payload: z.infer) => { + const user = await getUser(); + if (!user) { return { success: false, - message: "En feil har oppstått", + message: "Du er ikke logget inn", }; } -}; -const getCorrectSpotrange = ( - year: number, - spotRanges: Array, - canSkipSpotRange: boolean, -) => { - return ( - spotRanges.find((spotRange) => { - if (canSkipSpotRange) { - return true; - } + const json = await registerUser({ + userId: user.id, + happeningId: id, + questions: payload.questions, + }); + + revalidateRegistrations(id, user.id); + + void (async () => { + await pingBoomtown(id); + })(); - return year >= spotRange.minYear && year <= spotRange.maxYear; - }) ?? null - ); + return json; }; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index ad880005c6..78ecd64745 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -13,7 +13,7 @@ let pool; const createPool = () => { return postgres(process.env.DATABASE_URL!, { - max: 100, + max: 40, prepare: false, idle_timeout: 10000, // 10 seconds connect_timeout: 1000, // 10 seconds diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index 17b79c1329..85bcbe12bd 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -8,35 +8,32 @@ export default defineConfig({ fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: 1, reporter: "html", use: { trace: "on-first-retry", headless: !!process.env.CI, - baseURL: "http://localhost:3000", + baseURL: "http://127.0.0.1:3000", }, projects: [ - /* Test against desktop viewports. */ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, - // Commented out because of problems in CI - // { - // name: "Mobile Safari", - // use: { ...devices["iPhone 12"] }, - // }, ], webServer: [ { command: "pnpm --filter=web run start", - url: "http://localhost:3000", - timeout: 120 * 1000, + url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, cwd: "../", }, + { + command: "pnpm --filter=ars run start", + url: "http://127.0.0.1:4444", + cwd: "../", + }, ], }); diff --git a/playwright/web/login.spec.ts b/playwright/web/login.spec.ts index 00ca97819b..52cdd5c127 100644 --- a/playwright/web/login.spec.ts +++ b/playwright/web/login.spec.ts @@ -18,5 +18,7 @@ test("login as non-member", async ({ page }) => { await page.waitForLoadState("load"); - await expect(page.getByText("Grunn: NOT_MEMBER_OF_ECHO")).toBeVisible(); + await expect(page.getByTestId("callout")).toHaveText( + "Noe gikk galt. Dette kan være grunnet til at vi ikke automatisk får til å finne ut om du er medlem.", + ); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35a3e8454f..636cb2674e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,49 @@ importers: specifier: 5.5.4 version: 5.5.4 + apps/ars: + dependencies: + '@echo-webkom/db': + specifier: workspace:* + version: link:../../packages/db + '@hono/node-server': + specifier: 1.12.2 + version: 1.12.2(hono@4.5.9) + date-fns: + specifier: 3.6.0 + version: 3.6.0 + drizzle-orm: + specifier: 0.33.0 + version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) + hono: + specifier: 4.5.9 + version: 4.5.9 + zod: + specifier: 3.23.8 + version: 3.23.8 + devDependencies: + '@flydotio/dockerfile': + specifier: 0.5.8 + version: 0.5.8 + '@types/node': + specifier: 20.16.1 + version: 20.16.1 + dotenv-cli: + specifier: 7.4.2 + version: 7.4.2 + esbuild: + specifier: 0.23.1 + version: 0.23.1 + tsx: + specifier: 4.17.0 + version: 4.17.0 + typescript: + specifier: 5.5.4 + version: 5.5.4 + vitest: + specifier: 2.0.5 + version: 2.0.5(@types/node@20.16.1)(jsdom@24.1.1) + apps/cms: dependencies: '@echo-webkom/lib': @@ -112,7 +155,7 @@ importers: specifier: workspace:* version: link:../../packages/lib '@echo-webkom/sanity': - specifier: workspace:^ + specifier: workspace:* version: link:../../packages/sanity '@hookform/resolvers': specifier: 3.9.0 @@ -188,7 +231,7 @@ importers: version: 3.6.0 drizzle-orm: specifier: 0.33.0 - version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) + version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) framer-motion: specifier: 11.3.29 version: 11.3.29(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -321,7 +364,7 @@ importers: version: link:../db drizzle-orm: specifier: 0.33.0 - version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) + version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) next: specifier: 14.2.6 version: 14.2.6(@babel/core@7.24.6)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -399,10 +442,10 @@ importers: dependencies: drizzle-orm: specifier: 0.33.0 - version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) + version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) drizzle-zod: specifier: 0.5.1 - version: 0.5.1(drizzle-orm@0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8) + version: 0.5.1(drizzle-orm@0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8) nanoid: specifier: 5.0.7 version: 5.0.7 @@ -553,7 +596,7 @@ importers: version: 12.1.0 drizzle-orm: specifier: 0.33.0 - version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) + version: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) groq: specifier: 3.54.0 version: 3.54.0 @@ -1508,8 +1551,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.23.0': - resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -1532,8 +1575,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.23.0': - resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -1556,8 +1599,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.23.0': - resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -1580,8 +1623,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.23.0': - resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -1604,8 +1647,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.23.0': - resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -1628,8 +1671,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.23.0': - resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -1652,8 +1695,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.23.0': - resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -1676,8 +1719,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.0': - resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -1700,8 +1743,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.23.0': - resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -1724,8 +1767,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.23.0': - resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -1748,8 +1791,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.23.0': - resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1772,8 +1815,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.23.0': - resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1796,8 +1839,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.23.0': - resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1820,8 +1863,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.23.0': - resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1844,8 +1887,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.23.0': - resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1868,8 +1911,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.23.0': - resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1892,8 +1935,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.23.0': - resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -1916,14 +1959,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.23.0': - resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.23.0': - resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1946,8 +1989,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.0': - resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -1970,8 +2013,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.23.0': - resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1994,8 +2037,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.23.0': - resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2018,8 +2061,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.23.0': - resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2042,8 +2085,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.23.0': - resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2087,6 +2130,17 @@ packages: '@floating-ui/utils@0.2.1': resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + '@flydotio/dockerfile@0.5.8': + resolution: {integrity: sha512-119l9s4xUEATRWTQPddE0f+jZjVBhpnJiGA/aHzDiOUet2L7SD3G5jbSGvOXJm1Ks3kGmxUcPdt3BpLQRef+ow==} + engines: {node: '>=16.0.0'} + hasBin: true + + '@hono/node-server@1.12.2': + resolution: {integrity: sha512-xjzhqhSWUE/OhN0g3KCNVzNsQMlFUAL+/8GgPUr3TKcU7cvgZVBGswFofJ8WwGEHTqobzze1lDpGJl9ZNckDhA==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hookform/resolvers@3.9.0': resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} peerDependencies: @@ -3659,6 +3713,9 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/node@20.12.14': + resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} + '@types/node@20.16.1': resolution: {integrity: sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==} @@ -3731,6 +3788,9 @@ packages: '@types/uuid@8.3.4': resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@typescript-eslint/eslint-plugin@7.18.0': resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4294,6 +4354,9 @@ packages: builtins@1.0.3: resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} + bun-types@1.1.26: + resolution: {integrity: sha512-n7jDe62LsB2+WE8Q8/mT3azkPaatKlj/2MyP6hi3mKvPz9oPpB6JW/Ll6JHtNLudasFFuvfgklYSE+rreGvBjw==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -4819,6 +4882,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + diffie-hellman@5.0.3: resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} @@ -5011,6 +5078,11 @@ packages: engines: {node: '>=14'} hasBin: true + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + electron-to-chromium@1.4.748: resolution: {integrity: sha512-VWqjOlPZn70UZ8FTKUOkUvBLeTQ0xpty66qV0yJcAGY2/CthI4xyW9aEozRVtuwv3Kpf5xTesmJUcPwuJmgP4A==} @@ -5094,8 +5166,8 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.23.0: - resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} hasBin: true @@ -5343,6 +5415,9 @@ packages: resolution: {integrity: sha512-x3989K8a1jM6vulMigE8VngH7C5nci0Ks5d9kVjUXmNF28gmiZUNujk5HjwaS8dAzN2QmUfX56riJKgN00dNRw==} engines: {node: '>=4'} + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + filesize@9.0.11: resolution: {integrity: sha512-gTAiTtI0STpKa5xesyTA9hA3LX4ga8sm2nWRcffEa1L/5vQwb4mj2MdzMkoHoGv4QzfDshQZuYscQSf8c4TKOA==} engines: {node: '>= 0.4.0'} @@ -5747,6 +5822,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.5.9: + resolution: {integrity: sha512-zz8ktqMDRrZETjxBrv8C5PQRFbrTRCLNVAjD1SNQyOzv4VjmX68Uxw83xQ6oxdAB60HiWnGEatiKA8V3SZLDkQ==} + engines: {node: '>=16.0.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -6124,6 +6203,11 @@ packages: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true @@ -8618,6 +8702,9 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -10771,7 +10858,7 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.23.0': + '@esbuild/aix-ppc64@0.23.1': optional: true '@esbuild/android-arm64@0.18.20': @@ -10783,7 +10870,7 @@ snapshots: '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.23.0': + '@esbuild/android-arm64@0.23.1': optional: true '@esbuild/android-arm@0.18.20': @@ -10795,7 +10882,7 @@ snapshots: '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.23.0': + '@esbuild/android-arm@0.23.1': optional: true '@esbuild/android-x64@0.18.20': @@ -10807,7 +10894,7 @@ snapshots: '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.23.0': + '@esbuild/android-x64@0.23.1': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -10819,7 +10906,7 @@ snapshots: '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.23.0': + '@esbuild/darwin-arm64@0.23.1': optional: true '@esbuild/darwin-x64@0.18.20': @@ -10831,7 +10918,7 @@ snapshots: '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.23.0': + '@esbuild/darwin-x64@0.23.1': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -10843,7 +10930,7 @@ snapshots: '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.23.0': + '@esbuild/freebsd-arm64@0.23.1': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -10855,7 +10942,7 @@ snapshots: '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.23.0': + '@esbuild/freebsd-x64@0.23.1': optional: true '@esbuild/linux-arm64@0.18.20': @@ -10867,7 +10954,7 @@ snapshots: '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.23.0': + '@esbuild/linux-arm64@0.23.1': optional: true '@esbuild/linux-arm@0.18.20': @@ -10879,7 +10966,7 @@ snapshots: '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.23.0': + '@esbuild/linux-arm@0.23.1': optional: true '@esbuild/linux-ia32@0.18.20': @@ -10891,7 +10978,7 @@ snapshots: '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.23.0': + '@esbuild/linux-ia32@0.23.1': optional: true '@esbuild/linux-loong64@0.18.20': @@ -10903,7 +10990,7 @@ snapshots: '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.23.0': + '@esbuild/linux-loong64@0.23.1': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -10915,7 +11002,7 @@ snapshots: '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.23.0': + '@esbuild/linux-mips64el@0.23.1': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -10927,7 +11014,7 @@ snapshots: '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.23.0': + '@esbuild/linux-ppc64@0.23.1': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -10939,7 +11026,7 @@ snapshots: '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.23.0': + '@esbuild/linux-riscv64@0.23.1': optional: true '@esbuild/linux-s390x@0.18.20': @@ -10951,7 +11038,7 @@ snapshots: '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.23.0': + '@esbuild/linux-s390x@0.23.1': optional: true '@esbuild/linux-x64@0.18.20': @@ -10963,7 +11050,7 @@ snapshots: '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.23.0': + '@esbuild/linux-x64@0.23.1': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -10975,10 +11062,10 @@ snapshots: '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.23.0': + '@esbuild/netbsd-x64@0.23.1': optional: true - '@esbuild/openbsd-arm64@0.23.0': + '@esbuild/openbsd-arm64@0.23.1': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -10990,7 +11077,7 @@ snapshots: '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.23.0': + '@esbuild/openbsd-x64@0.23.1': optional: true '@esbuild/sunos-x64@0.18.20': @@ -11002,7 +11089,7 @@ snapshots: '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.23.0': + '@esbuild/sunos-x64@0.23.1': optional: true '@esbuild/win32-arm64@0.18.20': @@ -11014,7 +11101,7 @@ snapshots: '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.23.0': + '@esbuild/win32-arm64@0.23.1': optional: true '@esbuild/win32-ia32@0.18.20': @@ -11026,7 +11113,7 @@ snapshots: '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.23.0': + '@esbuild/win32-ia32@0.23.1': optional: true '@esbuild/win32-x64@0.18.20': @@ -11038,7 +11125,7 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.23.0': + '@esbuild/win32-x64@0.23.1': optional: true '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': @@ -11093,6 +11180,18 @@ snapshots: '@floating-ui/utils@0.2.1': {} + '@flydotio/dockerfile@0.5.8': + dependencies: + chalk: 5.3.0 + diff: 5.2.0 + ejs: 3.1.10 + shell-quote: 1.8.1 + yargs: 17.7.2 + + '@hono/node-server@1.12.2(hono@4.5.9)': + dependencies: + hono: 4.5.9 + '@hookform/resolvers@3.9.0(react-hook-form@7.52.1(react@18.3.1))': dependencies: react-hook-form: 7.52.1(react@18.3.1) @@ -12895,6 +12994,11 @@ snapshots: '@types/ms@0.7.34': {} + '@types/node@20.12.14': + dependencies: + undici-types: 5.26.5 + optional: true + '@types/node@20.16.1': dependencies: undici-types: 6.19.8 @@ -12975,6 +13079,11 @@ snapshots: '@types/uuid@8.3.4': {} + '@types/ws@8.5.12': + dependencies: + '@types/node': 20.16.1 + optional: true + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.10.0 @@ -13717,6 +13826,12 @@ snapshots: builtins@1.0.3: {} + bun-types@1.1.26: + dependencies: + '@types/node': 20.12.14 + '@types/ws': 8.5.12 + optional: true + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -14280,6 +14395,8 @@ snapshots: didyoumean@1.2.2: {} + diff@5.2.0: {} + diffie-hellman@5.0.3: dependencies: bn.js: 4.12.0 @@ -14361,17 +14478,18 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1): + drizzle-orm@0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1): optionalDependencies: '@types/pg': 8.11.6 '@types/react': 18.3.4 + bun-types: 1.1.26 pg: 8.12.0 postgres: 3.4.4 react: 18.3.1 - drizzle-zod@0.5.1(drizzle-orm@0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8): + drizzle-zod@0.5.1(drizzle-orm@0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8): dependencies: - drizzle-orm: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) + drizzle-orm: 0.33.0(@types/pg@8.11.6)(@types/react@18.3.4)(bun-types@1.1.26)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) zod: 3.23.8 duplexify@3.7.1: @@ -14405,6 +14523,10 @@ snapshots: minimatch: 9.0.1 semver: 7.6.0 + ejs@3.1.10: + dependencies: + jake: 10.9.2 + electron-to-chromium@1.4.748: {} electron-to-chromium@1.5.5: {} @@ -14645,32 +14767,32 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.23.0: + esbuild@0.23.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.23.0 - '@esbuild/android-arm': 0.23.0 - '@esbuild/android-arm64': 0.23.0 - '@esbuild/android-x64': 0.23.0 - '@esbuild/darwin-arm64': 0.23.0 - '@esbuild/darwin-x64': 0.23.0 - '@esbuild/freebsd-arm64': 0.23.0 - '@esbuild/freebsd-x64': 0.23.0 - '@esbuild/linux-arm': 0.23.0 - '@esbuild/linux-arm64': 0.23.0 - '@esbuild/linux-ia32': 0.23.0 - '@esbuild/linux-loong64': 0.23.0 - '@esbuild/linux-mips64el': 0.23.0 - '@esbuild/linux-ppc64': 0.23.0 - '@esbuild/linux-riscv64': 0.23.0 - '@esbuild/linux-s390x': 0.23.0 - '@esbuild/linux-x64': 0.23.0 - '@esbuild/netbsd-x64': 0.23.0 - '@esbuild/openbsd-arm64': 0.23.0 - '@esbuild/openbsd-x64': 0.23.0 - '@esbuild/sunos-x64': 0.23.0 - '@esbuild/win32-arm64': 0.23.0 - '@esbuild/win32-ia32': 0.23.0 - '@esbuild/win32-x64': 0.23.0 + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 escalade@3.1.2: {} @@ -15005,6 +15127,10 @@ snapshots: file-url@2.0.2: {} + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + filesize@9.0.11: {} fill-range@7.0.1: @@ -15536,6 +15662,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.5.9: {} + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: @@ -15869,6 +15997,13 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jake@10.9.2: + dependencies: + async: 3.2.5 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + jiti@1.21.0: {} jose@4.15.5: {} @@ -18868,7 +19003,7 @@ snapshots: tsx@4.17.0: dependencies: - esbuild: 0.23.0 + esbuild: 0.23.1 get-tsconfig: 4.7.5 optionalDependencies: fsevents: 2.3.3 @@ -18980,6 +19115,9 @@ snapshots: buffer: 5.7.1 through: 2.3.8 + undici-types@5.26.5: + optional: true + undici-types@6.19.8: {} unicode-canonical-property-names-ecmascript@2.0.0: {} diff --git a/turbo.json b/turbo.json index f2ffd733a8..21d492f999 100644 --- a/turbo.json +++ b/turbo.json @@ -74,6 +74,7 @@ "NEXT_PUBLIC_ECHOGRAM_URL", "ECHOGRAM_API_KEY", "DATABASE_LOG", - "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA" + "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA", + "NEXT_PUBLIC_ARS_URL" ] }