feat(acls): added backend acls

This commit is contained in:
DecDuck
2025-02-04 13:15:34 +11:00
parent 256fbd6afa
commit 090d2e6586
70 changed files with 397 additions and 474 deletions

View File

@ -785,7 +785,7 @@ async function deleteVersion(versionName: string) {
async function updateVersionOrder() { async function updateVersionOrder() {
try { try {
const newVersions = await $fetch("/api/v1/admin/game/version", { const newVersions = await $fetch("/api/v1/admin/game/version", {
method: "POST", method: "PATCH",
body: { body: {
id: gameId, id: gameId,
versions: game.value.versions.map((e) => e.versionName), versions: game.value.versions.map((e) => e.versionName),

View File

@ -0,0 +1,15 @@
-- CreateEnum
CREATE TYPE "APITokenMode" AS ENUM ('User', 'System');
-- CreateTable
CREATE TABLE "APIToken" (
"token" TEXT NOT NULL,
"mode" "APITokenMode" NOT NULL,
"userId" TEXT,
"acls" TEXT[],
CONSTRAINT "APIToken_pkey" PRIMARY KEY ("token")
);
-- AddForeignKey
ALTER TABLE "APIToken" ADD CONSTRAINT "APIToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "CollectionEntry" DROP CONSTRAINT "CollectionEntry_gameId_fkey";
-- AddForeignKey
ALTER TABLE "CollectionEntry" ADD CONSTRAINT "CollectionEntry_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -21,3 +21,18 @@ model Invitation {
email String? email String?
expires DateTime expires DateTime
} }
enum APITokenMode {
User
System
}
model APIToken {
token String @id @default(uuid())
mode APITokenMode
userId String?
user User? @relation(fields: [userId], references: [id])
acls String[]
}

View File

@ -14,7 +14,7 @@ model CollectionEntry {
collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade) collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
gameId String gameId String
game Game @relation(fields: [gameId], references: [id]) game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
@@id([collectionId, gameId]) @@id([collectionId, gameId])
} }

View File

@ -12,6 +12,8 @@ model User {
notifications Notification[] notifications Notification[]
collections Collection[] collections Collection[]
news News[] news News[]
tokens APIToken[]
} }
model Notification { model Notification {

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "auth:simple:invitation:delete",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const id = body.id; const id = body.id;

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "auth:simple:invitation:read",
]);
if (!allowed) throw createError({ statusCode: 403 });
await runTask("cleanup:invitations"); await runTask("cleanup:invitations");

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "auth:simple:invitation:new",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const isAdmin = body.isAdmin; const isAdmin = body.isAdmin;
@ -30,7 +33,7 @@ export default defineEventHandler(async (h3) => {
isAdmin: isAdmin, isAdmin: isAdmin,
username: username, username: username,
email: email, email: email,
expires: expiresDate expires: expiresDate,
}, },
}); });

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:image:delete",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const gameId = body.gameId; const gameId = body.gameId;

View File

@ -1,9 +1,12 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:image:new",
]);
if (!allowed) throw createError({ statusCode: 403 });
const form = await readMultipartFormData(h3); const form = await readMultipartFormData(h3);
if (!form) if (!form)

View File

@ -1,9 +1,12 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:delete",
]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3); const query = getQuery(h3);
const gameId = query.id?.toString(); const gameId = query.id?.toString();

View File

@ -1,9 +1,12 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:read",
]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3); const query = getQuery(h3);
const gameId = query.id?.toString(); const gameId = query.id?.toString();

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:update",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const id = body.id; const id = body.id;

View File

@ -1,9 +1,12 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:update",
]);
if (!allowed) throw createError({ statusCode: 403 });
const form = await readMultipartFormData(h3); const form = await readMultipartFormData(h3);
if (!form) if (!form)

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:version:delete",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const gameId = body.id.toString(); const gameId = body.id.toString();

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "game:version:update",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const gameId = body.id?.toString(); const gameId = body.id?.toString();

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "import:game:read",
]);
if (!allowed) throw createError({ statusCode: 403 });
const unimportedGames = await libraryManager.fetchAllUnimportedGames(); const unimportedGames = await libraryManager.fetchAllUnimportedGames();
return { unimportedGames }; return { unimportedGames };

