In-app store, torrential backend, locales (#332)
* feat: add store nav and fixes * fix: reduce password requirement & new task error ui * fix: client webtoken fix * fix: delta versions and dockerfile * fix: use setup platforms for filter & display * fix: setup not accounted when returning valid options * feat: tighter delta version support * feat: dl/disk size * feat: offload manifest generation to torrential * fix: bump torrential * feat: remove droplet * feat: bump torrential * feat: convert locales
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
import { ArkErrors, type } from "arktype";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import type { H3Event } from "h3";
|
||||
import { castManifest } from "~/server/internal/library/manifest";
|
||||
|
||||
const AUTHORIZATION_HEADER_PREFIX = "Bearer ";
|
||||
|
||||
const Query = type({
|
||||
version: "string",
|
||||
});
|
||||
|
||||
export async function depotAuthorization(h3: H3Event) {
|
||||
const authorization = getHeader(h3, "Authorization");
|
||||
if (!authorization) throw createError({ statusCode: 403 });
|
||||
|
||||
if (!authorization.startsWith(AUTHORIZATION_HEADER_PREFIX))
|
||||
throw createError({ statusCode: 403 });
|
||||
const key = authorization.slice(AUTHORIZATION_HEADER_PREFIX.length);
|
||||
|
||||
const depot = await prisma.depot.findFirst({ where: { key } });
|
||||
if (!depot) throw createError({ statusCode: 403 });
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
await depotAuthorization(h3);
|
||||
|
||||
const query = Query(getQuery(h3));
|
||||
if (query instanceof ArkErrors)
|
||||
throw createError({ statusCode: 400, message: query.summary });
|
||||
|
||||
const version = await prisma.gameVersion.findUnique({
|
||||
where: {
|
||||
versionId: query.version,
|
||||
},
|
||||
select: {
|
||||
dropletManifest: true,
|
||||
versionPath: true,
|
||||
game: {
|
||||
select: {
|
||||
library: true,
|
||||
libraryPath: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!version)
|
||||
throw createError({ statusCode: 404, message: "Game version not found" });
|
||||
|
||||
return {
|
||||
manifest: castManifest(version.dropletManifest),
|
||||
library: version.game.library,
|
||||
libraryPath: version.game.libraryPath,
|
||||
versionPath: version.versionPath,
|
||||
};
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { depotAuthorization } from "./manifest.get";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
await depotAuthorization(h3);
|
||||
|
||||
const games = await prisma.game.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
versions: {
|
||||
select: {
|
||||
versionId: true,
|
||||
},
|
||||
where: {
|
||||
versionPath: {
|
||||
not: null
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return games;
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { GameVersion, Prisma } from "~/prisma/client/client";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import gameSizeManager from "~/server/internal/gamesize";
|
||||
import type { UnimportedVersionInformation } from "~/server/internal/library";
|
||||
import libraryManager from "~/server/internal/library";
|
||||
|
||||
async function getGameVersionSize<
|
||||
T extends Omit<GameVersion, "dropletManifest">,
|
||||
>(gameId: string, version: T) {
|
||||
const size = await libraryManager.getGameVersionSize(
|
||||
gameId,
|
||||
version.versionId,
|
||||
);
|
||||
return { ...version, size };
|
||||
const clientSize = await gameSizeManager.getVersionSize(version.versionId);
|
||||
const diskSize = await gameSizeManager.getVersionDiskSize(version.versionId);
|
||||
return { ...version, diskSize, clientSize };
|
||||
}
|
||||
|
||||
export type AdminFetchGameType = Prisma.GameGetPayload<{
|
||||
|
||||
@@ -10,18 +10,10 @@ export default defineEventHandler(async (h3) => {
|
||||
|
||||
const sources = await libraryManager.fetchLibraries();
|
||||
const userStats = await userStatsManager.getUserStats();
|
||||
|
||||
const biggestGamesCombined =
|
||||
await libraryManager.getBiggestGamesCombinedVersions(5);
|
||||
const biggestGamesLatest =
|
||||
await libraryManager.getBiggestGamesLatestVersions(5);
|
||||
|
||||
return {
|
||||
gameCount: await prisma.game.count(),
|
||||
version: systemConfig.getDropVersion(),
|
||||
userStats,
|
||||
sources,
|
||||
biggestGamesLatest,
|
||||
biggestGamesCombined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -50,7 +50,12 @@ export default defineEventHandler(async (h3) => {
|
||||
where: {
|
||||
gameId: body.id,
|
||||
delta: false,
|
||||
launches: { some: { platform: platformObject.platform } },
|
||||
OR: [
|
||||
{ launches: { some: { platform: platformObject.platform } } },
|
||||
{
|
||||
setups: { some: { platform: platformObject.platform } },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (validOverlayVersions == 0)
|
||||
|
||||
@@ -23,7 +23,7 @@ export default defineEventHandler<{
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
message: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const body = signinValidator(await readBody(h3));
|
||||
@@ -33,7 +33,7 @@ export default defineEventHandler<{
|
||||
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: body.summary,
|
||||
message: body.summary,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,13 +57,13 @@ export default defineEventHandler<{
|
||||
if (!authMek)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
message: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
if (!authMek.user.enabled)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.disabled"),
|
||||
message: t("errors.auth.disabled"),
|
||||
});
|
||||
|
||||
// LEGACY bcrypt
|
||||
@@ -74,13 +74,13 @@ export default defineEventHandler<{
|
||||
if (!hash)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: t("errors.auth.invalidPassState"),
|
||||
message: t("errors.auth.invalidPassState"),
|
||||
});
|
||||
|
||||
if (!(await checkHashBcrypt(body.password, hash)))
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
message: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
// TODO: send user to forgot password screen or something to force them to change their password to new system
|
||||
@@ -101,13 +101,13 @@ export default defineEventHandler<{
|
||||
if (!hash || typeof hash !== "string")
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: t("errors.auth.invalidPassState"),
|
||||
message: t("errors.auth.invalidPassState"),
|
||||
});
|
||||
|
||||
if (!(await checkHashArgon2(body.password, hash)))
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
message: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
const result = await sessionHandler.signin(h3, authMek.userId, {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const SharedRegisterValidator = type({
|
||||
|
||||
const CreateUserValidator = SharedRegisterValidator.and({
|
||||
invitation: "string",
|
||||
password: "string >= 14",
|
||||
password: "string >= 8",
|
||||
"displayName?": "string | undefined",
|
||||
}).configure(throwingArktype);
|
||||
|
||||
@@ -27,7 +27,7 @@ export default defineEventHandler<{
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
message: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const user = await readValidatedBody(h3, CreateUserValidator);
|
||||
@@ -38,7 +38,7 @@ export default defineEventHandler<{
|
||||
if (!invitation)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
message: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
// reuse items from invite
|
||||
@@ -51,7 +51,7 @@ export default defineEventHandler<{
|
||||
if (existing > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: t("errors.auth.usernameTaken"),
|
||||
message: t("errors.auth.usernameTaken"),
|
||||
});
|
||||
|
||||
const userId = randomUUID();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import libraryManager from "~/server/internal/library";
|
||||
|
||||
export default defineClientEventHandler(async (h3) => {
|
||||
const id = getRouterParam(h3, "id");
|
||||
@@ -57,8 +56,5 @@ export default defineClientEventHandler(async (h3) => {
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
...gameVersionMapped,
|
||||
size: libraryManager.getGameVersionSize(id, version),
|
||||
};
|
||||
return gameVersionMapped;
|
||||
});
|
||||
|
||||
+12
-16
@@ -1,6 +1,7 @@
|
||||
import type { Platform } from "~/prisma/client/enums";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import type { GameVersionSize } from "~/server/internal/gamesize";
|
||||
import gameSizeManager from "~/server/internal/gamesize";
|
||||
|
||||
type VersionDownloadOption = {
|
||||
@@ -8,24 +9,23 @@ type VersionDownloadOption = {
|
||||
displayName?: string | undefined;
|
||||
versionPath?: string | undefined;
|
||||
platform: Platform;
|
||||
size: number;
|
||||
size: GameVersionSize;
|
||||
requiredContent: Array<{
|
||||
gameId: string;
|
||||
versionId: string;
|
||||
name: string;
|
||||
iconObjectId: string;
|
||||
shortDescription: string;
|
||||
size: number;
|
||||
size: GameVersionSize;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default defineClientEventHandler(async (h3) => {
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
const id = getRouterParam(h3, "id")!;
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "No ID in request query",
|
||||
statusMessage: "No ID in router params",
|
||||
});
|
||||
|
||||
const rawVersions = await prisma.gameVersion.findMany({
|
||||
@@ -62,6 +62,7 @@ export default defineClientEventHandler(async (h3) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
setups: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,11 +74,11 @@ export default defineClientEventHandler(async (h3) => {
|
||||
VersionDownloadOption["requiredContent"]
|
||||
> = new Map();
|
||||
|
||||
for (const launch of v.launches) {
|
||||
for (const launch of [...v.launches, ...v.setups]) {
|
||||
if (!platformOptions.has(launch.platform))
|
||||
platformOptions.set(launch.platform, []);
|
||||
|
||||
if (launch.executor) {
|
||||
if ("executor" in launch && launch.executor) {
|
||||
const old = platformOptions.get(launch.platform)!;
|
||||
old.push({
|
||||
gameId: launch.executor.gameVersion.game.id,
|
||||
@@ -86,19 +87,14 @@ export default defineClientEventHandler(async (h3) => {
|
||||
iconObjectId: launch.executor.gameVersion.game.mIconObjectId,
|
||||
shortDescription:
|
||||
launch.executor.gameVersion.game.mShortDescription,
|
||||
size:
|
||||
(await gameSizeManager.getGameVersionSize(
|
||||
launch.executor.gameVersion.game.id,
|
||||
launch.executor.gameVersion.versionId,
|
||||
)) ?? 0,
|
||||
size: (await gameSizeManager.getVersionSize(
|
||||
launch.executor.gameVersion.versionId,
|
||||
))!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const size = await gameSizeManager.getGameVersionSize(
|
||||
v.gameId,
|
||||
v.versionId,
|
||||
);
|
||||
const size = await gameSizeManager.getVersionSize(v.versionId);
|
||||
|
||||
return platformOptions
|
||||
.entries()
|
||||
@@ -1,29 +1,21 @@
|
||||
import { APITokenMode } from "~/prisma/client/enums";
|
||||
import { DateTime } from "luxon";
|
||||
import type { UserACL } from "~/server/internal/acls";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { CLIENT_WEBTOKEN_ACLS } from "~/server/plugins/04.auth-init";
|
||||
|
||||
export default defineClientEventHandler(
|
||||
async (h3, { fetchUser, fetchClient, clientId }) => {
|
||||
const user = await fetchUser();
|
||||
const client = await fetchClient();
|
||||
|
||||
const acls: UserACL = [
|
||||
"read",
|
||||
"store:read",
|
||||
"collections:read",
|
||||
"object:read",
|
||||
"settings:read",
|
||||
];
|
||||
|
||||
const token = await prisma.aPIToken.create({
|
||||
data: {
|
||||
name: `${client.name} Web Access Token ${DateTime.now().toISO()}`,
|
||||
clientId,
|
||||
userId: user.id,
|
||||
mode: APITokenMode.Client,
|
||||
acls,
|
||||
acls: CLIENT_WEBTOKEN_ACLS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { type } from "arktype";
|
||||
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
const CreateCollection = type({
|
||||
name: "string",
|
||||
}).configure(throwingArktype);
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
|
||||
const userId = await aclManager.getUserIdACL(h3, ["collections:new"]);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
|
||||
const name = body.name;
|
||||
if (!name)
|
||||
throw createError({ statusCode: 400, statusMessage: "Requires name" });
|
||||
const body = await readDropValidatedBody(h3, CreateCollection);
|
||||
|
||||
// Create the collection using the manager
|
||||
const newCollection = await userLibraryManager.collectionCreate(name, userId);
|
||||
const newCollection = await userLibraryManager.collectionCreate(
|
||||
body.name,
|
||||
userId,
|
||||
);
|
||||
return newCollection;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import libraryManager from "~/server/internal/library";
|
||||
import gameSizeManager from "~/server/internal/gamesize";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
|
||||
@@ -57,7 +57,7 @@ export default defineEventHandler(async (h3) => {
|
||||
},
|
||||
});
|
||||
|
||||
const size = await libraryManager.getGameVersionSize(game.id);
|
||||
const size = (await gameSizeManager.getGameBreakdown(gameId))!;
|
||||
|
||||
return { game, rating, size };
|
||||
});
|
||||
|
||||
@@ -32,6 +32,11 @@ export default defineEventHandler(async (h3) => {
|
||||
if (options instanceof ArkErrors)
|
||||
throw createError({ statusCode: 400, statusMessage: options.summary });
|
||||
|
||||
const filterPlatforms = options.platform
|
||||
?.split(",")
|
||||
.map(parsePlatform)
|
||||
.filter((e) => e !== undefined);
|
||||
|
||||
/**
|
||||
* Generic filters
|
||||
*/
|
||||
@@ -46,23 +51,27 @@ export default defineEventHandler(async (h3) => {
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const platformFilter = options.platform
|
||||
? {
|
||||
const platformFilter = filterPlatforms
|
||||
? ({
|
||||
versions: {
|
||||
some: {
|
||||
launches: {
|
||||
some: {
|
||||
platform: {
|
||||
in: options.platform
|
||||
.split(",")
|
||||
.map(parsePlatform)
|
||||
.filter((e) => e !== undefined),
|
||||
in: filterPlatforms,
|
||||
},
|
||||
},
|
||||
},
|
||||
setups: {
|
||||
some: {
|
||||
platform: {
|
||||
in: filterPlatforms,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} satisfies Prisma.GameWhereInput)
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { aclManager } from "~/server/internal/acls";
|
||||
import { type } from "arktype";
|
||||
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { MFAMec } from "~/prisma/client/client";
|
||||
import type { WebAuthNv1Credentials } from "~/server/internal/auth/webauthn";
|
||||
|
||||
const WebAuthnDelete = type({
|
||||
id: "string",
|
||||
}).configure(throwingArktype);
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.allowUserSuperlevel(h3); // No ACLs only allows session authentication
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: "Not signed in or superlevelled.",
|
||||
});
|
||||
|
||||
const body = await readDropValidatedBody(h3, WebAuthnDelete);
|
||||
|
||||
const webauthnMec = await prisma.linkedMFAMec.findUnique({
|
||||
where: { userId_mec: { userId, mec: MFAMec.WebAuthn } },
|
||||
});
|
||||
|
||||
if (!webauthnMec)
|
||||
throw createError({ statusCode: 400, message: "WebAuthn not enabled." });
|
||||
|
||||
const credentials =
|
||||
webauthnMec.credentials as unknown as WebAuthNv1Credentials;
|
||||
const index = credentials.passkeys.findIndex((v) => v.id === body.id);
|
||||
credentials.passkeys.splice(index, 1);
|
||||
|
||||
// SAFETY: we request the object further up
|
||||
// eslint-disable-next-line drop/no-prisma-delete
|
||||
await prisma.linkedMFAMec.update({
|
||||
where: {
|
||||
userId_mec: {
|
||||
userId,
|
||||
mec: MFAMec.WebAuthn,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
// This works, I don't know why the types don't line up
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
credentials: credentials as any,
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user