Merge remote-tracking branch 'origin/develop' into more-fixes

This commit is contained in:
Huskydog9988
2025-05-15 13:38:46 -04:00
22 changed files with 331 additions and 161 deletions
+35 -1
View File
@@ -70,7 +70,9 @@ const systemACLPrefix = "system:";
export type SystemACL = Array<(typeof systemACLs)[number]>;
export type ValidACLItems = Array<SystemACL[number] | UserACL[number]>;
export type GlobalACL =
| `${typeof systemACLPrefix}${(typeof systemACLs)[number]}`
| `${typeof userACLPrefix}${(typeof userACLs)[number]}`;
class ACLManager {
private getAuthorizationToken(request: MinimumRequestObject) {
@@ -175,6 +177,38 @@ 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();
+8 -7
View File
@@ -1,6 +1,4 @@
import type { EnumDictionary } from "../utils/types";
import https from "https";
import { useCertificateAuthority } from "~/server/plugins/ca";
import prisma from "../db/database";
import { ClientCapabilities } from "~/prisma/client";
@@ -17,7 +15,7 @@ export enum InternalClientCapability {
export const validCapabilities = Object.values(InternalClientCapability);
export type CapabilityConfiguration = {
[InternalClientCapability.PeerAPI]: { endpoints: string[] };
[InternalClientCapability.PeerAPI]: object;
[InternalClientCapability.UserStatus]: object;
[InternalClientCapability.CloudSaves]: object;
};
@@ -27,6 +25,7 @@ class CapabilityManager {
InternalClientCapability,
(configuration: object) => Promise<boolean>
> = {
/*
[InternalClientCapability.PeerAPI]: async (rawConfiguration) => {
const configuration =
rawConfiguration as CapabilityConfiguration[InternalClientCapability.PeerAPI];
@@ -71,12 +70,13 @@ class CapabilityManager {
valid = true;
break;
} catch {
/* empty */
}
}
return valid;
},
*/
[InternalClientCapability.PeerAPI]: async () => true,
[InternalClientCapability.UserStatus]: async () => true, // No requirements for user status
[InternalClientCapability.CloudSaves]: async () => true, // No requirements for cloud saves
};
@@ -92,7 +92,7 @@ class CapabilityManager {
async upsertClientCapability(
capability: InternalClientCapability,
rawCapability: object,
rawCapabilityConfiguration: object,
clientId: string,
) {
const upsertFunctions: EnumDictionary<
@@ -100,8 +100,7 @@ class CapabilityManager {
() => Promise<void> | void
> = {
[InternalClientCapability.PeerAPI]: async function () {
const configuration =
rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
const currentClient = await prisma.client.findUnique({
where: { id: clientId },
@@ -110,6 +109,7 @@ class CapabilityManager {
},
});
if (!currentClient) throw new Error("Invalid client ID");
/*
if (currentClient.capabilities.includes(ClientCapabilities.PeerAPI)) {
await prisma.clientPeerAPIConfiguration.update({
where: { clientId },
@@ -126,6 +126,7 @@ class CapabilityManager {
endpoints: configuration.endpoints,
},
});
*/
await prisma.client.update({
where: { id: clientId },
+18 -1
View File
@@ -2,10 +2,13 @@ import { randomUUID } from "node:crypto";
import prisma from "../db/database";
import type { Platform } from "~/prisma/client";
import { useCertificateAuthority } from "~/server/plugins/ca";
import type { CapabilityConfiguration, InternalClientCapability } from "./capabilities";
import capabilityManager from "./capabilities";
export interface ClientMetadata {
name: string;
platform: Platform;
capabilities: Partial<CapabilityConfiguration>;
}
export class ClientHandler {
@@ -75,7 +78,7 @@ export class ClientHandler {
if (!metadata) throw new Error("Invalid client ID");
if (!metadata.userId) throw new Error("Un-authorized client ID");
return await prisma.client.create({
const client = await prisma.client.create({
data: {
id: id,
userId: metadata.userId,
@@ -87,6 +90,20 @@ export class ClientHandler {
lastConnected: new Date(),
},
});
for (const [capability, configuration] of Object.entries(
metadata.data.capabilities,
)) {
await capabilityManager.upsertClientCapability(
capability as InternalClientCapability,
configuration,
client.id,
);
}
this.temporaryClientTable.delete(id);
return client;
}
async removeClient(id: string) {
+1 -1
View File
@@ -306,7 +306,7 @@ class LibraryManager {
title: `'${game.mName}' ('${versionName}') finished importing.`,
description: `Drop finished importing version ${versionName} for ${game.mName}.`,
actions: [`View|/admin/library/${gameId}`],
requiredPerms: ["import:game:new"],
acls: ["system:import:version:read"],
});
progress(100);
+23 -30
View File
@@ -8,28 +8,35 @@ Design goals:
import type { Notification } from "~/prisma/client";
import prisma from "../db/database";
import type { GlobalACL } from "../acls";
// type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
// TODO: document notification action format
export type NotificationCreateArgs = Pick<
Notification,
"title" | "description" | "actions" | "nonce" | "requiredPerms"
>;
"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);
}
@@ -39,23 +46,27 @@ 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);
for (const [_, listener] of this.listeners.get(userId) ?? []) {
const hasSome =
notification.acls.findIndex(
(e) => listener.acls.findIndex((v) => v === e) != -1,
) != -1;
if (hasSome) await listener.callback(notification);
}
}
@@ -100,25 +111,7 @@ class NotificationSystem {
}
async systemPush(notificationCreateArgs: NotificationCreateArgs) {
await this.push("system", notificationCreateArgs);
}
async pushAllAdmins(notificationCreateArgs: NotificationCreateArgs) {
const users = await prisma.user.findMany({
where: {
admin: true,
},
select: {
id: true,
},
});
const res: Promise<void>[] = [];
for (const user of users) {
res.push(this.push(user.id, notificationCreateArgs));
}
// wait for all notifications to pass
await Promise.all(res);
return await this.pushAll(notificationCreateArgs);
}
}
+15 -3
View File
@@ -1,5 +1,6 @@
import { randomUUID } from "crypto";
import prisma from "../db/database";
import type { User } from "~/prisma/client";
import { AuthMec } from "~/prisma/client";
import objectHandler from "../objects";
import type { Readable } from "stream";
@@ -12,10 +13,15 @@ interface OIDCWellKnown {
scopes_supported: string[];
}
interface OIDCAuthSessionOptions {
redirect: string | undefined;
}
interface OIDCAuthSession {
redirectUrl: string;
callbackUrl: string;
state: string;
options: OIDCAuthSessionOptions;
}
interface OIDCUserInfo {
@@ -132,7 +138,7 @@ export class OIDCManager {
};
}
generateAuthSession(): OIDCAuthSession {
generateAuthSession(options?: OIDCAuthSessionOptions): OIDCAuthSession {
const stateKey = randomUUID();
const normalisedUrl = new URL(
@@ -148,12 +154,16 @@ export class OIDCManager {
redirectUrl: finalUrl,
callbackUrl: redirectUrl,
state: stateKey,
options: options ?? { redirect: undefined },
};
this.signinStateTable[stateKey] = session;
return session;
}
async authorize(code: string, state: string) {
async authorize(
code: string,
state: string,
): Promise<{ user: User; options: OIDCAuthSessionOptions } | string> {
const session = this.signinStateTable[state];
if (!session) return "Invalid state parameter";
@@ -191,7 +201,9 @@ export class OIDCManager {
const user = await this.fetchOrCreateUser(userinfo);
return user;
if (typeof user === "string") return user;
return { user, options: session.options };
} catch (e) {
console.error(e);
return `Request to identity provider failed: ${e}`;