View File

@ -1,3 +1,4 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
import { import {
GameMetadataSearchResult, GameMetadataSearchResult,
@ -5,8 +6,10 @@ import {
} from "~/server/internal/metadata/types"; } from "~/server/internal/metadata/types";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "import:game:new",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "import:game:read",
]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3); const query = getQuery(h3);
const search = query.q?.toString(); const search = query.q?.toString();

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "import:version:read",
]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3); const query = await getQuery(h3);
const gameId = query.id?.toString(); const gameId = query.id?.toString();

View File

@ -1,10 +1,13 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
import { parsePlatform } from "~/server/internal/utils/parseplatform"; import { parsePlatform } from "~/server/internal/utils/parseplatform";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "import:version:new",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
const gameId = body.id; const gameId = body.id;

View File

@ -1,8 +1,11 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, [
if (!user) throw createError({ statusCode: 403 }); "import:version:read",
]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3); const query = await getQuery(h3);
const gameId = query.id?.toString(); const gameId = query.id?.toString();

View File

@ -1,6 +0,0 @@
export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getUser(h3);
if (!user)
throw createError({ statusCode: 403, statusMessage: "Not authenticated" });
return { admin: user.admin };
});

View File

@ -1,8 +1,9 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library"; import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, ["library:read"]);
if (!user) throw createError({ statusCode: 403 }); if (!allowed) throw createError({ statusCode: 403 });
const unimportedGames = await libraryManager.fetchAllUnimportedGames(); const unimportedGames = await libraryManager.fetchAllUnimportedGames();
const games = await libraryManager.fetchGamesWithStatus(); const games = await libraryManager.fetchGamesWithStatus();

View File

@ -1,8 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3); const allowed = await aclManager.allowSystemACL(h3, ["user:read"]);
if (!user) throw createError({ statusCode: 403 }); if (!allowed) throw createError({ statusCode: 403 });
const users = await prisma.user.findMany({}); const users = await prisma.user.findMany({});

View File

@ -2,6 +2,7 @@ import { AuthMec } from "@prisma/client";
import { JsonArray } from "@prisma/client/runtime/library"; import { JsonArray } from "@prisma/client/runtime/library";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { checkHash } from "~/server/internal/security/simple"; import { checkHash } from "~/server/internal/security/simple";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const body = await readBody(h3); const body = await readBody(h3);
@ -31,7 +32,7 @@ export default defineEventHandler(async (h3) => {
if (!await checkHash(password, hash.toString())) if (!await checkHash(password, hash.toString()))
throw createError({ statusCode: 401, statusMessage: "Invalid username or password." }); throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
await h3.context.session.setUserId(h3, authMek.userId, rememberMe); await sessionHandler.setUserId(h3, authMek.userId, rememberMe);
return { result: true, userId: authMek.userId } return { result: true, userId: authMek.userId }
}); });

View File

@ -1,7 +1,8 @@
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await sessionHandler.getUserId(h3);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const query = getQuery(h3); const query = getQuery(h3);

View File

@ -1,7 +1,8 @@
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await sessionHandler.getUserId(h3);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);

View File

@ -1,11 +1,11 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:remove"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "Requires authentication",
}); });
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");

View File

@ -1,11 +1,11 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:add"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "Requires authentication",
}); });
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:delete"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["library:remove"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["library:add"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,

View File

@ -1,11 +1,11 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:new"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "Requires authentication",
}); });
const collections = await userLibraryManager.fetchCollections(userId); const collections = await userLibraryManager.fetchCollections(userId);

View File

