mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
feat(acls): refactor & acl descriptions
This commit is contained in:
@ -32,7 +32,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
extends: ['./drop-base'],
|
||||
extends: ["./drop-base"],
|
||||
|
||||
// Module config from here down
|
||||
modules: ["@nuxt/content", "vue3-carousel-nuxt"],
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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({
|
||||
|
||||
9
server/api/v1/admin/user/token/token.get.ts
Normal file
9
server/api/v1/admin/user/token/token.get.ts
Normal file
@ -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;
|
||||
});
|
||||
@ -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),
|
||||
{},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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" });
|
||||
|
||||
|
||||
@ -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 };
|
||||
});
|
||||
|
||||
@ -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" });
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
58
server/internal/acls/descriptions.ts
Normal file
58
server/internal/acls/descriptions.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { systemACLs, userACLs } from ".";
|
||||
|
||||
type ObjectFromList<T extends ReadonlyArray<string>, V = string> = {
|
||||
[K in T extends ReadonlyArray<infer U> ? U : never]: V;
|
||||
};
|
||||
|
||||
export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
|
||||
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<typeof systemACLs> = {
|
||||
"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.",
|
||||
};
|
||||
@ -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",
|
||||
@ -32,7 +32,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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<T> = (
|
||||
h3: H3Event<EventHandlerRequest>,
|
||||
@ -47,8 +48,8 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
|
||||
@ -232,4 +232,5 @@ export class MetadataHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export default new MetadataHandler();
|
||||
export const metadataHandler = new MetadataHandler();
|
||||
export default metadataHandler;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Object, ObjectBackend, ObjectMetadata, ObjectReference, Source } from ".";
|
||||
import { Object, ObjectBackend, ObjectMetadata, ObjectReference, Source } from "./objectHandler";
|
||||
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
|
||||
@ -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> = [
|
||||
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<Source | undefined>;
|
||||
abstract write(id: ObjectReference, source: Source): Promise<boolean>;
|
||||
abstract create(
|
||||
id: string,
|
||||
source: Source,
|
||||
metadata: ObjectMetadata
|
||||
): Promise<ObjectReference | undefined>;
|
||||
abstract delete(id: ObjectReference): Promise<boolean>;
|
||||
abstract fetchMetadata(
|
||||
id: ObjectReference
|
||||
): Promise<ObjectMetadata | undefined>;
|
||||
abstract writeMetadata(
|
||||
id: ObjectReference,
|
||||
metadata: ObjectMetadata
|
||||
): Promise<boolean>;
|
||||
|
||||
async createFromSource(
|
||||
id: string,
|
||||
sourceFetcher: () => Promise<Source>,
|
||||
metadata: { [key: string]: string },
|
||||
permissions: Array<string>
|
||||
) {
|
||||
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<Source>,
|
||||
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
|
||||
187
server/internal/objects/objectHandler.ts
Normal file
187
server/internal/objects/objectHandler.ts
Normal file
@ -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> = [
|
||||
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<Source | undefined>;
|
||||
abstract write(id: ObjectReference, source: Source): Promise<boolean>;
|
||||
abstract create(
|
||||
id: string,
|
||||
source: Source,
|
||||
metadata: ObjectMetadata
|
||||
): Promise<ObjectReference | undefined>;
|
||||
abstract delete(id: ObjectReference): Promise<boolean>;
|
||||
abstract fetchMetadata(
|
||||
id: ObjectReference
|
||||
): Promise<ObjectMetadata | undefined>;
|
||||
abstract writeMetadata(
|
||||
id: ObjectReference,
|
||||
metadata: ObjectMetadata
|
||||
): Promise<boolean>;
|
||||
|
||||
async createFromSource(
|
||||
id: string,
|
||||
sourceFetcher: () => Promise<Source>,
|
||||
metadata: { [key: string]: string },
|
||||
permissions: Array<string>
|
||||
) {
|
||||
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<Source>,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user