diff --git a/.gitignore b/.gitignore index 705c5c9..7188be1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ deploy-template/* !deploy-template/compose.yml # generated prisma client -/prisma/client \ No newline at end of file +/prisma/client +/prisma/validate \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index ff77811..11c26e2 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -9,7 +9,7 @@ const dropVersion = process.env.BUILD_DROP_VERSION ?? "v0.3.0-alpha.1"; const commitHash = process.env.BUILD_GIT_REF ?? execSync("git rev-parse --short HEAD").toString().trim(); - + console.log(`Building Drop ${dropVersion} #${commitHash}`); // https://nuxt.com/docs/api/configuration/nuxt-config @@ -79,6 +79,7 @@ export default defineNuxtConfig({ openAPI: { // tracking for dynamic openapi schema https://github.com/nitrojs/nitro/issues/2974 + // create body from types: https://github.com/nitrojs/nitro/issues/3275 meta: { title: "Drop", description: diff --git a/package.json b/package.json index b9a52f9..86724d9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@drop-oss/droplet": "^0.7.2", "@headlessui/vue": "^1.7.23", "@heroicons/vue": "^2.1.5", + "@lobomfz/prismark": "0.0.3", "@nuxt/fonts": "^0.11.0", "@nuxt/image": "^1.10.0", "@prisma/client": "^6.7.0", diff --git a/pages/auth/register.vue b/pages/auth/register.vue index 3ee5bd4..14fd641 100644 --- a/pages/auth/register.vue +++ b/pages/auth/register.vue @@ -211,17 +211,19 @@ const confirmPassword = ref(undefined); const emailValidator = type("string.email"); const validEmail = computed( - () => !(emailValidator(email.value) instanceof type.errors), + () => !((emailValidator(email.value) as unknown) instanceof type.errors), ); const usernameValidator = type("string.alphanumeric >= 5").to("string.lower"); const validUsername = computed( - () => !(usernameValidator(username.value) instanceof type.errors), + () => + !((usernameValidator(username.value) as unknown) instanceof type.errors), ); const passwordValidator = type("string >= 14"); const validPassword = computed( - () => !(passwordValidator(password.value) instanceof type.errors), + () => + !((passwordValidator(password.value) as unknown) instanceof type.errors), ); const validConfirmPassword = computed( () => password.value == confirmPassword.value, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7765176..b6493a8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,6 +9,15 @@ generator client { binaryTargets = ["native", "debian-openssl-3.0.x"] } +/** + * generator arktype { + * provider = "yarn prismark" + * output = "./validate" + * fileName = "schema.ts" + * nullish = true + * } + */ + datasource db { provider = "postgresql" url = env("DATABASE_URL") diff --git a/server/api/v1/admin/auth/invitation/index.delete.ts b/server/api/v1/admin/auth/invitation/index.delete.ts index 34d24bd..4e8760c 100644 --- a/server/api/v1/admin/auth/invitation/index.delete.ts +++ b/server/api/v1/admin/auth/invitation/index.delete.ts @@ -1,10 +1,11 @@ import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; const DeleteInvite = type({ id: "string", -}); +}).configure(throwingArktype); export default defineEventHandler<{ body: typeof DeleteInvite.infer; @@ -14,16 +15,7 @@ export default defineEventHandler<{ ]); if (!allowed) throw createError({ statusCode: 403 }); - const body = DeleteInvite(await readBody(h3)); - if (body instanceof type.errors) { - // hover out.summary to see validation errors - console.error(body.summary); - - throw createError({ - statusCode: 400, - statusMessage: body.summary, - }); - } + const body = await readValidatedBody(h3, DeleteInvite); await prisma.invitation.delete({ where: { id: body.id } }); return {}; diff --git a/server/api/v1/admin/auth/invitation/index.post.ts b/server/api/v1/admin/auth/invitation/index.post.ts index 58305c2..2ae9415 100644 --- a/server/api/v1/admin/auth/invitation/index.post.ts +++ b/server/api/v1/admin/auth/invitation/index.post.ts @@ -1,13 +1,14 @@ import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; const CreateInvite = type({ - isAdmin: "boolean", - username: "string", - email: "string.email", + isAdmin: "boolean?", + username: "string?", + email: "string.email?", expires: "string.date.iso.parse", -}); +}).configure(throwingArktype); export default defineEventHandler<{ body: typeof CreateInvite.infer; @@ -17,16 +18,7 @@ export default defineEventHandler<{ ]); if (!allowed) throw createError({ statusCode: 403 }); - const body = CreateInvite(await readBody(h3)); - if (body instanceof type.errors) { - // hover out.summary to see validation errors - console.error(body.summary); - - throw createError({ - statusCode: 400, - statusMessage: body.summary, - }); - } + const body = await readValidatedBody(h3, CreateInvite); const invitation = await prisma.invitation.create({ data: body, diff --git a/server/api/v1/admin/game/image/index.delete.ts b/server/api/v1/admin/game/image/index.delete.ts index 70c7817..fe9a650 100644 --- a/server/api/v1/admin/game/image/index.delete.ts +++ b/server/api/v1/admin/game/image/index.delete.ts @@ -2,11 +2,12 @@ import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; import objectHandler from "~/server/internal/objects"; import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; const DeleteGameImage = type({ gameId: "string", imageId: "string", -}); +}).configure(throwingArktype); export default defineEventHandler<{ body: typeof DeleteGameImage.infer; @@ -14,14 +15,8 @@ export default defineEventHandler<{ const allowed = await aclManager.allowSystemACL(h3, ["game:image:delete"]); if (!allowed) throw createError({ statusCode: 403 }); - const body = DeleteGameImage(await readBody(h3)); - if (body instanceof type.errors) { - console.error(h3.path, body.summary); - throw createError({ - statusCode: 400, - statusMessage: body.summary, - }); - } + const body = await readValidatedBody(h3, DeleteGameImage); + const gameId = body.gameId; const imageId = body.imageId; diff --git a/server/api/v1/admin/game/index.delete.ts b/server/api/v1/admin/game/index.delete.ts index 6775177..730763c 100644 --- a/server/api/v1/admin/game/index.delete.ts +++ b/server/api/v1/admin/game/index.delete.ts @@ -1,7 +1,7 @@ import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; -export default defineEventHandler(async (h3) => { +export default defineEventHandler<{ query: { id: string } }>(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]); if (!allowed) throw createError({ statusCode: 403 }); diff --git a/server/api/v1/admin/game/metadata.post.ts b/server/api/v1/admin/game/metadata.post.ts index df64c04..40ae5ad 100644 --- a/server/api/v1/admin/game/metadata.post.ts +++ b/server/api/v1/admin/game/metadata.post.ts @@ -3,6 +3,7 @@ import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; + export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, ["game:update"]); if (!allowed) throw createError({ statusCode: 403 }); diff --git a/server/api/v1/admin/game/version/index.delete.ts b/server/api/v1/admin/game/version/index.delete.ts index 79aa04d..08d182b 100644 --- a/server/api/v1/admin/game/version/index.delete.ts +++ b/server/api/v1/admin/game/version/index.delete.ts @@ -1,27 +1,34 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; -export default defineEventHandler(async (h3) => { - const allowed = await aclManager.allowSystemACL(h3, ["game:version:delete"]); - if (!allowed) throw createError({ statusCode: 403 }); +const DeleteVersion = type({ + id: "string", + versionName: "string", +}).configure(throwingArktype); - const body = await readBody(h3); - const gameId = body.id.toString(); - const version = body.versionName.toString(); - if (!gameId || !version) - throw createError({ - statusCode: 400, - statusMessage: "Missing ID or versionName in body", +export default defineEventHandler<{ body: typeof DeleteVersion }>( + async (h3) => { + const allowed = await aclManager.allowSystemACL(h3, [ + "game:version:delete", + ]); + if (!allowed) throw createError({ statusCode: 403 }); + + const body = await readValidatedBody(h3, DeleteVersion); + + const gameId = body.id.toString(); + const version = body.versionName.toString(); + + await prisma.gameVersion.delete({ + where: { + gameId_versionName: { + gameId: gameId, + versionName: version, + }, + }, }); - await prisma.gameVersion.delete({ - where: { - gameId_versionName: { - gameId: gameId, - versionName: version, - }, - }, - }); - - return {}; -}); + return {}; + }, +); diff --git a/server/api/v1/admin/game/version/index.patch.ts b/server/api/v1/admin/game/version/index.patch.ts index 7509cd5..009c502 100644 --- a/server/api/v1/admin/game/version/index.patch.ts +++ b/server/api/v1/admin/game/version/index.patch.ts @@ -1,41 +1,47 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; -export default defineEventHandler(async (h3) => { - const allowed = await aclManager.allowSystemACL(h3, ["game:version:update"]); - if (!allowed) throw createError({ statusCode: 403 }); +const UpdateVersionOrder = type({ + id: "string", + versions: "string[]", +}).configure(throwingArktype); - const body = await readBody(h3); - const gameId = body.id?.toString(); - // We expect an array of the version names for this game - const versions: string[] | undefined = body.versions; - if (!gameId || !versions || !Array.isArray(versions)) - throw createError({ - statusCode: 400, - statusMessage: "Missing id, versions or versions is not an array", - }); +export default defineEventHandler<{ body: typeof UpdateVersionOrder }>( + async (h3) => { + const allowed = await aclManager.allowSystemACL(h3, [ + "game:version:update", + ]); + if (!allowed) throw createError({ statusCode: 403 }); - const newVersions = await prisma.$transaction( - versions.map((versionName, versionIndex) => - prisma.gameVersion.update({ - where: { - gameId_versionName: { - gameId: gameId, - versionName: versionName, + const body = await readValidatedBody(h3, UpdateVersionOrder); + const gameId = body.id; + // We expect an array of the version names for this game + const versions = body.versions; + + const newVersions = await prisma.$transaction( + versions.map((versionName, versionIndex) => + prisma.gameVersion.update({ + where: { + gameId_versionName: { + gameId: gameId, + versionName: versionName, + }, }, - }, - data: { - versionIndex: versionIndex, - }, - select: { - versionIndex: true, - versionName: true, - platform: true, - delta: true, - }, - }), - ), - ); + data: { + versionIndex: versionIndex, + }, + select: { + versionIndex: true, + versionName: true, + platform: true, + delta: true, + }, + }), + ), + ); - return newVersions; -}); + return newVersions; + }, +); diff --git a/server/api/v1/admin/import/version/index.post.ts b/server/api/v1/admin/import/version/index.post.ts index 6ec7b3e..90a5861 100644 --- a/server/api/v1/admin/import/version/index.post.ts +++ b/server/api/v1/admin/import/version/index.post.ts @@ -1,33 +1,39 @@ +import { type } from "arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; import libraryManager from "~/server/internal/library"; import { parsePlatform } from "~/server/internal/utils/parseplatform"; +const ImportVersion = type({ + id: "string", + version: "string", + + platform: "string", + launch: "string?", + launchArgs: "string?", + setup: "string?", + setupArgs: "string?", + onlySetup: "boolean?", + delta: "boolean?", + umuId: "string?", +}); + export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, ["import:version:new"]); if (!allowed) throw createError({ statusCode: 403 }); - const body = await readBody(h3); + const body = await readValidatedBody(h3, ImportVersion); const gameId = body.id; const versionName = body.version; - const platform = body.platform as string | undefined; - const launch = (body.launch ?? "") as string; - const launchArgs = (body.launchArgs ?? "") as string; - const setup = (body.setup ?? "") as string; - const setupArgs = (body.setupArgs ?? "") as string; - const onlySetup = body.onlySetup ?? (false as boolean); - const delta = (body.delta ?? false) as boolean; - const umuId = (body.umuId ?? "") as string; - - if (!gameId || !versionName) - throw createError({ - statusCode: 400, - statusMessage: "Game ID and version are required.", - }); - - if (!platform) - throw createError({ statusCode: 400, statusMessage: "Missing platform." }); + const platform = body.platform; + const launch = body.launch ?? ""; + const launchArgs = body.launchArgs ?? ""; + const setup = body.setup ?? ""; + const setupArgs = body.setupArgs ?? ""; + const onlySetup = body.onlySetup ?? false; + const delta = body.delta ?? false; + const umuId = body.umuId ?? ""; const platformParsed = parsePlatform(platform); if (!platformParsed) diff --git a/server/api/v1/client/chunk.get.ts b/server/api/v1/client/chunk.get.ts index bc78468..b24325a 100644 --- a/server/api/v1/client/chunk.get.ts +++ b/server/api/v1/client/chunk.get.ts @@ -59,5 +59,5 @@ export default defineEventHandler(async (h3) => { const gameReadStream = fs.createReadStream(gameFile, { start, end: end - 1 }); // end needs to be offset by 1 - return gameReadStream; + return sendStream(h3, gameReadStream); }); diff --git a/server/arktype.ts b/server/arktype.ts new file mode 100644 index 0000000..8acffd9 --- /dev/null +++ b/server/arktype.ts @@ -0,0 +1,14 @@ +import { configure } from "arktype/config"; + +export const throwingArktype = configure({ + onFail: (errors) => errors.throw(), + actual: () => "" +}); + +// be sure to specify both the runtime and static configs + +declare global { + interface ArkEnv { + onFail: typeof throwingArktype.onFail; + } +} diff --git a/yarn.lock b/yarn.lock index 634ac27..ee35fef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,11 +34,23 @@ dependencies: "@ark/util" "0.45.5" +"@ark/schema@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@ark/schema/-/schema-0.46.0.tgz#81a1a0dc1ff0f2faa098cba05de505a174bdc64e" + integrity sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ== + dependencies: + "@ark/util" "0.46.0" + "@ark/util@0.45.5": version "0.45.5" resolved "https://registry.yarnpkg.com/@ark/util/-/util-0.45.5.tgz#7742fa3973bfeea253442a44b51a0ea492c43e51" integrity sha512-BbuDOKJJYZ2dzZDNhBavkhgGD2hv4/idEhXpEPA5weOlE9/kU7XwAvEWn74tCuySHrD7peDqe2Ct/EDUlM40qg== +"@ark/util@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@ark/util/-/util-0.46.0.tgz#aee240bdaf413793e5ca4c4e8e3707aa965f4be3" + integrity sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg== + "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" @@ -752,6 +764,14 @@ resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== +"@lobomfz/prismark@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@lobomfz/prismark/-/prismark-0.0.3.tgz#8b4eb34963e96cf3fff01432e62de22f184f2f2d" + integrity sha512-g2xfR/F+sRBRUhWYlpUkafqZjqsQBetjfzdWvQndRU4wdoavn3zblM3OQwb7vrsrKB6Wmbs+DtLGaD5XBQ2v8A== + dependencies: + "@prisma/generator-helper" "^6.5.0" + arktype "^2.1.9" + "@mapbox/node-pre-gyp@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz#16d1d9049c0218820da81a12ae084e7fe67790d1" @@ -1390,6 +1410,16 @@ resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-6.7.0.tgz#a955174e4f481b0ca5d7c0fd458ef848432afd59" integrity sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w== +"@prisma/debug@6.8.2": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-6.8.2.tgz#59fb9e0ccb0f431fe7011c36c95f9bfcbe051749" + integrity sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg== + +"@prisma/dmmf@6.8.2": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@prisma/dmmf/-/dmmf-6.8.2.tgz#46632cfeda537b37f616febc68fb1ea7ef656fe4" + integrity sha512-okGJF/7hQZam/2wt+Y0hPyNxyY5S+L0FzAgtL932Q3YxUWHusRllrN39bCV45oF3QWY992g7rTWYdL2Rynt1qg== + "@prisma/engines-version@6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed": version "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz#4ec7c8ee187924a910ec244e3790412d1069d723" @@ -1414,6 +1444,20 @@ "@prisma/engines-version" "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed" "@prisma/get-platform" "6.7.0" +"@prisma/generator-helper@^6.5.0": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@prisma/generator-helper/-/generator-helper-6.8.2.tgz#09a72522384690223d4a047f71667eeb36d5ae70" + integrity sha512-KBLW47sbwFBKKYMiICIAEWsG6TdpPapPT7e7hpmpF3xMgYAm6YIXu4JGwfQVDY9Vbcb+0vPdPdSEQtInYOOe5g== + dependencies: + "@prisma/debug" "6.8.2" + "@prisma/dmmf" "6.8.2" + "@prisma/generator" "6.8.2" + +"@prisma/generator@6.8.2": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@prisma/generator/-/generator-6.8.2.tgz#6b523cc9107293d848cd59482271806dafe629bc" + integrity sha512-yExkvgqiKg1WHzjYttz40g5DsOtud8RhapM0Mum6pw+wrDoQyhSAxs5NHyFCV+9VPvRd2v+jAP2CTT07bsibjw== + "@prisma/get-platform@6.7.0": version "6.7.0" resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-6.7.0.tgz#967f21f8ad966b941dcb47b153b84958655390d1" @@ -2376,6 +2420,14 @@ arktype@^2.1.10: "@ark/schema" "0.45.5" "@ark/util" "0.45.5" +arktype@^2.1.9: + version "2.1.20" + resolved "https://registry.yarnpkg.com/arktype/-/arktype-2.1.20.tgz#dd46726b0faf23c2753369876c77bb037e7089d9" + integrity sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q== + dependencies: + "@ark/schema" "0.46.0" + "@ark/util" "0.46.0" + ast-kit@^1.0.1, ast-kit@^1.4.0: version "1.4.2" resolved "https://registry.yarnpkg.com/ast-kit/-/ast-kit-1.4.2.tgz#75d3706c12549573c7de9e7c38ed04fb7d0cc10f"