Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions components/GameEditor/Metadata.vue
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,8 @@ import {
import type { SerializeObject } from "nitropack";
import type { H3Error } from "h3";
import type { AdminFetchGameType } from "~/server/api/v1/admin/game/[id]/index.get";
import { FetchError } from "ofetch";
import { isImageMimeType } from "~/server/internal/mimetypes";

const showUploadModal = ref(false);
const showAddCarouselModal = ref(false);
Expand Down Expand Up @@ -548,7 +550,7 @@ const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
const coreMetadataIconFileUpload = ref<FileList | undefined>();
const coreMetadataLoading = ref(false);

function coreMetadataUploadFiles(e: InputEvent) {
async function coreMetadataUploadFiles(e: InputEvent) {
if (coreMetadataIconUrl.value.startsWith("blob")) {
URL.revokeObjectURL(coreMetadataIconUrl.value);
}
Expand All @@ -568,8 +570,10 @@ function coreMetadataUploadFiles(e: InputEvent) {
);
return;
}
const objectUrl = URL.createObjectURL(file);
coreMetadataIconUrl.value = objectUrl;
if (isImageMimeType(await file.arrayBuffer())) {
const objectUrl = URL.createObjectURL(file);
coreMetadataIconUrl.value = objectUrl;
}
}
async function coreMetadataUpdate() {
const formData = new FormData();
Expand All @@ -596,12 +600,18 @@ function coreMetadataUpdate_wrapper() {
coreMetadataLoading.value = true;
coreMetadataUpdate()
.catch((e) => {
let errorMessage = "";
if (e instanceof FetchError) {
errorMessage = e.data.message;
} else {
errorMessage = e as string;
}
createModal(
ModalType.Notification,
{
title: t("errors.game.metadata.title"),
description: t("errors.game.metadata.description", [
(e as H3Error)?.statusMessage ?? t("errors.unknown"),
errorMessage ?? t("errors.unknown"),
]),
buttonText: t("common.close"),
},
Expand Down
7 changes: 6 additions & 1 deletion components/Modal/UploadFile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
} from "@headlessui/vue";
import { ArrowUpTrayIcon } from "@heroicons/vue/20/solid";
import { XCircleIcon } from "@heroicons/vue/24/solid";
import { FetchError } from "ofetch";

const open = defineModel<boolean>({
required: true,
Expand Down Expand Up @@ -177,7 +178,11 @@ function uploadFile_wrapper() {
uploadLoading.value = true;
uploadFile()
.catch((error) => {
uploadError.value = error.statusMessage ?? t("errors.unknown");
if (error instanceof FetchError) {
uploadError.value = error.data.message ?? t("errors.unknown");
} else {
error.value = error as string;
}
})
.finally(() => {
uploadLoading.value = false;
Expand Down
8 changes: 6 additions & 2 deletions components/NewsArticleCreateButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ import {
XMarkIcon,
} from "@heroicons/vue/24/solid";
import { micromark } from "micromark";
import { FetchError } from "ofetch";

const news = useNews();
if (!news.value) {
Expand Down Expand Up @@ -414,8 +415,11 @@ async function createArticle() {

modalOpen.value = false;
} catch (e) {
// @ts-expect-error attempt to get statusMessage on error
error.value = e?.statusMessage ?? t("errors.unknown");
if (e instanceof FetchError) {
error.value = e.data.message ?? t("errors.unknown");
} else {
error.value = e as string;
}
} finally {
loading.value = false;
}
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions server/api/v1/admin/company/[id]/banner.post.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { IMAGE_EXTENSIONS, isImageMimeType } from "~/server/internal/mimetypes";
import objectHandler from "~/server/internal/objects";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";

Expand All @@ -15,21 +16,33 @@ export default defineEventHandler(async (h3) => {
});

if (!company)
throw createError({ statusCode: 400, statusMessage: "Invalid company id" });
throw createError({ statusCode: 400, message: "Invalid company id" });

const formData = await readMultipartFormData(h3);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe build this functionality into the handleFileUpload util? Like an optional argument that defines valid MIME types or MIME type prefixes

if (!formData) {
throw createError({ statusCode: 400, message: "No file detected" });
}
const buffer = new Uint8Array(formData[0].data).buffer;
if (!isImageMimeType(buffer)) {
throw createError({
statusCode: 400,
message: `File is not an image. Supported file formats: ${IMAGE_EXTENSIONS.join(", ")}`,
});
}

const result = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!result)
throw createError({
statusCode: 400,
statusMessage: "File upload required (multipart form)",
message: "File upload required (multipart form)",
});

const [ids, , pull, dump] = result;
const id = ids.at(0);
if (!id)
throw createError({
statusCode: 400,
statusMessage: "Upload at least one file.",
message: "Upload at least one file.",
});

await objectHandler.deleteAsSystem(company.mBannerObjectId);
Expand All @@ -42,7 +55,7 @@ export default defineEventHandler(async (h3) => {
},
});
if (count == 0) {
await dump();
dump();
throw createError({ statusCode: 404, message: "Company not found" });
}
await pull();
Expand Down
15 changes: 12 additions & 3 deletions server/api/v1/admin/company/[id]/icon.post.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { IMAGE_EXTENSIONS, isImageMimeType } from "~/server/internal/mimetypes";
import objectHandler from "~/server/internal/objects";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";

Expand All @@ -15,21 +16,29 @@ export default defineEventHandler(async (h3) => {
});

if (!company)
throw createError({ statusCode: 400, statusMessage: "Invalid company id" });
throw createError({ statusCode: 400, message: "Invalid company id" });

const formData = await readMultipartFormData(h3);
if (!formData || !isImageMimeType(new Uint8Array(formData[0].data).buffer)) {
throw createError({
statusCode: 400,
message: `File is not an image. Supported file formats: ${IMAGE_EXTENSIONS.join(", ")}`,
});
}

const result = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!result)
throw createError({
statusCode: 400,
statusMessage: "File upload required (multipart form)",
message: "File upload required (multipart form)",
});

const [ids, , pull, dump] = result;
const id = ids.at(0);
if (!id)
throw createError({
statusCode: 400,
statusMessage: "Upload at least one file.",
message: "Upload at least one file.",
});

await objectHandler.deleteAsSystem(company.mLogoObjectId);
Expand Down
8 changes: 5 additions & 3 deletions server/api/v1/admin/game/[id]/metadata.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@ import type { Prisma } from "~/prisma/client/client";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
import { IMAGE_EXTENSIONS, isImageMimeType } from "~/server/internal/mimetypes";

export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });

const form = await readMultipartFormData(h3);
if (!form)
if (!form || !isImageMimeType(new Uint8Array(form[0].data).buffer)) {
throw createError({
statusCode: 400,
statusMessage: "This endpoint requires multipart form data.",
message: `File is not an image. Supported file formats: ${IMAGE_EXTENSIONS.join(", ")}`,
});
}

const gameId = getRouterParam(h3, "id")!;

const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
message: "Failed to upload file",
});

const [ids, options, pull, dump] = uploadResult;
Expand Down
6 changes: 4 additions & 2 deletions server/api/v1/admin/game/image/index.post.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
import { IMAGE_EXTENSIONS, isImageMimeType } from "~/server/internal/mimetypes";

export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:image:new"]);
if (!allowed) throw createError({ statusCode: 403 });

const form = await readMultipartFormData(h3);
if (!form)
if (!form || !isImageMimeType(new Uint8Array(form[0].data).buffer)) {
throw createError({
statusCode: 400,
statusMessage: "This endpoint requires multipart form data.",
message: `File is not an image. Supported file formats: ${IMAGE_EXTENSIONS.join(", ")}`,
});
}

const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]);
if (!uploadResult)
Expand Down
16 changes: 9 additions & 7 deletions server/api/v1/admin/news/index.post.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ArkErrors, type } from "arktype";
import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import { IMAGE_EXTENSIONS, isImageMimeType } from "~/server/internal/mimetypes";
import newsManager from "~/server/internal/news";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";

Expand All @@ -16,30 +17,31 @@ export default defineEventHandler(async (h3) => {
if (!allowed) throw createError({ statusCode: 403 });

const form = await readMultipartFormData(h3);
if (!form)

if (!form || !isImageMimeType(new Uint8Array(form[0].data).buffer)) {
throw createError({
statusCode: 400,
statusMessage: "This endpoint requires multipart form data.",
message: `File is not an image. Supported file formats: ${IMAGE_EXTENSIONS.join(", ")}`,
});

}
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
message: "Failed to upload file",
});

const [imageIds, options, pull, _dump] = uploadResult;

const body = await CreateNews(options);
const body = CreateNews(options);
if (body instanceof ArkErrors)
throw createError({ statusCode: 400, statusMessage: body.summary });
throw createError({ statusCode: 400, message: body.summary });

const parsedTags = JSON.parse(body.tags);
if (typeof parsedTags !== "object" || !Array.isArray(parsedTags))
throw createError({
statusCode: 400,
statusMessage: "Tags must be an array",
message: "Tags must be an array",
});

const imageId = imageIds.at(0);
Expand Down
10 changes: 9 additions & 1 deletion server/api/v1/admin/settings/logo.post.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import aclManager from "~/server/internal/acls";
import { IMAGE_EXTENSIONS, isImageMimeType } from "~/server/internal/mimetypes";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";

export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["settings:update"]);
if (!allowed) throw createError({ statusCode: 403 });

