Rearchitecture for v0.4.0 (#197)

* feat: database redist support

* feat: rearchitecture of database schemas, migration reset, and #180

* feat: import redists

* fix: giantbomb logging bug

* feat: partial user platform support + statusMessage -> message

* feat: add user platform filters to store view

* fix: sanitize svg uploads

... copilot suggested this

I feel dirty.

* feat: beginnings of platform & redist management

* feat: add server side redist patching

* fix: update drop-base commit

* feat: import of custom platforms & file extensions

* fix: redelete platform

* fix: remove platform

* feat: uninstall commands, new R UI

* checkpoint: before migrating to nuxt v4

* update to nuxt 4

* fix: fixes for Nuxt v4 update

* fix: remaining type issues

* feat: initial feedback to import other kinds of versions

* working commit

* fix: lint

* feat: redist import
This commit is contained in:
DecDuck
2025-11-10 10:36:13 +11:00
committed by GitHub
parent dfa30c8a65
commit 251ddb8ff8
465 changed files with 8029 additions and 7509 deletions

View File

@ -24,7 +24,7 @@ export class CertificateAuthority {
let ca;
if (root === undefined) {
const [cert, priv] = droplet.generateRootCa();
const bundle: CertificateBundle = { priv, cert };
const bundle: CertificateBundle = { priv: priv!, cert: cert! };
await store.store("ca", bundle);
ca = new CertificateAuthority(store, bundle);
} else {
@ -50,8 +50,8 @@ export class CertificateAuthority {
caCertificate.priv,
);
const certBundle: CertificateBundle = {
priv,
cert,
priv: priv!,
cert: cert!,
};
return certBundle;
}

View File

@ -1,29 +1,19 @@
import type { EnumDictionary } from "../utils/types";
import prisma from "../db/database";
import { ClientCapabilities } from "~/prisma/client/enums";
import { ClientCapabilities } from "~~/prisma/client/enums";
// These values are technically mapped to the database,
// but Typescript/Prisma doesn't let me link them
// They are also what are required by clients in the API
// BREAKING CHANGE
export enum InternalClientCapability {
PeerAPI = "peerAPI",
UserStatus = "userStatus",
CloudSaves = "cloudSaves",
TrackPlaytime = "trackPlaytime",
}
export const validCapabilities = Object.values(InternalClientCapability);
export const validCapabilities = Object.values(ClientCapabilities);
export type CapabilityConfiguration = {
[InternalClientCapability.PeerAPI]: object;
[InternalClientCapability.UserStatus]: object;
[InternalClientCapability.CloudSaves]: object;
[ClientCapabilities.PeerAPI]: object;
[ClientCapabilities.UserStatus]: object;
[ClientCapabilities.CloudSaves]: object;
};
class CapabilityManager {
private validationFunctions: EnumDictionary<
InternalClientCapability,
ClientCapabilities,
(configuration: object) => Promise<boolean>
> = {
/*
@ -77,14 +67,14 @@ class CapabilityManager {
return valid;
},
*/
[InternalClientCapability.PeerAPI]: async () => true,
[InternalClientCapability.UserStatus]: async () => true, // No requirements for user status
[InternalClientCapability.CloudSaves]: async () => true, // No requirements for cloud saves
[InternalClientCapability.TrackPlaytime]: async () => true,
[ClientCapabilities.PeerAPI]: async () => true,
[ClientCapabilities.UserStatus]: async () => true, // No requirements for user status
[ClientCapabilities.CloudSaves]: async () => true, // No requirements for cloud saves
[ClientCapabilities.TrackPlaytime]: async () => true,
};
async validateCapabilityConfiguration(
capability: InternalClientCapability,
capability: ClientCapabilities,
configuration: object,
) {
const validationFunction = this.validationFunctions[capability];
@ -93,15 +83,15 @@ class CapabilityManager {
}
async upsertClientCapability(
capability: InternalClientCapability,
capability: ClientCapabilities,
rawCapabilityConfiguration: object,
clientId: string,
) {
const upsertFunctions: EnumDictionary<
InternalClientCapability,
ClientCapabilities,
() => Promise<void> | void
> = {
[InternalClientCapability.PeerAPI]: async function () {
[ClientCapabilities.PeerAPI]: async function () {
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
const currentClient = await prisma.client.findUnique({
@ -139,10 +129,10 @@ class CapabilityManager {
},
});
},
[InternalClientCapability.UserStatus]: function (): Promise<void> | void {
[ClientCapabilities.UserStatus]: function (): Promise<void> | void {
throw new Error("Function not implemented.");
},
[InternalClientCapability.CloudSaves]: async function () {
[ClientCapabilities.CloudSaves]: async function () {
const currentClient = await prisma.client.findUnique({
where: { id: clientId },
select: {
@ -162,7 +152,7 @@ class CapabilityManager {
},
});
},
[InternalClientCapability.TrackPlaytime]: async function () {
[ClientCapabilities.TrackPlaytime]: async function () {
const currentClient = await prisma.client.findUnique({
where: { id: clientId },
select: {

View File

@ -1,8 +1,8 @@
import type { ClientModel, UserModel } from "~/prisma/client/models";
import type { ClientModel, UserModel } from "~~/prisma/client/models";
import type { EventHandlerRequest, H3Event } from "h3";
import droplet from "@drop-oss/droplet";
import prisma from "../db/database";
import { useCertificateAuthority } from "~/server/plugins/ca";
import { useCertificateAuthority } from "~~/server/plugins/ca";
export type EventHandlerFunction<T> = (
h3: H3Event<EventHandlerRequest>,
@ -23,7 +23,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
if (!header) throw createError({ statusCode: 403 });
const [method, ...parts] = header.split(" ");
let clientId: string;
let clientId: string | undefined;
switch (method) {
case "Debug": {
if (!import.meta.dev) throw createError({ statusCode: 403 });
@ -31,7 +31,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
if (!client)
throw createError({
statusCode: 400,
statusMessage: "No clients created.",
message: "No clients created.",
});
clientId = client.id;
break;
@ -55,7 +55,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
// We reject the request
throw createError({
statusCode: 403,
statusMessage: "Nonce expired",
message: "Nonce expired",
});
}
@ -66,21 +66,21 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
if (!certBundle)
throw createError({
statusCode: 403,
statusMessage: "Invalid client ID",
message: "Invalid client ID",
});
const valid = droplet.verifyNonce(certBundle.cert, nonce, signature);
if (!valid)
throw createError({
statusCode: 403,
statusMessage: "Invalid nonce signature.",
message: "Invalid nonce signature.",
});
break;
}
default: {
throw createError({
statusCode: 403,
statusMessage: "No authentication",
message: "No authentication",
});
}
}
@ -88,12 +88,12 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
if (clientId === undefined)
throw createError({
statusCode: 500,
statusMessage: "Failed to execute authentication pipeline.",
message: "Failed to execute authentication pipeline.",
});
async function fetchClient() {
const client = await prisma.client.findUnique({
where: { id: clientId },
where: { id: clientId! },
});
if (!client)
throw new Error(
@ -104,7 +104,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
async function fetchUser() {
const client = await prisma.client.findUnique({
where: { id: clientId },
where: { id: clientId! },
select: {
user: true,
},

View File

@ -1,23 +1,20 @@
import { randomUUID } from "node:crypto";
import prisma from "../db/database";
import type { Platform } from "~/prisma/client/enums";
import { useCertificateAuthority } from "~/server/plugins/ca";
import type { ClientCapabilities, HardwarePlatform } from "~~/prisma/client/enums";
import { useCertificateAuthority } from "~~/server/plugins/ca";
import type {
CapabilityConfiguration,
InternalClientCapability,
} from "./capabilities";
import capabilityManager from "./capabilities";
import type { PeerImpl } from "../tasks";
import userStatsManager from "~/server/internal/userstats";
import userStatsManager from "~~/server/internal/userstats";
export enum AuthMode {
Callback = "callback",
Code = "code",
}
export const AuthModes = ["callback", "code"] as const;
export type AuthMode = (typeof AuthModes)[number];
export interface ClientMetadata {
name: string;
platform: Platform;
platform: HardwarePlatform;
capabilities: Partial<CapabilityConfiguration>;
mode: AuthMode;
}
@ -63,9 +60,9 @@ export class ClientHandler {
});
switch (metadata.mode) {
case AuthMode.Callback:
case "callback":
return `/client/authorize/${clientId}`;
case AuthMode.Code: {
case "code": {
const code = randomUUID()
.replaceAll(/-/g, "")
.slice(0, 7)
@ -81,15 +78,15 @@ export class ClientHandler {
if (!clientId)
throw createError({
statusCode: 403,
statusMessage: "Invalid or unknown code.",
message: "Invalid or unknown code.",
});
const metadata = this.temporaryClientTable.get(clientId);
if (!metadata)
throw createError({ statusCode: 500, statusMessage: "Broken code." });
throw createError({ statusCode: 500, message: "Broken code." });
if (metadata.peer)
throw createError({
statusCode: 400,
statusMessage: "Pre-existing listener for this code.",
message: "Pre-existing listener for this code.",
});
metadata.peer = peer;
this.temporaryClientTable.set(clientId, metadata);
@ -130,12 +127,12 @@ export class ClientHandler {
if (!client)
throw createError({
statusCode: 500,
statusMessage: "Corrupted code, please restart the process.",
message: "Corrupted code, please restart the process.",
});
if (!client.peer)
throw createError({
statusCode: 400,
statusMessage: "Client has not connected yet. Please try again later.",
message: "Client has not connected yet. Please try again later.",
});
client.peer.send(
JSON.stringify({ type: "token", value: `${clientId}/${token}` }),
@ -173,7 +170,7 @@ export class ClientHandler {
metadata.data.capabilities,
)) {
await capabilityManager.upsertClientCapability(
capability as InternalClientCapability,
capability as ClientCapabilities,
configuration,
client.id,
);