fix: remaining type issues

This commit is contained in:
DecDuck
2025-09-25 12:13:07 +10:00
parent 55878bdf5f
commit 4c9a2c681a
21 changed files with 120 additions and 136 deletions

View File

@ -0,0 +1 @@
<template></template>

View File

@ -163,9 +163,9 @@ const scheduledTasks: {
name: "",
description: "",
},
debug: {
name: "Debug Task",
description: "Does debugging things.",
"import:version": {
name: "",
description: "",
},
};

View File

@ -224,14 +224,14 @@ const scopes = [
href: "/docs/access/status",
icon: UserGroupIcon,
},
clientData.capabilities["peerAPI"] && {
clientData.capabilities["PeerAPI"] && {
name: "Access the Drop network",
description:
"The client will be able to establish P2P connections with other users to enable features like download aggregation, Remote LAN play and P2P multiplayer.",
href: "/docs/access/network",
icon: LockClosedIcon,
},
clientData.capabilities["cloudSaves"] && {
clientData.capabilities["CloudSaves"] && {
name: "Upload and sync cloud saves",
description:
"The client will be able to upload new cloud saves, and edit your existing ones.",

View File

@ -105,14 +105,14 @@ function input(index: number) {
function select(index: number) {
if (!codeElements.value) return;
if (index >= codeElements.value.length) return;
codeElements.value[index].select();
codeElements.value[index]!.select();
}
function paste(index: number, event: ClipboardEvent) {
const newCode = event.clipboardData!.getData("text/plain");
for (let i = 0; i < newCode.length && i < codeLength; i++) {
code.value[i] = newCode[i];
codeElements.value![i].focus();
code.value[i] = newCode[i]!;
codeElements.value![i]!.focus();
}
event.preventDefault();
}

View File

@ -110,7 +110,7 @@
>
<div>
<component
:is="actions[currentAction].page"
:is="actions[currentAction]!.page"
v-model="actionsComplete[currentAction]"
:token="bearerToken"
/>

View File

@ -254,7 +254,13 @@ import { StarIcon } from "@heroicons/vue/24/solid";
import { micromark } from "micromark";
const route = useRoute();
const gameId = route.params.id.toString();
const gameId = route.params.id?.toString();
if (!gameId)
throw createError({
statusCode: 404,
message: "Game not found",
fatal: true,
});
const user = useUser();

View File

@ -163,9 +163,11 @@ export default defineNuxtConfig({
tsConfig: {
compilerOptions: {
// Not having these options on is sloppy, but it's a task for later me
verbatimModuleSyntax: false,
strictNullChecks: true,
exactOptionalPropertyTypes: true,
exactOptionalPropertyTypes: false,
//erasableSyntaxOnly: true,
noUncheckedIndexedAccess: false,
},
},

View File

@ -42,10 +42,10 @@ export default defineEventHandler<{
await objectHandler.deleteAsSystem(imageId);
if (game.mBannerObjectId === imageId) {
game.mBannerObjectId = game.mImageLibraryObjectIds[0];
game.mBannerObjectId = game.mImageLibraryObjectIds[0] ?? "";
}
if (game.mCoverObjectId === imageId) {
game.mCoverObjectId = game.mImageLibraryObjectIds[0];
game.mCoverObjectId = game.mImageLibraryObjectIds[0] ?? "";
}
const result = await prisma.game.update({

View File

@ -2,11 +2,10 @@ import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import aclManager from "~~/server/internal/acls";
import taskHandler from "~~/server/internal/tasks";
import type { TaskGroup } from "~~/server/internal/tasks/group";
import { taskGroups } from "~~/server/internal/tasks/group";
import { TASK_GROUPS, type TaskGroup } from "~~/server/internal/tasks/group";
const StartTask = type({
taskGroup: type("string"),
taskGroup: type.enumerated(...TASK_GROUPS),
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
@ -14,14 +13,8 @@ export default defineEventHandler(async (h3) => {
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, StartTask);
const taskGroup = body.taskGroup as TaskGroup;
if (!taskGroups[taskGroup])
throw createError({
statusCode: 400,
message: "Invalid task group.",
});
const task = await taskHandler.runTaskGroupByName(taskGroup);
const task = await taskHandler.runTaskGroupByName(body.taskGroup);
if (!task)
throw createError({
statusCode: 500,

View File

@ -1,20 +1,20 @@
import { type } from "arktype";
import { ClientCapabilities } from "~~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import type {
CapabilityConfiguration,
InternalClientCapability,
} from "~~/server/internal/clients/capabilities";
import capabilityManager, {
validCapabilities,
} from "~~/server/internal/clients/capabilities";
import clientHandler, { AuthMode } from "~~/server/internal/clients/handler";
import clientHandler, { AuthMode, AuthModes } from "~~/server/internal/clients/handler";
import { parsePlatform } from "~~/server/internal/utils/parseplatform";
const ClientAuthInitiate = type({
name: "string",
platform: "string",
capabilities: "object",
mode: type.valueOf(AuthMode).default(AuthMode.Callback),
mode: type.enumerated(...AuthModes).default("callback"),
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
@ -32,7 +32,7 @@ export default defineEventHandler(async (h3) => {
});
const capabilityIterable = Object.entries(capabilities) as Array<
[InternalClientCapability, object]
[ClientCapabilities, object]
>;
if (
capabilityIterable.length > 0 &&

View File

@ -1,39 +1,24 @@
import type { InternalClientCapability } from "~~/server/internal/clients/capabilities";
import { type } from "arktype";
import { ClientCapabilities } from "~~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import capabilityManager, {
validCapabilities,
} from "~~/server/internal/clients/capabilities";
import { defineClientEventHandler } from "~~/server/internal/clients/event-handler";
import notificationSystem from "~~/server/internal/notifications";
const SetCapability = type({
capability: type.enumerated(...Object.values(ClientCapabilities)),
configuration: "object"
}).configure(throwingArktype);
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,
message: "capability must be a string",
});
if (!configuration || typeof configuration !== "object")
throw createError({
statusCode: 400,
message: "configuration must be an object",
});
const capability = rawCapability as InternalClientCapability;
if (!validCapabilities.includes(capability))
throw createError({
statusCode: 400,
message: "Invalid capability.",
});
const body = await readDropValidatedBody(h3, SetCapability);
const isValid = await capabilityManager.validateCapabilityConfiguration(
capability,
configuration,
body.capability,
body.configuration,
);
if (!isValid)
throw createError({
@ -42,8 +27,8 @@ export default defineClientEventHandler(
});
await capabilityManager.upsertClientCapability(
capability,
configuration,
body.capability,
body.configuration,
clientId,
);
@ -51,9 +36,9 @@ export default defineClientEventHandler(
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}.`,
nonce: `capability-${clientId}-${body.capability}`,
title: `"${client.name}" can now access ${body.capability}`,
description: `A device called "${client.name}" now has access to your ${body.capability}.`,
actions: ["Review|/account/devices"],
acls: ["user:clients:read"],
});

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

@ -2,28 +2,18 @@ import type { EnumDictionary } from "../utils/types";
import prisma from "../db/database";
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,18 +1,15 @@
import { randomUUID } from "node:crypto";
import prisma from "../db/database";
import type { HardwarePlatform } from "~~/prisma/client/enums";
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";
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;
@ -62,9 +59,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)
@ -171,7 +168,7 @@ export class ClientHandler {
metadata.data.capabilities,
)) {
await capabilityManager.upsertClientCapability(
capability as InternalClientCapability,
capability as ClientCapabilities,
configuration,
client.id,
);

View File

@ -200,7 +200,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
return url.pathname.replace("/games/", "").replace(/\/$/, "");
}
default: {
logger.warn("Pcgamingwiki, unknown host", url.hostname);
logger.warn("Pcgamingwiki, unknown host: %s", url.hostname);
return undefined;
}
}
@ -234,7 +234,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
});
if (ratingObj instanceof type.errors) {
logger.info(
"pcgamingwiki: failed to properly get review rating",
"pcgamingwiki: failed to properly get review rating: %s",
ratingObj.summary,
);
return undefined;

View File

@ -123,7 +123,7 @@ export class FsObjectBackend extends ObjectBackend {
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
const metadata = objectMetadata(metadataRaw);
if (metadata instanceof type.errors) {
logger.error("FsObjectBackend#fetchMetadata", metadata.summary);
logger.error("FsObjectBackend#fetchMetadata: %s", metadata.summary);
return undefined;
}
await this.metadataCache.set(id, metadata);
@ -194,11 +194,13 @@ export class FsObjectBackend extends ObjectBackend {
try {
fs.rmSync(filePath);
cleanupLogger.info(
`[FsObjectBackend#cleanupMetadata]: Removed ${file}`,
`[FsObjectBackend#cleanupMetadata]: Removed %s`,
file
);
} catch (error) {
cleanupLogger.error(
`[FsObjectBackend#cleanupMetadata]: Failed to remove ${file}`,
`[FsObjectBackend#cleanupMetadata]: Failed to remove %s: %s`,
file,
error,
);
}

View File

@ -32,15 +32,12 @@ export const objectMetadata = type({
});
export type ObjectMetadata = typeof objectMetadata.infer;
export enum ObjectPermission {
Read = "read",
Write = "write",
Delete = "delete",
}
export const ObjectPermissions = ["read", "write", "delete"] as const;
export type ObjectPermission = (typeof ObjectPermissions)[number];
export const ObjectPermissionPriority: Array<ObjectPermission> = [
ObjectPermission.Read,
ObjectPermission.Write,
ObjectPermission.Delete,
"read",
"write",
"delete",
];
export type Object = { mime: string; data: Source };

View File

@ -1,22 +1,32 @@
export const taskGroups = {
"cleanup:invitations": {
concurrency: false,
},
"cleanup:objects": {
concurrency: false,
},
"cleanup:sessions": {
concurrency: false,
},
"check:update": {
concurrency: false,
},
"import:game": {
concurrency: true,
},
debug: {
concurrency: true,
},
} as const;
export const TASK_GROUPS = [
"cleanup:invitations",
"cleanup:objects",
"cleanup:sessions",
"check:update",
"import:game",
"import:version",
] as const;
export type TaskGroup = keyof typeof taskGroups;
export type TaskGroup = (typeof TASK_GROUPS)[number];
export const TASK_GROUP_CONFIG: { [key in TaskGroup]: { concurrency: boolean } } =
{
"cleanup:invitations": {
concurrency: false
},
"cleanup:objects": {
concurrency: false
},
"cleanup:sessions": {
concurrency: false
},
"check:update": {
concurrency: false
},
"import:game": {
concurrency: true
},
"import:version": {
concurrency: true
}
};

View File

@ -7,7 +7,7 @@ import cleanupInvites from "./registry/invitations";
import cleanupSessions from "./registry/sessions";
import checkUpdate from "./registry/update";
import cleanupObjects from "./registry/objects";
import { taskGroups, type TaskGroup } from "./group";
import { TASK_GROUP_CONFIG, type TaskGroup } from "./group";
import prisma from "../db/database";
import { type } from "arktype";
import pino from "pino";
@ -54,7 +54,6 @@ class TaskHandler {
"cleanup:invitations",
"cleanup:sessions",
"check:update",
"debug",
];
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
@ -83,7 +82,7 @@ class TaskHandler {
let logOffset: number = 0;
// if taskgroup disallows concurrency
if (!taskGroups[task.taskGroup].concurrency) {
if (!TASK_GROUP_CONFIG[task.taskGroup].concurrency) {
for (const existingTask of this.taskPool.values()) {
// if a task is already running, we don't want to start another
if (existingTask.taskGroup === task.taskGroup) {
@ -150,7 +149,7 @@ class TaskHandler {
}
} catch (e) {
// fallback: ignore or log error
logger.error("Failed to parse log chunk", {
logger.error("Failed to parse log chunk %s", {
error: e,
chunk: chunk,
});
@ -178,7 +177,7 @@ class TaskHandler {
const progress = (progress: number) => {
if (progress < 0 || progress > 100) {
logger.error("Progress must be between 0 and 100", { progress });
logger.error("Progress must be between 0 and 100, actually %d", progress);
return;
}
const taskEntry = this.taskPool.get(task.id);

View File

@ -1,5 +1,6 @@
import { defineDropTask } from "..";
/*
export default defineDropTask({
buildId: () => `debug:${new Date().toISOString()}`,
name: "Debug Task",
@ -16,3 +17,4 @@ export default defineDropTask({
}
},
});
*/

View File

@ -49,7 +49,7 @@ export default defineDropTask({
// if response failed somehow
if (!response.ok) {
logger.info("Failed to check for update ", {
logger.info("Failed to check for update: %s", {
status: response.status,
body: response.body,
});