From 6853383e8628deea43d01a1804eefa858cdbb970 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 20 Aug 2025 11:50:59 +1000 Subject: [PATCH 01/22] feat: database redist support --- .../migration.sql | 35 +++ .../migration.sql | 14 ++ prisma/models/app.prisma | 3 +- prisma/models/content.prisma | 204 +++++------------- prisma/models/metadata.prisma | 117 ++++++++++ server/internal/library/REDIST-README.md | 3 + server/internal/library/index.ts | 103 +++++---- server/internal/metadata/index.ts | 13 +- 8 files changed, 283 insertions(+), 209 deletions(-) create mode 100644 prisma/migrations/20250819230647_add_redist_objects/migration.sql create mode 100644 prisma/migrations/20250820000344_add_library_key/migration.sql create mode 100644 prisma/models/metadata.prisma create mode 100644 server/internal/library/REDIST-README.md diff --git a/prisma/migrations/20250819230647_add_redist_objects/migration.sql b/prisma/migrations/20250819230647_add_redist_objects/migration.sql new file mode 100644 index 000000000..128469290 --- /dev/null +++ b/prisma/migrations/20250819230647_add_redist_objects/migration.sql @@ -0,0 +1,35 @@ +-- DropIndex +DROP INDEX "GameTag_name_idx"; + +-- CreateTable +CREATE TABLE "RedistVersion" ( + "redistId" TEXT NOT NULL, + "versionName" TEXT NOT NULL, + "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "platform" "Platform" NOT NULL, + "dropletManifest" JSONB NOT NULL, + + CONSTRAINT "RedistVersion_pkey" PRIMARY KEY ("redistId","versionName") +); + +-- CreateTable +CREATE TABLE "Redist" ( + "id" TEXT NOT NULL, + "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "iconObjectId" TEXT NOT NULL, + "libraryId" TEXT NOT NULL, + "libraryPath" TEXT NOT NULL, + + CONSTRAINT "Redist_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); + +-- AddForeignKey +ALTER TABLE "RedistVersion" ADD CONSTRAINT "RedistVersion_redistId_fkey" FOREIGN KEY ("redistId") REFERENCES "Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Redist" ADD CONSTRAINT "Redist_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250820000344_add_library_key/migration.sql b/prisma/migrations/20250820000344_add_library_key/migration.sql new file mode 100644 index 000000000..b1027c6e3 --- /dev/null +++ b/prisma/migrations/20250820000344_add_library_key/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - A unique constraint covering the columns `[libraryId,libraryPath]` on the table `Redist` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "GameTag_name_idx"; + +-- CreateIndex +CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); + +-- CreateIndex +CREATE UNIQUE INDEX "Redist_libraryId_libraryPath_key" ON "Redist"("libraryId", "libraryPath"); diff --git a/prisma/models/app.prisma b/prisma/models/app.prisma index 338af50ab..49f592539 100644 --- a/prisma/models/app.prisma +++ b/prisma/models/app.prisma @@ -28,5 +28,6 @@ model Library { backend LibraryBackend options Json - games Game[] + games Game[] + redists Redist[] } diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma index 3ab28be6f..e1830b5ec 100644 --- a/prisma/models/content.prisma +++ b/prisma/models/content.prisma @@ -1,184 +1,96 @@ -enum MetadataSource { - Manual - GiantBomb - PCGamingWiki - IGDB - Metacritic - OpenCritic -} - -model Game { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - created DateTime @default(now()) - - // Any field prefixed with m is filled in from metadata - // Acts as a cache so we can search and filter it - mName String // Name of game - mShortDescription String // Short description - mDescription String // Supports markdown - mReleased DateTime // When the game was released - - ratings GameRating[] - - featured Boolean @default(false) - - mIconObjectId String // linked to objects in s3 - mBannerObjectId String // linked to objects in s3 - mCoverObjectId String - mImageCarouselObjectIds String[] // linked to below array - mImageLibraryObjectIds String[] // linked to objects in s3 - - versions GameVersion[] - - // These fields will not be optional in the next version - // Any game without a library ID will be assigned one at startup, based on the defaults - libraryId String? - library Library? @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade) - libraryPath String - - collections CollectionEntry[] - saves SaveSlot[] - screenshots Screenshot[] - tags GameTag[] - playtime Playtime[] - - developers Company[] @relation(name: "developers") - publishers Company[] @relation(name: "publishers") - - @@unique([metadataSource, metadataId], name: "metadataKey") - @@unique([libraryId, libraryPath], name: "libraryKey") -} - -model GameTag { - id String @id @default(uuid()) - name String @unique - - games Game[] - - @@index([name(ops: raw("gist_trgm_ops(siglen=32)"))], type: Gist) -} +// A particular set of files that relate to the version +model GameVersion { + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + versionName String // Sub directory for the game files + created DateTime @default(now()) -model GameRating { - id String @id @default(uuid()) + platform Platform - metadataSource MetadataSource - metadataId String - created DateTime @default(now()) + launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine + launchArgs String[] + setupCommand String @default("") // Command to setup game (dependencies and such) + setupArgs String[] + onlySetup Boolean @default(false) - mReviewCount Int - mReviewRating Float // 0 to 1 + umuIdOverride String? - mReviewHref String? + dropletManifest Json // Results from droplet - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - gameId String + versionIndex Int + delta Boolean @default(false) - @@unique([metadataSource, metadataId], name: "metadataKey") + @@id([gameId, versionName]) } -// A particular set of files that relate to the version -model GameVersion { - gameId String - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - versionName String // Sub directory for the game files - - created DateTime @default(now()) - - platform Platform +model RedistVersion { + redistId String + redist Redist @relation(fields: [redistId], references: [id], onDelete: Cascade) + versionName String - launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine - launchArgs String[] - setupCommand String @default("") // Command to setup game (dependencies and such) - setupArgs String[] - onlySetup Boolean @default(false) + created DateTime @default(now()) - umuIdOverride String? + platform Platform - dropletManifest Json // Results from droplet + dropletManifest Json // Results from droplet - versionIndex Int - delta Boolean @default(false) - - @@id([gameId, versionName]) + @@id([redistId, versionName]) } // A save slot for a game model SaveSlot { - gameId String - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - index Int + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + index Int - createdAt DateTime @default(now()) - playtime Float @default(0) // hours + createdAt DateTime @default(now()) + playtime Float @default(0) // hours - lastUsedClientId String? - lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id]) + lastUsedClientId String? + lastUsedClient Client? @relation(fields: [lastUsedClientId], references: [id]) - historyObjectIds String[] // list of objects - historyChecksums String[] // list of hashes + historyObjectIds String[] // list of objects + historyChecksums String[] // list of hashes - @@id([gameId, userId, index], name: "id") + @@id([gameId, userId, index], name: "id") } model Screenshot { - id String @id @default(uuid()) + id String @id @default(uuid()) - gameId String - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - objectId String - private Boolean // if other users can see + objectId String + private Boolean // if other users can see - createdAt DateTime @default(now()) @db.Timestamptz(0) + createdAt DateTime @default(now()) @db.Timestamptz(0) - @@index([gameId, userId]) - @@index([userId]) + @@index([gameId, userId]) + @@index([userId]) } model Playtime { - gameId String - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - seconds Int // seconds user has spent playing the game - - updatedAt DateTime @updatedAt @db.Timestamptz(6) - createdAt DateTime @default(now()) @db.Timestamptz(6) - - @@id([gameId, userId]) - @@index([userId]) -} - -model Company { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - mName String - mShortDescription String - mDescription String - mLogoObjectId String - mBannerObjectId String - mWebsite String + seconds Int // seconds user has spent playing the game - developed Game[] @relation(name: "developers") - published Game[] @relation(name: "publishers") + updatedAt DateTime @updatedAt @db.Timestamptz(6) + createdAt DateTime @default(now()) @db.Timestamptz(6) - @@unique([metadataSource, metadataId], name: "metadataKey") + @@id([gameId, userId]) + @@index([userId]) } model ObjectHash { - id String @id - hash String + id String @id + hash String } diff --git a/prisma/models/metadata.prisma b/prisma/models/metadata.prisma new file mode 100644 index 000000000..9153558be --- /dev/null +++ b/prisma/models/metadata.prisma @@ -0,0 +1,117 @@ +enum MetadataSource { + Manual + GiantBomb + PCGamingWiki + IGDB + Metacritic + OpenCritic +} + +model Game { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + created DateTime @default(now()) + + // Any field prefixed with m is filled in from metadata + // Acts as a cache so we can search and filter it + mName String // Name of game + mShortDescription String // Short description + mDescription String // Supports markdown + mReleased DateTime // When the game was released + + ratings GameRating[] + + featured Boolean @default(false) + + mIconObjectId String // linked to objects in s3 + mBannerObjectId String // linked to objects in s3 + mCoverObjectId String + mImageCarouselObjectIds String[] // linked to below array + mImageLibraryObjectIds String[] // linked to objects in s3 + + versions GameVersion[] + + // These fields will not be optional in the next version + // Any game without a library ID will be assigned one at startup, based on the defaults + libraryId String? + library Library? @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade) + libraryPath String + + collections CollectionEntry[] + saves SaveSlot[] + screenshots Screenshot[] + tags GameTag[] + playtime Playtime[] + + developers Company[] @relation(name: "developers") + publishers Company[] @relation(name: "publishers") + + @@unique([metadataSource, metadataId], name: "metadataKey") + @@unique([libraryId, libraryPath], name: "libraryKey") +} + +model Redist { + id String @id @default(uuid()) + created DateTime @default(now()) + + name String + description String + iconObjectId String + + libraryId String + library Library @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade) + libraryPath String + + versions RedistVersion[] + + @@unique([libraryId, libraryPath], name: "libraryKey") +} + +model GameTag { + id String @id @default(uuid()) + name String @unique + + games Game[] + + @@index([name(ops: raw("gist_trgm_ops(siglen=32)"))], type: Gist) +} + +model GameRating { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + created DateTime @default(now()) + + mReviewCount Int + mReviewRating Float // 0 to 1 + + mReviewHref String? + + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + gameId String + + @@unique([metadataSource, metadataId], name: "metadataKey") +} + +model Company { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + metadataOriginalQuery String + + mName String + mShortDescription String + mDescription String + mLogoObjectId String + mBannerObjectId String + mWebsite String + + developed Game[] @relation(name: "developers") + published Game[] @relation(name: "publishers") + + @@unique([metadataSource, metadataId], name: "metadataKey") +} diff --git a/server/internal/library/REDIST-README.md b/server/internal/library/REDIST-README.md new file mode 100644 index 000000000..739a66d60 --- /dev/null +++ b/server/internal/library/REDIST-README.md @@ -0,0 +1,3 @@ +# Redistributables + +They are called 'redist' in the codebase/database models for brevity. Additionally, because they intentionally are drop-ins for games, they are added to all the functions that deal with games, without changing the names. \ No newline at end of file diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index 0a74dfe6e..236a913ab 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -13,7 +13,6 @@ import { parsePlatform } from "../utils/parseplatform"; import notificationSystem from "../notifications"; import { GameNotFoundError, type LibraryProvider } from "./provider"; import { logger } from "../logging"; -import type { GameModel } from "~/prisma/client/models"; import { createHash } from "node:crypto"; export function createGameImportTaskId(libraryId: string, libraryPath: string) { @@ -49,14 +48,15 @@ class LibraryManager { } async fetchGamesByLibrary() { - const results: { [key: string]: { [key: string]: GameModel } } = {}; + const results: { [key: string]: { [key: string]: boolean } } = {}; const games = await prisma.game.findMany({}); - for (const game of games) { - const libraryId = game.libraryId!; - const libraryPath = game.libraryPath!; + const redist = await prisma.redist.findMany({}); + for (const item of [...games, ...redist]) { + const libraryId = item.libraryId!; + const libraryPath = item.libraryPath!; results[libraryId] ??= {}; - results[libraryId][libraryPath] = game; + results[libraryId][libraryPath] = true; } return results; @@ -82,18 +82,31 @@ class LibraryManager { async fetchUnimportedGameVersions(libraryId: string, libraryPath: string) { const provider = this.libraries.get(libraryId); if (!provider) return undefined; - const game = await prisma.game.findUnique({ - where: { - libraryKey: { - libraryId, - libraryPath, + const game = + (await prisma.game.findUnique({ + where: { + libraryKey: { + libraryId, + libraryPath, + }, }, - }, - select: { - id: true, - versions: true, - }, - }); + select: { + id: true, + versions: true, + }, + })) ?? + (await prisma.redist.findUnique({ + where: { + libraryKey: { + libraryId, + libraryPath, + }, + }, + select: { + id: true, + versions: true, + }, + })); if (!game) return undefined; try { @@ -217,6 +230,10 @@ class LibraryManager { })) > 0; if (hasGame) return false; + const hasRedist = + (await prisma.redist.count({ where: { libraryId, libraryPath } })) > 0; + if (hasRedist) return false; + return true; } @@ -286,41 +303,23 @@ class LibraryManager { }); // Then, create the database object - if (metadata.onlySetup) { - await prisma.gameVersion.create({ - data: { - gameId: gameId, - versionName: versionName, - dropletManifest: manifest, - versionIndex: currentIndex, - delta: metadata.delta, - umuIdOverride: metadata.umuId, - platform: platform, - - onlySetup: true, - setupCommand: metadata.setup, - setupArgs: metadata.setupArgs.split(" "), - }, - }); - } else { - await prisma.gameVersion.create({ - data: { - gameId: gameId, - versionName: versionName, - dropletManifest: manifest, - versionIndex: currentIndex, - delta: metadata.delta, - umuIdOverride: metadata.umuId, - platform: platform, - - onlySetup: false, - setupCommand: metadata.setup, - setupArgs: metadata.setupArgs.split(" "), - launchCommand: metadata.launch, - launchArgs: metadata.launchArgs.split(" "), - }, - }); - } + await prisma.gameVersion.create({ + data: { + gameId: gameId, + versionName: versionName, + dropletManifest: manifest, + versionIndex: currentIndex, + delta: metadata.delta, + umuIdOverride: metadata.umuId, + platform: platform, + + onlySetup: metadata.onlySetup, + setupCommand: metadata.setup, + setupArgs: metadata.setupArgs.split(" "), + launchCommand: metadata.launch, + launchArgs: metadata.launchArgs.split(" "), + }, + }); logger.info("Successfully created version!"); diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index 88025e871..ac0381d88 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -18,7 +18,7 @@ import taskHandler, { wrapTaskContext } from "../tasks"; import { randomUUID } from "crypto"; import { fuzzy } from "fast-fuzzy"; import { logger } from "~/server/internal/logging"; -import { createGameImportTaskId } from "../library"; +import libraryManager, { createGameImportTaskId } from "../library"; import type { GameTagModel } from "~/prisma/client/models"; export class MissingMetadataProviderConfig extends Error { @@ -175,15 +175,8 @@ export class MetadataHandler { if (!provider) throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`); - const existing = await prisma.game.findUnique({ - where: { - metadataKey: { - metadataSource: provider.source(), - metadataId: result.id, - }, - }, - }); - if (existing) return undefined; + const okay = await libraryManager.checkUnimportedGamePath(libraryId, libraryPath); + if (!okay) return undefined; const gameId = randomUUID(); From 322af0b4ca2271c0f1e897b7b47e8e7d6e71947f Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 20 Aug 2025 20:35:50 +1000 Subject: [PATCH 02/22] feat: rearchitecture of database schemas, migration reset, and #180 --- components/GameEditor/Version.vue | 66 +- components/PlatformSelector.vue | 9 +- components/StoreView.vue | 3 +- composables/icons.ts | 8 +- composables/types.ts | 8 +- i18n/locales/en_us.json | 4 +- package.json | 2 +- pages/admin/library/[id]/import.vue | 334 +++++---- pages/store/[id]/index.vue | 3 +- .../migration.sql | 25 - .../migration.sql | 9 - .../migration.sql | 76 -- .../migration.sql | 24 - .../migration.sql | 16 - .../migration.sql | 12 - .../migration.sql | 2 - .../20241007065541_add_client/migration.sql | 15 - .../migration.sql | 19 - .../migration.sql | 12 - .../migration.sql | 25 - .../migration.sql | 25 - .../migration.sql | 12 - .../migration.sql | 18 - .../migration.sql | 8 - .../migration.sql | 14 - .../migration.sql | 10 - .../migration.sql | 8 - .../migration.sql | 9 - .../migration.sql | 8 - .../migration.sql | 9 - .../migration.sql | 7 - .../migration.sql | 14 - .../migration.sql | 14 - .../migration.sql | 23 - .../migration.sql | 9 - .../migration.sql | 8 - .../migration.sql | 18 - .../migration.sql | 2 - .../migration.sql | 5 - .../migration.sql | 8 - .../migration.sql | 2 - .../migration.sql | 5 - .../migration.sql | 11 - .../migration.sql | 16 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 6 - .../migration.sql | 29 - .../migration.sql | 28 - .../migration.sql | 5 - .../20250128102738_add_news/migration.sql | 16 - .../20250204010021_add_tokens/migration.sql | 15 - .../migration.sql | 5 - .../migration.sql | 8 - .../migration.sql | 14 - .../migration.sql | 20 - .../migration.sql | 52 -- .../migration.sql | 8 - .../migration.sql | 8 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 10 - .../migration.sql | 5 - .../migration.sql | 17 - .../migration.sql | 2 - .../migration.sql | 24 - .../migration.sql | 3 - .../migration.sql | 14 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 10 - .../migration.sql | 13 - .../migration.sql | 8 - .../migration.sql | 2 - .../migration.sql | 5 - .../migration.sql | 7 - .../migration.sql | 2 - .../migration.sql | 8 - .../migration.sql | 20 - .../migration.sql | 16 - .../migration.sql | 35 - .../migration.sql | 76 -- .../migration.sql | 16 - .../migration.sql | 11 - .../migration.sql | 41 -- .../migration.sql | 2 - .../migration.sql | 53 -- .../migration.sql | 17 - .../migration.sql | 8 - .../migration.sql | 14 - .../migration.sql | 8 - .../migration.sql | 41 -- .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 5 - .../migration.sql | 14 - .../20250721053514_add_featured/migration.sql | 2 - .../migration.sql | 11 - .../20250721062509_add_pg_trgm/migration.sql | 5 - .../migration.sql | 2 - .../migration.sql | 87 --- .../migration.sql | 11 - .../migration.sql | 15 - .../migration.sql | 35 - .../migration.sql | 14 - .../20250820044328_init/migration.sql | 666 ++++++++++++++++++ .../20250820044424_add_indexes/migration.sql | 5 + .../migration.sql | 8 + .../migration.sql | 35 + .../migration.sql | 42 ++ .../migration.sql | 35 + prisma/models/app.prisma | 11 +- prisma/models/content.prisma | 86 ++- prisma/models/metadata.prisma | 55 +- server/api/v1/admin/game/[id]/index.get.ts | 18 +- .../api/v1/admin/game/version/index.delete.ts | 11 +- .../api/v1/admin/game/version/index.patch.ts | 20 +- .../api/v1/admin/import/version/index.post.ts | 66 +- server/api/v1/client/game/manifest.get.ts | 7 +- server/api/v1/client/game/version.get.ts | 5 +- server/api/v1/client/game/versions.get.ts | 21 +- server/internal/downloads/coordinator.ts | 17 +- server/internal/downloads/manifest.ts | 48 +- server/internal/library/index.ts | 71 +- yarn.lock | 120 ++-- 125 files changed, 1374 insertions(+), 1827 deletions(-) delete mode 100644 prisma/migrations/20240928081254_create_user_and_auth_mechanisms/migration.sql delete mode 100644 prisma/migrations/20240928085121_move_to_json_for_credentials/migration.sql delete mode 100644 prisma/migrations/20240929000950_add_game_data/migration.sql delete mode 100644 prisma/migrations/20240929010842_updates_to_metadata_schema/migration.sql delete mode 100644 prisma/migrations/20241004020835_unique_constraints/migration.sql delete mode 100644 prisma/migrations/20241004025235_add_dev_pub_websites/migration.sql delete mode 100644 prisma/migrations/20241007043002_add_user_admin/migration.sql delete mode 100644 prisma/migrations/20241007065541_add_client/migration.sql delete mode 100644 prisma/migrations/20241008062519_remove_shared_token_and_add_last_connected/migration.sql delete mode 100644 prisma/migrations/20241009032354_add_account_details/migration.sql delete mode 100644 prisma/migrations/20241010062956_add_constraints/migration.sql delete mode 100644 prisma/migrations/20241010095344_various_fixes/migration.sql delete mode 100644 prisma/migrations/20241010104439_added_original_query_field/migration.sql delete mode 100644 prisma/migrations/20241010104722_fix_unique_constraints/migration.sql delete mode 100644 prisma/migrations/20241011035227_add_droplet_manifest_to_game_versions/migration.sql delete mode 100644 prisma/migrations/20241011093950_update_game_images_system/migration.sql delete mode 100644 prisma/migrations/20241011101243_revert_banner_system/migration.sql delete mode 100644 prisma/migrations/20241011103116_add_cover_image/migration.sql delete mode 100644 prisma/migrations/20241014052934_add_delta_and_order/migration.sql delete mode 100644 prisma/migrations/20241014053941_remove_version_order/migration.sql delete mode 100644 prisma/migrations/20241025091103_add_invitations/migration.sql delete mode 100644 prisma/migrations/20241102000813_create_application_configuration/migration.sql delete mode 100644 prisma/migrations/20241105221904_different_client_capabilities/migration.sql delete mode 100644 prisma/migrations/20241105222110_trackable_names_for_capabilities/migration.sql delete mode 100644 prisma/migrations/20241105225732_peer_api_configuration/migration.sql delete mode 100644 prisma/migrations/20241105230021_move_to_endpoint_configuration/migration.sql delete mode 100644 prisma/migrations/20241107080421_add_expiry_for_invitations/migration.sql delete mode 100644 prisma/migrations/20241116053120_add_notifications/migration.sql delete mode 100644 prisma/migrations/20241116054212_add_created_time_stamp_to_notifications/migration.sql delete mode 100644 prisma/migrations/20241122215933_add_created_timestamps_for_games/migration.sql delete mode 100644 prisma/migrations/20241124042825_add_released_date_for_the_game/migration.sql delete mode 100644 prisma/migrations/20241223022005_add_umu_id_to_game_version/migration.sql delete mode 100644 prisma/migrations/20241223100329_add_referential_deletion_for_game_versions/migration.sql delete mode 100644 prisma/migrations/20241223100418_update_to_prisma_6/migration.sql delete mode 100644 prisma/migrations/20241226065709_rename_custom_to_manual/migration.sql delete mode 100644 prisma/migrations/20241226230207_add_image_carousel/migration.sql delete mode 100644 prisma/migrations/20241227033610_move_image_carousel_to_image_ids/migration.sql delete mode 100644 prisma/migrations/20241230053403_add_args_and_only_setup/migration.sql delete mode 100644 prisma/migrations/20250103202348_add_collections/migration.sql delete mode 100644 prisma/migrations/20250109005948_use_collection_entry_to_ensure_unique_games/migration.sql delete mode 100644 prisma/migrations/20250128060446_casacad_e_delete_for_collection_entries/migration.sql delete mode 100644 prisma/migrations/20250128102738_add_news/migration.sql delete mode 100644 prisma/migrations/20250204010021_add_tokens/migration.sql delete mode 100644 prisma/migrations/20250204020918_add_collection_entry_casacade_delete/migration.sql delete mode 100644 prisma/migrations/20250208004345_add_api_token_name/migration.sql delete mode 100644 prisma/migrations/20250208005625_add_id_to_token/migration.sql delete mode 100644 prisma/migrations/20250211230021_ensure_non_null_launch_and_setup_commands/migration.sql delete mode 100644 prisma/migrations/20250309234300_news_articles/migration.sql delete mode 100644 prisma/migrations/20250309234801_make_tags_unique/migration.sql delete mode 100644 prisma/migrations/20250309234846_make_tokens_unique/migration.sql delete mode 100644 prisma/migrations/20250311073601_add_macos_as_a_platform/migration.sql delete mode 100644 prisma/migrations/20250312230736_add_metadata_providers_to_appconfig/migration.sql delete mode 100644 prisma/migrations/20250313042306_add_igdb_pcgamingwiki_metadata/migration.sql delete mode 100644 prisma/migrations/20250313053250_add_enable_fields_to_auth_and_users/migration.sql delete mode 100644 prisma/migrations/20250314153636_store_ca_and_session_in_db/migration.sql delete mode 100644 prisma/migrations/20250324014736_add_auth_mek_version/migration.sql delete mode 100644 prisma/migrations/20250401082200_add_save_slots/migration.sql delete mode 100644 prisma/migrations/20250401082605_add_save_slot_limits_to_application_settings/migration.sql delete mode 100644 prisma/migrations/20250401083942_rename_save_to_cloud_saves/migration.sql delete mode 100644 prisma/migrations/20250401084907_add_history_limit/migration.sql delete mode 100644 prisma/migrations/20250401085406_add_default_to_playtime/migration.sql delete mode 100644 prisma/migrations/20250401091937_add_history_and_hashes/migration.sql delete mode 100644 prisma/migrations/20250403233442_apply_store_changes/migration.sql delete mode 100644 prisma/migrations/20250405062945_make_last_accessed_optional_on_save_slots/migration.sql delete mode 100644 prisma/migrations/20250407090729_add_client_token_mode/migration.sql delete mode 100644 prisma/migrations/20250407091012_add_client_token_field_to_apitoken/migration.sql delete mode 100644 prisma/migrations/20250414002714_add_object_hash/migration.sql delete mode 100644 prisma/migrations/20250507120031_add_openid_authmek/migration.sql delete mode 100644 prisma/migrations/20250507223112_remove_authentication_option_from_applicationsettings/migration.sql delete mode 100644 prisma/migrations/20250508153613_add_screenshots/migration.sql delete mode 100644 prisma/migrations/20250508224553_cleanup_old_objects/migration.sql delete mode 100644 prisma/migrations/20250509003340_init_unified_company_metadata/migration.sql delete mode 100644 prisma/migrations/20250510013650_remove_devlopers_and_publishers/migration.sql delete mode 100644 prisma/migrations/20250511154134_add_tags_to_games/migration.sql delete mode 100644 prisma/migrations/20250514193830_allow_notification_nonce_reuse_per_user/migration.sql delete mode 100644 prisma/migrations/20250515021331_add_game_ratings/migration.sql delete mode 100644 prisma/migrations/20250515043254_add_acls_to_notifications/migration.sql delete mode 100644 prisma/migrations/20250601022736_add_database_library/migration.sql delete mode 100644 prisma/migrations/20250601032211_add_library_relation_to_game/migration.sql delete mode 100644 prisma/migrations/20250601032938_add_unique_constraint/migration.sql delete mode 100644 prisma/migrations/20250606013242_add_tasks_to_database/migration.sql delete mode 100644 prisma/migrations/20250606023802_add_name_to_task/migration.sql delete mode 100644 prisma/migrations/20250608010030_delete_user_cascade/migration.sql delete mode 100644 prisma/migrations/20250621205541_add_show_title_description_on_game_panel/migration.sql delete mode 100644 prisma/migrations/20250702102624_add_flat_fs_provider/migration.sql delete mode 100644 prisma/migrations/20250720070939_static_genres/migration.sql delete mode 100644 prisma/migrations/20250721053244_update_genre_names/migration.sql delete mode 100644 prisma/migrations/20250721053514_add_featured/migration.sql delete mode 100644 prisma/migrations/20250721061200_remove_genres/migration.sql delete mode 100644 prisma/migrations/20250721062509_add_pg_trgm/migration.sql delete mode 100644 prisma/migrations/20250721063518_add_index_for_tag_name/migration.sql delete mode 100644 prisma/migrations/20250721070713_split_game_and_news_tags/migration.sql delete mode 100644 prisma/migrations/20250801035031_add_delete_casacde/migration.sql delete mode 100644 prisma/migrations/20250815014713_use_task_id_started_composite_id/migration.sql delete mode 100644 prisma/migrations/20250819230647_add_redist_objects/migration.sql delete mode 100644 prisma/migrations/20250820000344_add_library_key/migration.sql create mode 100644 prisma/migrations/20250820044328_init/migration.sql create mode 100644 prisma/migrations/20250820044424_add_indexes/migration.sql create mode 100644 prisma/migrations/20250820050413_add_hidden_field_to_game_version/migration.sql create mode 100644 prisma/migrations/20250820095728_add_args_back/migration.sql create mode 100644 prisma/migrations/20250820101258_split_ids_into_separate_fields/migration.sql create mode 100644 prisma/migrations/20250820102120_add_delete_casacades/migration.sql diff --git a/components/GameEditor/Version.vue b/components/GameEditor/Version.vue index d2d38ad65..ecf3b269a 100644 --- a/components/GameEditor/Version.vue +++ b/components/GameEditor/Version.vue @@ -51,7 +51,11 @@ @update="() => updateVersionOrder()" > diff --git a/components/PlatformSelector.vue b/components/PlatformSelector.vue index 1168b044d..4b73c18a4 100644 --- a/components/PlatformSelector.vue +++ b/components/PlatformSelector.vue @@ -32,7 +32,7 @@ class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-950 ring-opacity-5 focus:outline-none sm:text-sm" > (); +const model = defineModel(); -const typedModel = computed({ +const typedModel = computed({ get() { return model.value || null; }, @@ -95,5 +96,5 @@ const typedModel = computed({ }, }); -const values = Object.fromEntries(Object.entries(PlatformClient)); +const values = Object.entries(Platform); diff --git a/components/StoreView.vue b/components/StoreView.vue index ea9bdaa7d..957c6f5fd 100644 --- a/components/StoreView.vue +++ b/components/StoreView.vue @@ -361,6 +361,7 @@ import { import type { SerializeObject } from "nitropack"; import type { GameModel, GameTagModel } from "~/prisma/client/models"; import MultiItemSelector from "./MultiItemSelector.vue"; +import { Platform } from "~/prisma/client/enums"; const { showGamePanelTextDecoration } = await $dropFetch(`/api/v1/settings`); const mobileFiltersOpen = ref(false); @@ -407,7 +408,7 @@ const options: Array = [ name: "Platform", param: "platform", multiple: true, - options: Object.values(PlatformClient).map((e) => ({ name: e, param: e })), + options: Object.values(Platform).map((e) => ({ name: e, param: e })), }, ...(props.extraOptions ?? []), ]; diff --git a/composables/icons.ts b/composables/icons.ts index 247cf8372..d56cfcfea 100644 --- a/composables/icons.ts +++ b/composables/icons.ts @@ -1,8 +1,8 @@ import { IconsLinuxLogo, IconsWindowsLogo, IconsMacLogo } from "#components"; -import { PlatformClient } from "./types"; +import { Platform } from "~/prisma/client/enums"; export const PLATFORM_ICONS = { - [PlatformClient.Linux]: IconsLinuxLogo, - [PlatformClient.Windows]: IconsWindowsLogo, - [PlatformClient.macOS]: IconsMacLogo, + [Platform.Linux]: IconsLinuxLogo, + [Platform.Windows]: IconsWindowsLogo, + [Platform.macOS]: IconsMacLogo, }; diff --git a/composables/types.ts b/composables/types.ts index 87b2a252f..7725de0f7 100644 --- a/composables/types.ts +++ b/composables/types.ts @@ -10,10 +10,4 @@ export type QuickActionNav = { icon: Component; notifications?: Ref; action: () => Promise; -}; - -export enum PlatformClient { - Windows = "Windows", - Linux = "Linux", - macOS = "macOS", -} +}; \ No newline at end of file diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index 2356acb90..f3f1523c1 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -293,8 +293,8 @@ "advancedOptions": "Advanced options", "import": "Import version", "installDir": "(install_dir)/", - "launchCmd": "Launch executable/command", - "launchDesc": "Executable to launch the game", + "launchCmd": "Launch executables/commands", + "launchDesc": "Executables to launch the game", "launchPlaceholder": "game.exe", "loadingVersion": "Loading version metadata…", "noAdv": "No advanced options for this configuration.", diff --git a/package.json b/package.json index 9eb9722c8..e25510e4b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "nuxt-security": "2.2.0", "pino": "^9.7.0", "pino-pretty": "^13.0.0", - "prisma": "^6.11.1", + "prisma": "^6.14.0", "sanitize-filename": "^1.6.3", "semver": "^7.7.1", "stream-mime-type": "^2.0.0", diff --git a/pages/admin/library/[id]/import.vue b/pages/admin/library/[id]/import.vue index 485a57c4a..f92d7f6ce 100644 --- a/pages/admin/library/[id]/import.vue +++ b/pages/admin/library/[id]/import.vue @@ -1,8 +1,9 @@ + + diff --git a/components/Import/Redist.vue b/components/Import/Redist.vue new file mode 100644 index 000000000..3e6d69c69 --- /dev/null +++ b/components/Import/Redist.vue @@ -0,0 +1,116 @@ + + + diff --git a/nuxt.config.ts b/nuxt.config.ts index 49cc4e378..5d23e9344 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -251,6 +251,7 @@ export default defineNuxtConfig({ "img-src": [ "'self'", "data:", + "blob:", "https://www.giantbomb.com", "https://images.pcgamingwiki.com", "https://images.igdb.com", diff --git a/pages/admin/library/[id]/import.vue b/pages/admin/library/g/[id]/import.vue similarity index 100% rename from pages/admin/library/[id]/import.vue rename to pages/admin/library/g/[id]/import.vue diff --git a/pages/admin/library/[id]/index.vue b/pages/admin/library/g/[id]/index.vue similarity index 100% rename from pages/admin/library/[id]/index.vue rename to pages/admin/library/g/[id]/index.vue diff --git a/pages/admin/library/import.vue b/pages/admin/library/import.vue index d6eede497..e78eb7c47 100644 --- a/pages/admin/library/import.vue +++ b/pages/admin/library/import.vue @@ -3,7 +3,7 @@ {{ $t("library.admin.import.selectGame") }} @@ -80,10 +80,93 @@ +
+ Nothing to import +
+ + + Import as + +
+ + + +
+

+ {{ importModes[currentImportMode].name }} +

+

+ {{ importModes[currentImportMode].description }} +

+
+
+ + +
+ + + + +
  • + {{ metadata.name }} + + + + +
  • +
    +
    +
    +
    +
    +
    -
    - -
    - - {{ $t("library.admin.import.withoutMetadata") }} - -
    - - -
    -
    - {{ $t("auth.signin.or") }} -
    -
    - - - -
    -
    - -
    -
    - -
    - - -
    -
    - - - {{ $t("library.admin.import.selectGameSearch") }} -
    - - - - {{ $t("library.admin.import.selectGamePlaceholder") }} - - - - - - - - -
  • - -
  • -
    -
    -
    -
    -
    -
    - {{ $t("library.admin.import.loading") }} - -
    - -
    -
    -
    -
    -
    -

    - {{ gameSearchResultsError }} -

    -
    -
    -
    - -
    - - {{ $t("library.admin.import.import") }} - - -
    -
    -
    -
    -
    -

    - {{ importError }} -

    -
    -
    -
    -
    -
    -
    +
    diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index cae781738..ab011f567 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -140,7 +140,7 @@

    diff --git a/prisma/migrations/20250820223509_add_library_source_mode/migration.sql b/prisma/migrations/20250820223509_add_library_source_mode/migration.sql new file mode 100644 index 000000000..a6c572a73 --- /dev/null +++ b/prisma/migrations/20250820223509_add_library_source_mode/migration.sql @@ -0,0 +1,8 @@ +-- DropIndex +DROP INDEX "public"."GameTag_name_idx"; + +-- AlterTable +ALTER TABLE "public"."Library" ADD COLUMN "mode" "public"."LibraryMode" NOT NULL DEFAULT 'Game'; + +-- CreateIndex +CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); diff --git a/prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql b/prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql new file mode 100644 index 000000000..1c7f1f0eb --- /dev/null +++ b/prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql @@ -0,0 +1,99 @@ +/* + Warnings: + + - The values [Addon] on the enum `LibraryMode` will be removed. If these variants are still used in the database, this will fail. + - You are about to drop the column `addonId` on the `Version` table. All the data in the column will be lost. + - You are about to drop the `Addon` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `AddonVersion` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_AddonVersionToRedistVersion` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "public"."LibraryMode_new" AS ENUM ('Game', 'Redist', 'DLC', 'Mod'); +ALTER TABLE "public"."Library" ALTER COLUMN "mode" DROP DEFAULT; +ALTER TABLE "public"."Library" ALTER COLUMN "mode" TYPE "public"."LibraryMode_new" USING ("mode"::text::"public"."LibraryMode_new"); +ALTER TYPE "public"."LibraryMode" RENAME TO "LibraryMode_old"; +ALTER TYPE "public"."LibraryMode_new" RENAME TO "LibraryMode"; +DROP TYPE "public"."LibraryMode_old"; +ALTER TABLE "public"."Library" ALTER COLUMN "mode" SET DEFAULT 'Game'; +COMMIT; + +-- DropForeignKey +ALTER TABLE "public"."Addon" DROP CONSTRAINT "Addon_libraryId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."AddonVersion" DROP CONSTRAINT "AddonVersion_versionId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."Version" DROP CONSTRAINT "addon_link"; + +-- DropForeignKey +ALTER TABLE "public"."_AddonVersionToRedistVersion" DROP CONSTRAINT "_AddonVersionToRedistVersion_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_AddonVersionToRedistVersion" DROP CONSTRAINT "_AddonVersionToRedistVersion_B_fkey"; + +-- DropIndex +DROP INDEX "public"."GameTag_name_idx"; + +-- AlterTable +ALTER TABLE "public"."Version" DROP COLUMN "addonId", +ADD COLUMN "dlcId" TEXT; + +-- DropTable +DROP TABLE "public"."Addon"; + +-- DropTable +DROP TABLE "public"."AddonVersion"; + +-- DropTable +DROP TABLE "public"."_AddonVersionToRedistVersion"; + +-- CreateTable +CREATE TABLE "public"."DLCVersion" ( + "versionId" TEXT NOT NULL, + + CONSTRAINT "DLCVersion_pkey" PRIMARY KEY ("versionId") +); + +-- CreateTable +CREATE TABLE "public"."DLC" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "iconObjectId" TEXT NOT NULL, + "libraryId" TEXT NOT NULL, + "libraryPath" TEXT NOT NULL, + + CONSTRAINT "DLC_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."_DLCVersionToRedistVersion" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_DLCVersionToRedistVersion_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_DLCVersionToRedistVersion_B_index" ON "public"."_DLCVersionToRedistVersion"("B"); + +-- CreateIndex +CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); + +-- AddForeignKey +ALTER TABLE "public"."Version" ADD CONSTRAINT "dlc_link" FOREIGN KEY ("dlcId") REFERENCES "public"."DLC"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."DLCVersion" ADD CONSTRAINT "DLCVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."DLC" ADD CONSTRAINT "DLC_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_DLCVersionToRedistVersion" ADD CONSTRAINT "_DLCVersionToRedistVersion_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."DLCVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_DLCVersionToRedistVersion" ADD CONSTRAINT "_DLCVersionToRedistVersion_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/models/app.prisma b/prisma/models/app.prisma index 59fa64535..ed79ac936 100644 --- a/prisma/models/app.prisma +++ b/prisma/models/app.prisma @@ -24,7 +24,7 @@ enum LibraryBackend { enum LibraryMode { Game Redist - Addon + DLC Mod } @@ -34,9 +34,10 @@ model Library { backend LibraryBackend options Json + mode LibraryMode @default(Game) games Game[] redists Redist[] - addons Addon[] - mods Mod[] + dlcs DLC[] + mods Mod[] } diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma index 286d80295..7c566468e 100644 --- a/prisma/models/content.prisma +++ b/prisma/models/content.prisma @@ -14,9 +14,9 @@ model Version { redist Redist? @relation(fields: [redistId], references: [id], map: "redist_link", onDelete: Cascade, onUpdate: Cascade) redistVersion RedistVersion? - addonId String? - addon Addon? @relation(fields: [addonId], references: [id], map: "addon_link", onDelete: Cascade, onUpdate: Cascade) - addonVersion AddonVersion? + dlcId String? + dlc DLC? @relation(fields: [dlcId], references: [id], map: "dlc_link", onDelete: Cascade, onUpdate: Cascade) + dlcVersion DLCVersion? modId String? mod Mod? @relation(fields: [modId], references: [id], map: "mod_link", onDelete: Cascade, onUpdate: Cascade) @@ -58,7 +58,7 @@ model GameVersionLaunch { launchArgs String @default("") } -model AddonVersion { +model DLCVersion { versionId String @id version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade) @@ -70,7 +70,7 @@ model RedistVersion { version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade) gameDependees GameVersion[] - addonDependees AddonVersion[] + dlcDependees DLCVersion[] } model ModVersion { diff --git a/prisma/models/metadata.prisma b/prisma/models/metadata.prisma index 8477804e6..71c085783 100644 --- a/prisma/models/metadata.prisma +++ b/prisma/models/metadata.prisma @@ -53,7 +53,7 @@ model Game { @@unique([libraryId, libraryPath], name: "libraryKey") } -model Addon { +model DLC { id String @id @default(uuid()) name String diff --git a/server/api/v1/admin/import/game/index.get.ts b/server/api/v1/admin/import/game/index.get.ts index 8ef65fb09..cf310a7fb 100644 --- a/server/api/v1/admin/import/game/index.get.ts +++ b/server/api/v1/admin/import/game/index.get.ts @@ -2,7 +2,7 @@ import aclManager from "~/server/internal/acls"; import libraryManager from "~/server/internal/library"; export default defineEventHandler(async (h3) => { - const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]); + const allowed = await aclManager.allowSystemACL(h3, ["import:game:read", "import:redist:read"]); if (!allowed) throw createError({ statusCode: 403 }); const unimportedGames = await libraryManager.fetchUnimportedGames(); diff --git a/server/api/v1/admin/import/redist/index.get.ts b/server/api/v1/admin/import/redist/index.get.ts new file mode 100644 index 000000000..862066860 --- /dev/null +++ b/server/api/v1/admin/import/redist/index.get.ts @@ -0,0 +1,3 @@ +import handler from "../game/index.get"; + +export default handler; \ No newline at end of file diff --git a/server/api/v1/admin/import/redist/index.post.ts b/server/api/v1/admin/import/redist/index.post.ts new file mode 100644 index 000000000..510847b6d --- /dev/null +++ b/server/api/v1/admin/import/redist/index.post.ts @@ -0,0 +1,56 @@ +import { ArkErrors, type } from "arktype"; +import aclManager from "~/server/internal/acls"; +import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; +import * as jdenticon from "jdenticon"; +import prisma from "~/server/internal/db/database"; +import libraryManager from "~/server/internal/library"; + +export const ImportRedist = type({ + library: "string", + path: "string", + + name: "string", + description: "string", +}); + +export default defineEventHandler(async (h3) => { + const allowed = await aclManager.allowSystemACL(h3, ["import:redist:new"]); + if (!allowed) throw createError({ statusCode: 403 }); + + const body = await handleFileUpload(h3, {}, ["internal:read"], 1); + if (!body) + throw createError({ statusCode: 400, statusMessage: "Body required." }); + + const [[id], rawOptions, pull,, add] = body; + + const options = ImportRedist(rawOptions); + if (options instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: options.summary }); + + const valid = await libraryManager.checkUnimportedGamePath( + options.library, + options.path, + ); + if (!valid) + throw createError({ + statusCode: 400, + statusMessage: "Invalid library or game.", + }); + + const icon = id ?? add(jdenticon.toPng(options.name, 512)); + + const redist = await prisma.redist.create({ + data: { + libraryId: options.library, + libraryPath: options.path, + + mName: options.name, + mShortDescription: options.description, + mIconObjectId: icon, + }, + }); + + await pull(); + + return redist; +}); diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts index 1f090a135..54a47fbdd 100644 --- a/server/internal/acls/descriptions.ts +++ b/server/internal/acls/descriptions.ts @@ -84,6 +84,8 @@ export const systemACLDescriptions: ObjectFromList = { "import:game:read": "Fetch games to be imported, and search the metadata for games.", "import:game:new": "Import a game.", + "import:redist:read": "Fetch redists to be imported.", + "import:redist:new": "Import a redist.", "tags:read": "Fetch all tags", "tags:create": "Create a tag", diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts index d6997c5b3..394277464 100644 --- a/server/internal/acls/index.ts +++ b/server/internal/acls/index.ts @@ -74,9 +74,10 @@ export const systemACLs = [ "import:version:read", "import:version:new", - "import:game:read", "import:game:new", + "import:redist:read", + "import:redist:new", "user:read", "user:delete", diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index aef4e208c..e10cd29ff 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -336,7 +336,7 @@ class LibraryManager { nonce: `version-create-${gameId}-${versionPath}`, title: `'${game.mName}' ('${versionPath}') finished importing.`, description: `Drop finished importing version ${versionPath} for ${game.mName}.`, - actions: [`View|/admin/library/${gameId}`], + actions: [`View|/admin/library/g/${gameId}`], acls: ["system:import:version:read"], }); diff --git a/server/internal/utils/handlefileupload.ts b/server/internal/utils/handlefileupload.ts index 4e36c4434..91a748936 100644 --- a/server/internal/utils/handlefileupload.ts +++ b/server/internal/utils/handlefileupload.ts @@ -1,5 +1,5 @@ import type { EventHandlerRequest, H3Event } from "h3"; -import type { Dump, Pull } from "../objects/transactional"; +import type { Dump, Pull, Register } from "../objects/transactional"; import { ObjectTransactionalHandler } from "../objects/transactional"; export async function handleFileUpload( @@ -7,7 +7,7 @@ export async function handleFileUpload( metadata: { [key: string]: string }, permissions: Array, max = -1, -): Promise<[string[], { [key: string]: string }, Pull, Dump] | undefined> { +): Promise<[string[], { [key: string]: string }, Pull, Dump, Register] | undefined> { const formData = await readMultipartFormData(h3); if (!formData) return undefined; const transactionalHandler = new ObjectTransactionalHandler(); @@ -28,5 +28,5 @@ export async function handleFileUpload( options[entry.name] = entry.data.toString("utf-8"); } - return [ids, options, pull, dump]; + return [ids, options, pull, dump, add]; } From 3af00e085e5ed6f43f3e13eb81c4aae9c512719b Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 25 Aug 2025 16:22:53 +1000 Subject: [PATCH 04/22] fix: giantbomb logging bug --- server/internal/metadata/giantbomb.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts index aa7e6d60f..5a7898524 100644 --- a/server/internal/metadata/giantbomb.ts +++ b/server/internal/metadata/giantbomb.ts @@ -191,7 +191,7 @@ export class GiantBombProvider implements MetadataProvider { const res = await publisher(pub.name); if (res === undefined) { - context?.logger.warn(`Failed to import publisher "${pub}"`); + context?.logger.warn(`Failed to import publisher "${pub.name}"`); continue; } context?.logger.info(`Imported publisher "${pub.name}"`); @@ -208,10 +208,10 @@ export class GiantBombProvider implements MetadataProvider { const res = await developer(dev.name); if (res === undefined) { - context?.logger.warn(`Failed to import developer "${dev}"`); + context?.logger.warn(`Failed to import developer "${dev.name}"`); continue; } - context?.logger.info(`Imported developer "${dev}"`); + context?.logger.info(`Imported developer "${dev.name}"`); developers.push(res); } } From 8efddc07bce53af45e057742252cb7ad0a16c533 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 27 Aug 2025 11:25:23 +1000 Subject: [PATCH 05/22] feat: partial user platform support + statusMessage -> message --- components/Auth/Simple.vue | 2 +- components/GameEditor/Metadata.vue | 14 +- components/GameEditor/Version.vue | 8 +- components/Import/Redist.vue | 131 ++++++++++- components/Modal/AddCompanyGame.vue | 2 +- components/Modal/CreateCollection.vue | 4 +- components/Modal/DeleteCollection.vue | 4 +- components/Modal/DeleteNews.vue | 4 +- components/Modal/DeleteUser.vue | 4 +- components/Modal/UploadFile.vue | 2 +- components/NewsArticleCreateButton.vue | 4 +- components/StoreView.vue | 7 +- composables/request.ts | 2 +- composables/ws.ts | 2 +- drop-base | 2 +- package.json | 2 + pages/account/tokens.vue | 2 +- pages/admin/library/g/[id]/import.vue | 2 +- pages/admin/library/import.vue | 26 ++- pages/admin/library/sources/index.vue | 6 +- pages/admin/settings/index.vue | 2 +- pages/admin/settings/tokens.vue | 2 +- pages/admin/users/auth/simple/index.vue | 2 +- pages/auth/register.vue | 4 +- pages/client/authorize/[id].vue | 4 +- pages/client/code/index.vue | 2 +- pages/library/collection/[id]/index.vue | 3 +- pages/news/[id]/index.vue | 2 +- pages/setup.vue | 4 +- pages/store/[id]/index.vue | 21 +- .../migration.sql | 35 --- .../migration.sql | 42 ---- .../migration.sql | 35 --- .../migration.sql | 8 - .../migration.sql | 99 --------- .../migration.sql | 8 - .../20250825061943_re_add_index/migration.sql | 5 - .../migration.sql | 83 ++++--- .../migration.sql | 0 .../migration.sql | 2 +- prisma/models/content.prisma | 38 +++- prisma/models/metadata.prisma | 3 +- .../api/v1/admin/company/[id]/banner.post.ts | 6 +- server/api/v1/admin/company/[id]/game.post.ts | 2 +- server/api/v1/admin/company/[id]/icon.post.ts | 6 +- .../api/v1/admin/company/[id]/index.delete.ts | 2 +- server/api/v1/admin/company/[id]/index.get.ts | 2 +- server/api/v1/admin/game/[id]/index.get.ts | 2 +- .../api/v1/admin/game/[id]/metadata.post.ts | 20 +- .../api/v1/admin/game/image/index.delete.ts | 4 +- server/api/v1/admin/game/image/index.post.ts | 12 +- server/api/v1/admin/import/game/index.post.ts | 6 +- server/api/v1/admin/import/game/search.get.ts | 4 +- .../api/v1/admin/import/redist/index.post.ts | 42 +++- .../api/v1/admin/import/version/index.get.ts | 6 +- .../api/v1/admin/import/version/index.post.ts | 22 +- .../v1/admin/import/version/preload.get.ts | 4 +- .../v1/admin/library/sources/index.patch.ts | 4 +- .../v1/admin/library/sources/index.post.ts | 4 +- server/api/v1/admin/news/index.get.ts | 4 +- server/api/v1/admin/news/index.post.ts | 8 +- server/api/v1/admin/tags/[id]/index.delete.ts | 2 +- server/api/v1/admin/task/index.get.ts | 2 +- server/api/v1/admin/task/index.post.ts | 4 +- .../api/v1/admin/token/[id]/index.delete.ts | 4 +- server/api/v1/admin/token/index.post.ts | 2 +- .../api/v1/admin/users/[id]/index.delete.ts | 4 +- server/api/v1/admin/users/[id]/index.get.ts | 6 +- server/api/v1/auth/signin/simple.post.ts | 16 +- server/api/v1/auth/signup/simple.get.ts | 6 +- server/api/v1/auth/signup/simple.post.ts | 6 +- .../api/v1/client/auth/callback/index.post.ts | 4 +- server/api/v1/client/auth/code/index.get.ts | 4 +- server/api/v1/client/auth/code/index.post.ts | 6 +- server/api/v1/client/auth/code/ws.get.ts | 4 +- server/api/v1/client/auth/handshake.post.ts | 8 +- server/api/v1/client/auth/index.get.ts | 6 +- server/api/v1/client/auth/initiate.post.ts | 6 +- server/api/v1/client/capability/index.post.ts | 8 +- server/api/v1/client/chunk.get.ts | 12 +- .../v1/client/collection/[id]/entry.delete.ts | 6 +- .../v1/client/collection/[id]/entry.post.ts | 4 +- .../v1/client/collection/[id]/index.delete.ts | 6 +- .../v1/client/collection/[id]/index.get.ts | 6 +- .../client/collection/default/entry.delete.ts | 2 +- .../client/collection/default/entry.post.ts | 2 +- server/api/v1/client/collection/index.post.ts | 2 +- server/api/v1/client/game/[id]/index.get.ts | 4 +- server/api/v1/client/game/manifest.get.ts | 4 +- server/api/v1/client/game/version.get.ts | 4 +- server/api/v1/client/game/versions.get.ts | 2 +- server/api/v1/client/news/index.get.ts | 4 +- server/api/v1/client/object/[id]/index.get.ts | 4 +- .../[gameid]/[slotindex]/index.delete.ts | 12 +- .../saves/[gameid]/[slotindex]/index.get.ts | 12 +- .../saves/[gameid]/[slotindex]/push.post.ts | 10 +- .../api/v1/client/saves/[gameid]/index.get.ts | 6 +- .../v1/client/saves/[gameid]/index.post.ts | 8 +- server/api/v1/client/saves/index.get.ts | 2 +- server/api/v1/client/saves/settings.get.ts | 2 +- server/api/v1/collection/[id]/entry.delete.ts | 6 +- server/api/v1/collection/[id]/entry.post.ts | 4 +- server/api/v1/collection/[id]/index.delete.ts | 8 +- server/api/v1/collection/[id]/index.get.ts | 8 +- .../api/v1/collection/default/entry.delete.ts | 4 +- .../api/v1/collection/default/entry.post.ts | 4 +- server/api/v1/collection/default/index.get.ts | 2 +- server/api/v1/collection/index.post.ts | 2 +- server/api/v1/companies/[id]/index.get.ts | 4 +- server/api/v1/games/[id]/index.get.ts | 10 +- server/api/v1/news/[id]/index.get.ts | 2 +- server/api/v1/news/index.get.ts | 6 +- .../api/v1/notifications/[id]/index.delete.ts | 4 +- server/api/v1/notifications/[id]/index.get.ts | 4 +- server/api/v1/notifications/[id]/read.post.ts | 4 +- server/api/v1/notifications/index.get.ts | 2 +- server/api/v1/notifications/readall.post.ts | 2 +- server/api/v1/object/[id]/index.delete.ts | 2 +- server/api/v1/object/[id]/index.get.ts | 4 +- server/api/v1/object/[id]/index.head.ts | 4 +- server/api/v1/object/[id]/index.post.ts | 4 +- .../api/v1/screenshots/[id]/index.delete.ts | 2 +- server/api/v1/screenshots/[id]/index.get.ts | 2 +- .../api/v1/screenshots/game/[id]/index.get.ts | 2 +- .../v1/screenshots/game/[id]/index.post.ts | 4 +- server/api/v1/setup.post.ts | 2 +- server/api/v1/store/index.get.ts | 2 +- server/api/v1/store/platforms.get.ts | 16 ++ server/api/v1/tags/[id]/index.get.ts | 4 +- .../api/v1/user/client/[id]/index.delete.ts | 2 +- server/api/v1/user/token/[id]/index.delete.ts | 4 +- server/api/v1/user/token/index.post.ts | 2 +- server/api/v2/client/chunk.post.ts | 8 +- server/api/v2/client/context.post.ts | 2 +- server/arktype.ts | 4 +- server/internal/clients/event-handler.ts | 12 +- server/internal/clients/handler.ts | 10 +- server/internal/library/index.ts | 6 +- server/internal/saves/index.ts | 6 +- server/internal/screenshots/index.ts | 2 +- server/internal/utils/handlefileupload.ts | 22 +- server/routes/auth/callback/oidc.get.ts | 6 +- yarn.lock | 208 +++++++++++++++++- 143 files changed, 831 insertions(+), 593 deletions(-) delete mode 100644 prisma/migrations/20250820095728_add_args_back/migration.sql delete mode 100644 prisma/migrations/20250820101258_split_ids_into_separate_fields/migration.sql delete mode 100644 prisma/migrations/20250820102120_add_delete_casacades/migration.sql delete mode 100644 prisma/migrations/20250820223509_add_library_source_mode/migration.sql delete mode 100644 prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql delete mode 100644 prisma/migrations/20250823005001_add_api_token_expiry/migration.sql delete mode 100644 prisma/migrations/20250825061943_re_add_index/migration.sql rename prisma/migrations/{20250820044328_init => 20250826053451_init}/migration.sql (85%) rename prisma/migrations/{20250820044424_add_indexes => 20250826053639_reapply_index}/migration.sql (100%) rename prisma/migrations/{20250820050413_add_hidden_field_to_game_version => 20250826054115_add_file_extensions_to_userplatform}/migration.sql (64%) create mode 100644 server/api/v1/store/platforms.get.ts diff --git a/components/Auth/Simple.vue b/components/Auth/Simple.vue index 7324467af..696a2da8d 100644 --- a/components/Auth/Simple.vue +++ b/components/Auth/Simple.vue @@ -106,7 +106,7 @@ function signin_wrapper() { router.push(route.query.redirect?.toString() ?? "/"); }) .catch((response) => { - const message = response.statusMessage || t("errors.unknown"); + const message = response.message || t("errors.unknown"); error.value = message; }) .finally(() => { diff --git a/components/GameEditor/Metadata.vue b/components/GameEditor/Metadata.vue index 5dfa85fa2..95c7e2547 100644 --- a/components/GameEditor/Metadata.vue +++ b/components/GameEditor/Metadata.vue @@ -466,7 +466,7 @@ const game = defineModel() as Ref; if (!game.value) throw createError({ statusCode: 500, - statusMessage: "Game not provided to editor component", + message: "Game not provided to editor component", }); const currentTags = ref<{ [key: string]: boolean }>( @@ -553,7 +553,7 @@ function coreMetadataUpdate_wrapper() { { title: t("errors.game.metadata.title"), description: t("errors.game.metadata.description", [ - (e as H3Error)?.statusMessage ?? t("errors.unknown"), + (e as H3Error)?.message ?? t("errors.unknown"), ]), buttonText: t("common.close"), }, @@ -614,7 +614,7 @@ watch(descriptionHTML, (_v) => { { title: t("errors.game.description.title"), description: t("errors.game.description.description", [ - (e as H3Error)?.statusMessage ?? t("errors.unknown"), + (e as H3Error)?.message ?? t("errors.unknown"), ]), buttonText: t("common.close"), }, @@ -660,7 +660,7 @@ async function updateBannerImage(id: string) { { title: t("errors.game.banner.title"), description: t("errors.game.banner.description", [ - (e as H3Error)?.statusMessage ?? t("errors.unknown"), + (e as H3Error)?.message ?? t("errors.unknown"), ]), buttonText: t("common.close"), }, @@ -688,7 +688,7 @@ async function updateCoverImage(id: string) { { title: t("errors.game.cover.title"), description: t("errors.game.cover.description", [ - (e as H3Error)?.statusMessage ?? t("errors.unknown"), + (e as H3Error)?.message ?? t("errors.unknown"), ]), buttonText: t("common.close"), }, @@ -717,7 +717,7 @@ async function deleteImage(id: string) { { title: t("errors.game.deleteImage.title"), description: t("errors.game.deleteImage.description", [ - (e as H3Error)?.statusMessage ?? t("errors.unknown"), + (e as H3Error)?.message ?? t("errors.unknown"), ]), buttonText: t("common.close"), }, @@ -761,7 +761,7 @@ async function updateImageCarousel() { { title: t("errors.game.carousel.title"), description: t("errors.game.carousel.description", [ - (e as H3Error)?.statusMessage ?? t("errors.unknown"), + (e as H3Error)?.message ?? t("errors.unknown"), ]), buttonText: t("common.close"), }, diff --git a/components/GameEditor/Version.vue b/components/GameEditor/Version.vue index 4af58d72d..1d9cd1c6f 100644 --- a/components/GameEditor/Version.vue +++ b/components/GameEditor/Version.vue @@ -71,10 +71,6 @@ }}

    - @@ -151,7 +147,7 @@ const game = defineModel>() as Ref< if (!game.value) throw createError({ statusCode: 500, - statusMessage: "Game not provided to editor component", + message: "Game not provided to editor component", }); async function updateVersionOrder() { @@ -170,7 +166,7 @@ async function updateVersionOrder() { { title: t("errors.version.order.title"), description: t("errors.version.order.desc", { - error: (e as H3Error)?.statusMessage ?? t("errors.unknown"), + error: (e as H3Error)?.message ?? t("errors.unknown"), }), buttonText: t("common.close"), }, diff --git a/components/Import/Redist.vue b/components/Import/Redist.vue index 3e6d69c69..d52b579fc 100644 --- a/components/Import/Redist.vue +++ b/components/Import/Redist.vue @@ -1,3 +1,4 @@ + @@ -233,7 +233,7 @@ const importModes = shallowRef<{ description: string; icon: Component; component: Component; - import: (v: unknown) => void; + import: (...v: unknown[]) => void; }; }>({ Game: { @@ -249,7 +249,7 @@ const importModes = shallowRef<{ "Redistributables are packaged dependencies for games, that are installed alongside and required to play certain games.", icon: ArchiveBoxIcon, component: ImportRedist, - import: importRedist as (v: unknown) => void, + import: importRedist as (v: unknown, k: unknown) => void, }, }); @@ -303,12 +303,12 @@ async function importGame_wrapper( } catch (error) { console.warn(error); importError.value = - (error as FetchError)?.statusMessage || t("errors.unknown"); + (error as FetchError)?.message || t("errors.unknown"); } importLoading.value = false; } -async function importRedist(data: object) { +async function importRedist(data: object, platform: object | undefined) { importLoading.value = true; importError.value = undefined; try { @@ -316,7 +316,19 @@ async function importRedist(data: object) { const formData = new FormData(); for (const [key, value] of Object.entries(data)) { - formData.append(key, value); + formData.append( + key, + typeof value === "object" ? JSON.stringify(value) : value, + ); + } + + if (platform) { + for (const [key, value] of Object.entries(platform)) { + formData.append( + `platform.${key}`, + typeof value === "object" ? JSON.stringify(value) : value, + ); + } } formData.append("library", option.library.id); @@ -335,7 +347,7 @@ async function importRedist(data: object) { } catch (error) { console.warn(error); importError.value = - (error as FetchError)?.statusMessage || t("errors.unknown"); + (error as FetchError)?.message || t("errors.unknown"); } importLoading.value = false; } diff --git a/pages/admin/library/sources/index.vue b/pages/admin/library/sources/index.vue index 77870014f..66fe668a8 100644 --- a/pages/admin/library/sources/index.vue +++ b/pages/admin/library/sources/index.vue @@ -412,7 +412,7 @@ function performActionSource_wrapper() { }) .catch((e) => { if (e instanceof FetchError) { - modalError.value = e.statusMessage ?? e.message; + modalError.value = e.message ?? e.message; } else { modalError.value = e as string; } @@ -449,8 +449,8 @@ async function deleteSource(index: number) { { title: t("errors.library.source.delete.title"), description: t("errors.library.source.delete.desc", [ - // @ts-expect-error attempt to display statusMessage on error - e?.statusMessage ?? t("errors.unknown"), + // @ts-expect-error attempt to display message on error + e?.message ?? t("errors.unknown"), ]), }, (_, c) => c(), diff --git a/pages/admin/settings/index.vue b/pages/admin/settings/index.vue index 6402b634a..74ceaa279 100644 --- a/pages/admin/settings/index.vue +++ b/pages/admin/settings/index.vue @@ -96,7 +96,7 @@ async function saveSettings() { title: `Failed to save settings.`, description: e instanceof FetchError - ? (e.statusMessage ?? e.message) + ? (e.message) : (e as string).toString(), }, (_, c) => c(), diff --git a/pages/admin/settings/tokens.vue b/pages/admin/settings/tokens.vue index c307695ed..7dc548d8f 100644 --- a/pages/admin/settings/tokens.vue +++ b/pages/admin/settings/tokens.vue @@ -184,7 +184,7 @@ if (route.query.payload) { } catch { throw createError({ statusCode: 400, - statusMessage: "Failed to parse the token create payload.", + message: "Failed to parse the token create payload.", fatal: true, }); } diff --git a/pages/admin/users/auth/simple/index.vue b/pages/admin/users/auth/simple/index.vue index ca0bb7ab9..0095f6639 100644 --- a/pages/admin/users/auth/simple/index.vue +++ b/pages/admin/users/auth/simple/index.vue @@ -502,7 +502,7 @@ function invite_wrapper() { invitations.value.push(invitation); }) .catch((response) => { - const message = response.statusMessage || t("errors.unknown"); + const message = response.message || t("errors.unknown"); error.value = message; }) .finally(() => { diff --git a/pages/auth/register.vue b/pages/auth/register.vue index 0784eb138..8849a6614 100644 --- a/pages/auth/register.vue +++ b/pages/auth/register.vue @@ -203,7 +203,7 @@ const invitationId = route.query.id?.toString(); if (!invitationId) throw createError({ statusCode: 400, - statusMessage: t("errors.inviteRequired"), + message: t("errors.inviteRequired"), }); const invitation = await $dropFetch( @@ -271,7 +271,7 @@ function register_wrapper() { router.push("/auth/signin"); }) .catch((response) => { - const message = response.statusMessage || t("errors.unknown"); + const message = response.message || t("errors.unknown"); error.value = message; }) .finally(() => { diff --git a/pages/client/authorize/[id].vue b/pages/client/authorize/[id].vue index a3d2a9d10..b3602d5e4 100644 --- a/pages/client/authorize/[id].vue +++ b/pages/client/authorize/[id].vue @@ -192,7 +192,7 @@ async function authorize() { } throw createError({ statusCode: 500, - statusMessage: "Unknown client auth mode: " + clientData.mode, + message: "Unknown client auth mode: " + clientData.mode, fatal: true, }); } @@ -202,7 +202,7 @@ async function authorize_wrapper() { await authorize(); } catch (e) { const errorMessage = - (e as FetchError)?.statusMessage || "An unknown error occurred."; + (e as FetchError)?.message || "An unknown error occurred."; error.value = errorMessage; } finally { completed.value = true; diff --git a/pages/client/code/index.vue b/pages/client/code/index.vue index 266f03cae..843b2e98f 100644 --- a/pages/client/code/index.vue +++ b/pages/client/code/index.vue @@ -125,7 +125,7 @@ async function complete(code: string) { } catch (e) { if (e instanceof FetchError) { error.value = - e.statusMessage ?? e.message ?? "An unknown error occurred."; + e.message ?? "An unknown error occurred."; } else { error.value = (e as string)?.toString(); } diff --git a/pages/library/collection/[id]/index.vue b/pages/library/collection/[id]/index.vue index 9176d99a4..091ffcaf1 100644 --- a/pages/library/collection/[id]/index.vue +++ b/pages/library/collection/[id]/index.vue @@ -44,7 +44,8 @@ const collection = computed(() => if (collection.value === undefined) { throw createError({ statusCode: 404, - statusMessage: t("library.collection.notFound"), + message: t("library.collection.notFound"), + fatal: true, }); } diff --git a/pages/news/[id]/index.vue b/pages/news/[id]/index.vue index 17837d3cd..ac5a95597 100644 --- a/pages/news/[id]/index.vue +++ b/pages/news/[id]/index.vue @@ -99,7 +99,7 @@ const article = computed(() => if (!article.value) throw createError({ statusCode: 404, - statusMessage: t("news.notFound"), + message: t("news.notFound"), fatal: true, }); diff --git a/pages/setup.vue b/pages/setup.vue index 2acdc1dbb..25b112d82 100644 --- a/pages/setup.vue +++ b/pages/setup.vue @@ -159,7 +159,7 @@ const token = route.query.token; if (!token) throw createError({ statusCode: 400, - statusMessage: "No token.", + message: "No token.", fatal: true, }); const bearerToken = `Bearer ${token}`; @@ -170,7 +170,7 @@ const allowed = await $dropFetch("/api/v1/admin", { if (!allowed) throw createError({ statusCode: 400, - statusMessage: "Invalid setup token. Please check the URL you opened.", + message: "Invalid setup token. Please check the URL you opened.", fatal: true, }); diff --git a/pages/store/[id]/index.vue b/pages/store/[id]/index.vue index 09101ef1e..b5e559a8e 100644 --- a/pages/store/[id]/index.vue +++ b/pages/store/[id]/index.vue @@ -73,15 +73,21 @@ - + :key="typeof platform === 'string' ? platform : platform.id" + > + +
    +
    + {{ $t("store.commingSoon") }} @@ -278,8 +284,9 @@ const descriptionHTML = micromark(game.mDescription); const showReadMore = previewHTML != descriptionHTML; const platforms = game.versions - .map((e) => e.platform) + .map((e) => e.platform ?? e.userPlatform) .flat() + .filter((e) => e !== null) .filter((e, i, u) => u.indexOf(e) === i); // const rating = Math.round(game.mReviewRating * 5); diff --git a/prisma/migrations/20250820095728_add_args_back/migration.sql b/prisma/migrations/20250820095728_add_args_back/migration.sql deleted file mode 100644 index 6db6720b4..000000000 --- a/prisma/migrations/20250820095728_add_args_back/migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "addon_link"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "game_link"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "mod_link"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "redist_link"; - --- DropIndex -DROP INDEX "public"."GameTag_name_idx"; - --- AlterTable -ALTER TABLE "public"."GameVersion" ADD COLUMN "setupArgs" TEXT NOT NULL DEFAULT ''; - --- AlterTable -ALTER TABLE "public"."GameVersionLaunch" ADD COLUMN "launchArgs" TEXT NOT NULL DEFAULT ''; - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "game_link" FOREIGN KEY ("rootId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "redist_link" FOREIGN KEY ("rootId") REFERENCES "public"."Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "addon_link" FOREIGN KEY ("rootId") REFERENCES "public"."Addon"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "mod_link" FOREIGN KEY ("rootId") REFERENCES "public"."Mod"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250820101258_split_ids_into_separate_fields/migration.sql b/prisma/migrations/20250820101258_split_ids_into_separate_fields/migration.sql deleted file mode 100644 index 24f3de69a..000000000 --- a/prisma/migrations/20250820101258_split_ids_into_separate_fields/migration.sql +++ /dev/null @@ -1,42 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `rootId` on the `Version` table. All the data in the column will be lost. - -*/ --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "addon_link"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "game_link"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "mod_link"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "redist_link"; - --- DropIndex -DROP INDEX "public"."GameTag_name_idx"; - --- AlterTable -ALTER TABLE "public"."Version" DROP COLUMN "rootId", -ADD COLUMN "addonId" TEXT, -ADD COLUMN "gameId" TEXT, -ADD COLUMN "modId" TEXT, -ADD COLUMN "redistId" TEXT; - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "game_link" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "redist_link" FOREIGN KEY ("redistId") REFERENCES "public"."Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "addon_link" FOREIGN KEY ("addonId") REFERENCES "public"."Addon"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "mod_link" FOREIGN KEY ("modId") REFERENCES "public"."Mod"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250820102120_add_delete_casacades/migration.sql b/prisma/migrations/20250820102120_add_delete_casacades/migration.sql deleted file mode 100644 index f29933648..000000000 --- a/prisma/migrations/20250820102120_add_delete_casacades/migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- DropForeignKey -ALTER TABLE "public"."AddonVersion" DROP CONSTRAINT "AddonVersion_versionId_fkey"; - --- DropForeignKey -ALTER TABLE "public"."GameVersion" DROP CONSTRAINT "GameVersion_versionId_fkey"; - --- DropForeignKey -ALTER TABLE "public"."GameVersionLaunch" DROP CONSTRAINT "GameVersionLaunch_versionId_fkey"; - --- DropForeignKey -ALTER TABLE "public"."ModVersion" DROP CONSTRAINT "ModVersion_versionId_fkey"; - --- DropForeignKey -ALTER TABLE "public"."RedistVersion" DROP CONSTRAINT "RedistVersion_versionId_fkey"; - --- DropIndex -DROP INDEX "public"."GameTag_name_idx"; - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); - --- AddForeignKey -ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."GameVersionLaunch" ADD CONSTRAINT "GameVersionLaunch_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."AddonVersion" ADD CONSTRAINT "AddonVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."ModVersion" ADD CONSTRAINT "ModVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250820223509_add_library_source_mode/migration.sql b/prisma/migrations/20250820223509_add_library_source_mode/migration.sql deleted file mode 100644 index a6c572a73..000000000 --- a/prisma/migrations/20250820223509_add_library_source_mode/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- DropIndex -DROP INDEX "public"."GameTag_name_idx"; - --- AlterTable -ALTER TABLE "public"."Library" ADD COLUMN "mode" "public"."LibraryMode" NOT NULL DEFAULT 'Game'; - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); diff --git a/prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql b/prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql deleted file mode 100644 index 1c7f1f0eb..000000000 --- a/prisma/migrations/20250820224054_rename_addons_to_dlc/migration.sql +++ /dev/null @@ -1,99 +0,0 @@ -/* - Warnings: - - - The values [Addon] on the enum `LibraryMode` will be removed. If these variants are still used in the database, this will fail. - - You are about to drop the column `addonId` on the `Version` table. All the data in the column will be lost. - - You are about to drop the `Addon` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `AddonVersion` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `_AddonVersionToRedistVersion` table. If the table is not empty, all the data it contains will be lost. - -*/ --- AlterEnum -BEGIN; -CREATE TYPE "public"."LibraryMode_new" AS ENUM ('Game', 'Redist', 'DLC', 'Mod'); -ALTER TABLE "public"."Library" ALTER COLUMN "mode" DROP DEFAULT; -ALTER TABLE "public"."Library" ALTER COLUMN "mode" TYPE "public"."LibraryMode_new" USING ("mode"::text::"public"."LibraryMode_new"); -ALTER TYPE "public"."LibraryMode" RENAME TO "LibraryMode_old"; -ALTER TYPE "public"."LibraryMode_new" RENAME TO "LibraryMode"; -DROP TYPE "public"."LibraryMode_old"; -ALTER TABLE "public"."Library" ALTER COLUMN "mode" SET DEFAULT 'Game'; -COMMIT; - --- DropForeignKey -ALTER TABLE "public"."Addon" DROP CONSTRAINT "Addon_libraryId_fkey"; - --- DropForeignKey -ALTER TABLE "public"."AddonVersion" DROP CONSTRAINT "AddonVersion_versionId_fkey"; - --- DropForeignKey -ALTER TABLE "public"."Version" DROP CONSTRAINT "addon_link"; - --- DropForeignKey -ALTER TABLE "public"."_AddonVersionToRedistVersion" DROP CONSTRAINT "_AddonVersionToRedistVersion_A_fkey"; - --- DropForeignKey -ALTER TABLE "public"."_AddonVersionToRedistVersion" DROP CONSTRAINT "_AddonVersionToRedistVersion_B_fkey"; - --- DropIndex -DROP INDEX "public"."GameTag_name_idx"; - --- AlterTable -ALTER TABLE "public"."Version" DROP COLUMN "addonId", -ADD COLUMN "dlcId" TEXT; - --- DropTable -DROP TABLE "public"."Addon"; - --- DropTable -DROP TABLE "public"."AddonVersion"; - --- DropTable -DROP TABLE "public"."_AddonVersionToRedistVersion"; - --- CreateTable -CREATE TABLE "public"."DLCVersion" ( - "versionId" TEXT NOT NULL, - - CONSTRAINT "DLCVersion_pkey" PRIMARY KEY ("versionId") -); - --- CreateTable -CREATE TABLE "public"."DLC" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT NOT NULL, - "iconObjectId" TEXT NOT NULL, - "libraryId" TEXT NOT NULL, - "libraryPath" TEXT NOT NULL, - - CONSTRAINT "DLC_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."_DLCVersionToRedistVersion" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL, - - CONSTRAINT "_DLCVersionToRedistVersion_AB_pkey" PRIMARY KEY ("A","B") -); - --- CreateIndex -CREATE INDEX "_DLCVersionToRedistVersion_B_index" ON "public"."_DLCVersionToRedistVersion"("B"); - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); - --- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "dlc_link" FOREIGN KEY ("dlcId") REFERENCES "public"."DLC"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."DLCVersion" ADD CONSTRAINT "DLCVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."DLC" ADD CONSTRAINT "DLC_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."_DLCVersionToRedistVersion" ADD CONSTRAINT "_DLCVersionToRedistVersion_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."DLCVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."_DLCVersionToRedistVersion" ADD CONSTRAINT "_DLCVersionToRedistVersion_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250823005001_add_api_token_expiry/migration.sql b/prisma/migrations/20250823005001_add_api_token_expiry/migration.sql deleted file mode 100644 index c028e9911..000000000 --- a/prisma/migrations/20250823005001_add_api_token_expiry/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- DropIndex -DROP INDEX "GameTag_name_idx"; - --- AlterTable -ALTER TABLE "APIToken" ADD COLUMN "expiresAt" TIMESTAMP(3); - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); diff --git a/prisma/migrations/20250825061943_re_add_index/migration.sql b/prisma/migrations/20250825061943_re_add_index/migration.sql deleted file mode 100644 index 79b60253b..000000000 --- a/prisma/migrations/20250825061943_re_add_index/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- DropIndex -DROP INDEX "GameTag_name_idx"; - --- CreateIndex -CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); diff --git a/prisma/migrations/20250820044328_init/migration.sql b/prisma/migrations/20250826053451_init/migration.sql similarity index 85% rename from prisma/migrations/20250820044328_init/migration.sql rename to prisma/migrations/20250826053451_init/migration.sql index 249a8c96a..a257ed3f1 100644 --- a/prisma/migrations/20250820044328_init/migration.sql +++ b/prisma/migrations/20250826053451_init/migration.sql @@ -1,4 +1,4 @@ --- Add pg_trgm +-- enable pg_trgm CREATE EXTENSION pg_trgm; -- CreateEnum @@ -8,7 +8,7 @@ CREATE TYPE "public"."Platform" AS ENUM ('windows', 'linux', 'macos'); CREATE TYPE "public"."LibraryBackend" AS ENUM ('Filesystem', 'FlatFilesystem'); -- CreateEnum -CREATE TYPE "public"."LibraryMode" AS ENUM ('Game', 'Redist', 'Addon', 'Mod'); +CREATE TYPE "public"."LibraryMode" AS ENUM ('Game', 'Redist', 'DLC', 'Mod'); -- CreateEnum CREATE TYPE "public"."AuthMec" AS ENUM ('Simple', 'OpenID'); @@ -40,6 +40,7 @@ CREATE TABLE "public"."Library" ( "name" TEXT NOT NULL, "backend" "public"."LibraryBackend" NOT NULL, "options" JSONB NOT NULL, + "mode" "public"."LibraryMode" NOT NULL DEFAULT 'Game', CONSTRAINT "Library_pkey" PRIMARY KEY ("id") ); @@ -75,6 +76,7 @@ CREATE TABLE "public"."APIToken" ( "userId" TEXT, "clientId" TEXT, "acls" TEXT[], + "expiresAt" TIMESTAMP(3), CONSTRAINT "APIToken_pkey" PRIMARY KEY ("id") ); @@ -129,14 +131,28 @@ CREATE TABLE "public"."CollectionEntry" ( CONSTRAINT "CollectionEntry_pkey" PRIMARY KEY ("collectionId","gameId") ); +-- CreateTable +CREATE TABLE "public"."UserPlatform" ( + "id" TEXT NOT NULL, + "redistId" TEXT, + "platformName" TEXT NOT NULL, + "iconSvg" TEXT NOT NULL, + + CONSTRAINT "UserPlatform_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "public"."Version" ( "versionId" TEXT NOT NULL, - "rootId" TEXT NOT NULL, "versionPath" TEXT NOT NULL, "versionName" TEXT NOT NULL, "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "platform" "public"."Platform" NOT NULL, + "gameId" TEXT, + "redistId" TEXT, + "dlcId" TEXT, + "modId" TEXT, + "platform" "public"."Platform", + "userPlatformRedistId" TEXT, "dropletManifest" JSONB NOT NULL, CONSTRAINT "Version_pkey" PRIMARY KEY ("versionId") @@ -145,31 +161,34 @@ CREATE TABLE "public"."Version" ( -- CreateTable CREATE TABLE "public"."GameVersion" ( "versionId" TEXT NOT NULL, - "setup" TEXT NOT NULL DEFAULT '', + "setupCommand" TEXT NOT NULL DEFAULT '', + "setupArgs" TEXT NOT NULL DEFAULT '', "onlySetup" BOOLEAN NOT NULL DEFAULT false, "umuIdOverride" TEXT, "versionIndex" INTEGER NOT NULL, "delta" BOOLEAN NOT NULL DEFAULT false, + "hidden" BOOLEAN NOT NULL DEFAULT false, CONSTRAINT "GameVersion_pkey" PRIMARY KEY ("versionId") ); -- CreateTable -CREATE TABLE "public"."GameVersionLaunch" ( +CREATE TABLE "public"."LaunchOption" ( "launchId" TEXT NOT NULL, "versionId" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT NOT NULL, "launchCommand" TEXT NOT NULL, + "launchArgs" TEXT NOT NULL DEFAULT '', - CONSTRAINT "GameVersionLaunch_pkey" PRIMARY KEY ("launchId") + CONSTRAINT "LaunchOption_pkey" PRIMARY KEY ("launchId") ); -- CreateTable -CREATE TABLE "public"."AddonVersion" ( +CREATE TABLE "public"."DLCVersion" ( "versionId" TEXT NOT NULL, - CONSTRAINT "AddonVersion_pkey" PRIMARY KEY ("versionId") + CONSTRAINT "DLCVersion_pkey" PRIMARY KEY ("versionId") ); -- CreateTable @@ -255,7 +274,7 @@ CREATE TABLE "public"."Game" ( ); -- CreateTable -CREATE TABLE "public"."Addon" ( +CREATE TABLE "public"."DLC" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT NOT NULL, @@ -263,7 +282,7 @@ CREATE TABLE "public"."Addon" ( "libraryId" TEXT NOT NULL, "libraryPath" TEXT NOT NULL, - CONSTRAINT "Addon_pkey" PRIMARY KEY ("id") + CONSTRAINT "DLC_pkey" PRIMARY KEY ("id") ); -- CreateTable @@ -411,11 +430,11 @@ CREATE TABLE "public"."_GameVersionToRedistVersion" ( ); -- CreateTable -CREATE TABLE "public"."_AddonVersionToRedistVersion" ( +CREATE TABLE "public"."_DLCVersionToRedistVersion" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL, - CONSTRAINT "_AddonVersionToRedistVersion_AB_pkey" PRIMARY KEY ("A","B") + CONSTRAINT "_DLCVersionToRedistVersion_AB_pkey" PRIMARY KEY ("A","B") ); -- CreateTable @@ -464,6 +483,9 @@ CREATE UNIQUE INDEX "APIToken_token_key" ON "public"."APIToken"("token"); -- CreateIndex CREATE INDEX "APIToken_token_idx" ON "public"."APIToken"("token"); +-- CreateIndex +CREATE UNIQUE INDEX "UserPlatform_redistId_key" ON "public"."UserPlatform"("redistId"); + -- CreateIndex CREATE UNIQUE INDEX "Version_versionId_key" ON "public"."Version"("versionId"); @@ -510,7 +532,7 @@ CREATE UNIQUE INDEX "Notification_userId_nonce_key" ON "public"."Notification"(" CREATE INDEX "_GameVersionToRedistVersion_B_index" ON "public"."_GameVersionToRedistVersion"("B"); -- CreateIndex -CREATE INDEX "_AddonVersionToRedistVersion_B_index" ON "public"."_AddonVersionToRedistVersion"("B"); +CREATE INDEX "_DLCVersionToRedistVersion_B_index" ON "public"."_DLCVersionToRedistVersion"("B"); -- CreateIndex CREATE INDEX "_GameToGameTag_B_index" ON "public"."_GameToGameTag"("B"); @@ -552,31 +574,40 @@ ALTER TABLE "public"."CollectionEntry" ADD CONSTRAINT "CollectionEntry_collectio ALTER TABLE "public"."CollectionEntry" ADD CONSTRAINT "CollectionEntry_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "game_link" FOREIGN KEY ("rootId") REFERENCES "public"."Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."UserPlatform" ADD CONSTRAINT "UserPlatform_redistId_fkey" FOREIGN KEY ("redistId") REFERENCES "public"."Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Version" ADD CONSTRAINT "game_link" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Version" ADD CONSTRAINT "redist_link" FOREIGN KEY ("redistId") REFERENCES "public"."Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Version" ADD CONSTRAINT "dlc_link" FOREIGN KEY ("dlcId") REFERENCES "public"."DLC"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "redist_link" FOREIGN KEY ("rootId") REFERENCES "public"."Redist"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."Version" ADD CONSTRAINT "mod_link" FOREIGN KEY ("modId") REFERENCES "public"."Mod"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "addon_link" FOREIGN KEY ("rootId") REFERENCES "public"."Addon"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."Version" ADD CONSTRAINT "Version_userPlatformRedistId_fkey" FOREIGN KEY ("userPlatformRedistId") REFERENCES "public"."UserPlatform"("redistId") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."Version" ADD CONSTRAINT "mod_link" FOREIGN KEY ("rootId") REFERENCES "public"."Mod"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "gameVersion_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."GameVersionLaunch" ADD CONSTRAINT "GameVersionLaunch_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."GameVersion"("versionId") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "redistVersion_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."AddonVersion" ADD CONSTRAINT "AddonVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."DLCVersion" ADD CONSTRAINT "DLCVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."ModVersion" ADD CONSTRAINT "ModVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "public"."ModVersion" ADD CONSTRAINT "ModVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "public"."SaveSlot" ADD CONSTRAINT "SaveSlot_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; @@ -603,7 +634,7 @@ ALTER TABLE "public"."Playtime" ADD CONSTRAINT "Playtime_userId_fkey" FOREIGN KE ALTER TABLE "public"."Game" ADD CONSTRAINT "Game_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."Addon" ADD CONSTRAINT "Addon_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "public"."DLC" ADD CONSTRAINT "DLC_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "public"."Redist" ADD CONSTRAINT "Redist_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE; @@ -630,10 +661,10 @@ ALTER TABLE "public"."_GameVersionToRedistVersion" ADD CONSTRAINT "_GameVersionT ALTER TABLE "public"."_GameVersionToRedistVersion" ADD CONSTRAINT "_GameVersionToRedistVersion_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."_AddonVersionToRedistVersion" ADD CONSTRAINT "_AddonVersionToRedistVersion_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."AddonVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "public"."_DLCVersionToRedistVersion" ADD CONSTRAINT "_DLCVersionToRedistVersion_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."DLCVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "public"."_AddonVersionToRedistVersion" ADD CONSTRAINT "_AddonVersionToRedistVersion_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "public"."_DLCVersionToRedistVersion" ADD CONSTRAINT "_DLCVersionToRedistVersion_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "public"."_GameToGameTag" ADD CONSTRAINT "_GameToGameTag_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250820044424_add_indexes/migration.sql b/prisma/migrations/20250826053639_reapply_index/migration.sql similarity index 100% rename from prisma/migrations/20250820044424_add_indexes/migration.sql rename to prisma/migrations/20250826053639_reapply_index/migration.sql diff --git a/prisma/migrations/20250820050413_add_hidden_field_to_game_version/migration.sql b/prisma/migrations/20250826054115_add_file_extensions_to_userplatform/migration.sql similarity index 64% rename from prisma/migrations/20250820050413_add_hidden_field_to_game_version/migration.sql rename to prisma/migrations/20250826054115_add_file_extensions_to_userplatform/migration.sql index 5220684d0..0614ba13a 100644 --- a/prisma/migrations/20250820050413_add_hidden_field_to_game_version/migration.sql +++ b/prisma/migrations/20250826054115_add_file_extensions_to_userplatform/migration.sql @@ -2,7 +2,7 @@ DROP INDEX "public"."GameTag_name_idx"; -- AlterTable -ALTER TABLE "public"."GameVersion" ADD COLUMN "hidden" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "public"."UserPlatform" ADD COLUMN "fileExtensions" TEXT[] DEFAULT ARRAY[]::TEXT[]; -- CreateIndex CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32)); diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma index 7c566468e..e08d1928a 100644 --- a/prisma/models/content.prisma +++ b/prisma/models/content.prisma @@ -1,3 +1,16 @@ +model UserPlatform { + id String @id @default(uuid()) + + redistId String? @unique + redist Redist? @relation(fields: [redistId], references: [id], onDelete: Cascade, onUpdate: Cascade) + + platformName String + iconSvg String + fileExtensions String[] @default([]) + + versions Version[] +} + model Version { versionId String @id @unique @default(uuid()) @@ -22,7 +35,11 @@ model Version { mod Mod? @relation(fields: [modId], references: [id], map: "mod_link", onDelete: Cascade, onUpdate: Cascade) modVersion ModVersion? - platform Platform + platform Platform? + + userPlatformRedistId String? + userPlatform UserPlatform? @relation(fields: [userPlatformRedistId], references: [redistId]) + dropletManifest Json // Results from droplet } @@ -32,11 +49,11 @@ model GameVersion { redistDeps RedistVersion[] - launches GameVersionLaunch[] + launches LaunchOption[] - setup String @default("") // Command to setup game (dependencies and such) - setupArgs String @default("") - onlySetup Boolean @default(false) + setupCommand String @default("") + setupArgs String @default("") + onlySetup Boolean @default(false) umuIdOverride String? @@ -45,11 +62,12 @@ model GameVersion { hidden Boolean @default(false) } -model GameVersionLaunch { +model LaunchOption { launchId String @id @default(uuid()) versionId String - gameVersion GameVersion @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade) + gameVersion GameVersion? @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade, map: "gameVersion_fkey") + redistVersion RedistVersion? @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade, map: "redistVersion_fkey") name String description String @@ -69,8 +87,10 @@ model RedistVersion { versionId String @id version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade) - gameDependees GameVersion[] - dlcDependees DLCVersion[] + launches LaunchOption[] + + gameDependees GameVersion[] + dlcDependees DLCVersion[] } model ModVersion { diff --git a/prisma/models/metadata.prisma b/prisma/models/metadata.prisma index 71c085783..343928fe8 100644 --- a/prisma/models/metadata.prisma +++ b/prisma/models/metadata.prisma @@ -79,7 +79,8 @@ model Redist { library Library @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade) libraryPath String - versions Version[] + versions Version[] + platform UserPlatform? @@unique([libraryId, libraryPath], name: "libraryKey") } diff --git a/server/api/v1/admin/company/[id]/banner.post.ts b/server/api/v1/admin/company/[id]/banner.post.ts index 0c8744a54..2650728fe 100644 --- a/server/api/v1/admin/company/[id]/banner.post.ts +++ b/server/api/v1/admin/company/[id]/banner.post.ts @@ -15,13 +15,13 @@ export default defineEventHandler(async (h3) => { }); if (!company) - throw createError({ statusCode: 400, statusMessage: "Invalid company id" }); + throw createError({ statusCode: 400, message: "Invalid company id" }); const result = await handleFileUpload(h3, {}, ["internal:read"], 1); if (!result) throw createError({ statusCode: 400, - statusMessage: "File upload required (multipart form)", + message: "File upload required (multipart form)", }); const [ids, , pull, dump] = result; @@ -29,7 +29,7 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "Upload at least one file.", + message: "Upload at least one file.", }); try { diff --git a/server/api/v1/admin/company/[id]/game.post.ts b/server/api/v1/admin/company/[id]/game.post.ts index f8731186d..50f8daf05 100644 --- a/server/api/v1/admin/company/[id]/game.post.ts +++ b/server/api/v1/admin/company/[id]/game.post.ts @@ -20,7 +20,7 @@ export default defineEventHandler(async (h3) => { if (!body.published && !body.developed) throw createError({ statusCode: 400, - statusMessage: "Must be related (either developed or published).", + message: "Must be related (either developed or published).", }); const publisherConnect = body.published diff --git a/server/api/v1/admin/company/[id]/icon.post.ts b/server/api/v1/admin/company/[id]/icon.post.ts index 0a4cefdbe..6bf944798 100644 --- a/server/api/v1/admin/company/[id]/icon.post.ts +++ b/server/api/v1/admin/company/[id]/icon.post.ts @@ -15,13 +15,13 @@ export default defineEventHandler(async (h3) => { }); if (!company) - throw createError({ statusCode: 400, statusMessage: "Invalid company id" }); + throw createError({ statusCode: 400, message: "Invalid company id" }); const result = await handleFileUpload(h3, {}, ["internal:read"], 1); if (!result) throw createError({ statusCode: 400, - statusMessage: "File upload required (multipart form)", + message: "File upload required (multipart form)", }); const [ids, , pull, dump] = result; @@ -29,7 +29,7 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "Upload at least one file.", + message: "Upload at least one file.", }); try { diff --git a/server/api/v1/admin/company/[id]/index.delete.ts b/server/api/v1/admin/company/[id]/index.delete.ts index b27d3939f..cb71e37d2 100644 --- a/server/api/v1/admin/company/[id]/index.delete.ts +++ b/server/api/v1/admin/company/[id]/index.delete.ts @@ -9,6 +9,6 @@ export default defineEventHandler(async (h3) => { const company = await prisma.company.deleteMany({ where: { id } }); if (company.count == 0) - throw createError({ statusCode: 404, statusMessage: "Company not found" }); + throw createError({ statusCode: 404, message: "Company not found" }); return; }); diff --git a/server/api/v1/admin/company/[id]/index.get.ts b/server/api/v1/admin/company/[id]/index.get.ts index 50c4115dd..1d3eadc65 100644 --- a/server/api/v1/admin/company/[id]/index.get.ts +++ b/server/api/v1/admin/company/[id]/index.get.ts @@ -23,7 +23,7 @@ export default defineEventHandler(async (h3) => { }, }); if (!company) - throw createError({ statusCode: 404, statusMessage: "Company not found" }); + throw createError({ statusCode: 404, message: "Company not found" }); const games = await prisma.game.findMany({ where: { OR: [ diff --git a/server/api/v1/admin/game/[id]/index.get.ts b/server/api/v1/admin/game/[id]/index.get.ts index 138426b29..abdedebd1 100644 --- a/server/api/v1/admin/game/[id]/index.get.ts +++ b/server/api/v1/admin/game/[id]/index.get.ts @@ -33,7 +33,7 @@ export default defineEventHandler(async (h3) => { }); if (!game || !game.libraryId) - throw createError({ statusCode: 404, statusMessage: "Game ID not found" }); + throw createError({ statusCode: 404, message: "Game ID not found" }); const unimportedVersions = await libraryManager.fetchUnimportedGameVersions( game.libraryId, diff --git a/server/api/v1/admin/game/[id]/metadata.post.ts b/server/api/v1/admin/game/[id]/metadata.post.ts index cd1dcbc64..eccf3c44b 100644 --- a/server/api/v1/admin/game/[id]/metadata.post.ts +++ b/server/api/v1/admin/game/[id]/metadata.post.ts @@ -1,8 +1,14 @@ +import { ArkErrors, type } from "arktype"; import type { Prisma } from "~/prisma/client/client"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; +const UpdateMetadata = type({ + name: "string?", + description: "string?", +}); + export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, ["game:update"]); if (!allowed) throw createError({ statusCode: 403 }); @@ -11,7 +17,7 @@ export default defineEventHandler(async (h3) => { if (!form) throw createError({ statusCode: 400, - statusMessage: "This endpoint requires multipart form data.", + message: "This endpoint requires multipart form data.", }); const gameId = getRouterParam(h3, "id")!; @@ -20,20 +26,20 @@ export default defineEventHandler(async (h3) => { if (!uploadResult) throw createError({ statusCode: 400, - statusMessage: "Failed to upload file", + message: "Failed to upload file", }); const [ids, options, pull, dump] = uploadResult; const id = ids.at(0); - // handleFileUpload reads the rest of the options for us. - const name = options.name; - const description = options.description; + const body = UpdateMetadata(options); + if (body instanceof ArkErrors) + throw createError({ statusCode: 400, message: body.summary }); const updateModel: Prisma.GameUpdateInput = { - mName: name, - mShortDescription: description, + ...(body.name ? { mName: body.name } : undefined), + ...(body.description ? { mShortDescription: body.description } : undefined), }; // handle if user uploaded new icon diff --git a/server/api/v1/admin/game/image/index.delete.ts b/server/api/v1/admin/game/image/index.delete.ts index d89ae76a2..2089eb259 100644 --- a/server/api/v1/admin/game/image/index.delete.ts +++ b/server/api/v1/admin/game/image/index.delete.ts @@ -32,11 +32,11 @@ export default defineEventHandler<{ }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); const imageIndex = game.mImageLibraryObjectIds.findIndex((e) => e == imageId); if (imageIndex == -1) - throw createError({ statusCode: 400, statusMessage: "Image not found" }); + throw createError({ statusCode: 400, message: "Image not found" }); game.mImageLibraryObjectIds.splice(imageIndex, 1); await objectHandler.deleteAsSystem(imageId); diff --git a/server/api/v1/admin/game/image/index.post.ts b/server/api/v1/admin/game/image/index.post.ts index e230e9fd1..7c819d6a2 100644 --- a/server/api/v1/admin/game/image/index.post.ts +++ b/server/api/v1/admin/game/image/index.post.ts @@ -10,14 +10,14 @@ export default defineEventHandler(async (h3) => { if (!form) throw createError({ statusCode: 400, - statusMessage: "This endpoint requires multipart form data.", + message: "This endpoint requires multipart form data.", }); const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]); if (!uploadResult) throw createError({ statusCode: 400, - statusMessage: "Failed to upload file", + message: "Failed to upload file", }); const [ids, options, pull, dump] = uploadResult; @@ -25,21 +25,21 @@ export default defineEventHandler(async (h3) => { dump(); throw createError({ statusCode: 400, - statusMessage: "Did not upload a file", + message: "Did not upload a file", }); } - const gameId = options.id; + const gameId = options.id as string; if (!gameId) throw createError({ statusCode: 400, - statusMessage: "No game ID attached", + message: "No game ID attached", }); const hasGame = (await prisma.game.count({ where: { id: gameId } })) != 0; if (!hasGame) { dump(); - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); } const result = await prisma.game.update({ diff --git a/server/api/v1/admin/import/game/index.post.ts b/server/api/v1/admin/import/game/index.post.ts index e3e32478a..75c128f79 100644 --- a/server/api/v1/admin/import/game/index.post.ts +++ b/server/api/v1/admin/import/game/index.post.ts @@ -27,14 +27,14 @@ export default defineEventHandler<{ body: typeof ImportGameBody.infer }>( if (!path) throw createError({ statusCode: 400, - statusMessage: "Path missing from body", + message: "Path missing from body", }); const valid = await libraryManager.checkUnimportedGamePath(library, path); if (!valid) throw createError({ statusCode: 400, - statusMessage: "Invalid library or game.", + message: "Invalid library or game.", }); const taskId = metadata @@ -44,7 +44,7 @@ export default defineEventHandler<{ body: typeof ImportGameBody.infer }>( if (!taskId) throw createError({ statusCode: 400, - statusMessage: + message: "Duplicate metadata import. Please chose a different game or metadata provider.", }); diff --git a/server/api/v1/admin/import/game/search.get.ts b/server/api/v1/admin/import/game/search.get.ts index a68f45d94..536c4e32e 100644 --- a/server/api/v1/admin/import/game/search.get.ts +++ b/server/api/v1/admin/import/game/search.get.ts @@ -8,14 +8,14 @@ export default defineEventHandler(async (h3) => { const query = getQuery(h3); const search = query.q?.toString(); if (!search) - throw createError({ statusCode: 400, statusMessage: "Invalid search" }); + throw createError({ statusCode: 400, message: "Invalid search" }); const results = await metadataHandler.search(search); if (results.length == 0) throw createError({ statusCode: 404, - statusMessage: "No metadata provider returned search results.", + message: "No metadata provider returned search results.", }); return results; diff --git a/server/api/v1/admin/import/redist/index.post.ts b/server/api/v1/admin/import/redist/index.post.ts index 510847b6d..95cb22f80 100644 --- a/server/api/v1/admin/import/redist/index.post.ts +++ b/server/api/v1/admin/import/redist/index.post.ts @@ -4,6 +4,7 @@ import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; import * as jdenticon from "jdenticon"; import prisma from "~/server/internal/db/database"; import libraryManager from "~/server/internal/library"; +import jsdom from "jsdom"; export const ImportRedist = type({ library: "string", @@ -11,6 +12,12 @@ export const ImportRedist = type({ name: "string", description: "string", + + "platform?": type({ + name: "string", + icon: "string", + fileExts: type("string").pipe.try((s) => JSON.parse(s), type("string[]")), + }), }); export default defineEventHandler(async (h3) => { @@ -18,14 +25,13 @@ export default defineEventHandler(async (h3) => { if (!allowed) throw createError({ statusCode: 403 }); const body = await handleFileUpload(h3, {}, ["internal:read"], 1); - if (!body) - throw createError({ statusCode: 400, statusMessage: "Body required." }); + if (!body) throw createError({ statusCode: 400, message: "Body required." }); - const [[id], rawOptions, pull,, add] = body; + const [[id], rawOptions, pull, , add] = body; const options = ImportRedist(rawOptions); if (options instanceof ArkErrors) - throw createError({ statusCode: 400, statusMessage: options.summary }); + throw createError({ statusCode: 400, message: options.summary }); const valid = await libraryManager.checkUnimportedGamePath( options.library, @@ -34,11 +40,25 @@ export default defineEventHandler(async (h3) => { if (!valid) throw createError({ statusCode: 400, - statusMessage: "Invalid library or game.", + message: "Invalid library or game.", }); const icon = id ?? add(jdenticon.toPng(options.name, 512)); + let svgContent = ""; + if (options.platform) { + const dom = new jsdom.JSDOM(options.platform.icon); + const svg = dom.window.document.getElementsByTagName("svg").item(0); + if (!svg) + throw createError({ + statusCode: 400, + statusMessage: "No SVG in uploaded image.", + }); + svg.removeAttribute("width"); + svg.removeAttribute("height"); + svgContent = svg.outerHTML; + } + const redist = await prisma.redist.create({ data: { libraryId: options.library, @@ -47,6 +67,18 @@ export default defineEventHandler(async (h3) => { mName: options.name, mShortDescription: options.description, mIconObjectId: icon, + + platform: { + ...(options.platform + ? { + create: { + platformName: options.platform.name, + iconSvg: svgContent, + fileExtensions: options.platform.fileExts, + }, + } + : undefined), + }, }, }); diff --git a/server/api/v1/admin/import/version/index.get.ts b/server/api/v1/admin/import/version/index.get.ts index 893d295c2..88e169c4a 100644 --- a/server/api/v1/admin/import/version/index.get.ts +++ b/server/api/v1/admin/import/version/index.get.ts @@ -11,7 +11,7 @@ export default defineEventHandler(async (h3) => { if (!gameId) throw createError({ statusCode: 400, - statusMessage: "Missing id in request params", + message: "Missing id in request params", }); const game = await prisma.game.findUnique({ @@ -19,14 +19,14 @@ export default defineEventHandler(async (h3) => { select: { libraryId: true, libraryPath: true }, }); if (!game || !game.libraryId) - throw createError({ statusCode: 404, statusMessage: "Game not found" }); + throw createError({ statusCode: 404, message: "Game not found" }); const unimportedVersions = await libraryManager.fetchUnimportedGameVersions( game.libraryId, game.libraryPath, ); if (!unimportedVersions) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); return unimportedVersions; }); diff --git a/server/api/v1/admin/import/version/index.post.ts b/server/api/v1/admin/import/version/index.post.ts index d4ab1802e..a6b1adfde 100644 --- a/server/api/v1/admin/import/version/index.post.ts +++ b/server/api/v1/admin/import/version/index.post.ts @@ -5,6 +5,13 @@ import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; import libraryManager from "~/server/internal/library"; +export const LaunchCommands = type({ + name: "string > 0", + description: "string = ''", + launchCommand: "string > 0", + launchArgs: "string = ''", + }).array(); + export const ImportVersion = type({ id: "string", version: "string", @@ -17,12 +24,7 @@ export const ImportVersion = type({ delta: "boolean = false", umuId: "string = ''", - launches: type({ - name: "string > 0", - description: "string = ''", - launchCommand: "string > 0", - launchArgs: "string = ''", - }).array(), + launches: LaunchCommands, }).configure(throwingArktype); export default defineEventHandler(async (h3) => { @@ -41,7 +43,7 @@ export default defineEventHandler(async (h3) => { if (validOverlayVersions == 0) throw createError({ statusCode: 400, - statusMessage: + message: "Update mode requires a pre-existing version for this platform.", }); } @@ -50,13 +52,13 @@ export default defineEventHandler(async (h3) => { if (!body.setup) throw createError({ statusCode: 400, - statusMessage: 'Setup required in "setup mode".', + message: 'Setup required in "setup mode".', }); } else { if (!body.delta && body.launches.length == 0) throw createError({ statusCode: 400, - statusMessage: + message: "At least one launch command is required for non-delta versions", }); } @@ -70,7 +72,7 @@ export default defineEventHandler(async (h3) => { if (!taskId) throw createError({ statusCode: 400, - statusMessage: "Invalid options for import", + message: "Invalid options for import", }); return { taskId: taskId }; diff --git a/server/api/v1/admin/import/version/preload.get.ts b/server/api/v1/admin/import/version/preload.get.ts index d83b9363f..8fbc24643 100644 --- a/server/api/v1/admin/import/version/preload.get.ts +++ b/server/api/v1/admin/import/version/preload.get.ts @@ -11,7 +11,7 @@ export default defineEventHandler(async (h3) => { if (!gameId || !versionName) throw createError({ statusCode: 400, - statusMessage: "Missing id or version in request params", + message: "Missing id or version in request params", }); const preload = await libraryManager.fetchUnimportedVersionInformation( @@ -21,7 +21,7 @@ export default defineEventHandler(async (h3) => { if (!preload) throw createError({ statusCode: 400, - statusMessage: "Invalid game or version id/name", + message: "Invalid game or version id/name", }); return preload; diff --git a/server/api/v1/admin/library/sources/index.patch.ts b/server/api/v1/admin/library/sources/index.patch.ts index f098b7a42..393ee447d 100644 --- a/server/api/v1/admin/library/sources/index.patch.ts +++ b/server/api/v1/admin/library/sources/index.patch.ts @@ -26,7 +26,7 @@ export default defineEventHandler<{ body: typeof UpdateLibrarySource.infer }>( if (!source) throw createError({ statusCode: 400, - statusMessage: "Library source not found", + message: "Library source not found", }); const constructor = libraryConstructors[source.backend]; @@ -61,7 +61,7 @@ export default defineEventHandler<{ body: typeof UpdateLibrarySource.infer }>( } catch (e) { throw createError({ statusCode: 400, - statusMessage: `Failed to create source: ${e}`, + message: `Failed to create source: ${e}`, }); } }, diff --git a/server/api/v1/admin/library/sources/index.post.ts b/server/api/v1/admin/library/sources/index.post.ts index ae32b7530..31c9e7eb3 100644 --- a/server/api/v1/admin/library/sources/index.post.ts +++ b/server/api/v1/admin/library/sources/index.post.ts @@ -29,7 +29,7 @@ export default defineEventHandler<{ body: typeof CreateLibrarySource.infer }>( if (!backend) throw createError({ statusCode: 400, - statusMessage: "Invalid source backend.", + message: "Invalid source backend.", }); const constructor = libraryConstructors[backend]; @@ -63,7 +63,7 @@ export default defineEventHandler<{ body: typeof CreateLibrarySource.infer }>( } catch (e) { throw createError({ statusCode: 400, - statusMessage: `Failed to create source: ${e}`, + message: `Failed to create source: ${e}`, }); } }, diff --git a/server/api/v1/admin/news/index.get.ts b/server/api/v1/admin/news/index.get.ts index 392f4b7b2..4e6b06526 100644 --- a/server/api/v1/admin/news/index.get.ts +++ b/server/api/v1/admin/news/index.get.ts @@ -14,13 +14,13 @@ export default defineEventHandler(async (h3) => { const orderBy = query.order as "asc" | "desc"; if (orderBy) { if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) - throw createError({ statusCode: 400, statusMessage: "Invalid order" }); + throw createError({ statusCode: 400, message: "Invalid order" }); } const tags = query.tags as string[] | undefined; if (tags) { if (typeof tags !== "object" || !Array.isArray(tags)) - throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); + throw createError({ statusCode: 400, message: "Invalid tags" }); } const options = { diff --git a/server/api/v1/admin/news/index.post.ts b/server/api/v1/admin/news/index.post.ts index fb09c2e40..838e4f426 100644 --- a/server/api/v1/admin/news/index.post.ts +++ b/server/api/v1/admin/news/index.post.ts @@ -19,27 +19,27 @@ export default defineEventHandler(async (h3) => { if (!form) throw createError({ statusCode: 400, - statusMessage: "This endpoint requires multipart form data.", + message: "This endpoint requires multipart form data.", }); const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1); if (!uploadResult) throw createError({ statusCode: 400, - statusMessage: "Failed to upload file", + message: "Failed to upload file", }); const [imageIds, options, pull, _dump] = uploadResult; const body = await CreateNews(options); if (body instanceof ArkErrors) - throw createError({ statusCode: 400, statusMessage: body.summary }); + throw createError({ statusCode: 400, message: body.summary }); const parsedTags = JSON.parse(body.tags); if (typeof parsedTags !== "object" || !Array.isArray(parsedTags)) throw createError({ statusCode: 400, - statusMessage: "Tags must be an array", + message: "Tags must be an array", }); const imageId = imageIds.at(0); diff --git a/server/api/v1/admin/tags/[id]/index.delete.ts b/server/api/v1/admin/tags/[id]/index.delete.ts index 42792579d..2f300f21a 100644 --- a/server/api/v1/admin/tags/[id]/index.delete.ts +++ b/server/api/v1/admin/tags/[id]/index.delete.ts @@ -9,6 +9,6 @@ export default defineEventHandler(async (h3) => { const tag = await prisma.gameTag.deleteMany({ where: { id } }); if (tag.count == 0) - throw createError({ statusCode: 404, statusMessage: "Tag not found" }); + throw createError({ statusCode: 404, message: "Tag not found" }); return; }); diff --git a/server/api/v1/admin/task/index.get.ts b/server/api/v1/admin/task/index.get.ts index 690dce82e..44395c5fe 100644 --- a/server/api/v1/admin/task/index.get.ts +++ b/server/api/v1/admin/task/index.get.ts @@ -10,7 +10,7 @@ export default defineEventHandler(async (h3) => { if (!allAcls) throw createError({ statusCode: 403, - statusMessage: "Somehow no ACLs on authenticated request.", + message: "Somehow no ACLs on authenticated request.", }); const runningTasks = (await taskHandler.runningTasks()).map((e) => e.id); diff --git a/server/api/v1/admin/task/index.post.ts b/server/api/v1/admin/task/index.post.ts index dc281908c..7a11ef3c0 100644 --- a/server/api/v1/admin/task/index.post.ts +++ b/server/api/v1/admin/task/index.post.ts @@ -18,14 +18,14 @@ export default defineEventHandler(async (h3) => { if (!taskGroups[taskGroup]) throw createError({ statusCode: 400, - statusMessage: "Invalid task group.", + message: "Invalid task group.", }); const task = await taskHandler.runTaskGroupByName(taskGroup); if (!task) throw createError({ statusCode: 500, - statusMessage: "Could not start task.", + message: "Could not start task.", }); return { id: task }; }); diff --git a/server/api/v1/admin/token/[id]/index.delete.ts b/server/api/v1/admin/token/[id]/index.delete.ts index 1e33730c2..dda8261c7 100644 --- a/server/api/v1/admin/token/[id]/index.delete.ts +++ b/server/api/v1/admin/token/[id]/index.delete.ts @@ -10,14 +10,14 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "No id in router params", + message: "No id in router params", }); const deleted = await prisma.aPIToken.delete({ where: { id: id, mode: APITokenMode.System }, })!; if (!deleted) - throw createError({ statusCode: 404, statusMessage: "Token not found" }); + throw createError({ statusCode: 404, message: "Token not found" }); return; }); diff --git a/server/api/v1/admin/token/index.post.ts b/server/api/v1/admin/token/index.post.ts index 421fe8d1d..077c8efb9 100644 --- a/server/api/v1/admin/token/index.post.ts +++ b/server/api/v1/admin/token/index.post.ts @@ -22,7 +22,7 @@ export default defineEventHandler(async (h3) => { if (invalidACLs.length > 0) throw createError({ statusCode: 400, - statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`, + message: `Invalid ACLs: ${invalidACLs.join(", ")}`, }); const token = await prisma.aPIToken.create({ diff --git a/server/api/v1/admin/users/[id]/index.delete.ts b/server/api/v1/admin/users/[id]/index.delete.ts index c514817ca..35749b7bb 100644 --- a/server/api/v1/admin/users/[id]/index.delete.ts +++ b/server/api/v1/admin/users/[id]/index.delete.ts @@ -19,12 +19,12 @@ export default defineEventHandler(async (h3) => { if (userId === "system") throw createError({ statusCode: 400, - statusMessage: "Cannot interact with system user.", + message: "Cannot interact with system user.", }); const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) - throw createError({ statusCode: 404, statusMessage: "User not found." }); + throw createError({ statusCode: 404, message: "User not found." }); await prisma.user.delete({ where: { id: userId } }); return { success: true }; diff --git a/server/api/v1/admin/users/[id]/index.get.ts b/server/api/v1/admin/users/[id]/index.get.ts index 0ccb84211..df703305a 100644 --- a/server/api/v1/admin/users/[id]/index.get.ts +++ b/server/api/v1/admin/users/[id]/index.get.ts @@ -9,18 +9,18 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 400, - statusMessage: "No userId in route.", + message: "No userId in route.", }); if (userId == "system") throw createError({ statusCode: 400, - statusMessage: "Cannot fetch system user.", + message: "Cannot fetch system user.", }); const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) - throw createError({ statusCode: 404, statusMessage: "User not found." }); + throw createError({ statusCode: 404, message: "User not found." }); return user; }); diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts index 3e4ca03ed..3e30bdfda 100644 --- a/server/api/v1/auth/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -23,7 +23,7 @@ export default defineEventHandler<{ if (!authManager.getAuthProviders().Simple) throw createError({ statusCode: 403, - statusMessage: t("errors.auth.method.signinDisabled"), + message: t("errors.auth.method.signinDisabled"), }); const body = signinValidator(await readBody(h3)); @@ -33,7 +33,7 @@ export default defineEventHandler<{ throw createError({ statusCode: 400, - statusMessage: body.summary, + message: body.summary, }); } @@ -57,13 +57,13 @@ export default defineEventHandler<{ if (!authMek) throw createError({ statusCode: 401, - statusMessage: t("errors.auth.invalidUserOrPass"), + message: t("errors.auth.invalidUserOrPass"), }); if (!authMek.user.enabled) throw createError({ statusCode: 403, - statusMessage: t("errors.auth.disabled"), + message: t("errors.auth.disabled"), }); // LEGACY bcrypt @@ -74,13 +74,13 @@ export default defineEventHandler<{ if (!hash) throw createError({ statusCode: 500, - statusMessage: t("errors.auth.invalidPassState"), + message: t("errors.auth.invalidPassState"), }); if (!(await checkHashBcrypt(body.password, hash))) throw createError({ statusCode: 401, - statusMessage: t("errors.auth.invalidUserOrPass"), + message: t("errors.auth.invalidUserOrPass"), }); // TODO: send user to forgot password screen or something to force them to change their password to new system @@ -93,13 +93,13 @@ export default defineEventHandler<{ if (!hash || typeof hash !== "string") throw createError({ statusCode: 500, - statusMessage: t("errors.auth.invalidPassState"), + message: t("errors.auth.invalidPassState"), }); if (!(await checkHashArgon2(body.password, hash))) throw createError({ statusCode: 401, - statusMessage: t("errors.auth.invalidUserOrPass"), + message: t("errors.auth.invalidUserOrPass"), }); await sessionHandler.signin(h3, authMek.userId, body.rememberMe); diff --git a/server/api/v1/auth/signup/simple.get.ts b/server/api/v1/auth/signup/simple.get.ts index ad6c49696..f7c6d79f5 100644 --- a/server/api/v1/auth/signup/simple.get.ts +++ b/server/api/v1/auth/signup/simple.get.ts @@ -8,7 +8,7 @@ export default defineEventHandler(async (h3) => { if (!authManager.getAuthProviders().Simple) throw createError({ statusCode: 403, - statusMessage: t("errors.auth.method.signinDisabled"), + message: t("errors.auth.method.signinDisabled"), }); const query = getQuery(h3); @@ -16,7 +16,7 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: t("errors.auth.inviteIdRequired"), + message: t("errors.auth.inviteIdRequired"), }); taskHandler.runTaskGroupByName("cleanup:invitations"); @@ -24,7 +24,7 @@ export default defineEventHandler(async (h3) => { if (!invitation) throw createError({ statusCode: 404, - statusMessage: t("errors.auth.invalidInvite"), + message: t("errors.auth.invalidInvite"), }); return invitation; diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index 8434f658a..5bb9fdf68 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -26,7 +26,7 @@ export default defineEventHandler<{ if (!authManager.getAuthProviders().Simple) throw createError({ statusCode: 403, - statusMessage: t("errors.auth.method.signinDisabled"), + message: t("errors.auth.method.signinDisabled"), }); const user = await readValidatedBody(h3, CreateUserValidator); @@ -37,7 +37,7 @@ export default defineEventHandler<{ if (!invitation) throw createError({ statusCode: 401, - statusMessage: t("errors.auth.invalidInvite"), + message: t("errors.auth.invalidInvite"), }); // reuse items from invite @@ -50,7 +50,7 @@ export default defineEventHandler<{ if (existing > 0) throw createError({ statusCode: 400, - statusMessage: t("errors.auth.usernameTaken"), + message: t("errors.auth.usernameTaken"), }); const userId = randomUUID(); diff --git a/server/api/v1/client/auth/callback/index.post.ts b/server/api/v1/client/auth/callback/index.post.ts index bc63be703..cef645b11 100644 --- a/server/api/v1/client/auth/callback/index.post.ts +++ b/server/api/v1/client/auth/callback/index.post.ts @@ -12,13 +12,13 @@ export default defineEventHandler(async (h3) => { if (!client) throw createError({ statusCode: 400, - statusMessage: "Invalid or expired client ID.", + message: "Invalid or expired client ID.", }); if (client.userId != user.userId) throw createError({ statusCode: 403, - statusMessage: "Not allowed to authorize this client.", + message: "Not allowed to authorize this client.", }); const token = await clientHandler.generateAuthToken(clientId); diff --git a/server/api/v1/client/auth/code/index.get.ts b/server/api/v1/client/auth/code/index.get.ts index 74bc6e871..5110b101b 100644 --- a/server/api/v1/client/auth/code/index.get.ts +++ b/server/api/v1/client/auth/code/index.get.ts @@ -10,12 +10,12 @@ export default defineEventHandler(async (h3) => { if (!code) throw createError({ statusCode: 400, - statusMessage: "Code required in query params.", + message: "Code required in query params.", }); const clientId = await clientHandler.fetchClientIdByCode(code); if (!clientId) - throw createError({ statusCode: 400, statusMessage: "Invalid code." }); + throw createError({ statusCode: 400, message: "Invalid code." }); return clientId; }); diff --git a/server/api/v1/client/auth/code/index.post.ts b/server/api/v1/client/auth/code/index.post.ts index 593b773b9..f9971ebe0 100644 --- a/server/api/v1/client/auth/code/index.post.ts +++ b/server/api/v1/client/auth/code/index.post.ts @@ -12,19 +12,19 @@ export default defineEventHandler(async (h3) => { if (!client) throw createError({ statusCode: 400, - statusMessage: "Invalid or expired client ID.", + message: "Invalid or expired client ID.", }); if (client.userId != user.userId) throw createError({ statusCode: 403, - statusMessage: "Not allowed to authorize this client.", + message: "Not allowed to authorize this client.", }); if (!client.peer) throw createError({ statusCode: 500, - statusMessage: "No client listening for authorization.", + message: "No client listening for authorization.", }); const token = await clientHandler.generateAuthToken(clientId); diff --git a/server/api/v1/client/auth/code/ws.get.ts b/server/api/v1/client/auth/code/ws.get.ts index 637d8df4d..820589f21 100644 --- a/server/api/v1/client/auth/code/ws.get.ts +++ b/server/api/v1/client/auth/code/ws.get.ts @@ -9,14 +9,14 @@ export default defineWebSocketHandler({ if (!code) throw createError({ statusCode: 400, - statusMessage: "Code required in Authorization header.", + message: "Code required in Authorization header.", }); await clientHandler.connectCodeListener(code, peer); } catch (e) { peer.send( JSON.stringify({ type: "error", - value: (e as FetchError)?.statusMessage, + value: (e as FetchError)?.message, }), ); peer.close(); diff --git a/server/api/v1/client/auth/handshake.post.ts b/server/api/v1/client/auth/handshake.post.ts index c154c8c07..528d5c3cf 100644 --- a/server/api/v1/client/auth/handshake.post.ts +++ b/server/api/v1/client/auth/handshake.post.ts @@ -8,24 +8,24 @@ export default defineEventHandler(async (h3) => { if (!clientId || !token) throw createError({ statusCode: 400, - statusMessage: "Missing token or client ID from body", + message: "Missing token or client ID from body", }); const metadata = await clientHandler.fetchClient(clientId); if (!metadata) throw createError({ statusCode: 403, - statusMessage: "Invalid client ID", + message: "Invalid client ID", }); if (!metadata.authToken || !metadata.userId) throw createError({ statusCode: 400, - statusMessage: "Un-authorized client ID", + message: "Un-authorized client ID", }); if (metadata.authToken !== token) throw createError({ statusCode: 403, - statusMessage: "Invalid token", + message: "Invalid token", }); const certificateAuthority = useCertificateAuthority(); diff --git a/server/api/v1/client/auth/index.get.ts b/server/api/v1/client/auth/index.get.ts index 92cf16c5f..b42d14ff6 100644 --- a/server/api/v1/client/auth/index.get.ts +++ b/server/api/v1/client/auth/index.get.ts @@ -10,20 +10,20 @@ export default defineEventHandler(async (h3) => { if (!providedClientId) throw createError({ statusCode: 400, - statusMessage: "Provide client ID in request params as 'id'", + message: "Provide client ID in request params as 'id'", }); const client = await clientHandler.fetchClient(providedClientId); if (!client) throw createError({ statusCode: 404, - statusMessage: "Request not found.", + message: "Request not found.", }); if (client.userId && user.userId !== client.userId) throw createError({ statusCode: 400, - statusMessage: "Client already claimed.", + message: "Client already claimed.", }); await clientHandler.attachUserId(providedClientId, user.userId); diff --git a/server/api/v1/client/auth/initiate.post.ts b/server/api/v1/client/auth/initiate.post.ts index 64186b5e7..be435e1ba 100644 --- a/server/api/v1/client/auth/initiate.post.ts +++ b/server/api/v1/client/auth/initiate.post.ts @@ -28,7 +28,7 @@ export default defineEventHandler(async (h3) => { if (!platform) throw createError({ statusCode: 400, - statusMessage: "Invalid or unsupported platform", + message: "Invalid or unsupported platform", }); const capabilityIterable = Object.entries(capabilities) as Array< @@ -42,7 +42,7 @@ export default defineEventHandler(async (h3) => { ) throw createError({ statusCode: 400, - statusMessage: "Invalid capabilities.", + message: "Invalid capabilities.", }); if ( @@ -57,7 +57,7 @@ export default defineEventHandler(async (h3) => { ) throw createError({ statusCode: 400, - statusMessage: "Invalid capability configuration.", + message: "Invalid capability configuration.", }); const result = await clientHandler.initiate({ diff --git a/server/api/v1/client/capability/index.post.ts b/server/api/v1/client/capability/index.post.ts index a33f690ba..1135f0fc3 100644 --- a/server/api/v1/client/capability/index.post.ts +++ b/server/api/v1/client/capability/index.post.ts @@ -14,13 +14,13 @@ export default defineClientEventHandler( if (!rawCapability || typeof rawCapability !== "string") throw createError({ statusCode: 400, - statusMessage: "capability must be a string", + message: "capability must be a string", }); if (!configuration || typeof configuration !== "object") throw createError({ statusCode: 400, - statusMessage: "configuration must be an object", + message: "configuration must be an object", }); const capability = rawCapability as InternalClientCapability; @@ -28,7 +28,7 @@ export default defineClientEventHandler( if (!validCapabilities.includes(capability)) throw createError({ statusCode: 400, - statusMessage: "Invalid capability.", + message: "Invalid capability.", }); const isValid = await capabilityManager.validateCapabilityConfiguration( @@ -38,7 +38,7 @@ export default defineClientEventHandler( if (!isValid) throw createError({ statusCode: 400, - statusMessage: "Invalid capability configuration.", + message: "Invalid capability configuration.", }); await capabilityManager.upsertClientCapability( diff --git a/server/api/v1/client/chunk.get.ts b/server/api/v1/client/chunk.get.ts index 030b79607..899046779 100644 --- a/server/api/v1/client/chunk.get.ts +++ b/server/api/v1/client/chunk.get.ts @@ -20,7 +20,7 @@ export default defineClientEventHandler(async (h3) => { if (!gameId || !versionName || !filename || Number.isNaN(chunkIndex)) throw createError({ statusCode: 400, - statusMessage: "Invalid chunk arguments", + message: "Invalid chunk arguments", }); let game = await gameLookupCache.getItem(gameId); @@ -35,7 +35,7 @@ export default defineClientEventHandler(async (h3) => { }, }); if (!game || !game.libraryId) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); await gameLookupCache.setItem(gameId, game); } @@ -43,7 +43,7 @@ export default defineClientEventHandler(async (h3) => { if (!game.libraryId) throw createError({ statusCode: 500, - statusMessage: "Somehow, we got here.", + message: "Somehow, we got here.", }); const peek = await libraryManager.peekFile( @@ -53,7 +53,7 @@ export default defineClientEventHandler(async (h3) => { filename, ); if (!peek) - throw createError({ status: 400, statusMessage: "Failed to peek file" }); + throw createError({ status: 400, message: "Failed to peek file" }); const start = chunkIndex * chunkSize; const end = Math.min((chunkIndex + 1) * chunkSize, peek.size); @@ -63,7 +63,7 @@ export default defineClientEventHandler(async (h3) => { if (start >= end) throw createError({ statusCode: 400, - statusMessage: "Invalid chunk index", + message: "Invalid chunk index", }); const gameReadStream = await libraryManager.readFile( @@ -76,7 +76,7 @@ export default defineClientEventHandler(async (h3) => { if (!gameReadStream) throw createError({ statusCode: 400, - statusMessage: "Failed to create stream", + message: "Failed to create stream", }); return sendStream(h3, gameReadStream); diff --git a/server/api/v1/client/collection/[id]/entry.delete.ts b/server/api/v1/client/collection/[id]/entry.delete.ts index 204e1335f..33c5666fa 100644 --- a/server/api/v1/client/collection/[id]/entry.delete.ts +++ b/server/api/v1/client/collection/[id]/entry.delete.ts @@ -8,13 +8,13 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); const successful = await userLibraryManager.collectionRemove( gameId, @@ -24,7 +24,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!successful) throw createError({ statusCode: 404, - statusMessage: "Collection not found", + message: "Collection not found", }); return {}; }); diff --git a/server/api/v1/client/collection/[id]/entry.post.ts b/server/api/v1/client/collection/[id]/entry.post.ts index 357e236de..b0899f4ce 100644 --- a/server/api/v1/client/collection/[id]/entry.post.ts +++ b/server/api/v1/client/collection/[id]/entry.post.ts @@ -8,13 +8,13 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); return await userLibraryManager.collectionAdd(gameId, id, user.id); }); diff --git a/server/api/v1/client/collection/[id]/index.delete.ts b/server/api/v1/client/collection/[id]/index.delete.ts index ffe6b7103..494139511 100644 --- a/server/api/v1/client/collection/[id]/index.delete.ts +++ b/server/api/v1/client/collection/[id]/index.delete.ts @@ -8,7 +8,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); // Verify collection exists and user owns it @@ -17,13 +17,13 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!collection) throw createError({ statusCode: 404, - statusMessage: "Collection not found", + message: "Collection not found", }); if (collection.userId !== user.id) throw createError({ statusCode: 403, - statusMessage: "Not authorized to delete this collection", + message: "Not authorized to delete this collection", }); await userLibraryManager.deleteCollection(id); diff --git a/server/api/v1/client/collection/[id]/index.get.ts b/server/api/v1/client/collection/[id]/index.get.ts index 9267f69a6..dab440864 100644 --- a/server/api/v1/client/collection/[id]/index.get.ts +++ b/server/api/v1/client/collection/[id]/index.get.ts @@ -8,7 +8,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); // Fetch specific collection @@ -17,14 +17,14 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { if (!collection) throw createError({ statusCode: 404, - statusMessage: "Collection not found", + message: "Collection not found", }); // Verify user owns this collection if (collection.userId !== user.id) throw createError({ statusCode: 403, - statusMessage: "Not authorized to access this collection", + message: "Not authorized to access this collection", }); return collection; diff --git a/server/api/v1/client/collection/default/entry.delete.ts b/server/api/v1/client/collection/default/entry.delete.ts index 774c1bd6a..4fccd7f07 100644 --- a/server/api/v1/client/collection/default/entry.delete.ts +++ b/server/api/v1/client/collection/default/entry.delete.ts @@ -8,7 +8,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); await userLibraryManager.libraryRemove(gameId, user.id); return {}; diff --git a/server/api/v1/client/collection/default/entry.post.ts b/server/api/v1/client/collection/default/entry.post.ts index 5e7f60389..b4500334d 100644 --- a/server/api/v1/client/collection/default/entry.post.ts +++ b/server/api/v1/client/collection/default/entry.post.ts @@ -7,7 +7,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); // Add the game to the default collection await userLibraryManager.libraryAdd(gameId, user.id); diff --git a/server/api/v1/client/collection/index.post.ts b/server/api/v1/client/collection/index.post.ts index c08d355ff..94590e981 100644 --- a/server/api/v1/client/collection/index.post.ts +++ b/server/api/v1/client/collection/index.post.ts @@ -8,7 +8,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { const name = body.name; if (!name) - throw createError({ statusCode: 400, statusMessage: "Requires name" }); + throw createError({ statusCode: 400, message: "Requires name" }); // Create the collection using the manager const newCollection = await userLibraryManager.collectionCreate( diff --git a/server/api/v1/client/game/[id]/index.get.ts b/server/api/v1/client/game/[id]/index.get.ts index 13e06fb47..9f75101c6 100644 --- a/server/api/v1/client/game/[id]/index.get.ts +++ b/server/api/v1/client/game/[id]/index.get.ts @@ -4,7 +4,7 @@ import prisma from "~/server/internal/db/database"; export default defineClientEventHandler(async (h3) => { const id = getRouterParam(h3, "id"); if (!id) - throw createError({ statusCode: 400, statusMessage: "No ID in route" }); + throw createError({ statusCode: 400, message: "No ID in route" }); const game = await prisma.game.findUnique({ where: { @@ -12,7 +12,7 @@ export default defineClientEventHandler(async (h3) => { }, }); if (!game) - throw createError({ statusCode: 404, statusMessage: "Game not found" }); + throw createError({ statusCode: 404, message: "Game not found" }); return game; }); diff --git a/server/api/v1/client/game/manifest.get.ts b/server/api/v1/client/game/manifest.get.ts index cc4e725e0..a190493c7 100644 --- a/server/api/v1/client/game/manifest.get.ts +++ b/server/api/v1/client/game/manifest.get.ts @@ -7,14 +7,14 @@ export default defineClientEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "Missing version id in query", + message: "Missing version id in query", }); const manifest = await manifestGenerator.generateManifest(id); if (!manifest) throw createError({ statusCode: 400, - statusMessage: "Invalid game or version, or no versions added.", + message: "Invalid game or version, or no versions added.", }); return manifest; }); diff --git a/server/api/v1/client/game/version.get.ts b/server/api/v1/client/game/version.get.ts index b8df4ad59..d40f5ab05 100644 --- a/server/api/v1/client/game/version.get.ts +++ b/server/api/v1/client/game/version.get.ts @@ -8,7 +8,7 @@ export default defineClientEventHandler(async (h3) => { if (!id || !version) throw createError({ statusCode: 400, - statusMessage: "Missing id or version in query", + message: "Missing id or version in query", }); const gameVersion = await prisma.gameVersion.findUnique({ @@ -20,7 +20,7 @@ export default defineClientEventHandler(async (h3) => { if (!gameVersion) throw createError({ statusCode: 404, - statusMessage: "Game version not found", + message: "Game version not found", }); return gameVersion; diff --git a/server/api/v1/client/game/versions.get.ts b/server/api/v1/client/game/versions.get.ts index 0c50429d2..33bba5667 100644 --- a/server/api/v1/client/game/versions.get.ts +++ b/server/api/v1/client/game/versions.get.ts @@ -7,7 +7,7 @@ export default defineClientEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "No ID in request query", + message: "No ID in request query", }); const versions = await prisma.gameVersion.findMany({ diff --git a/server/api/v1/client/news/index.get.ts b/server/api/v1/client/news/index.get.ts index 8ab6e1393..dadcb8000 100644 --- a/server/api/v1/client/news/index.get.ts +++ b/server/api/v1/client/news/index.get.ts @@ -7,13 +7,13 @@ export default defineClientEventHandler(async (h3) => { const orderBy = query.order as "asc" | "desc"; if (orderBy) { if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) - throw createError({ statusCode: 400, statusMessage: "Invalid order" }); + throw createError({ statusCode: 400, message: "Invalid order" }); } const tags = query.tags as string[] | undefined; if (tags) { if (typeof tags !== "object" || !Array.isArray(tags)) - throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); + throw createError({ statusCode: 400, message: "Invalid tags" }); } const options = { diff --git a/server/api/v1/client/object/[id]/index.get.ts b/server/api/v1/client/object/[id]/index.get.ts index 962d30d3c..b4ca42752 100644 --- a/server/api/v1/client/object/[id]/index.get.ts +++ b/server/api/v1/client/object/[id]/index.get.ts @@ -3,13 +3,13 @@ import objectHandler from "~/server/internal/objects"; export default defineClientEventHandler(async (h3, utils) => { const id = getRouterParam(h3, "id"); - if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); + if (!id) throw createError({ statusCode: 400, message: "Invalid ID" }); const user = await utils.fetchUser(); const object = await objectHandler.fetchWithPermissions(id, user.id); if (!object) - throw createError({ statusCode: 404, statusMessage: "Object not found" }); + throw createError({ statusCode: 404, message: "Object not found" }); setHeader(h3, "Content-Type", object.mime); return object.data; diff --git a/server/api/v1/client/saves/[gameid]/[slotindex]/index.delete.ts b/server/api/v1/client/saves/[gameid]/[slotindex]/index.delete.ts index 0f36008d4..a6f02ff4a 100644 --- a/server/api/v1/client/saves/[gameid]/[slotindex]/index.delete.ts +++ b/server/api/v1/client/saves/[gameid]/[slotindex]/index.delete.ts @@ -8,27 +8,27 @@ export default defineClientEventHandler( if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const user = await fetchUser(); const gameId = getRouterParam(h3, "gameid"); if (!gameId) throw createError({ statusCode: 400, - statusMessage: "No gameID in route params", + message: "No gameID in route params", }); const slotIndexString = getRouterParam(h3, "slotindex"); if (!slotIndexString) throw createError({ statusCode: 400, - statusMessage: "No slotIndex in route params", + message: "No slotIndex in route params", }); const slotIndex = parseInt(slotIndexString); if (Number.isNaN(slotIndex)) throw createError({ statusCode: 400, - statusMessage: "Invalid slotIndex", + message: "Invalid slotIndex", }); const game = await prisma.game.findUnique({ @@ -36,7 +36,7 @@ export default defineClientEventHandler( select: { id: true }, }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); const save = await prisma.saveSlot.delete({ where: { @@ -48,6 +48,6 @@ export default defineClientEventHandler( }, }); if (!save) - throw createError({ statusCode: 404, statusMessage: "Save not found" }); + throw createError({ statusCode: 404, message: "Save not found" }); }, ); diff --git a/server/api/v1/client/saves/[gameid]/[slotindex]/index.get.ts b/server/api/v1/client/saves/[gameid]/[slotindex]/index.get.ts index 1ea38ae64..b54f86eb3 100644 --- a/server/api/v1/client/saves/[gameid]/[slotindex]/index.get.ts +++ b/server/api/v1/client/saves/[gameid]/[slotindex]/index.get.ts @@ -8,27 +8,27 @@ export default defineClientEventHandler( if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const user = await fetchUser(); const gameId = getRouterParam(h3, "gameid"); if (!gameId) throw createError({ statusCode: 400, - statusMessage: "No gameID in route params", + message: "No gameID in route params", }); const slotIndexString = getRouterParam(h3, "slotindex"); if (!slotIndexString) throw createError({ statusCode: 400, - statusMessage: "No slotIndex in route params", + message: "No slotIndex in route params", }); const slotIndex = parseInt(slotIndexString); if (Number.isNaN(slotIndex)) throw createError({ statusCode: 400, - statusMessage: "Invalid slotIndex", + message: "Invalid slotIndex", }); const game = await prisma.game.findUnique({ @@ -36,7 +36,7 @@ export default defineClientEventHandler( select: { id: true }, }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); const save = await prisma.saveSlot.findUnique({ where: { @@ -48,7 +48,7 @@ export default defineClientEventHandler( }, }); if (!save) - throw createError({ statusCode: 404, statusMessage: "Save not found" }); + throw createError({ statusCode: 404, message: "Save not found" }); return save; }, diff --git a/server/api/v1/client/saves/[gameid]/[slotindex]/push.post.ts b/server/api/v1/client/saves/[gameid]/[slotindex]/push.post.ts index 8034b8153..59fe0ff3b 100644 --- a/server/api/v1/client/saves/[gameid]/[slotindex]/push.post.ts +++ b/server/api/v1/client/saves/[gameid]/[slotindex]/push.post.ts @@ -9,27 +9,27 @@ export default defineClientEventHandler( if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const user = await fetchUser(); const gameId = getRouterParam(h3, "gameid"); if (!gameId) throw createError({ statusCode: 400, - statusMessage: "No gameID in route params", + message: "No gameID in route params", }); const slotIndexString = getRouterParam(h3, "slotindex"); if (!slotIndexString) throw createError({ statusCode: 400, - statusMessage: "No slotIndex in route params", + message: "No slotIndex in route params", }); const slotIndex = parseInt(slotIndexString); if (Number.isNaN(slotIndex)) throw createError({ statusCode: 400, - statusMessage: "Invalid slotIndex", + message: "Invalid slotIndex", }); const game = await prisma.game.findUnique({ @@ -37,7 +37,7 @@ export default defineClientEventHandler( select: { id: true }, }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); await saveManager.pushSave( gameId, diff --git a/server/api/v1/client/saves/[gameid]/index.get.ts b/server/api/v1/client/saves/[gameid]/index.get.ts index 53ce8a5a9..98c23ecb0 100644 --- a/server/api/v1/client/saves/[gameid]/index.get.ts +++ b/server/api/v1/client/saves/[gameid]/index.get.ts @@ -8,14 +8,14 @@ export default defineClientEventHandler( if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const user = await fetchUser(); const gameId = getRouterParam(h3, "gameid"); if (!gameId) throw createError({ statusCode: 400, - statusMessage: "No gameID in route params", + message: "No gameID in route params", }); const game = await prisma.game.findUnique({ @@ -23,7 +23,7 @@ export default defineClientEventHandler( select: { id: true }, }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); const saves = await prisma.saveSlot.findMany({ where: { diff --git a/server/api/v1/client/saves/[gameid]/index.post.ts b/server/api/v1/client/saves/[gameid]/index.post.ts index 91193ced1..2bd9dc8d7 100644 --- a/server/api/v1/client/saves/[gameid]/index.post.ts +++ b/server/api/v1/client/saves/[gameid]/index.post.ts @@ -9,14 +9,14 @@ export default defineClientEventHandler( if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const user = await fetchUser(); const gameId = getRouterParam(h3, "gameid"); if (!gameId) throw createError({ statusCode: 400, - statusMessage: "No gameID in route params", + message: "No gameID in route params", }); const game = await prisma.game.findUnique({ @@ -24,7 +24,7 @@ export default defineClientEventHandler( select: { id: true }, }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); const saves = await prisma.saveSlot.findMany({ where: { @@ -40,7 +40,7 @@ export default defineClientEventHandler( if (saves.length + 1 > limit) throw createError({ statusCode: 400, - statusMessage: "Out of save slots", + message: "Out of save slots", }); let firstIndex = 0; diff --git a/server/api/v1/client/saves/index.get.ts b/server/api/v1/client/saves/index.get.ts index 506151e5b..71d583553 100644 --- a/server/api/v1/client/saves/index.get.ts +++ b/server/api/v1/client/saves/index.get.ts @@ -8,7 +8,7 @@ export default defineClientEventHandler( if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const user = await fetchUser(); diff --git a/server/api/v1/client/saves/settings.get.ts b/server/api/v1/client/saves/settings.get.ts index 05c3026e3..579293001 100644 --- a/server/api/v1/client/saves/settings.get.ts +++ b/server/api/v1/client/saves/settings.get.ts @@ -7,7 +7,7 @@ export default defineClientEventHandler(async (_h3, { fetchClient }) => { if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({ statusCode: 403, - statusMessage: "Capability not allowed.", + message: "Capability not allowed.", }); const slotLimit = await applicationSettings.get("saveSlotCountLimit"); diff --git a/server/api/v1/collection/[id]/entry.delete.ts b/server/api/v1/collection/[id]/entry.delete.ts index d0a4faffd..e2f7d183f 100644 --- a/server/api/v1/collection/[id]/entry.delete.ts +++ b/server/api/v1/collection/[id]/entry.delete.ts @@ -12,13 +12,13 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); const successful = await userLibraryManager.collectionRemove( gameId, @@ -28,7 +28,7 @@ export default defineEventHandler(async (h3) => { if (!successful) throw createError({ statusCode: 404, - statusMessage: "Collection not found", + message: "Collection not found", }); return {}; }); diff --git a/server/api/v1/collection/[id]/entry.post.ts b/server/api/v1/collection/[id]/entry.post.ts index d6a2394fd..5f6ed709f 100644 --- a/server/api/v1/collection/[id]/entry.post.ts +++ b/server/api/v1/collection/[id]/entry.post.ts @@ -12,13 +12,13 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); return await userLibraryManager.collectionAdd(gameId, id, userId); }); diff --git a/server/api/v1/collection/[id]/index.delete.ts b/server/api/v1/collection/[id]/index.delete.ts index e00b3d634..bafe8f883 100644 --- a/server/api/v1/collection/[id]/index.delete.ts +++ b/server/api/v1/collection/[id]/index.delete.ts @@ -6,14 +6,14 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const id = getRouterParam(h3, "id"); if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); // Verify collection exists and user owns it @@ -22,13 +22,13 @@ export default defineEventHandler(async (h3) => { if (!collection) throw createError({ statusCode: 404, - statusMessage: "Collection not found", + message: "Collection not found", }); if (collection.userId !== userId) throw createError({ statusCode: 403, - statusMessage: "Not authorized to delete this collection", + message: "Not authorized to delete this collection", }); await userLibraryManager.deleteCollection(id); diff --git a/server/api/v1/collection/[id]/index.get.ts b/server/api/v1/collection/[id]/index.get.ts index 9bb185403..89a6e87d9 100644 --- a/server/api/v1/collection/[id]/index.get.ts +++ b/server/api/v1/collection/[id]/index.get.ts @@ -6,14 +6,14 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const id = getRouterParam(h3, "id"); if (!id) throw createError({ statusCode: 400, - statusMessage: "ID required in route params", + message: "ID required in route params", }); // Fetch specific collection @@ -22,14 +22,14 @@ export default defineEventHandler(async (h3) => { if (!collection) throw createError({ statusCode: 404, - statusMessage: "Collection not found", + message: "Collection not found", }); // Verify user owns this collection if (collection.userId !== userId) throw createError({ statusCode: 403, - statusMessage: "Not authorized to access this collection", + message: "Not authorized to access this collection", }); return collection; diff --git a/server/api/v1/collection/default/entry.delete.ts b/server/api/v1/collection/default/entry.delete.ts index 77f3c3934..aba95be99 100644 --- a/server/api/v1/collection/default/entry.delete.ts +++ b/server/api/v1/collection/default/entry.delete.ts @@ -6,14 +6,14 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); await userLibraryManager.libraryRemove(gameId, userId); return {}; diff --git a/server/api/v1/collection/default/entry.post.ts b/server/api/v1/collection/default/entry.post.ts index 4c8b8fde5..625587361 100644 --- a/server/api/v1/collection/default/entry.post.ts +++ b/server/api/v1/collection/default/entry.post.ts @@ -6,13 +6,13 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const body = await readBody(h3); const gameId = body.id; if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); + throw createError({ statusCode: 400, message: "Game ID required" }); // Add the game to the default collection await userLibraryManager.libraryAdd(gameId, userId); diff --git a/server/api/v1/collection/default/index.get.ts b/server/api/v1/collection/default/index.get.ts index 22a357d5b..470a669fd 100644 --- a/server/api/v1/collection/default/index.get.ts +++ b/server/api/v1/collection/default/index.get.ts @@ -6,7 +6,7 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const collection = await userLibraryManager.fetchLibrary(userId); diff --git a/server/api/v1/collection/index.post.ts b/server/api/v1/collection/index.post.ts index 1d3daaddd..ac7461b33 100644 --- a/server/api/v1/collection/index.post.ts +++ b/server/api/v1/collection/index.post.ts @@ -12,7 +12,7 @@ export default defineEventHandler(async (h3) => { const name = body.name; if (!name) - throw createError({ statusCode: 400, statusMessage: "Requires name" }); + throw createError({ statusCode: 400, message: "Requires name" }); // Create the collection using the manager const newCollection = await userLibraryManager.collectionCreate(name, userId); diff --git a/server/api/v1/companies/[id]/index.get.ts b/server/api/v1/companies/[id]/index.get.ts index a32637111..881d8c744 100644 --- a/server/api/v1/companies/[id]/index.get.ts +++ b/server/api/v1/companies/[id]/index.get.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!companyId) throw createError({ statusCode: 400, - statusMessage: "Missing gameId in route params (somehow...?)", + message: "Missing gameId in route params (somehow...?)", }); const company = await prisma.company.findUnique({ @@ -17,7 +17,7 @@ export default defineEventHandler(async (h3) => { }); if (!company) - throw createError({ statusCode: 404, statusMessage: "Company not found" }); + throw createError({ statusCode: 404, message: "Company not found" }); return { company }; }); diff --git a/server/api/v1/games/[id]/index.get.ts b/server/api/v1/games/[id]/index.get.ts index 029b9b291..f038b3a74 100644 --- a/server/api/v1/games/[id]/index.get.ts +++ b/server/api/v1/games/[id]/index.get.ts @@ -9,13 +9,17 @@ export default defineEventHandler(async (h3) => { if (!gameId) throw createError({ statusCode: 400, - statusMessage: "Missing gameId in route params (somehow...?)", + message: "Missing gameId in route params (somehow...?)", }); const game = await prisma.game.findUnique({ where: { id: gameId }, include: { - versions: true, + versions: { + include: { + userPlatform: true, + }, + }, publishers: { select: { id: true, @@ -37,7 +41,7 @@ export default defineEventHandler(async (h3) => { }); if (!game) - throw createError({ statusCode: 404, statusMessage: "Game not found" }); + throw createError({ statusCode: 404, message: "Game not found" }); const rating = await prisma.gameRating.aggregate({ where: { diff --git a/server/api/v1/news/[id]/index.get.ts b/server/api/v1/news/[id]/index.get.ts index 2499779b8..0793b49e7 100644 --- a/server/api/v1/news/[id]/index.get.ts +++ b/server/api/v1/news/[id]/index.get.ts @@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const id = h3.context.params?.id; diff --git a/server/api/v1/news/index.get.ts b/server/api/v1/news/index.get.ts index e5b6aac5c..4d41545f6 100644 --- a/server/api/v1/news/index.get.ts +++ b/server/api/v1/news/index.get.ts @@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => { if (!userId) throw createError({ statusCode: 403, - statusMessage: "Requires authentication", + message: "Requires authentication", }); const query = getQuery(h3); @@ -15,13 +15,13 @@ export default defineEventHandler(async (h3) => { const orderBy = query.order as "asc" | "desc"; if (orderBy) { if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) - throw createError({ statusCode: 400, statusMessage: "Invalid order" }); + throw createError({ statusCode: 400, message: "Invalid order" }); } const tags = query.tags as string[] | undefined; if (tags) { if (typeof tags !== "object" || !Array.isArray(tags)) - throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); + throw createError({ statusCode: 400, message: "Invalid tags" }); } const options = { diff --git a/server/api/v1/notifications/[id]/index.delete.ts b/server/api/v1/notifications/[id]/index.delete.ts index ac6083902..7708a8129 100644 --- a/server/api/v1/notifications/[id]/index.delete.ts +++ b/server/api/v1/notifications/[id]/index.delete.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!notificationId) throw createError({ statusCode: 400, - statusMessage: "Missing notification ID", + message: "Missing notification ID", }); const userIds = [userId]; @@ -30,7 +30,7 @@ export default defineEventHandler(async (h3) => { if (!notification) throw createError({ statusCode: 400, - statusMessage: "Invalid notification ID", + message: "Invalid notification ID", }); return {}; diff --git a/server/api/v1/notifications/[id]/index.get.ts b/server/api/v1/notifications/[id]/index.get.ts index ba2c6fad4..a5b1cfe1f 100644 --- a/server/api/v1/notifications/[id]/index.get.ts +++ b/server/api/v1/notifications/[id]/index.get.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!notificationId) throw createError({ statusCode: 400, - statusMessage: "Missing notification ID", + message: "Missing notification ID", }); const userIds = [userId]; @@ -30,7 +30,7 @@ export default defineEventHandler(async (h3) => { if (!notification) throw createError({ statusCode: 400, - statusMessage: "Invalid notification ID", + message: "Invalid notification ID", }); return notification; diff --git a/server/api/v1/notifications/[id]/read.post.ts b/server/api/v1/notifications/[id]/read.post.ts index 4ffe007ec..851cddd50 100644 --- a/server/api/v1/notifications/[id]/read.post.ts +++ b/server/api/v1/notifications/[id]/read.post.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!notificationId) throw createError({ statusCode: 400, - statusMessage: "Missing notification ID", + message: "Missing notification ID", }); const userIds = [userId]; @@ -33,7 +33,7 @@ export default defineEventHandler(async (h3) => { if (!notification) throw createError({ statusCode: 400, - statusMessage: "Invalid notification ID", + message: "Invalid notification ID", }); return notification; diff --git a/server/api/v1/notifications/index.get.ts b/server/api/v1/notifications/index.get.ts index 982d52041..a069b1e23 100644 --- a/server/api/v1/notifications/index.get.ts +++ b/server/api/v1/notifications/index.get.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!acls) throw createError({ statusCode: 500, - statusMessage: "Got userId but no ACLs - what?", + message: "Got userId but no ACLs - what?", }); const notifications = await prisma.notification.findMany({ diff --git a/server/api/v1/notifications/readall.post.ts b/server/api/v1/notifications/readall.post.ts index 0e4c8c71f..7497df705 100644 --- a/server/api/v1/notifications/readall.post.ts +++ b/server/api/v1/notifications/readall.post.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!acls) throw createError({ statusCode: 500, - statusMessage: "Got userId but no ACLs - what?", + message: "Got userId but no ACLs - what?", }); await prisma.notification.updateMany({ diff --git a/server/api/v1/object/[id]/index.delete.ts b/server/api/v1/object/[id]/index.delete.ts index fbdd5ee25..a74108e50 100644 --- a/server/api/v1/object/[id]/index.delete.ts +++ b/server/api/v1/object/[id]/index.delete.ts @@ -5,7 +5,7 @@ import sanitize from "sanitize-filename"; export default defineEventHandler(async (h3) => { const unsafeId = getRouterParam(h3, "id"); if (!unsafeId) - throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); + throw createError({ statusCode: 400, message: "Invalid ID" }); const userId = await aclManager.getUserIdACL(h3, ["object:delete"]); diff --git a/server/api/v1/object/[id]/index.get.ts b/server/api/v1/object/[id]/index.get.ts index 649bfcd5d..0de449c2a 100644 --- a/server/api/v1/object/[id]/index.get.ts +++ b/server/api/v1/object/[id]/index.get.ts @@ -5,14 +5,14 @@ import sanitize from "sanitize-filename"; export default defineEventHandler(async (h3) => { const unsafeId = getRouterParam(h3, "id"); if (!unsafeId) - throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); + throw createError({ statusCode: 400, message: "Invalid ID" }); const userId = await aclManager.getUserIdACL(h3, ["object:read"]); const id = sanitize(unsafeId); const object = await objectHandler.fetchWithPermissions(id, userId); if (!object) - throw createError({ statusCode: 404, statusMessage: "Object not found" }); + throw createError({ statusCode: 404, message: "Object not found" }); // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag const etagRequestValue = h3.headers.get("If-None-Match"); diff --git a/server/api/v1/object/[id]/index.head.ts b/server/api/v1/object/[id]/index.head.ts index e762de5c6..8d3f8c4cf 100644 --- a/server/api/v1/object/[id]/index.head.ts +++ b/server/api/v1/object/[id]/index.head.ts @@ -6,14 +6,14 @@ import sanitize from "sanitize-filename"; export default defineEventHandler(async (h3) => { const unsafeId = getRouterParam(h3, "id"); if (!unsafeId) - throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); + throw createError({ statusCode: 400, message: "Invalid ID" }); const userId = await aclManager.getUserIdACL(h3, ["object:read"]); const id = sanitize(unsafeId); const object = await objectHandler.fetchWithPermissions(id, userId); if (!object) - throw createError({ statusCode: 404, statusMessage: "Object not found" }); + throw createError({ statusCode: 404, message: "Object not found" }); // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag const etagRequestValue = h3.headers.get("If-None-Match"); diff --git a/server/api/v1/object/[id]/index.post.ts b/server/api/v1/object/[id]/index.post.ts index e6779fff4..c86f1bbc7 100644 --- a/server/api/v1/object/[id]/index.post.ts +++ b/server/api/v1/object/[id]/index.post.ts @@ -5,13 +5,13 @@ import sanitize from "sanitize-filename"; export default defineEventHandler(async (h3) => { const unsafeId = getRouterParam(h3, "id"); if (!unsafeId) - throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); + throw createError({ statusCode: 400, message: "Invalid ID" }); const body = await readRawBody(h3, "binary"); if (!body) throw createError({ statusCode: 400, - statusMessage: "Invalid upload", + message: "Invalid upload", }); const userId = await aclManager.getUserIdACL(h3, ["object:update"]); diff --git a/server/api/v1/screenshots/[id]/index.delete.ts b/server/api/v1/screenshots/[id]/index.delete.ts index 41e2bf51b..ae4248344 100644 --- a/server/api/v1/screenshots/[id]/index.delete.ts +++ b/server/api/v1/screenshots/[id]/index.delete.ts @@ -11,7 +11,7 @@ export default defineEventHandler(async (h3) => { if (!unsafeId) throw createError({ statusCode: 400, - statusMessage: "Missing screenshot ID", + message: "Missing screenshot ID", }); const screenshotId = sanitize(unsafeId); diff --git a/server/api/v1/screenshots/[id]/index.get.ts b/server/api/v1/screenshots/[id]/index.get.ts index 79d569a9f..d55ff17e5 100644 --- a/server/api/v1/screenshots/[id]/index.get.ts +++ b/server/api/v1/screenshots/[id]/index.get.ts @@ -11,7 +11,7 @@ export default defineEventHandler(async (h3) => { if (!unsafeId) throw createError({ statusCode: 400, - statusMessage: "Missing screenshot ID", + message: "Missing screenshot ID", }); const result = await screenshotManager.get(sanitize(unsafeId)); diff --git a/server/api/v1/screenshots/game/[id]/index.get.ts b/server/api/v1/screenshots/game/[id]/index.get.ts index 71addaed1..3310ce252 100644 --- a/server/api/v1/screenshots/game/[id]/index.get.ts +++ b/server/api/v1/screenshots/game/[id]/index.get.ts @@ -10,7 +10,7 @@ export default defineEventHandler(async (h3) => { if (!gameId) throw createError({ statusCode: 400, - statusMessage: "Missing game ID", + message: "Missing game ID", }); const results = await screenshotManager.getUserAllByGame(userId, gameId); diff --git a/server/api/v1/screenshots/game/[id]/index.post.ts b/server/api/v1/screenshots/game/[id]/index.post.ts index c06b8157d..58bfe5eaa 100644 --- a/server/api/v1/screenshots/game/[id]/index.post.ts +++ b/server/api/v1/screenshots/game/[id]/index.post.ts @@ -13,7 +13,7 @@ export default defineEventHandler(async (h3) => { if (!gameId) throw createError({ statusCode: 400, - statusMessage: "Missing game ID", + message: "Missing game ID", }); const game = await prisma.game.findUnique({ @@ -21,7 +21,7 @@ export default defineEventHandler(async (h3) => { select: { id: true }, }); if (!game) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + throw createError({ statusCode: 400, message: "Invalid game ID" }); await screenshotManager.upload(userId, gameId, h3.node.req); }); diff --git a/server/api/v1/setup.post.ts b/server/api/v1/setup.post.ts index acd83f64f..af30aa214 100644 --- a/server/api/v1/setup.post.ts +++ b/server/api/v1/setup.post.ts @@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => { if (!allowed) throw createError({ statusCode: 403, - statusMessage: "Must use a setup token.", + message: "Must use a setup token.", }); await prisma.aPIToken.deleteMany({ where: { diff --git a/server/api/v1/store/index.get.ts b/server/api/v1/store/index.get.ts index 9a61e676d..44252d281 100644 --- a/server/api/v1/store/index.get.ts +++ b/server/api/v1/store/index.get.ts @@ -28,7 +28,7 @@ export default defineEventHandler(async (h3) => { const query = getQuery(h3); const options = StoreRead(query); if (options instanceof ArkErrors) - throw createError({ statusCode: 400, statusMessage: options.summary }); + throw createError({ statusCode: 400, message: options.summary }); /** * Generic filters diff --git a/server/api/v1/store/platforms.get.ts b/server/api/v1/store/platforms.get.ts new file mode 100644 index 000000000..d3f32cf87 --- /dev/null +++ b/server/api/v1/store/platforms.get.ts @@ -0,0 +1,16 @@ +import aclManager from "~/server/internal/acls"; +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const userId = await aclManager.getUserIdACL(h3, ["store:read"]); + if (!userId) throw createError({ statusCode: 403 }); + + const platforms = await prisma.userPlatform.findMany({ + orderBy: { platformName: "asc" }, + select: { + id: true, + platformName: true, + }, + }); + return platforms; +}); diff --git a/server/api/v1/tags/[id]/index.get.ts b/server/api/v1/tags/[id]/index.get.ts index 5d5d686c0..89dac05d7 100644 --- a/server/api/v1/tags/[id]/index.get.ts +++ b/server/api/v1/tags/[id]/index.get.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!tagId) throw createError({ statusCode: 400, - statusMessage: "Missing gameId in route params (somehow...?)", + message: "Missing gameId in route params (somehow...?)", }); const tag = await prisma.gameTag.findUnique({ @@ -17,7 +17,7 @@ export default defineEventHandler(async (h3) => { }); if (!tag) - throw createError({ statusCode: 404, statusMessage: "Tag not found" }); + throw createError({ statusCode: 404, message: "Tag not found" }); return { tag }; }); diff --git a/server/api/v1/user/client/[id]/index.delete.ts b/server/api/v1/user/client/[id]/index.delete.ts index 2f1727365..15aa6ae28 100644 --- a/server/api/v1/user/client/[id]/index.delete.ts +++ b/server/api/v1/user/client/[id]/index.delete.ts @@ -9,7 +9,7 @@ export default defineEventHandler(async (h3) => { if (!clientId) throw createError({ statusCode: 400, - statusMessage: "Client ID missing in route params", + message: "Client ID missing in route params", }); await clientHandler.removeClient(clientId); diff --git a/server/api/v1/user/token/[id]/index.delete.ts b/server/api/v1/user/token/[id]/index.delete.ts index 54d1be144..d3a432bff 100644 --- a/server/api/v1/user/token/[id]/index.delete.ts +++ b/server/api/v1/user/token/[id]/index.delete.ts @@ -10,14 +10,14 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "No id in router params", + message: "No id in router params", }); const deleted = await prisma.aPIToken.delete({ where: { id: id, userId: userId, mode: APITokenMode.User }, })!; if (!deleted) - throw createError({ statusCode: 404, statusMessage: "Token not found" }); + throw createError({ statusCode: 404, message: "Token not found" }); return; }); diff --git a/server/api/v1/user/token/index.post.ts b/server/api/v1/user/token/index.post.ts index aafacf3c0..3e5aacef0 100644 --- a/server/api/v1/user/token/index.post.ts +++ b/server/api/v1/user/token/index.post.ts @@ -22,7 +22,7 @@ export default defineEventHandler(async (h3) => { if (invalidACLs.length > 0) throw createError({ statusCode: 400, - statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`, + message: `Invalid ACLs: ${invalidACLs.join(", ")}`, }); const token = await prisma.aPIToken.create({ diff --git a/server/api/v2/client/chunk.post.ts b/server/api/v2/client/chunk.post.ts index 84022038a..50c94474a 100644 --- a/server/api/v2/client/chunk.post.ts +++ b/server/api/v2/client/chunk.post.ts @@ -19,7 +19,7 @@ export default defineEventHandler(async (h3) => { if (!context) throw createError({ statusCode: 400, - statusMessage: "Invalid download context.", + message: "Invalid download context.", }); const streamFiles = []; @@ -29,7 +29,7 @@ export default defineEventHandler(async (h3) => { if (!manifestFile) throw createError({ statusCode: 400, - statusMessage: `Unknown file: ${file.filename}`, + message: `Unknown file: ${file.filename}`, }); const start = manifestFile.lengths @@ -57,7 +57,7 @@ export default defineEventHandler(async (h3) => { if (!gameReadStream) throw createError({ statusCode: 500, - statusMessage: "Failed to create read stream", + message: "Failed to create read stream", }); let length = 0; await gameReadStream.pipeTo( @@ -75,7 +75,7 @@ export default defineEventHandler(async (h3) => { ); throw createError({ statusCode: 500, - statusMessage: "Failed to read enough from stream.", + message: "Failed to read enough from stream.", }); } } diff --git a/server/api/v2/client/context.post.ts b/server/api/v2/client/context.post.ts index e54356a9b..bf4db04f9 100644 --- a/server/api/v2/client/context.post.ts +++ b/server/api/v2/client/context.post.ts @@ -15,7 +15,7 @@ export default defineClientEventHandler(async (h3) => { if (!context) throw createError({ statusCode: 400, - statusMessage: "Invalid game or version", + message: "Invalid game or version", }); return { context }; diff --git a/server/arktype.ts b/server/arktype.ts index 466851efb..d9679ebe9 100644 --- a/server/arktype.ts +++ b/server/arktype.ts @@ -39,12 +39,12 @@ export async function readDropValidatedBody( if (e instanceof ArkErrors) { throw createError({ statusCode: 400, - statusMessage: t("errors.invalidBody", [e.summary]), + message: t("errors.invalidBody", [e.summary]), }); } throw createError({ statusCode: 400, - statusMessage: t("errors.invalidBody", [ + message: t("errors.invalidBody", [ e instanceof Error ? e.message : `${e}`, ]), }); diff --git a/server/internal/clients/event-handler.ts b/server/internal/clients/event-handler.ts index dfbdf0d2a..fa15c674e 100644 --- a/server/internal/clients/event-handler.ts +++ b/server/internal/clients/event-handler.ts @@ -31,7 +31,7 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { if (!client) throw createError({ statusCode: 400, - statusMessage: "No clients created.", + message: "No clients created.", }); clientId = client.id; break; @@ -55,7 +55,7 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { // We reject the request throw createError({ statusCode: 403, - statusMessage: "Nonce expired", + message: "Nonce expired", }); } @@ -66,21 +66,21 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { if (!certBundle) throw createError({ statusCode: 403, - statusMessage: "Invalid client ID", + message: "Invalid client ID", }); const valid = droplet.verifyNonce(certBundle.cert, nonce, signature); if (!valid) throw createError({ statusCode: 403, - statusMessage: "Invalid nonce signature.", + message: "Invalid nonce signature.", }); break; } default: { throw createError({ statusCode: 403, - statusMessage: "No authentication", + message: "No authentication", }); } } @@ -88,7 +88,7 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { if (clientId === undefined) throw createError({ statusCode: 500, - statusMessage: "Failed to execute authentication pipeline.", + message: "Failed to execute authentication pipeline.", }); async function fetchClient() { diff --git a/server/internal/clients/handler.ts b/server/internal/clients/handler.ts index eaae288fe..7b673a14e 100644 --- a/server/internal/clients/handler.ts +++ b/server/internal/clients/handler.ts @@ -80,15 +80,15 @@ export class ClientHandler { if (!clientId) throw createError({ statusCode: 403, - statusMessage: "Invalid or unknown code.", + message: "Invalid or unknown code.", }); const metadata = this.temporaryClientTable.get(clientId); if (!metadata) - throw createError({ statusCode: 500, statusMessage: "Broken code." }); + throw createError({ statusCode: 500, message: "Broken code." }); if (metadata.peer) throw createError({ statusCode: 400, - statusMessage: "Pre-existing listener for this code.", + message: "Pre-existing listener for this code.", }); metadata.peer = peer; this.temporaryClientTable.set(clientId, metadata); @@ -129,12 +129,12 @@ export class ClientHandler { if (!client) throw createError({ statusCode: 500, - statusMessage: "Corrupted code, please restart the process.", + message: "Corrupted code, please restart the process.", }); if (!client.peer) throw createError({ statusCode: 400, - statusMessage: "Client has not connected yet. Please try again later.", + message: "Client has not connected yet. Please try again later.", }); await client.peer.send( JSON.stringify({ type: "token", value: `${clientId}/${token}` }), diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index f9db479f9..062ef41fc 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -15,7 +15,7 @@ import { GameNotFoundError, type LibraryProvider } from "./provider"; import { logger } from "../logging"; import { createHash } from "node:crypto"; import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post"; -import type { GameVersionLaunchCreateManyGameVersionInputEnvelope } from "~/prisma/client/models"; +import type { LaunchOptionCreateManyGameVersionInput } from "~/prisma/client/models"; export function createGameImportTaskId(libraryId: string, libraryPath: string) { return createHash("md5") @@ -311,7 +311,7 @@ class LibraryManager { umuIdOverride: metadata.umuId, onlySetup: metadata.onlySetup, - setup: metadata.setup, + setupCommand: metadata.setup, setupArgs: metadata.setupArgs, launches: { @@ -323,7 +323,7 @@ class LibraryManager { description: v.description, launchCommand: v.launchCommand, launchArgs: v.launchArgs, - }) satisfies GameVersionLaunchCreateManyGameVersionInputEnvelope["data"], + }) satisfies LaunchOptionCreateManyGameVersionInput, ), }, }, diff --git a/server/internal/saves/index.ts b/server/internal/saves/index.ts index 7b76f90e3..a53a7c3c3 100644 --- a/server/internal/saves/index.ts +++ b/server/internal/saves/index.ts @@ -32,7 +32,7 @@ class SaveManager { }, }); if (!save) - throw createError({ statusCode: 404, statusMessage: "Save not found" }); + throw createError({ statusCode: 404, message: "Save not found" }); const newSaveObjectId = randomUUID(); const newSaveStream = await objectHandler.createWithStream( @@ -43,7 +43,7 @@ class SaveManager { if (!newSaveStream) throw createError({ statusCode: 500, - statusMessage: "Failed to create writing stream to storage backend.", + message: "Failed to create writing stream to storage backend.", }); let hash: string | undefined; @@ -64,7 +64,7 @@ class SaveManager { await objectHandler.deleteAsSystem(newSaveObjectId); throw createError({ statusCode: 500, - statusMessage: "Hash failed to generate", + message: "Hash failed to generate", }); } diff --git a/server/internal/screenshots/index.ts b/server/internal/screenshots/index.ts index b41ac03b2..891d47531 100644 --- a/server/internal/screenshots/index.ts +++ b/server/internal/screenshots/index.ts @@ -80,7 +80,7 @@ class ScreenshotManager { if (!saveStream) throw createError({ statusCode: 500, - statusMessage: "Failed to create writing stream to storage backend.", + message: "Failed to create writing stream to storage backend.", }); // pipe into object store diff --git a/server/internal/utils/handlefileupload.ts b/server/internal/utils/handlefileupload.ts index 91a748936..efef965e6 100644 --- a/server/internal/utils/handlefileupload.ts +++ b/server/internal/utils/handlefileupload.ts @@ -1,18 +1,27 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type { EventHandlerRequest, H3Event } from "h3"; import type { Dump, Pull, Register } from "../objects/transactional"; import { ObjectTransactionalHandler } from "../objects/transactional"; +type RecursiveType = + | { [key: string]: RecursiveType } + | string + | number + | Array; + export async function handleFileUpload( h3: H3Event, metadata: { [key: string]: string }, permissions: Array, max = -1, -): Promise<[string[], { [key: string]: string }, Pull, Dump, Register] | undefined> { +): Promise< + [string[], { [key: string]: RecursiveType }, Pull, Dump, Register] | undefined +> { const formData = await readMultipartFormData(h3); if (!formData) return undefined; const transactionalHandler = new ObjectTransactionalHandler(); const [add, pull, dump] = transactionalHandler.new(metadata, permissions); - const options: { [key: string]: string } = {}; + const options: any = {}; const ids = []; for (const entry of formData) { @@ -25,7 +34,14 @@ export async function handleFileUpload( } if (!entry.name) continue; - options[entry.name] = entry.data.toString("utf-8"); + const path = entry.name.split("."); + let v = options; + for (const pathPart of path.slice(0, -1)) { + (v as any)[pathPart] ??= {}; + v = (v as any)[pathPart]; + } + + (v as any)[path.at(-1)!] = entry.data.toString("utf-8"); } return [ids, options, pull, dump, add]; diff --git a/server/routes/auth/callback/oidc.get.ts b/server/routes/auth/callback/oidc.get.ts index 5aba8fd94..eb41ae562 100644 --- a/server/routes/auth/callback/oidc.get.ts +++ b/server/routes/auth/callback/oidc.get.ts @@ -20,14 +20,14 @@ export default defineEventHandler(async (h3) => { if (!code) throw createError({ statusCode: 400, - statusMessage: "No code in query params.", + message: "No code in query params.", }); const state = query.state?.toString(); if (!state) throw createError({ statusCode: 400, - statusMessage: "No state in query params.", + message: "No state in query params.", }); const result = await manager.authorize(code, state); @@ -35,7 +35,7 @@ export default defineEventHandler(async (h3) => { if (typeof result === "string") throw createError({ statusCode: 403, - statusMessage: `Failed to sign in: "${result}". Please try again.`, + message: `Failed to sign in: "${result}". Please try again.`, }); await sessionHandler.signin(h3, result.user.id, true); diff --git a/yarn.lock b/yarn.lock index 54561c05f..7e2450ba2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,6 +38,17 @@ resolved "https://registry.yarnpkg.com/@ark/util/-/util-0.46.0.tgz#aee240bdaf413793e5ca4c4e8e3707aa965f4be3" integrity sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg== +"@asamuzakjp/css-color@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794" + integrity sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw== + dependencies: + "@csstools/css-calc" "^2.1.3" + "@csstools/css-color-parser" "^3.0.9" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + lru-cache "^10.4.3" + "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" @@ -315,6 +326,34 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== +"@csstools/color-helpers@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz#106c54c808cabfd1ab4c602d8505ee584c2996ef" + integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== + +"@csstools/css-calc@^2.1.3", "@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.0.9": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz#4e386af3a99dd36c46fef013cfe4c1c341eed6f0" + integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== + dependencies: + "@csstools/color-helpers" "^5.1.0" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.4": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== + +"@csstools/css-tokenizer@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -2388,6 +2427,15 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== +"@types/jsdom@^21.1.7": + version "21.1.7" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.7.tgz#9edcb09e0b07ce876e7833922d3274149c898cfa" + integrity sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -2439,6 +2487,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA== +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/triple-beam@^1.3.2": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" @@ -3010,7 +3063,7 @@ acorn@^8.14.0, acorn@^8.14.1, acorn@^8.15.0, acorn@^8.5.0, acorn@^8.6.0, acorn@^ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== -agent-base@^7.1.2: +agent-base@^7.1.0, agent-base@^7.1.2: version "7.1.4" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== @@ -3938,6 +3991,14 @@ csso@^5.0.5: dependencies: css-tree "~2.2.0" +cssstyle@^4.2.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.6.0.tgz#ea18007024e3167f4f105315f3ec2d982bf48ed9" + integrity sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg== + dependencies: + "@asamuzakjp/css-color" "^3.2.0" + rrweb-cssom "^0.8.0" + csstype@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -3948,6 +4009,14 @@ data-uri-to-buffer@^4.0.0: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + dateformat@^4.6.3: version "4.6.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" @@ -3977,6 +4046,11 @@ decache@^4.6.2: dependencies: callsite "^1.0.0" +decimal.js@^10.5.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== + decode-named-character-reference@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed" @@ -5308,6 +5382,13 @@ hosted-git-info@^7.0.0: dependencies: lru-cache "^10.0.1" +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + htmlparser2@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.0.0.tgz#77ad249037b66bf8cc99c6e286ef73b83aeb621d" @@ -5329,12 +5410,20 @@ http-errors@^2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-shutdown@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== -https-proxy-agent@^7.0.5: +https-proxy-agent@^7.0.5, https-proxy-agent@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== @@ -5577,6 +5666,11 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-reference@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -5710,6 +5804,32 @@ jsdoc-type-pratt-parser@^4.0.0, jsdoc-type-pratt-parser@~4.1.0: resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113" integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg== +jsdom@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-26.1.0.tgz#ab5f1c1cafc04bd878725490974ea5e8bf0c72b3" + integrity sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg== + dependencies: + cssstyle "^4.2.1" + data-urls "^5.0.0" + decimal.js "^10.5.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.6" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.16" + parse5 "^7.2.1" + rrweb-cssom "^0.8.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^5.1.1" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.1.1" + ws "^8.18.0" + xml-name-validator "^5.0.0" + jsesc@^3.0.2, jsesc@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -6838,6 +6958,11 @@ nuxt@^3.17.4: vue-devtools-stub "^0.1.0" vue-router "^4.5.1" +nwsapi@^2.2.16: + version "2.2.21" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.21.tgz#8df7797079350adda208910d8c33fc4c2d7520c3" + integrity sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA== + nypm@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.6.0.tgz#3a04623d1c358a93fc4b3cb9cfb6a11af080feca" @@ -7112,7 +7237,7 @@ parse5-parser-stream@^7.1.2: dependencies: parse5 "^7.0.0" -parse5@^7.0.0, parse5@^7.1.2, parse5@^7.3.0: +parse5@^7.0.0, parse5@^7.1.2, parse5@^7.2.1, parse5@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== @@ -7627,7 +7752,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -7924,6 +8049,11 @@ rollup@^4.34.9, rollup@^4.40.0, rollup@^4.45.0: "@rollup/rollup-win32-x64-msvc" "4.45.1" fsevents "~2.3.2" +rrweb-cssom@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2" + integrity sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw== + run-applescript@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" @@ -7979,6 +8109,13 @@ sax@^1.4.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scslre@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/scslre/-/scslre-0.3.0.tgz#c3211e9bfc5547fc86b1eabaa34ed1a657060155" @@ -8481,6 +8618,11 @@ svgo@^4.0.0: picocolors "^1.1.1" sax "^1.4.1" +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + synckit@^0.10.0: version "0.10.4" resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.10.4.tgz#a8af59be664d324be5271e86146075f6e93d5fe9" @@ -8621,6 +8763,18 @@ tinyglobby@0.2.14, tinyglobby@^0.2.13, tinyglobby@^0.2.14: fdir "^6.4.4" picomatch "^4.0.2" +tldts-core@^6.1.86: + version "6.1.86" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8" + integrity sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA== + +tldts@^6.1.32: + version "6.1.86" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.86.tgz#087e0555b31b9725ee48ca7e77edc56115cd82f7" + integrity sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ== + dependencies: + tldts-core "^6.1.86" + tmp-promise@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" @@ -8668,6 +8822,20 @@ totalist@^3.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== +tough-cookie@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7" + integrity sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A== + dependencies: + tldts "^6.1.32" + +tr46@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca" + integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw== + dependencies: + punycode "^2.3.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -9263,6 +9431,13 @@ vuedraggable@^4.1.0: dependencies: sortablejs "1.14.0" +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + web-streams-polyfill@^3.0.3: version "3.3.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" @@ -9273,6 +9448,11 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-virtual-modules@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" @@ -9290,6 +9470,14 @@ whatwg-mimetype@^4.0.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== +whatwg-url@^14.0.0, whatwg-url@^14.1.1: + version "14.2.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663" + integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw== + dependencies: + tr46 "^5.1.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -9383,7 +9571,7 @@ write-file-atomic@^6.0.0: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@^8.18.2, ws@^8.18.3: +ws@^8.18.0, ws@^8.18.2, ws@^8.18.3: version "8.18.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== @@ -9400,6 +9588,16 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + xss@^1.0.14: version "1.0.15" resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.15.tgz#96a0e13886f0661063028b410ed1b18670f4e59a" From 367d349a68b185d14ee0fe5caddb54663f33d67f Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 27 Aug 2025 12:16:27 +1000 Subject: [PATCH 06/22] feat: add user platform filters to store view --- components/Icons/Platform.vue | 26 ++++++++++++++++++++++++++ components/StoreView.vue | 20 +++++++++++++++++--- composables/store.ts | 1 + pages/store/[id]/index.vue | 20 ++++++++++---------- server/api/v1/store/platforms.get.ts | 1 + 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 components/Icons/Platform.vue diff --git a/components/Icons/Platform.vue b/components/Icons/Platform.vue new file mode 100644 index 000000000..32b32f40a --- /dev/null +++ b/components/Icons/Platform.vue @@ -0,0 +1,26 @@ + + + + diff --git a/components/StoreView.vue b/components/StoreView.vue index 3491d1c18..b5944c07d 100644 --- a/components/StoreView.vue +++ b/components/StoreView.vue @@ -247,7 +247,7 @@
    @@ -272,6 +272,12 @@ />
    +