const form = await readMultipartFormData(h3);
if (!form || !isImageMimeType(new Uint8Array(form[0].data).buffer)) {
throw createError({
statusCode: 400,
message: `File is not an image. Supported file formats: ${IMAGE_EXTENSIONS.join(", ")}`,
});
}
const result = await handleFileUpload(h3, {}, ["anonymous:read"], 1);
if (!result)
throw createError({
Expand All @@ -17,7 +25,7 @@ export default defineEventHandler(async (h3) => {
if (!id)
throw createError({
statusCode: 400,
statusMessage: "Upload at least one file.",
message: "Upload at least one file.",
});

await pull();
Expand Down
29 changes: 29 additions & 0 deletions server/internal/mimetypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { parse } from "file-type-mime";

export const IMAGE_EXTENSIONS = [
"bmp",
"gif",
"ico",
"jpeg",
"heic",
"png",
"tiff",
Comment on lines +3 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we absolutely need to cover every possible item, but support for svg, webp, avif, and maybe even jxl are a must imo.

];

export const IMAGE_MIME_TYPES = [
"image/bmp",
"image/gif",
"image/x-icon",
"image/jpeg",
"image/heic",
"image/png",
"image/tiff",
];

export function isImageMimeType(file: ArrayBuffer) {
const fileType = parse(new Uint8Array(file).buffer);
if (!fileType || !IMAGE_MIME_TYPES.indexOf(fileType.mime)) {
return false;
}
return true;
}