@ -1,11 +1,11 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId) if (!userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "Requires authentication",
}); });
const body = await readBody(h3); const body = await readBody(h3);

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const gameId = getRouterParam(h3, "id"); const gameId = getRouterParam(h3, "id");

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["notifications:delete"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const notificationId = getRouterParam(h3, "id"); const notificationId = getRouterParam(h3, "id");

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const notificationId = getRouterParam(h3, "id"); const notificationId = getRouterParam(h3, "id");

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const notificationId = getRouterParam(h3, "id"); const notificationId = getRouterParam(h3, "id");

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const notifications = await prisma.notification.findMany({ const notifications = await prisma.notification.findMany({

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
await prisma.notification.updateMany({ await prisma.notification.updateMany({

View File

@ -1,6 +1,7 @@
import notificationSystem from "~/server/internal/notifications"; import notificationSystem from "~/server/internal/notifications";
import session from "~/server/internal/session"; import session from "~/server/internal/session";
import { parse as parseCookies } from "cookie-es"; import { parse as parseCookies } from "cookie-es";
import aclManager from "~/server/internal/acls";
// TODO add web socket sessions for horizontal scaling // TODO add web socket sessions for horizontal scaling
// Peer ID to user ID // Peer ID to user ID
@ -8,16 +9,10 @@ const socketSessions: { [key: string]: string } = {};
export default defineWebSocketHandler({ export default defineWebSocketHandler({
async open(peer) { async open(peer) {
const cookies = peer.request?.headers?.get("Cookie"); const userId = await aclManager.getUserIdACL(
if (!cookies) { { headers: peer.request?.headers ?? new Headers() },
peer.send("unauthenticated"); ["notifications:listen"]
return; );
}
const parsedCookies = parseCookies(cookies);
const token = parsedCookies[session.getDropTokenCookie()];
const userId = await session.getUserIdRaw(token);
if (!userId) { if (!userId) {
peer.send("unauthenticated"); peer.send("unauthenticated");
return; return;

View File

@ -1,8 +1,10 @@
import aclManager from "~/server/internal/acls";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");
if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["object:delete"]);
const result = await h3.context.objects.deleteWithPermission(id, userId); const result = await h3.context.objects.deleteWithPermission(id, userId);
return { success: result }; return { success: result };

View File

@ -1,8 +1,10 @@
import aclManager from "~/server/internal/acls";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");
if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["object:read"]);
const object = await h3.context.objects.fetchWithPermissions(id, userId); const object = await h3.context.objects.fetchWithPermissions(id, userId);
if (!object) if (!object)

View File

@ -1,3 +1,5 @@
import aclManager from "~/server/internal/acls";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");
if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
@ -9,7 +11,7 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Invalid upload", statusMessage: "Invalid upload",
}); });
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserIdACL(h3, ["object:update"]);
const buffer = Buffer.from(body); const buffer = Buffer.from(body);
const result = await h3.context.objects.writeWithPermissions( const result = await h3.context.objects.writeWithPermissions(

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const developers = await prisma.developer.findMany({ const developers = await prisma.developer.findMany({

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const publishers = await prisma.publisher.findMany({ const publishers = await prisma.publisher.findMany({

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const games = await prisma.game.findMany({ const games = await prisma.game.findMany({

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const games = await prisma.game.findMany({ const games = await prisma.game.findMany({

View File

@ -1,7 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await h3.context.session.getUserId(h3); const userId = await aclManager.getUserACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 }); if (!userId) throw createError({ statusCode: 403 });
const versions = await prisma.gameVersion.findMany({ const versions = await prisma.gameVersion.findMany({

View File

@ -1,46 +1,39 @@
import session from "~/server/internal/session"; import session from "~/server/internal/session";
import taskHandler, { TaskMessage } from "~/server/internal/tasks"; import taskHandler, { TaskMessage } from "~/server/internal/tasks";
import { parse as parseCookies } from "cookie-es"; import { parse as parseCookies } from "cookie-es";
import { MinimumRequestObject } from "~/server/h3";
// TODO add web socket sessions for horizontal scaling // TODO add web socket sessions for horizontal scaling
// ID to admin // ID to admin
const adminSocketSessions: { [key: string]: boolean } = {}; const socketHeaders: { [key: string]: MinimumRequestObject } = {};
export default defineWebSocketHandler({ export default defineWebSocketHandler({
async open(peer) { async open(peer) {
const cookies = peer.request?.headers?.get("Cookie"); const request = peer.request;
if (!cookies) { if (!request) {
peer.send("unauthenticated"); peer.send("unauthenticated");
return; return;
} }
const parsedCookies = parseCookies(cookies); socketHeaders[peer.id] = {
const token = parsedCookies[session.getDropTokenCookie()]; headers: request.headers ?? new Headers(),
};
const userId = await session.getUserIdRaw(token);
if (!userId) {
peer.send("unauthenticated");
return;
}
const admin = session.getAdminUser(token);
adminSocketSessions[peer.id] = admin !== undefined;
peer.send(`connect`); peer.send(`connect`);
}, },
message(peer, message) { message(peer, message) {
if (!peer.id) return; if (!peer.id) return;
if (adminSocketSessions[peer.id] === undefined) return; if (socketHeaders[peer.id] === undefined) return;
const text = message.text(); const text = message.text();
if (text.startsWith("connect/")) { if (text.startsWith("connect/")) {
const id = text.substring("connect/".length); const id = text.substring("connect/".length);
taskHandler.connect(peer.id, id, peer, adminSocketSessions[peer.id]); taskHandler.connect(peer.id, id, peer, socketHeaders[peer.id]);
return; return;
} }
}, },
close(peer, details) { close(peer, details) {
if (!peer.id) return; if (!peer.id) return;
if (adminSocketSessions[peer.id] === undefined) return; if (socketHeaders[peer.id] === undefined) return;
delete adminSocketSessions[peer.id]; delete socketHeaders[peer.id];
taskHandler.disconnectAll(peer.id); taskHandler.disconnectAll(peer.id);
}, },

View File

@ -1,4 +1,6 @@
import aclManager from "~/server/internal/acls";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getUser(h3); const user = await aclManager.getUserACL(h3, ["read"]);
return user ?? null; // Need to specifically return null return user ?? null; // Need to specifically return null
}); });

6
server/h3.d.ts vendored
View File

@ -6,9 +6,9 @@ import { SessionHandler } from "./internal/session";
export * from "h3"; export * from "h3";
declare module "h3" { declare module "h3" {
interface H3EventContext { interface H3EventContext {
session: SessionHandler;
metadataHandler: MetadataHandler;
ca: CertificateAuthority; ca: CertificateAuthority;
objects: ObjectBackend objects: ObjectBackend;
} }
} }
export type MinimumRequestObject = { headers: Headers };

View File

@ -0,0 +1,152 @@
import { APITokenMode, User } from "@prisma/client";
import { H3Context, H3Event } from "h3";
import prisma from "../db/database";
import sessionHandler from "../session";
import { MinimumRequestObject } from "~/server/h3";
const userACLs = [
"read",
"store:read",
"object:read",
"object:update",
"object:delete",
"notifications:read",
"notifications:mark",
"notifications:listen",
"notifications:delete",
"collections:new",
"collections:read",
"collections:delete",
"collections:add",
"collections:remove",
"library:add",
"library:remove",
"news:read",
] as const;
const userACLPrefix = "user:";
type UserACL = Array<(typeof userACLs)[number]>;
const systemACLs = [
"auth:simple:invitation:read",
"auth:simple:invitation:new",
"auth:simple:invitation:delete",
"library:read",
"game:read",
"game:update",
"game:delete",
"game:version:update",
"game:version:delete",
"game:image:new",
"game:image:delete",
"import:version:read",
"import:version:new",
"import:game:read",
"import:game:new",
"user:read",
] as const;
const systemACLPrefix = "system:";
type SystemACL = Array<(typeof systemACLs)[number]>;
class ACLManager {
private getAuthorizationToken(request: MinimumRequestObject) {
const [type, token] =
request.headers.get("Authorization")?.split(" ") ?? [];
if (!type || !token) return undefined;
if (type != "Bearer") return undefined;
return token;
}
async getUserIdACL(request: MinimumRequestObject | undefined, acls: UserACL) {
if (!request)
throw new Error("Native web requests not available - weird deployment?");
// Sessions automatically have all ACLs
const userId = await sessionHandler.getUserId(request);
if (userId) return userId;
const authorizationToken = this.getAuthorizationToken(request);
if (!authorizationToken) return undefined;
const token = await prisma.aPIToken.findUnique({
where: { token: authorizationToken },
});
if (!token) return undefined;
if (token.mode != APITokenMode.User || !token.userId) return undefined; // If it's a system token
for (const acl of acls) {
const tokenACLIndex = token.acls.findIndex((e) => e == acl);
if (tokenACLIndex != -1) return token.userId;
}
return undefined;
}
async getUserACL(request: MinimumRequestObject | undefined, acls: UserACL) {
if (!request)
throw new Error("Native web requests not available - weird deployment?");
const userId = await this.getUserIdACL(request, acls);
if (!userId) return undefined;
const user = await prisma.user.findUnique({ where: { id: userId } });
if (user) return user;
return undefined;
}
async allowSystemACL(
request: MinimumRequestObject | undefined,
acls: SystemACL
) {
if (!request)
throw new Error("Native web requests not available - weird deployment?");
const userId = await sessionHandler.getUserId(request);
if (userId) {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) return false;
if (user.admin) return true;
return false;
}
const authorizationToken = this.getAuthorizationToken(request);
if (!authorizationToken) return false;
const token = await prisma.aPIToken.findUnique({
where: { token: authorizationToken },
});
if (!token) return false;
if (token.mode != APITokenMode.System) return false;
for (const acl of acls) {
const tokenACLIndex = token.acls.findIndex((e) => e == acl);
if (tokenACLIndex != -1) return true;
}
return false;
}
async hasACL(request: MinimumRequestObject | undefined, acls: string[]) {
for (const acl of acls) {
if (acl.startsWith(userACLPrefix)) {
const rawACL = acl.substring(userACLPrefix.length);
const userId = await this.getUserIdACL(request, [rawACL as any]);
if (!userId) return false;
}
if (acl.startsWith(systemACLPrefix)) {
const rawACL = acl.substring(systemACLPrefix.length);
const allowed = await this.allowSystemACL(request, [rawACL as any]);
if (!allowed) return false;
}
}
return true;
}
}
export const aclManager = new ACLManager();
export default aclManager;

View File

@ -1,11 +0,0 @@
# Library Format
Drop uses a filesystem-based library format, as it targets homelabs and not enterprise-grade solutions. The format works as follows:
## /{game name}
The game name is only used for initial matching, and doesn't affect actual metadata. Metadata is linked to the game's database entry, which is linked to it's filesystem name (they, however, can be completely different).
## /{game name}/{version name}
The version name can be anything. Versions have to manually imported within the web UI. There, you can change the order of the updates and mark them as deltas. Delta updates apply files over the previous versions.

View File

@ -1,309 +0,0 @@
/**
* The Library Manager keeps track of games in Drop's library and their various states.
* It uses path relative to the library, so it can moved without issue
*
* It also provides the endpoints with information about unmatched games
*/
import fs from "fs";
import path from "path";
import prisma from "../db/database";
import { GameVersion, Platform } from "@prisma/client";
import { fuzzy } from "fast-fuzzy";
import { recursivelyReaddir } from "../utils/recursivedirs";
import taskHandler from "../tasks";
import { parsePlatform } from "../utils/parseplatform";
import droplet from "@drop/droplet";
class AppLibraryManager {
private basePath: string;
constructor() {
this.basePath = process.env.LIBRARY ?? "./.data/library";
fs.mkdirSync(this.basePath, { recursive: true });
}
fetchLibraryPath() {
return this.basePath;
}
async fetchAllUnimportedGames() {
const dirs = fs.readdirSync(this.basePath).filter((e) => {
const fullDir = path.join(this.basePath, e);
return fs.lstatSync(fullDir).isDirectory();
});
const validGames = await prisma.game.findMany({
where: {
libraryBasePath: { in: dirs },
},
select: {
libraryBasePath: true,
},
});
const validGameDirs = validGames.map((e) => e.libraryBasePath);
const unregisteredGames = dirs.filter((e) => !validGameDirs.includes(e));
return unregisteredGames;
}
async fetchUnimportedGameVersions(
libraryBasePath: string,
versions: Array<GameVersion>
) {
const gameDir = path.join(this.basePath, libraryBasePath);
const versionsDirs = fs.readdirSync(gameDir);
const importedVersionDirs = versions.map((e) => e.versionName);
const unimportedVersions = versionsDirs.filter(
(e) => !importedVersionDirs.includes(e)
);
return unimportedVersions;
}
async fetchGamesWithStatus() {
const games = await prisma.game.findMany({
select: {
id: true,
versions: true,
mName: true,
mShortDescription: true,
metadataSource: true,
mDevelopers: true,
mPublishers: true,
mIconId: true,
libraryBasePath: true,
},
orderBy: {
mName: "asc",
},
});
return await Promise.all(
games.map(async (e) => ({
game: e,
status: {
noVersions: e.versions.length == 0,
unimportedVersions: await this.fetchUnimportedGameVersions(
e.libraryBasePath,
e.versions
),
},
}))
);
}
async fetchUnimportedVersions(gameId: string) {
const game = await prisma.game.findUnique({
where: { id: gameId },
select: {
versions: {
select: {
versionName: true,
},
},
libraryBasePath: true,
},
});
if (!game) return undefined;
const targetDir = path.join(this.basePath, game.libraryBasePath);
if (!fs.existsSync(targetDir))
throw new Error(
"Game in database, but no physical directory? Something is very very wrong..."
);
const versions = fs.readdirSync(targetDir);
const validVersions = versions.filter((versionDir) => {
const versionPath = path.join(targetDir, versionDir);
const stat = fs.statSync(versionPath);
return stat.isDirectory();
});
const currentVersions = game.versions.map((e) => e.versionName);
const unimportedVersions = validVersions.filter(
(e) => !currentVersions.includes(e)
);
return unimportedVersions;
}
async fetchUnimportedVersionInformation(gameId: string, versionName: string) {
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { libraryBasePath: true, mName: true },
});
if (!game) return undefined;
const targetDir = path.join(
this.basePath,
game.libraryBasePath,
versionName
);
if (!fs.existsSync(targetDir)) return undefined;
const fileExts: { [key: string]: string[] } = {
Linux: [
// Ext for Unity games
".x86_64",
// Shell scripts
".sh",
// No extension is common for Linux binaries
"",
],
Windows: [
// Pretty much the only one
".exe",
],
};
const options: Array<{
filename: string;
platform: string;
match: number;
}> = [];
const files = recursivelyReaddir(targetDir, 2);
for (const file of files) {
const filename = path.basename(file);
const dotLocation = file.lastIndexOf(".");
const ext = dotLocation == -1 ? "" : file.slice(dotLocation);
for (const [platform, checkExts] of Object.entries(fileExts)) {
for (const checkExt of checkExts) {
if (checkExt != ext) continue;
const fuzzyValue = fuzzy(filename, game.mName);
const relative = path.relative(targetDir, file);
options.push({
filename: relative,
platform: platform,
match: fuzzyValue,
});
}
}
}
const sortedOptions = options.sort((a, b) => b.match - a.match);
return sortedOptions;
}
// Checks are done in least to most expensive order
async checkUnimportedGamePath(targetPath: string) {
const targetDir = path.join(this.basePath, targetPath);
if (!fs.existsSync(targetDir)) return false;
const hasGame =
(await prisma.game.count({ where: { libraryBasePath: targetPath } })) > 0;
if (hasGame) return false;
return true;
}
async importVersion(
gameId: string,
versionName: string,
metadata: {
platform: string;
onlySetup: boolean;
setup: string;
setupArgs: string;
launch: string;
launchArgs: string;
delta: boolean;
umuId: string;
}
) {
const taskId = `import:${gameId}:${versionName}`;
const platform = parsePlatform(metadata.platform);
if (!platform) return undefined;
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { mName: true, libraryBasePath: true },
});
if (!game) return undefined;
const baseDir = path.join(this.basePath, game.libraryBasePath, versionName);
if (!fs.existsSync(baseDir)) return undefined;
taskHandler.create({
id: taskId,
name: `Importing version ${versionName} for ${game.mName}`,
requireAdmin: true,
async run({ progress, log }) {
// First, create the manifest via droplet.
// This takes up 90% of our progress, so we wrap it in a *0.9
const manifest = await new Promise<string>((resolve, reject) => {
droplet.generateManifest(
baseDir,
(err, value) => {
if (err) return reject(err);
progress(value * 0.9);
},
(err, line) => {
if (err) return reject(err);
log(line);
},
(err, manifest) => {
if (err) return reject(err);
resolve(manifest);
}
);
});
log("Created manifest successfully!");
const currentIndex = await prisma.gameVersion.count({
where: { gameId: gameId },
});
// 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(" "),
},
});
}
log("Successfully created version!");
progress(100);
},
});
return taskId;
}
}
export const appLibraryManager = new AppLibraryManager();
export default appLibraryManager;

View File

@ -230,7 +230,7 @@ class LibraryManager {
taskHandler.create({ taskHandler.create({
id: taskId, id: taskId,
name: `Importing version ${versionName} for ${game.mName}`, name: `Importing version ${versionName} for ${game.mName}`,
requireAdmin: true, acls: ["system:import:version:read"],
async run({ progress, log }) { async run({ progress, log }) {
// First, create the manifest via droplet. // First, create the manifest via droplet.
// This takes up 90% of our progress, so we wrap it in a *0.9 // This takes up 90% of our progress, so we wrap it in a *0.9

View File

@ -76,7 +76,7 @@ export abstract class ObjectBackend {
} }
if (source instanceof Buffer) { if (source instanceof Buffer) {
const mime = const mime =
getMimeTypeBuffer(source)?.mime ?? "application/octet-stream"; getMimeTypeBuffer(new Uint8Array(source).buffer)?.mime ?? "application/octet-stream";
return { source: source, mime }; return { source: source, mime };
} }

View File

@ -4,6 +4,8 @@ import { SessionProvider } from "./types";
import prisma from "../db/database"; import prisma from "../db/database";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import moment from "moment"; import moment from "moment";
import { parse as parseCookies } from "cookie-es";
import { MinimumRequestObject } from "~/server/h3";
/* /*
This implementation may need work. This implementation may need work.
@ -25,8 +27,12 @@ export class SessionHandler {
this.sessionProvider = createMemorySessionProvider(); this.sessionProvider = createMemorySessionProvider();
} }
private getSessionToken(h3: H3Event) { private getSessionToken(request: MinimumRequestObject | undefined) {
const cookie = getCookie(h3, dropTokenCookie); if(!request) throw new Error("Native web request not available");
const cookieHeader = request.headers.get("Cookie");
if (!cookieHeader) return undefined;
const cookies = parseCookies(cookieHeader);
const cookie = cookies[dropTokenCookie];
return cookie; return cookie;
} }
@ -47,8 +53,8 @@ export class SessionHandler {
return dropTokenCookie; return dropTokenCookie;
} }
async getSession<T extends Session>(h3: H3Event) { async getSession<T extends Session>(request: MinimumRequestObject) {
const token = this.getSessionToken(h3); const token = this.getSessionToken(request);
if (!token) return undefined; if (!token) return undefined;
const data = await this.sessionProvider.getSession<{ [userSessionKey]: T }>( const data = await this.sessionProvider.getSession<{ [userSessionKey]: T }>(
token token
@ -68,14 +74,14 @@ export class SessionHandler {
return result; return result;
} }
async clearSession(h3: H3Event) { async clearSession(request: MinimumRequestObject) {
const token = this.getSessionToken(h3); const token = this.getSessionToken(request);
if (!token) return false; if (!token) return false;
await this.sessionProvider.clearSession(token); await this.sessionProvider.clearSession(token);
return true; return true;
} }
async getUserId(h3: H3Event) { async getUserId(h3: MinimumRequestObject) {
const token = this.getSessionToken(h3); const token = this.getSessionToken(h3);
if (!token) return undefined; if (!token) return undefined;
@ -91,17 +97,6 @@ export class SessionHandler {
return session[userIdKey]; return session[userIdKey];
} }
async getUser(obj: H3Event | string) {
const userId =
typeof obj === "string"
? await this.getUserIdRaw(obj)
: await this.getUserId(obj);
if (!userId) return undefined;
const user = await prisma.user.findFirst({ where: { id: userId } });
return user;
}
async setUserId(h3: H3Event, userId: string, extend = false) { async setUserId(h3: H3Event, userId: string, extend = false) {
const token = const token =
this.getSessionToken(h3) ?? (await this.createSession(h3, extend)); this.getSessionToken(h3) ?? (await this.createSession(h3, extend));
@ -112,13 +107,7 @@ export class SessionHandler {
userId userId
); );
} }
async getAdminUser(h3: H3Event | string) {
const user = await this.getUser(h3);
if (!user) return undefined;
if (!user.admin) return undefined;
return user;
}
} }
export default new SessionHandler(); export const sessionHandler = new SessionHandler();
export default sessionHandler;

View File

@ -1,4 +1,6 @@
import droplet from "@drop/droplet"; import droplet from "@drop/droplet";
import { MinimumRequestObject } from "~/server/h3";
import aclManager from "../acls";
/** /**
* The TaskHandler setups up two-way connections to web clients and manages the state for them * The TaskHandler setups up two-way connections to web clients and manages the state for them
@ -13,7 +15,7 @@ type TaskRegistryEntry = {
error: { title: string; description: string } | undefined; error: { title: string; description: string } | undefined;
clients: { [key: string]: boolean }; clients: { [key: string]: boolean };
name: string; name: string;
requireAdmin: boolean; acls: string[];
}; };
class TaskHandler { class TaskHandler {
@ -84,7 +86,7 @@ class TaskHandler {
error: undefined, error: undefined,
log: [], log: [],
clients: {}, clients: {},
requireAdmin: task.requireAdmin ?? false, acls: task.acls,
}; };
updateAllClients(true); updateAllClients(true);
@ -113,7 +115,12 @@ class TaskHandler {
}); });
} }
connect(id: string, taskId: string, peer: PeerImpl, isAdmin = false) { async connect(
id: string,
taskId: string,
peer: PeerImpl,
request: MinimumRequestObject
) {
const task = this.taskRegistry[taskId]; const task = this.taskRegistry[taskId];
if (!task) { if (!task) {
peer.send( peer.send(
@ -122,8 +129,9 @@ class TaskHandler {
return; return;
} }
if (task.requireAdmin && !isAdmin) { const allowed = await aclManager.hasACL(request, task.acls);
console.warn("user is not an admin, so cannot view this task"); if (!allowed) {
console.warn("user does not have necessary ACLs");
peer.send( peer.send(
`error/${taskId}/Unknown task/Drop couldn't find the task you're looking for.` `error/${taskId}/Unknown task/Drop couldn't find the task you're looking for.`
); );
@ -186,7 +194,7 @@ export interface Task {
id: string; id: string;
name: string; name: string;
run: (context: TaskRunContext) => Promise<void>; run: (context: TaskRunContext) => Promise<void>;
requireAdmin?: boolean; acls: string[];
} }
export type TaskMessage = { export type TaskMessage = {

View File

@ -1,4 +1,5 @@
import { H3Error } from "h3"; import { H3Error } from "h3";
import sessionHandler from "../internal/session";
export default defineNitroPlugin((nitro) => { export default defineNitroPlugin((nitro) => {
nitro.hooks.hook("error", async (error, { event }) => { nitro.hooks.hook("error", async (error, { event }) => {
@ -13,9 +14,8 @@ export default defineNitroPlugin((nitro) => {
switch (error.statusCode) { switch (error.statusCode) {
case 401: case 401:
case 403: case 403:
const userId = await event.context.session.getUserId(event); const userId = await sessionHandler.getUserId(event);
if (userId) break; if (userId) break;
console.log("user is signed out, redirecting");
return sendRedirect( return sendRedirect(
event, event,
`/signin?redirect=${encodeURIComponent(event.path)}` `/signin?redirect=${encodeURIComponent(event.path)}`

View File

@ -1,7 +0,0 @@
import session from "../internal/session";
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('request', (h3) => {
h3.context.session = session;
})
});

View File

@ -1,5 +1,7 @@
import sessionHandler from "../internal/session";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
await h3.context.session.clearSession(h3); await sessionHandler.clearSession(h3);
return sendRedirect(h3, "/signin"); return sendRedirect(h3, "/signin");
}); });