Combined fixes (#116)

* fix: missing CheckIcon import in LanguageSelector

* fix: #114 and error handling

* fix: #97

* fix: lint

* feat: #104

* fix: #72
This commit is contained in:
DecDuck
2025-06-10 10:08:01 +10:00
committed by GitHub
parent 60abc03091
commit 1bfdd73e4c
10 changed files with 53 additions and 47 deletions

View File

@ -107,7 +107,10 @@ import {
ListboxOptions, ListboxOptions,
} from "@headlessui/vue"; } from "@headlessui/vue";
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid"; import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline"; import {
ArrowTopRightOnSquareIcon,
CheckIcon,
} from "@heroicons/vue/24/outline";
import type { Locale } from "vue-i18n"; import type { Locale } from "vue-i18n";
const { locales, locale: currLocale, setLocale } = useI18n(); const { locales, locale: currLocale, setLocale } = useI18n();

View File

@ -193,6 +193,7 @@
<LoadingButton <LoadingButton
:loading="loading" :loading="loading"
class="bg-blue-600 text-white hover:bg-blue-500" class="bg-blue-600 text-white hover:bg-blue-500"
:disabled="!isValidArticle"
@click="() => createArticle()" @click="() => createArticle()"
> >
{{ $t("news.article.submit") }} {{ $t("news.article.submit") }}
@ -235,6 +236,13 @@ const newArticle = ref({
tags: [] as string[], tags: [] as string[],
}); });
const isValidArticle = computed(
() =>
newArticle.value.title &&
newArticle.value.description &&
newArticle.value.content,
);
const markdownPreview = computed(() => { const markdownPreview = computed(() => {
// TODO: maybe?? add https://github.com/cure53/DOMPurify // TODO: maybe?? add https://github.com/cure53/DOMPurify
// micromark says its safe, but this is straight html we are injecting // micromark says its safe, but this is straight html we are injecting

View File

@ -21,13 +21,18 @@ async function signIn() {
redirect: `/auth/signin?redirect=${encodeURIComponent(route.fullPath)}`, redirect: `/auth/signin?redirect=${encodeURIComponent(route.fullPath)}`,
}); });
} }
switch (statusCode) {
case 401:
case 403:
await signIn();
}
useHead({ useHead({
title: t("errors.pageTitle", [statusCode ?? message]), title: t("errors.pageTitle", [statusCode ?? message]),
}); });
if (import.meta.client) { if (import.meta.client) {
console.log(props.error); console.warn(props.error);
} }
</script> </script>

View File

@ -254,5 +254,6 @@ export default defineNuxtConfig({
}, },
rateLimiter: false, rateLimiter: false,
xssValidator: false, xssValidator: false,
requestSizeLimiter: false,
}, },
}); });

View File

@ -9,7 +9,7 @@
</h2> </h2>
<button <button
:disabled="notifications.length === 0" :disabled="notifications.length === 0"
class="inline-flex items-center justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none" class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
@click="markAllAsRead" @click="markAllAsRead"
> >
<CheckIcon class="size-4" /> <CheckIcon class="size-4" />

View File

@ -1,5 +1,5 @@
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => { nuxtApp.hook("vue:error", (error, instance, info) => {
console.error(error, instance, info); console.error(error, instance, info);
}; });
}); });

View File

@ -1,8 +1,16 @@
import { ArkErrors, type } from "arktype";
import { defineEventHandler, createError } from "h3"; import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls"; import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news"; import newsManager from "~/server/internal/news";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
const CreateNews = type({
title: "string",
description: "string",
content: "string",
tags: "string = '[]'",
});
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["news:create"]); const allowed = await aclManager.allowSystemACL(h3, ["news:create"]);
if (!allowed) throw createError({ statusCode: 403 }); if (!allowed) throw createError({ statusCode: 403 });
@ -23,24 +31,25 @@ export default defineEventHandler(async (h3) => {
const [imageIds, options, pull, _dump] = uploadResult; const [imageIds, options, pull, _dump] = uploadResult;
const title = options.title; const body = await CreateNews(options);
const description = options.description; if (body instanceof ArkErrors)
const content = options.content; throw createError({ statusCode: 400, statusMessage: body.summary });
const tags = options.tags ? (JSON.parse(options.tags) as string[]) : [];
const imageId = imageIds.at(0);
if (!title || !description || !content) const parsedTags = JSON.parse(body.tags);
if (typeof parsedTags !== "object" || !Array.isArray(parsedTags))
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Missing or invalid title, description or content.", statusMessage: "Tags must be an array",
}); });
const article = await newsManager.create({ const imageId = imageIds.at(0);
title: title,
description: description,
content: content,
tags: tags, const article = await newsManager.create({
title: body.title,
description: body.description,
content: body.content,
tags: parsedTags,
...(imageId && { image: imageId }), ...(imageId && { image: imageId }),
authorId: "system", authorId: "system",

View File

@ -39,7 +39,7 @@ export class ObjectTransactionalHandler {
if (!transaction) return; if (!transaction) return;
let progress = 0; let progress = 0;
const increment = (1 / transaction.size) * 100; const increment = 100 / transaction.size;
for (const [id, data] of transaction) { for (const [id, data] of transaction) {
if (typeof data === "string") { if (typeof data === "string") {

View File

@ -374,8 +374,15 @@ export function wrapTaskContext(
): TaskRunContext { ): TaskRunContext {
return { return {
progress(progress) { progress(progress) {
const scalar = 100 / (options.max - options.min); if (progress > 100 || progress < 0) {
const adjustedProgress = progress * scalar + options.min; console.warn("[wrapTaskContext] progress must be between 0 and 100");
}
// I was too tired to figure this out
// https://stackoverflow.com/a/929107
const oldRange = 100;
const newRange = options.max - options.min;
const adjustedProgress = (progress * newRange) / oldRange + options.min;
return context.progress(adjustedProgress); return context.progress(adjustedProgress);
}, },
log(message) { log(message) {

View File

@ -1,27 +0,0 @@
import { H3Error } from "h3";
import sessionHandler from "../internal/session";
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook("error", async (error, { event }) => {
if (!event) return;
// Don't handle for API routes
if (event.path.startsWith("/api")) return;
if (event.path.startsWith("/auth")) return;
// Make sure it's a web error
if (!(error instanceof H3Error)) return;
switch (error.statusCode) {
case 401:
case 403: {
const user = await sessionHandler.getSession(event);
if (user) break;
return sendRedirect(
event,
`/auth/signin?redirect=${encodeURIComponent(event.path)}`,
);
}
}
});
});