mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-01-31 15:37:09 +01:00
feat: add ability to review and revoke clients
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
);
|
||||
|
||||
17
server/api/v1/user/client/[id]/index.delete.ts
Normal file
17
server/api/v1/user/client/[id]/index.delete.ts
Normal 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);
|
||||
});
|
||||
15
server/api/v1/user/client/index.get.ts
Normal file
15
server/api/v1/user/client/index.get.ts
Normal 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;
|
||||
});
|
||||
@@ -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.",
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ export const userACLs = [
|
||||
"library:add",
|
||||
"library:remove",
|
||||
|
||||
"clients:read",
|
||||
"clients:revoke",
|
||||
|
||||
"news:read",
|
||||
] as const;
|
||||
const userACLPrefix = "user:";
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user