diff --git a/nuxt.config.ts b/nuxt.config.ts index 2852dbb..f241a58 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -32,7 +32,7 @@ export default defineNuxtConfig({ }, }, - extends: ['./drop-base'], + extends: ["./drop-base"], // Module config from here down modules: ["@nuxt/content", "vue3-carousel-nuxt"], diff --git a/server/api/v1/admin/game/image/index.delete.ts b/server/api/v1/admin/game/image/index.delete.ts index 3b6252c..feb338e 100644 --- a/server/api/v1/admin/game/image/index.delete.ts +++ b/server/api/v1/admin/game/image/index.delete.ts @@ -1,5 +1,6 @@ import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; +import objectHandler from "~/server/internal/objects"; export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ @@ -36,7 +37,7 @@ export default defineEventHandler(async (h3) => { throw createError({ statusCode: 400, statusMessage: "Image not found" }); game.mImageLibrary.splice(imageIndex, 1); - await h3.context.objects.delete(imageId); + await objectHandler.delete(imageId); if (game.mBannerId === imageId) { game.mBannerId = game.mImageLibrary[0]; diff --git a/server/api/v1/admin/import/game/index.post.ts b/server/api/v1/admin/import/game/index.post.ts index 1599233..504ea5f 100644 --- a/server/api/v1/admin/import/game/index.post.ts +++ b/server/api/v1/admin/import/game/index.post.ts @@ -1,5 +1,6 @@ import aclManager from "~/server/internal/acls"; import libraryManager from "~/server/internal/library"; +import metadataHandler from "~/server/internal/metadata"; import { GameMetadataSearchResult, GameMetadataSource, @@ -30,8 +31,8 @@ export default defineEventHandler(async (h3) => { }); if (!metadata || !metadata.id || !metadata.sourceId) { - return await h3.context.metadataHandler.createGameWithoutMetadata(path); + return await metadataHandler.createGameWithoutMetadata(path); } else { - return await h3.context.metadataHandler.createGame(metadata, path); + return await metadataHandler.createGame(metadata, path); } }); diff --git a/server/api/v1/admin/import/game/search.get.ts b/server/api/v1/admin/import/game/search.get.ts index adf5109..723de48 100644 --- a/server/api/v1/admin/import/game/search.get.ts +++ b/server/api/v1/admin/import/game/search.get.ts @@ -1,5 +1,5 @@ import aclManager from "~/server/internal/acls"; -import libraryManager from "~/server/internal/library"; +import metadataHandler from "~/server/internal/metadata"; export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ @@ -12,7 +12,7 @@ export default defineEventHandler(async (h3) => { if (!search) throw createError({ statusCode: 400, statusMessage: "Invalid search" }); - const results = await h3.context.metadataHandler.search(search); + const results = await metadataHandler.search(search); if (results.length == 0) throw createError({ diff --git a/server/api/v1/admin/user/token/token.get.ts b/server/api/v1/admin/user/token/token.get.ts new file mode 100644 index 0000000..d118809 --- /dev/null +++ b/server/api/v1/admin/user/token/token.get.ts @@ -0,0 +1,9 @@ +import aclManager from "~/server/internal/acls"; +import { userACLDescriptions } from "~/server/internal/acls/descriptions"; + +export default defineEventHandler(async (h3) => { + const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication + if (!userId) throw createError({ statusCode: 403 }); + + return userACLDescriptions; +}); diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index e4e4418..f6e27fa 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -3,6 +3,7 @@ import prisma from "~/server/internal/db/database"; import { createHash } from "~/server/internal/security/simple"; import { v4 as uuidv4 } from "uuid"; import * as jdenticon from "jdenticon"; +import objectHandler from "~/server/internal/objects"; // Only really a simple test, in case people mistype their emails const mailRegex = /^\S+@\S+\.\S+$/; @@ -88,7 +89,7 @@ export default defineEventHandler(async (h3) => { const userId = uuidv4(); const profilePictureId = uuidv4(); - await h3.context.objects.createFromSource( + await objectHandler.createFromSource( profilePictureId, async () => jdenticon.toPng(username, 256), {}, diff --git a/server/api/v1/client/auth/handshake.post.ts b/server/api/v1/client/auth/handshake.post.ts index 9d69b51..62785f5 100644 --- a/server/api/v1/client/auth/handshake.post.ts +++ b/server/api/v1/client/auth/handshake.post.ts @@ -1,4 +1,5 @@ import clientHandler from "~/server/internal/clients/handler"; +import { useCertificateAuthority } from "~/server/plugins/ca"; export default defineEventHandler(async (h3) => { const body = await readBody(h3); @@ -27,14 +28,14 @@ export default defineEventHandler(async (h3) => { statusMessage: "Invalid token", }); - const ca = h3.context.ca; - const bundle = await ca.generateClientCertificate( + const certificateAuthority = useCertificateAuthority(); + const bundle = await certificateAuthority.generateClientCertificate( clientId, metadata.data.name ); const client = await clientHandler.finialiseClient(clientId); - await ca.storeClientCertificate(clientId, bundle); + await certificateAuthority.storeClientCertificate(clientId, bundle); return { private: bundle.priv, diff --git a/server/api/v1/client/object/[id]/index.get.ts b/server/api/v1/client/object/[id]/index.get.ts index 5793b69..962d30d 100644 --- a/server/api/v1/client/object/[id]/index.get.ts +++ b/server/api/v1/client/object/[id]/index.get.ts @@ -1,4 +1,5 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import objectHandler from "~/server/internal/objects"; export default defineClientEventHandler(async (h3, utils) => { const id = getRouterParam(h3, "id"); @@ -6,7 +7,7 @@ export default defineClientEventHandler(async (h3, utils) => { const user = await utils.fetchUser(); - const object = await h3.context.objects.fetchWithPermissions(id, user.id); + const object = await objectHandler.fetchWithPermissions(id, user.id); if (!object) throw createError({ statusCode: 404, statusMessage: "Object not found" }); diff --git a/server/api/v1/object/[id]/index.delete.ts b/server/api/v1/object/[id]/index.delete.ts index 60802f1..ba8f568 100644 --- a/server/api/v1/object/[id]/index.delete.ts +++ b/server/api/v1/object/[id]/index.delete.ts @@ -1,4 +1,5 @@ import aclManager from "~/server/internal/acls"; +import objectHandler from "~/server/internal/objects"; export default defineEventHandler(async (h3) => { const id = getRouterParam(h3, "id"); @@ -6,6 +7,6 @@ export default defineEventHandler(async (h3) => { const userId = await aclManager.getUserIdACL(h3, ["object:delete"]); - const result = await h3.context.objects.deleteWithPermission(id, userId); + const result = await objectHandler.deleteWithPermission(id, userId); return { success: result }; }); diff --git a/server/api/v1/object/[id]/index.get.ts b/server/api/v1/object/[id]/index.get.ts index 3e48981..a67115c 100644 --- a/server/api/v1/object/[id]/index.get.ts +++ b/server/api/v1/object/[id]/index.get.ts @@ -1,4 +1,5 @@ import aclManager from "~/server/internal/acls"; +import objectHandler from "~/server/internal/objects"; export default defineEventHandler(async (h3) => { const id = getRouterParam(h3, "id"); @@ -6,7 +7,7 @@ export default defineEventHandler(async (h3) => { const userId = await aclManager.getUserIdACL(h3, ["object:read"]); - const object = await h3.context.objects.fetchWithPermissions(id, userId); + const object = await objectHandler.fetchWithPermissions(id, userId); if (!object) throw createError({ statusCode: 404, statusMessage: "Object not found" }); diff --git a/server/api/v1/object/[id]/index.post.ts b/server/api/v1/object/[id]/index.post.ts index a27e18d..7b08d2f 100644 --- a/server/api/v1/object/[id]/index.post.ts +++ b/server/api/v1/object/[id]/index.post.ts @@ -1,4 +1,5 @@ import aclManager from "~/server/internal/acls"; +import objectHandler from "~/server/internal/objects"; export default defineEventHandler(async (h3) => { const id = getRouterParam(h3, "id"); @@ -14,7 +15,7 @@ export default defineEventHandler(async (h3) => { const userId = await aclManager.getUserIdACL(h3, ["object:update"]); const buffer = Buffer.from(body); - const result = await h3.context.objects.writeWithPermissions( + const result = await objectHandler.writeWithPermissions( id, async () => buffer, userId diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts new file mode 100644 index 0000000..7b3bad5 --- /dev/null +++ b/server/internal/acls/descriptions.ts @@ -0,0 +1,58 @@ +import { systemACLs, userACLs } from "."; + +type ObjectFromList, V = string> = { + [K in T extends ReadonlyArray ? U : never]: V; +}; + +export const userACLDescriptions: ObjectFromList = { + read: "Fetch user information like username, display name, email, etc...", + + "store:read": + "Fetch and search the store for games, developers and publishers.", + + "object:read": + "Read object objects like game images, profile pictures, and downloads.", + "object:update": + "Update objects like game images, profile pictures, and downloads.", + "object:delete": + "Delete objects like game images, profile pictures, and downloads.", + + "notifications:read": "Fetch this account's notifications.", + "notifications:mark": "Mark notifications as read for this account.", + "notifications:listen": "Connect to a websocket to recieve notifications.", + "notifications:delete": "Delete this account's notifications.", + + "collections:new": "Create collections for this account.", + "collections:read": "Fetch all collections (including library).", + "collections:delete": "Delete a collection for this account.", + "collections:add": "Add a game to any collection (excluding library).", + "collections:remove": + "Remove a game from any collection (excluding library).", + "library:add": "Add a game to your library.", + "library:remove": "Remove a game from your library.", +}; + +export const systemACLDescriptions: ObjectFromList = { + "auth:simple:invitation:read": "Fetch simple auth invitations.", + "auth:simple:invitation:new": "Create new simple auth invitations.", + "auth:simple:invitation:delete": "Delete a simple auth invitation.", + + "library:read": "Fetch a list of all games on this instance.", + + "game:read": "Fetch a given game on this instance.", + "game:update": "Update a game on this instance.", + "game:delete": "Delete a game on this instance.", + "game:version:update": "Update the version order on a game.", + "game:version:delete": "Delete a version for a game.", + "game:image:new": "Upload an image for a game.", + "game:image:delete": "Delete an image for a game.", + + "import:version:read": + "Fetch versions to be imported, and information about versions to be imported.", + "import:version:new": "Import a game version.", + "import:game:read": + "Fetch games to be imported, and search the metadata for games.", + "import:game:new": "Import a game.", + + "user:read": "Fetch any user's information.", +}; diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts index 81d51f0..2e747ca 100644 --- a/server/internal/acls/index.ts +++ b/server/internal/acls/index.ts @@ -4,7 +4,7 @@ import prisma from "../db/database"; import sessionHandler from "../session"; import { MinimumRequestObject } from "~/server/h3"; -const userACLs = [ +export const userACLs = [ "read", "store:read", @@ -30,7 +30,7 @@ const userACLPrefix = "user:"; type UserACL = Array<(typeof userACLs)[number]>; -const systemACLs = [ +export const systemACLs = [ "auth:simple:invitation:read", "auth:simple:invitation:new", "auth:simple:invitation:delete", diff --git a/server/internal/clients/ca.ts b/server/internal/clients/ca.ts index 7665ec4..065b185 100644 --- a/server/internal/clients/ca.ts +++ b/server/internal/clients/ca.ts @@ -1,6 +1,7 @@ import path from "path"; +import fs from "fs"; import droplet from "@drop/droplet"; -import { CertificateStore } from "./ca-store"; +import { CertificateStore, fsCertificateStore } from "./ca-store"; export type CertificateBundle = { priv: string; @@ -72,4 +73,4 @@ export class CertificateAuthority { async blacklistClient(clientId: string) { await this.certificateStore.blacklistCertificate(clientId); } -} +} \ No newline at end of file diff --git a/server/internal/clients/event-handler.ts b/server/internal/clients/event-handler.ts index 66249fb..aea667d 100644 --- a/server/internal/clients/event-handler.ts +++ b/server/internal/clients/event-handler.ts @@ -2,6 +2,7 @@ import { Client, User } from "@prisma/client"; import { EventHandlerRequest, H3Event } from "h3"; import droplet from "@drop/droplet"; import prisma from "../db/database"; +import { useCertificateAuthority } from "~/server/plugins/ca"; export type EventHandlerFunction = ( h3: H3Event, @@ -47,8 +48,8 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { }); } - const ca = h3.context.ca; - const certBundle = await ca.fetchClientCertificate(clientId); + const certificateAuthority = useCertificateAuthority(); + const certBundle = await certificateAuthority.fetchClientCertificate(clientId); // This does the blacklist check already if (!certBundle) throw createError({ diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index 5588f8c..3746525 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -232,4 +232,5 @@ export class MetadataHandler { } } -export default new MetadataHandler(); +export const metadataHandler = new MetadataHandler(); +export default metadataHandler; diff --git a/server/internal/objects/fsBackend.ts b/server/internal/objects/fsBackend.ts index 9ce0d31..3d7f000 100644 --- a/server/internal/objects/fsBackend.ts +++ b/server/internal/objects/fsBackend.ts @@ -1,4 +1,4 @@ -import { Object, ObjectBackend, ObjectMetadata, ObjectReference, Source } from "."; +import { Object, ObjectBackend, ObjectMetadata, ObjectReference, Source } from "./objectHandler"; import sanitize from "sanitize-filename"; diff --git a/server/internal/objects/index.ts b/server/internal/objects/index.ts index 5d97c45..e18afa2 100644 --- a/server/internal/objects/index.ts +++ b/server/internal/objects/index.ts @@ -1,186 +1,3 @@ -/** - * Objects are basically files, like images or downloads, that have a set of metadata and permissions attached - * They're served by the API from the /api/v1/object/${objectId} endpoint. - * - * It supports streams and buffers, depending on the use case. Buffers will likely only be used internally if - * the data needs to be manipulated somehow. - * - * Objects are designed to be created once, and link to a single ID. For example, each user gets a single object - * that's tied to their profile picture. If they want to update their profile picture, they overwrite that object. - * - * Permissions are a list of strings. Each permission string is in the id:permission format. Eg - * anonymous:read - * myUserId:read - * anotherUserId:write - */ - -import { parse as getMimeTypeBuffer } from "file-type-mime"; -import { Readable } from "stream"; -import { getMimeType as getMimeTypeStream } from "stream-mime-type"; -import { v4 as uuidv4 } from "uuid"; - -export type ObjectReference = string; -export type ObjectMetadata = { - mime: string; - permissions: string[]; - userMetadata: { [key: string]: string }; -}; - -export enum ObjectPermission { - Read = "read", - Write = "write", - Delete = "delete", -} -export const ObjectPermissionPriority: Array = [ - ObjectPermission.Read, - ObjectPermission.Write, - ObjectPermission.Delete, -]; - -export type Object = { mime: string; data: Source }; - -export type Source = Readable | Buffer; - -export abstract class ObjectBackend { - // Interface functions, not designed to be called directly. - // They don't check permissions to provide any utilities - abstract fetch(id: ObjectReference): Promise; - abstract write(id: ObjectReference, source: Source): Promise; - abstract create( - id: string, - source: Source, - metadata: ObjectMetadata - ): Promise; - abstract delete(id: ObjectReference): Promise; - abstract fetchMetadata( - id: ObjectReference - ): Promise; - abstract writeMetadata( - id: ObjectReference, - metadata: ObjectMetadata - ): Promise; - - async createFromSource( - id: string, - sourceFetcher: () => Promise, - metadata: { [key: string]: string }, - permissions: Array - ) { - async function fetchMimeType(source: Source) { - if (source instanceof ReadableStream) { - source = Readable.from(source); - } - if (source instanceof Readable) { - const { stream, mime } = await getMimeTypeStream(source); - return { source: Readable.from(stream), mime: mime }; - } - if (source instanceof Buffer) { - const mime = - getMimeTypeBuffer(new Uint8Array(source).buffer)?.mime ?? "application/octet-stream"; - return { source: source, mime }; - } - - return { source: undefined, mime: undefined }; - } - const { source, mime } = await fetchMimeType(await sourceFetcher()); - if (!mime) - throw new Error("Unable to calculate MIME type - is the source empty?"); - - await this.create(id, source, { - permissions, - userMetadata: metadata, - mime, - }); - } - - async fetchWithPermissions(id: ObjectReference, userId?: string) { - const metadata = await this.fetchMetadata(id); - if (!metadata) return; - - // We only need one permission, so find instead of filter is faster - const myPermissions = metadata.permissions.find((e) => { - if (userId !== undefined && e.startsWith(userId)) return true; - if (userId !== undefined && e.startsWith("internal")) return true; - if (e.startsWith("anonymous")) return true; - return false; - }); - - if (!myPermissions) { - // We do not have access to this object - return; - } - - // Because any permission can be read or up, we automatically know we can read this object - // So just straight return the object - const source = await this.fetch(id); - if (!source) return undefined; - const object: Object = { - data: source, - mime: metadata.mime, - }; - return object; - } - - // If we need to fetch a remote resource, it doesn't make sense - // to immediately fetch the object, *then* check permissions. - // Instead the caller can pass a simple anonymous funciton, like - // () => $fetch('/my-image'); - // And if we actually have permission to write, it fetches it then. - async writeWithPermissions( - id: ObjectReference, - sourceFetcher: () => Promise, - userId?: string - ) { - const metadata = await this.fetchMetadata(id); - if (!metadata) return false; - - const myPermissions = metadata.permissions - .filter((e) => { - if (userId !== undefined && e.startsWith(userId)) return true; - if (userId !== undefined && e.startsWith("internal")) return true; - if (e.startsWith("anonymous")) return true; - return false; - }) - // Strip IDs from permissions - .map((e) => e.split(":").at(1)) - // Map to priority according to array - .map((e) => ObjectPermissionPriority.findIndex((c) => c === e)); - - const requiredPermissionIndex = 1; - const hasPermission = - myPermissions.find((e) => e >= requiredPermissionIndex) != undefined; - - if (!hasPermission) return false; - - const source = await sourceFetcher(); - const result = await this.write(id, source); - - return result; - } - - async deleteWithPermission(id: ObjectReference, userId?: string) { - const metadata = await this.fetchMetadata(id); - if (!metadata) return false; - - const myPermissions = metadata.permissions - .filter((e) => { - if (userId !== undefined && e.startsWith(userId)) return true; - if (userId !== undefined && e.startsWith("internal")) return true; - if (e.startsWith("anonymous")) return true; - return false; - }) - // Strip IDs from permissions - .map((e) => e.split(":").at(1)) - // Map to priority according to array - .map((e) => ObjectPermissionPriority.findIndex((c) => c === e)); - - const requiredPermissionIndex = 2; - const hasPermission = - myPermissions.find((e) => e >= requiredPermissionIndex) != undefined; - - if (!hasPermission) return false; - - const result = await this.delete(id); - return result; - } -} +import { FsObjectBackend } from "./fsBackend"; +export const objectHandler = new FsObjectBackend(); +export default objectHandler \ No newline at end of file diff --git a/server/internal/objects/objectHandler.ts b/server/internal/objects/objectHandler.ts new file mode 100644 index 0000000..8ea4803 --- /dev/null +++ b/server/internal/objects/objectHandler.ts @@ -0,0 +1,187 @@ +/** + * Objects are basically files, like images or downloads, that have a set of metadata and permissions attached + * They're served by the API from the /api/v1/object/${objectId} endpoint. + * + * It supports streams and buffers, depending on the use case. Buffers will likely only be used internally if + * the data needs to be manipulated somehow. + * + * Objects are designed to be created once, and link to a single ID. For example, each user gets a single object + * that's tied to their profile picture. If they want to update their profile picture, they overwrite that object. + * + * Permissions are a list of strings. Each permission string is in the id:permission format. Eg + * anonymous:read + * myUserId:read + * anotherUserId:write + */ + +import { parse as getMimeTypeBuffer } from "file-type-mime"; +import { Readable } from "stream"; +import { getMimeType as getMimeTypeStream } from "stream-mime-type"; +import { v4 as uuidv4 } from "uuid"; + +export type ObjectReference = string; +export type ObjectMetadata = { + mime: string; + permissions: string[]; + userMetadata: { [key: string]: string }; +}; + +export enum ObjectPermission { + Read = "read", + Write = "write", + Delete = "delete", +} +export const ObjectPermissionPriority: Array = [ + ObjectPermission.Read, + ObjectPermission.Write, + ObjectPermission.Delete, +]; + +export type Object = { mime: string; data: Source }; + +export type Source = Readable | Buffer; + +export abstract class ObjectBackend { + // Interface functions, not designed to be called directly. + // They don't check permissions to provide any utilities + abstract fetch(id: ObjectReference): Promise; + abstract write(id: ObjectReference, source: Source): Promise; + abstract create( + id: string, + source: Source, + metadata: ObjectMetadata + ): Promise; + abstract delete(id: ObjectReference): Promise; + abstract fetchMetadata( + id: ObjectReference + ): Promise; + abstract writeMetadata( + id: ObjectReference, + metadata: ObjectMetadata + ): Promise; + + async createFromSource( + id: string, + sourceFetcher: () => Promise, + metadata: { [key: string]: string }, + permissions: Array + ) { + async function fetchMimeType(source: Source) { + if (source instanceof ReadableStream) { + source = Readable.from(source); + } + if (source instanceof Readable) { + const { stream, mime } = await getMimeTypeStream(source); + return { source: Readable.from(stream), mime: mime }; + } + if (source instanceof Buffer) { + const mime = + getMimeTypeBuffer(new Uint8Array(source).buffer)?.mime ?? + "application/octet-stream"; + return { source: source, mime }; + } + + return { source: undefined, mime: undefined }; + } + const { source, mime } = await fetchMimeType(await sourceFetcher()); + if (!mime) + throw new Error("Unable to calculate MIME type - is the source empty?"); + + await this.create(id, source, { + permissions, + userMetadata: metadata, + mime, + }); + } + + async fetchWithPermissions(id: ObjectReference, userId?: string) { + const metadata = await this.fetchMetadata(id); + if (!metadata) return; + + // We only need one permission, so find instead of filter is faster + const myPermissions = metadata.permissions.find((e) => { + if (userId !== undefined && e.startsWith(userId)) return true; + if (userId !== undefined && e.startsWith("internal")) return true; + if (e.startsWith("anonymous")) return true; + return false; + }); + + if (!myPermissions) { + // We do not have access to this object + return; + } + + // Because any permission can be read or up, we automatically know we can read this object + // So just straight return the object + const source = await this.fetch(id); + if (!source) return undefined; + const object: Object = { + data: source, + mime: metadata.mime, + }; + return object; + } + + // If we need to fetch a remote resource, it doesn't make sense + // to immediately fetch the object, *then* check permissions. + // Instead the caller can pass a simple anonymous funciton, like + // () => $fetch('/my-image'); + // And if we actually have permission to write, it fetches it then. + async writeWithPermissions( + id: ObjectReference, + sourceFetcher: () => Promise, + userId?: string + ) { + const metadata = await this.fetchMetadata(id); + if (!metadata) return false; + + const myPermissions = metadata.permissions + .filter((e) => { + if (userId !== undefined && e.startsWith(userId)) return true; + if (userId !== undefined && e.startsWith("internal")) return true; + if (e.startsWith("anonymous")) return true; + return false; + }) + // Strip IDs from permissions + .map((e) => e.split(":").at(1)) + // Map to priority according to array + .map((e) => ObjectPermissionPriority.findIndex((c) => c === e)); + + const requiredPermissionIndex = 1; + const hasPermission = + myPermissions.find((e) => e >= requiredPermissionIndex) != undefined; + + if (!hasPermission) return false; + + const source = await sourceFetcher(); + const result = await this.write(id, source); + + return result; + } + + async deleteWithPermission(id: ObjectReference, userId?: string) { + const metadata = await this.fetchMetadata(id); + if (!metadata) return false; + + const myPermissions = metadata.permissions + .filter((e) => { + if (userId !== undefined && e.startsWith(userId)) return true; + if (userId !== undefined && e.startsWith("internal")) return true; + if (e.startsWith("anonymous")) return true; + return false; + }) + // Strip IDs from permissions + .map((e) => e.split(":").at(1)) + // Map to priority according to array + .map((e) => ObjectPermissionPriority.findIndex((c) => c === e)); + + const requiredPermissionIndex = 2; + const hasPermission = + myPermissions.find((e) => e >= requiredPermissionIndex) != undefined; + + if (!hasPermission) return false; + + const result = await this.delete(id); + return result; + } +} diff --git a/server/internal/objects/transactional.ts b/server/internal/objects/transactional.ts index 15fb41b..922ff6a 100644 --- a/server/internal/objects/transactional.ts +++ b/server/internal/objects/transactional.ts @@ -4,7 +4,7 @@ This is used as a utility in metadata handling, so we only fetch the objects if */ import { Readable } from "stream"; import { v4 as uuidv4 } from "uuid"; -import { objectHandler } from "~/server/plugins/objects"; +import objectHandler from "."; export type TransactionDataType = string | Readable | Buffer; type TransactionTable = { [key: string]: TransactionDataType }; // ID to data diff --git a/server/plugins/ca.ts b/server/plugins/ca.ts index 1b99d79..000764a 100644 --- a/server/plugins/ca.ts +++ b/server/plugins/ca.ts @@ -15,13 +15,4 @@ export default defineNitroPlugin(async (nitro) => { const store = fsCertificateStore(basePath); ca = await CertificateAuthority.new(store); - - nitro.hooks.hook("request", (h3) => { - if (!ca) - throw createError({ - statusCode: 500, - statusMessage: "Certificate authority not initialised", - }); - h3.context.ca = ca; - }); }); diff --git a/server/plugins/metadata.ts b/server/plugins/metadata.ts deleted file mode 100644 index 8fbd8fa..0000000 --- a/server/plugins/metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MetadataHandler, MetadataProvider } from "../internal/metadata"; -import { GiantBombProvider } from "../internal/metadata/giantbomb"; -import { ManualMetadataProvider } from "../internal/metadata/manual"; - -export const metadataHandler = new MetadataHandler(); - -const providerCreators: Array<() => MetadataProvider> = [ - () => new GiantBombProvider(), - () => new ManualMetadataProvider(), -]; - -export default defineNitroPlugin(async (nitro) => { - for (const creator of providerCreators) { - try { - const instance = creator(); - metadataHandler.addProvider(instance); - } catch (e) { - console.warn(e); - } - } - - nitro.hooks.hook("request", (h3) => { - h3.context.metadataHandler = metadataHandler; - }); -}); diff --git a/server/plugins/objects.ts b/server/plugins/objects.ts deleted file mode 100644 index 8e9ee40..0000000 --- a/server/plugins/objects.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FsObjectBackend } from "../internal/objects/fsBackend"; - -// To-do insert logic surrounding deciding what object backend to use -export const objectHandler = new FsObjectBackend(); - -export default defineNitroPlugin((nitro) => { - nitro.hooks.hook("request", (h3) => { - h3.context.objects = objectHandler; - }); -});