mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
feat: separate library and metadata pages, notification acls
This commit is contained in:
@ -55,6 +55,7 @@ export default defineClientEventHandler(
|
||||
title: `"${client.name}" can now access ${capability}`,
|
||||
description: `A device called "${client.name}" now has access to your ${capability}.`,
|
||||
actions: ["Review|/account/devices"],
|
||||
acls: ["user:clients:read"]
|
||||
});
|
||||
|
||||
return {};
|
||||
|
||||
@ -5,17 +5,19 @@ export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const userIds = [userId];
|
||||
const hasSystemPerms = await aclManager.allowSystemACL(h3, [
|
||||
"notifications:mark",
|
||||
]);
|
||||
if (hasSystemPerms) {
|
||||
userIds.push("system");
|
||||
}
|
||||
const acls = await aclManager.fetchAllACLs(h3);
|
||||
if (!acls)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Got userId but no ACLs - what?",
|
||||
});
|
||||
|
||||
const notifications = await prisma.notification.findMany({
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
userId,
|
||||
acls: {
|
||||
hasSome: acls,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
created: "desc", // Newest first
|
||||
|
||||
@ -5,17 +5,19 @@ export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const userIds = [userId];
|
||||
const hasSystemPerms = await aclManager.allowSystemACL(h3, [
|
||||
"notifications:mark",
|
||||
]);
|
||||
if (hasSystemPerms) {
|
||||
userIds.push("system");
|
||||
}
|
||||
const acls = await aclManager.fetchAllACLs(h3);
|
||||
if (!acls)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Got userId but no ACLs - what?",
|
||||
});
|
||||
|
||||
await prisma.notification.updateMany({
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
userId,
|
||||
acls: {
|
||||
hasSome: acls,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
read: true,
|
||||
|
||||
@ -14,22 +14,17 @@ export default defineWebSocketHandler({
|
||||
return;
|
||||
}
|
||||
|
||||
const userIds = [userId];
|
||||
|
||||
const hasSystemPerms = await aclManager.allowSystemACL(h3, [
|
||||
"notifications:listen",
|
||||
]);
|
||||
if (hasSystemPerms) {
|
||||
userIds.push("system");
|
||||
const acls = await aclManager.fetchAllACLs(h3);
|
||||
if (!acls) {
|
||||
peer.send("unauthenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
socketSessions.set(peer.id, userId);
|
||||
|
||||
for (const listenUserId of userIds) {
|
||||
notificationSystem.listen(listenUserId, peer.id, (notification) => {
|
||||
peer.send(JSON.stringify(notification));
|
||||
});
|
||||
}
|
||||
notificationSystem.listen(userId, acls, peer.id, (notification) => {
|
||||
peer.send(JSON.stringify(notification));
|
||||
});
|
||||
},
|
||||
async close(peer, _details) {
|
||||
const userId = socketSessions.get(peer.id);
|
||||
|
||||
@ -70,6 +70,8 @@ const systemACLPrefix = "system:";
|
||||
|
||||
export type SystemACL = Array<(typeof systemACLs)[number]>;
|
||||
|
||||
export type GlobalACL = `${typeof systemACLPrefix}${(typeof systemACLs)[number]}` | `${typeof userACLPrefix}${(typeof userACLs)[number]}`;
|
||||
|
||||
class ACLManager {
|
||||
private getAuthorizationToken(request: MinimumRequestObject) {
|
||||
const [type, token] =
|
||||
@ -173,6 +175,36 @@ class ACLManager {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchAllACLs(request: MinimumRequestObject): Promise<GlobalACL[] | undefined> {
|
||||
const userSession = await sessionHandler.getSession(request);
|
||||
if (!userSession) {
|
||||
const authorizationToken = this.getAuthorizationToken(request);
|
||||
if (!authorizationToken) return undefined;
|
||||
const token = await prisma.aPIToken.findUnique({
|
||||
where: { token: authorizationToken },
|
||||
});
|
||||
if (!token) return undefined;
|
||||
return token.acls as GlobalACL[];
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userSession.userId },
|
||||
select: {
|
||||
admin: true,
|
||||
},
|
||||
});
|
||||
if (!user)
|
||||
throw new Error("User session without user - did something break?");
|
||||
|
||||
const acls = userACLs.map((e) => `${userACLPrefix}${e}`);
|
||||
|
||||
if (user.admin) {
|
||||
acls.push(...systemACLs.map((e) => `${systemACLPrefix}${e}`));
|
||||
}
|
||||
|
||||
return acls as GlobalACL[];
|
||||
}
|
||||
}
|
||||
|
||||
export const aclManager = new ACLManager();
|
||||
|
||||
@ -305,6 +305,7 @@ class LibraryManager {
|
||||
title: `'${game.mName}' ('${versionName}') finished importing.`,
|
||||
description: `Drop finished importing version ${versionName} for ${game.mName}.`,
|
||||
actions: [`View|/admin/library/${gameId}`],
|
||||
acls: ["system:import:version:read"]
|
||||
});
|
||||
|
||||
progress(100);
|
||||
|
||||
@ -8,25 +8,32 @@ Design goals:
|
||||
|
||||
import type { Notification } from "~/prisma/client";
|
||||
import prisma from "../db/database";
|
||||
import type { GlobalACL } from "../acls";
|
||||
|
||||
export type NotificationCreateArgs = Pick<
|
||||
Notification,
|
||||
"title" | "description" | "actions" | "nonce"
|
||||
>;
|
||||
> & { acls: Array<GlobalACL> };
|
||||
|
||||
class NotificationSystem {
|
||||
// userId to acl to listenerId
|
||||
private listeners = new Map<
|
||||
string,
|
||||
Map<string, (notification: Notification) => void>
|
||||
Map<
|
||||
string,
|
||||
{ callback: (notification: Notification) => void; acls: GlobalACL[] }
|
||||
>
|
||||
>();
|
||||
|
||||
listen(
|
||||
userId: string,
|
||||
acls: Array<GlobalACL>,
|
||||
id: string,
|
||||
callback: (notification: Notification) => void,
|
||||
) {
|
||||
this.listeners.set(userId, new Map());
|
||||
this.listeners.get(userId)?.set(id, callback);
|
||||
if (!this.listeners.has(userId)) this.listeners.set(userId, new Map());
|
||||
// eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
|
||||
this.listeners.get(userId)!!.set(id, { callback, acls });
|
||||
|
||||
this.catchupListener(userId, id);
|
||||
}
|
||||
@ -36,23 +43,23 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
private async catchupListener(userId: string, id: string) {
|
||||
const callback = this.listeners.get(userId)?.get(id);
|
||||
if (!callback)
|
||||
const listener = this.listeners.get(userId)?.get(id);
|
||||
if (!listener)
|
||||
throw new Error("Failed to catch-up listener: callback does not exist");
|
||||
const notifications = await prisma.notification.findMany({
|
||||
where: { userId: userId },
|
||||
where: { userId: userId, acls: { hasSome: listener.acls } },
|
||||
orderBy: {
|
||||
created: "asc", // Oldest first, because they arrive in reverse order
|
||||
},
|
||||
});
|
||||
for (const notification of notifications) {
|
||||
await callback(notification);
|
||||
await listener.callback(notification);
|
||||
}
|
||||
}
|
||||
|
||||
private async pushNotification(userId: string, notification: Notification) {
|
||||
for (const listener of this.listeners.get(userId) ?? []) {
|
||||
await listener[1](notification);
|
||||
await listener[1].callback(notification);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +97,7 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
async systemPush(notificationCreateArgs: NotificationCreateArgs) {
|
||||
return await this.push("system", notificationCreateArgs);
|
||||
return await this.pushAll(notificationCreateArgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user