feat: add ability to review and revoke clients

This commit is contained in:
DecDuck
2025-04-05 17:42:32 +11:00
parent 7263ec53ac
commit 2cbee3d495
14 changed files with 248 additions and 54 deletions

View File

@@ -27,7 +27,8 @@ export default defineEventHandler(async (h3) => {
take: parseInt(query.limit as string),
skip: parseInt(query.skip as string),
orderBy: orderBy,
...(tags && { tags: tags.map((e) => e.toString()) }),
...(tags && { tags: tags
.map((e) => e.toString()) }),
search: query.search as string,
};

View File

@@ -3,47 +3,60 @@ import capabilityManager, {
validCapabilities,
} from "~/server/internal/clients/capabilities";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import notificationSystem from "~/server/internal/notifications";
export default defineClientEventHandler(async (h3, { clientId }) => {
const body = await readBody(h3);
const rawCapability = body.capability;
const configuration = body.configuration;
export default defineClientEventHandler(
async (h3, { clientId, fetchClient, fetchUser }) => {
const body = await readBody(h3);
const rawCapability = body.capability;
const configuration = body.configuration;
if (!rawCapability || typeof rawCapability !== "string")
throw createError({
statusCode: 400,
statusMessage: "capability must be a string",
if (!rawCapability || typeof rawCapability !== "string")
throw createError({
statusCode: 400,
statusMessage: "capability must be a string",
});
if (!configuration || typeof configuration !== "object")
throw createError({
statusCode: 400,
statusMessage: "configuration must be an object",
});
const capability = rawCapability as InternalClientCapability;
if (!validCapabilities.includes(capability))
throw createError({
statusCode: 400,
statusMessage: "Invalid capability.",
});
const isValid = await capabilityManager.validateCapabilityConfiguration(
capability,
configuration
);
if (!isValid)
throw createError({
statusCode: 400,
statusMessage: "Invalid capability configuration.",
});
await capabilityManager.upsertClientCapability(
capability,
configuration,
clientId
);
const client = await fetchClient();
const user = await fetchUser();
await notificationSystem.push(user.id, {
nonce: `capability-${clientId}-${capability}`,
title: `"${client.name}" can now access ${capability}`,
description: `A device called "${client.name}" now has access to your ${capability}.`,
actions: ["Review|/account/devices"],
});
if (!configuration || typeof configuration !== "object")
throw createError({
statusCode: 400,
statusMessage: "configuration must be an object",
});
const capability = rawCapability as InternalClientCapability;
if (!validCapabilities.includes(capability))
throw createError({
statusCode: 400,
statusMessage: "Invalid capability.",
});
const isValid = await capabilityManager.validateCapabilityConfiguration(
capability,
configuration
);
if (!isValid)
throw createError({
statusCode: 400,
statusMessage: "Invalid capability configuration.",
});
await capabilityManager.upsertClientCapability(
capability,
configuration,
clientId
);
return {};
});
return {};
}
);

View File

@@ -0,0 +1,17 @@
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]);
if (!userId) throw createError({ statusCode: 403 });
const clientId = getRouterParam(h3, "id");
if (!clientId)
throw createError({
statusCode: 400,
statusMessage: "Client ID missing in route params",
});
await clientHandler.removeClient(clientId);
});

View File

@@ -0,0 +1,15 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["clients:read"]);
if (!userId) throw createError({ statusCode: 403 });
const clients = await prisma.client.findMany({
where: {
userId,
},
});
return clients;
});

View File

@@ -31,6 +31,9 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
"library:add": "Add a game to your library.",
"library:remove": "Remove a game from your library.",
"clients:read": "Read the clients connected to this account",
"clients:revoke": "",
"news:read": "Read the server's news articles.",
};

View File

@@ -26,6 +26,9 @@ export const userACLs = [
"library:add",
"library:remove",
"clients:read",
"clients:revoke",
"news:read",
] as const;
const userACLPrefix = "user:";

View File

@@ -64,13 +64,13 @@ export class CertificateAuthority {
async fetchClientCertificate(clientId: string) {
const isBlacklist = await this.certificateStore.checkBlacklistCertificate(
clientId
`client:${clientId}`
);
if (isBlacklist) return undefined;
return await this.certificateStore.fetch(`client:${clientId}`);
}
async blacklistClient(clientId: string) {
await this.certificateStore.blacklistCertificate(clientId);
await this.certificateStore.blacklistCertificate(`client:${clientId}`);
}
}
}

View File

@@ -3,6 +3,7 @@ import { EventHandlerRequest, H3Event } from "h3";
import droplet from "@drop-oss/droplet";
import prisma from "../db/database";
import { useCertificateAuthority } from "~/server/plugins/ca";
import moment from "moment";
export type EventHandlerFunction<T> = (
h3: H3Event<EventHandlerRequest>,
@@ -122,7 +123,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
fetchUser,
};
prisma.client.update({
await prisma.client.update({
where: { id: clientId },
data: { lastConnected: new Date() },
});

View File

@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
import { CertificateBundle } from "./ca";
import prisma from "../db/database";
import { Platform } from "@prisma/client";
import { useCertificateAuthority } from "~/server/plugins/ca";
export interface ClientMetadata {
name: string;
@@ -82,6 +83,17 @@ export class ClientHandler {
},
});
}
async removeClient(id: string) {
const ca = useCertificateAuthority();
await ca.blacklistClient(id);
await prisma.client.delete({
where: {
id,
},
});
}
}
export const clientHandler = new ClientHandler();