mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
Various bug fixes (#102)
* feat: set lang in html head * fix: add # in front of git ref * fix: remove unused vars from example env * fix: package name and license field * fix: enable sourcemap for client and server * fix: emojis not showing in prod this is extremely cursed, but it works * chore: refactor auth manager * feat: disable invitations if simple auth disabled * feat: add drop version to footer * feat: translate auth endpoints * chore: move oidc module * feat: add weekly tasks enabled object cleanup as weekly task * feat: add timestamp to task log msgs * feat: add guard to prevent invalid progress % * fix: add missing global scope to i18n components * feat: set base url for i18n * feat: switch task log to json format * ci: run ci on develop branch only * fix: UserWidget text not updating #109 * fix: EXTERNAL_URL being computed at build * feat: add basic language outlines for translation * feat: add more english dialects
This commit is contained in:
@ -1,8 +1,5 @@
|
||||
DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop"
|
||||
|
||||
CLIENT_CERTIFICATES="./.data/ca"
|
||||
|
||||
FS_BACKEND_PATH="./.data/objects"
|
||||
|
||||
GIANT_BOMB_API_KEY=""
|
||||
|
||||
EXTERNAL_URL="localhost:3000"
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -1,6 +1,12 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -31,6 +31,7 @@
|
||||
],
|
||||
"i18n-ally.extract.ignoredByFiles": {
|
||||
"pages/admin/library/sources/index.vue": ["Filesystem"],
|
||||
"components/NewsArticleCreateButton.vue": ["[", "`", "Enter"]
|
||||
"components/NewsArticleCreateButton.vue": ["[", "`", "Enter"],
|
||||
"server/api/v1/auth/signin/simple.post.ts": ["boolean | undefined"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,7 @@ const props = defineProps<{
|
||||
emoji: string;
|
||||
}>();
|
||||
|
||||
const emojiEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const url = computed(() => {
|
||||
return `/api/v1/emojis/${twemoji.convert.toCodePoint(props.emoji)}.svg`;
|
||||
return `/twemoji/${twemoji.convert.toCodePoint(props.emoji)}.svg`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -9,9 +9,10 @@
|
||||
class="grid w-full cursor-default grid-cols-1 rounded-md bg-zinc-900 py-1.5 pr-2 pl-3 text-left text-zinc-300 outline-1 -outline-offset-1 outline-zinc-700 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
>
|
||||
<span class="col-start-1 row-start-1 flex items-center gap-3 pr-6">
|
||||
<span alt="" class="-mt-0.5 shrink-0 rounded-full">{{
|
||||
localeToEmoji(wiredLocale)
|
||||
}}</span>
|
||||
<EmojiText
|
||||
:emoji="localeToEmoji(wiredLocale)"
|
||||
class="-mt-0.5 shrink-0 max-w-6"
|
||||
/>
|
||||
<span class="block truncate">{{
|
||||
currentLocaleInformation?.name ?? wiredLocale
|
||||
}}</span>
|
||||
@ -46,9 +47,10 @@
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="-mt-0.5 shrink-0 rounded-full">
|
||||
{{ localeToEmoji(listLocale.code) }}
|
||||
</span>
|
||||
<EmojiText
|
||||
:emoji="localeToEmoji(listLocale.code)"
|
||||
class="-mt-0.5 shrink-0 max-w-6"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
@ -106,21 +108,50 @@ import {
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
|
||||
import type { Locale } from "vue-i18n";
|
||||
|
||||
const { locales, locale, setLocale } = useI18n();
|
||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
||||
|
||||
function changeLocale(locale: Locale) {
|
||||
setLocale(locale);
|
||||
|
||||
// dynamically update the HTML attributes for language and direction
|
||||
// this is necessary for proper rendering of the page in the new language
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: locale,
|
||||
dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function localeToEmoji(local: string): string {
|
||||
switch (local) {
|
||||
// Default locale
|
||||
case "en":
|
||||
case "en-gb":
|
||||
case "en-ca":
|
||||
case "en-au":
|
||||
case "en-us": {
|
||||
case "en-us":
|
||||
return "🇺🇸";
|
||||
}
|
||||
case "en-pirate": {
|
||||
|
||||
case "en-gb":
|
||||
return "🇬🇧";
|
||||
case "en-ca":
|
||||
return "🇨🇦";
|
||||
case "en-au":
|
||||
return "🇦🇺";
|
||||
case "en-pirate":
|
||||
return "🏴☠️";
|
||||
}
|
||||
case "fr":
|
||||
return "🇫🇷";
|
||||
case "de":
|
||||
return "🇩🇪";
|
||||
case "es":
|
||||
return "🇪🇸";
|
||||
case "it":
|
||||
return "🇮🇹";
|
||||
case "zh":
|
||||
return "🇨🇳";
|
||||
case "zh-tw":
|
||||
return "🇹🇼";
|
||||
|
||||
default: {
|
||||
return "❓";
|
||||
@ -130,10 +161,10 @@ function localeToEmoji(local: string): string {
|
||||
|
||||
const wiredLocale = computed({
|
||||
get() {
|
||||
return locale.value;
|
||||
return currLocale.value;
|
||||
},
|
||||
set(v) {
|
||||
setLocale(v);
|
||||
changeLocale(v);
|
||||
},
|
||||
});
|
||||
const currentLocaleInformation = computed(() =>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<footer class="bg-zinc-950" aria-labelledby="footer-heading">
|
||||
<h2 id="footer-heading" class="sr-only">{{ $t("footer.footer") }}</h2>
|
||||
<div class="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
||||
<!-- Drop Info -->
|
||||
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div class="space-y-8">
|
||||
<DropWordmark class="h-10" />
|
||||
@ -24,6 +25,8 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foot links -->
|
||||
<div class="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
||||
<div class="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
@ -86,6 +89,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center xl:col-span-3 mt-8">
|
||||
<p
|
||||
class="text-xs text-zinc-700 hover:text-zinc-400 transition-colors duration-200 cursor-default select-none"
|
||||
>
|
||||
<i18n-t keypath="footer.version" tag="p" scope="global">
|
||||
<template #version>
|
||||
<span>{{ versionInfo.version }}</span>
|
||||
</template>
|
||||
<template #gitRef>
|
||||
<span>{{ versionInfo.gitRef }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@ -96,6 +114,8 @@ import { IconsDiscordLogo, IconsGithubLogo } from "#components";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const versionInfo = await $dropFetch("/api/v1");
|
||||
|
||||
const navigation = {
|
||||
games: [
|
||||
{ name: t("store.recentlyAdded"), href: "#" },
|
||||
|
||||
@ -85,20 +85,21 @@ import { useObject } from "~/composables/objects";
|
||||
import type { NavigationItem } from "~/composables/types";
|
||||
|
||||
const user = useUser();
|
||||
const { t } = useI18n();
|
||||
|
||||
const navigation: NavigationItem[] = [
|
||||
const navigation = computed<NavigationItem[]>(() =>
|
||||
[
|
||||
user.value?.admin
|
||||
? {
|
||||
label: t("userHeader.profile.admin"),
|
||||
label: $t("userHeader.profile.admin"),
|
||||
route: "/admin",
|
||||
prefix: "",
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
label: t("userHeader.profile.settings"),
|
||||
label: $t("userHeader.profile.settings"),
|
||||
route: "/account",
|
||||
prefix: "",
|
||||
},
|
||||
].filter((e) => e !== undefined);
|
||||
].filter((e) => e !== undefined),
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -21,7 +21,15 @@ export default defineI18nConfig(() => {
|
||||
// https://vue-i18n.intlify.dev/guide/essentials/datetime.html
|
||||
datetimeFormats: {
|
||||
"en-us": defaultDateTimeFormat,
|
||||
"en-gb": defaultDateTimeFormat,
|
||||
"en-au": defaultDateTimeFormat,
|
||||
"en-pirate": defaultDateTimeFormat,
|
||||
fr: defaultDateTimeFormat,
|
||||
de: defaultDateTimeFormat,
|
||||
it: defaultDateTimeFormat,
|
||||
es: defaultDateTimeFormat,
|
||||
zh: defaultDateTimeFormat,
|
||||
"zh-tw": defaultDateTimeFormat,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
1
i18n/locales/de.json
Normal file
1
i18n/locales/de.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
i18n/locales/en_au.json
Normal file
1
i18n/locales/en_au.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
i18n/locales/en_gb.json
Normal file
1
i18n/locales/en_gb.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -107,6 +107,17 @@
|
||||
"listItemPlaceholder": "list item"
|
||||
},
|
||||
"errors": {
|
||||
"auth": {
|
||||
"method": {
|
||||
"signinDisabled": "Sign in method not enabled"
|
||||
},
|
||||
"invalidUserOrPass": "Invalid username or password.",
|
||||
"disabled": "Invalid or disabled account. Please contact the server administrator.",
|
||||
"invalidPassState": "Invalid password state. Please contact the server administrator.",
|
||||
"inviteIdRequired": "id required in fetching invitation",
|
||||
"invalidInvite": "Invalid or expired invitation",
|
||||
"usernameTaken": "Username already taken."
|
||||
},
|
||||
"backHome": "{arrow} Back to home",
|
||||
"invalidBody": "Invalid request body: {0}",
|
||||
"inviteRequired": "Invitation required to sign up.",
|
||||
@ -201,7 +212,8 @@
|
||||
"discord": "Discord",
|
||||
"github": "GitHub"
|
||||
},
|
||||
"topSellers": "Top Sellers"
|
||||
"topSellers": "Top Sellers",
|
||||
"version": "Drop {version} {gitRef}"
|
||||
},
|
||||
"header": {
|
||||
"admin": {
|
||||
@ -218,17 +230,14 @@
|
||||
"admin": {
|
||||
"description": "Manage the users on your Drop instance, and configure your authentication methods.",
|
||||
"authLink": "Authentication {arrow}",
|
||||
|
||||
"displayNameHeader": "Display Name",
|
||||
"usernameHeader": "Username",
|
||||
"emailHeader": "Email",
|
||||
"adminHeader": "Admin?",
|
||||
"authoptionsHeader": "Auth Options",
|
||||
"srEditLabel": "Edit",
|
||||
|
||||
"adminUserLabel": "Admin user",
|
||||
"normalUserLabel": "Normal user",
|
||||
|
||||
"authentication": {
|
||||
"title": "Authentication",
|
||||
"description": "Drop supports a variety of \"authentication mechanisms\". As you enable or disable them, they are shown on the sign in screen for users to select from. Click the dot menu to configure the authentication mechanism.",
|
||||
@ -237,47 +246,33 @@
|
||||
"disabled": "Disabled",
|
||||
"srOpenOptions": "Open options",
|
||||
"configure": "Configure",
|
||||
|
||||
"simple": "Simple (username/password)",
|
||||
"oidc": "OpenID Connect"
|
||||
},
|
||||
|
||||
"simple": {
|
||||
"title": "Simple authentication",
|
||||
"description": "Simple authentication uses a system of 'invitations' to create users. You can create an invitation, and optionally specify a username or email for the user, and then it will generate a magic URL that can be used to create an account.",
|
||||
|
||||
"invitationTitle": "invitations",
|
||||
"createInvitation": "Create invitation",
|
||||
|
||||
"noUsernameEnforced": "No username enforced.",
|
||||
"noEmailEnforced": "No email enforced.",
|
||||
|
||||
"adminInvitation": "Admin invitation",
|
||||
"userInvitation": "User invitation",
|
||||
|
||||
"expires": "Expires: {expiry}",
|
||||
"neverExpires": "Never expires.",
|
||||
|
||||
"noInvitations": "No invitations.",
|
||||
|
||||
"inviteTitle": "Invite user to Drop",
|
||||
"inviteDescription": "Drop will generate a URL that you can send to the person you want to invite. You can optionally specify a username or email for them to use.",
|
||||
|
||||
"inviteUsernameLabel": "Username (optional)",
|
||||
"inviteUsernameFormat": "Must be 5 or more characters",
|
||||
"inviteUsernamePlaceholder": "myUsername",
|
||||
|
||||
"inviteEmailLabel": "Email address (optional)",
|
||||
"inviteEmailDescription": "Must be in the format user{'@'}example.com",
|
||||
"inviteEmailPlaceholder": "me{'@'}example.com",
|
||||
|
||||
"inviteAdminSwitchLabel": "Admin invitation",
|
||||
"inviteAdminSwitchDescription": "Create this user as an administrator",
|
||||
|
||||
"inviteExpiryLabel": "Expires",
|
||||
|
||||
"inviteButton": "Invite",
|
||||
|
||||
"invite3Days": "3 days",
|
||||
"inviteWeek": "1 week",
|
||||
"inviteMonth": "1 month",
|
||||
@ -347,18 +342,14 @@
|
||||
"imageCarouselEmpty": "No images added to the carousel yet.",
|
||||
"removeImageCarousel": "Remove image",
|
||||
"addCarouselNoImages": "No images to add.",
|
||||
|
||||
"imageLibrary": "Image library",
|
||||
"imageLibraryDescription": "Please note all images uploaded are accessible to all users through browser dev-tools.",
|
||||
"setBanner": "Set as banner",
|
||||
"setCover": "Set as cover",
|
||||
"deleteImage": "Delete image",
|
||||
|
||||
"currentBanner": "banner",
|
||||
"currentCover": "cover",
|
||||
|
||||
"addDescriptionNoImages": "No images to add.",
|
||||
|
||||
"editGameName": "Game Name",
|
||||
"editGameDescription": "Game Description"
|
||||
},
|
||||
@ -407,23 +398,19 @@
|
||||
"scheduled": {
|
||||
"cleanupInvitationsName": "Clean up invitations",
|
||||
"cleanupInvitationsDescription": "Cleans up expired invitations from the database to save space.",
|
||||
|
||||
"cleanupObjectsName": "Clean up objects",
|
||||
"cleanupObjectsDescription": "Detects and deletes unreferenced and unused objects to save space.",
|
||||
|
||||
"cleanupSessionsName": "Clean up sessions.",
|
||||
"cleanupSessionsDescription": "Cleans up expired sessions to save space and ensure security.",
|
||||
|
||||
"checkUpdateName": "Check update.",
|
||||
"checkUpdateDescription": "Check if Drop has an update."
|
||||
},
|
||||
|
||||
"runningTasksTitle": "Running tasks",
|
||||
"noTasksRunning": "No tasks currently running",
|
||||
"completedTasksTitle": "Completed tasks",
|
||||
"dailyScheduledTitle": "Daily scheduled tasks",
|
||||
"weeklyScheduledTitle": "Weekly scheduled tasks",
|
||||
"viewTask": "View {arrow}",
|
||||
|
||||
"back": "{arrow} Back to Tasks"
|
||||
}
|
||||
},
|
||||
|
||||
1
i18n/locales/es.json
Normal file
1
i18n/locales/es.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
i18n/locales/fr.json
Normal file
1
i18n/locales/fr.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
i18n/locales/it.json
Normal file
1
i18n/locales/it.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
i18n/locales/zh.json
Normal file
1
i18n/locales/zh.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
i18n/locales/zh_tw.json
Normal file
1
i18n/locales/zh_tw.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -170,36 +170,36 @@ import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
||||
import { ArrowLeftIcon } from "@heroicons/vue/16/solid";
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
const { t } = useI18n();
|
||||
const i18nHead = useLocaleHead();
|
||||
|
||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||
{ label: t("home"), route: "/admin", prefix: "/admin", icon: HomeIcon },
|
||||
{ label: $t("home"), route: "/admin", prefix: "/admin", icon: HomeIcon },
|
||||
{
|
||||
label: t("userHeader.links.library"),
|
||||
label: $t("userHeader.links.library"),
|
||||
route: "/admin/library",
|
||||
prefix: "/admin/library",
|
||||
icon: ServerStackIcon,
|
||||
},
|
||||
{
|
||||
label: t("header.admin.users"),
|
||||
label: $t("header.admin.users"),
|
||||
route: "/admin/users",
|
||||
prefix: "/admin/users",
|
||||
icon: UserGroupIcon,
|
||||
},
|
||||
{
|
||||
label: t("header.admin.tasks"),
|
||||
label: $t("header.admin.tasks"),
|
||||
route: "/admin/task",
|
||||
prefix: "/admin/task",
|
||||
icon: RectangleStackIcon,
|
||||
},
|
||||
{
|
||||
label: t("settings"),
|
||||
label: $t("settings"),
|
||||
route: "/admin/settings",
|
||||
prefix: "/admin/settings",
|
||||
icon: Cog6ToothIcon,
|
||||
},
|
||||
{
|
||||
label: t("header.back"),
|
||||
label: $t("header.back"),
|
||||
route: "/store",
|
||||
prefix: ".",
|
||||
icon: ArrowLeftIcon,
|
||||
@ -221,11 +221,12 @@ router.afterEach(() => {
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: "en",
|
||||
lang: i18nHead.value.htmlAttrs.lang,
|
||||
// @ts-expect-error head.value.htmlAttrs.dir is not typed as strictly as it should be
|
||||
dir: i18nHead.value.htmlAttrs.dir,
|
||||
},
|
||||
link: [],
|
||||
titleTemplate(title) {
|
||||
return title ? t("adminTitleTemplate", [title]) : t("adminTitle");
|
||||
return title ? $t("adminTitleTemplate", [title]) : $t("adminTitle");
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -13,15 +13,20 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const i18nHead = useLocaleHead();
|
||||
const noWrapper = !!route.query.noWrapper;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: "en",
|
||||
lang: i18nHead.value.htmlAttrs.lang,
|
||||
// @ts-expect-error head.value.htmlAttrs.dir is not typed as strictly as it should be
|
||||
dir: i18nHead.value.htmlAttrs.dir,
|
||||
},
|
||||
link: [],
|
||||
// // seo headers
|
||||
// link: [...i18nHead.value.link],
|
||||
// meta: [...i18nHead.value.meta],
|
||||
titleTemplate(title) {
|
||||
return title ? t("titleTemplate", [title]) : t("title");
|
||||
},
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { execSync } from "node:child_process";
|
||||
import { cpSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import module from "module";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
|
||||
// get drop version
|
||||
const dropVersion = process.env.BUILD_DROP_VERSION ?? "v0.3.0-alpha.1";
|
||||
@ -12,6 +16,14 @@ const commitHash =
|
||||
|
||||
console.log(`Building Drop ${dropVersion} #${commitHash}`);
|
||||
|
||||
const twemojiJson = module.findPackageJSON(
|
||||
"@discordapp/twemoji",
|
||||
import.meta.url,
|
||||
);
|
||||
if (!twemojiJson) {
|
||||
throw new Error("Could not find @discordapp/twemoji package.");
|
||||
}
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
extends: ["./drop-base"],
|
||||
@ -40,6 +52,11 @@ export default defineNuxtConfig({
|
||||
},
|
||||
css: ["~/assets/tailwindcss.css", "~/assets/core.scss"],
|
||||
|
||||
sourcemap: {
|
||||
server: true,
|
||||
client: true,
|
||||
},
|
||||
|
||||
experimental: {
|
||||
buildCache: true,
|
||||
viewTransition: true,
|
||||
@ -51,7 +68,31 @@ export default defineNuxtConfig({
|
||||
// },
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
// only used in dev server, not build because nitro sucks
|
||||
// see build hook below
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: "node_modules/@discordapp/twemoji/dist/svg/*",
|
||||
dest: "twemoji",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
hooks: {
|
||||
"nitro:build:public-assets": (nitro) => {
|
||||
// this is only run during build, not dev server
|
||||
// https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964
|
||||
// copy emojis to .output/public/twemoji
|
||||
const targetDir = path.join(nitro.options.output.publicDir, "twemoji");
|
||||
cpSync(path.join(path.dirname(twemojiJson), "dist", "svg"), targetDir, {
|
||||
recursive: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
@ -130,6 +171,7 @@ export default defineNuxtConfig({
|
||||
strategy: "no_prefix",
|
||||
experimental: {
|
||||
localeDetector: "localeDetector.ts",
|
||||
autoImportTranslationFunctions: true,
|
||||
},
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
@ -137,12 +179,61 @@ export default defineNuxtConfig({
|
||||
fallbackLocale: "en-us",
|
||||
},
|
||||
locales: [
|
||||
{ code: "en-us", name: "English", file: "en_us.json" },
|
||||
{ code: "en-us", language: "en-us", name: "English", file: "en_us.json" },
|
||||
{
|
||||
code: "en-gb",
|
||||
language: "en-gb",
|
||||
name: "English (UK)",
|
||||
file: "en_gb.json",
|
||||
},
|
||||
{
|
||||
code: "en-au",
|
||||
language: "en-au",
|
||||
name: "English (Australia)",
|
||||
file: "en_au.json",
|
||||
},
|
||||
{
|
||||
code: "en-pirate",
|
||||
language: "en-pirate",
|
||||
name: "English (Pirate)",
|
||||
file: "en_pirate.json",
|
||||
},
|
||||
{
|
||||
code: "fr",
|
||||
language: "fr",
|
||||
name: "French",
|
||||
file: "fr.json",
|
||||
},
|
||||
{
|
||||
code: "de",
|
||||
language: "de",
|
||||
name: "German",
|
||||
file: "de.json",
|
||||
},
|
||||
{
|
||||
code: "it",
|
||||
language: "it",
|
||||
name: "Italian",
|
||||
file: "it.json",
|
||||
},
|
||||
{
|
||||
code: "es",
|
||||
language: "es",
|
||||
name: "Spanish",
|
||||
file: "es.json",
|
||||
},
|
||||
{
|
||||
code: "zh",
|
||||
language: "zh",
|
||||
name: "Chinese",
|
||||
file: "zh.json",
|
||||
},
|
||||
{
|
||||
code: "zh-tw",
|
||||
language: "zh-tw",
|
||||
name: "Chinese (Taiwan)",
|
||||
file: "zh_tw.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"name": "drop",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
@ -45,6 +46,7 @@
|
||||
"stream-mime-type": "^2.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
"unstorage": "^1.15.0",
|
||||
"vite-plugin-static-copy": "^3.0.0",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vue3-carousel": "^0.15.0",
|
||||
|
||||
@ -14,7 +14,11 @@
|
||||
to="/admin/library/sources"
|
||||
class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<i18n-t keypath="library.admin.sources.link" tag="span">
|
||||
<i18n-t
|
||||
keypath="library.admin.sources.link"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
@ -39,7 +43,11 @@
|
||||
href="/admin/library/import"
|
||||
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
|
||||
>
|
||||
<i18n-t keypath="library.admin.import.link" tag="span">
|
||||
<i18n-t
|
||||
keypath="library.admin.import.link"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
@ -140,7 +148,11 @@
|
||||
:href="`/admin/library/${game.id}/import`"
|
||||
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
|
||||
>
|
||||
<i18n-t keypath="library.admin.import.link" tag="span">
|
||||
<i18n-t
|
||||
keypath="library.admin.import.link"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
|
||||
@ -54,7 +54,9 @@
|
||||
<div
|
||||
class="relative bg-zinc-950/50 rounded-md p-2 text-zinc-100 h-[80vh] overflow-y-scroll"
|
||||
>
|
||||
<pre v-for="(line, idx) in task.log" :key="idx">{{ line }}</pre>
|
||||
<pre v-for="(line, idx) in task.log" :key="idx">{{
|
||||
formatLine(line)
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else role="status" class="w-full flex items-center justify-center">
|
||||
@ -88,6 +90,11 @@ const taskId = route.params.id.toString();
|
||||
|
||||
const task = useTask(taskId);
|
||||
|
||||
function formatLine(line: string): string {
|
||||
const res = parseTaskLog(line);
|
||||
return `[${res.timestamp}] ${res.message}`;
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
{{ task.id }}
|
||||
</p>
|
||||
<p class="mt-1 truncate text-sm text-zinc-400">
|
||||
{{ task.log.at(-1) }}
|
||||
{{ parseTaskLog(task.log.at(-1) ?? "").message }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
type="button"
|
||||
@ -151,11 +151,34 @@
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h3 class="text-sm font-medium text-zinc-100">
|
||||
{{ dailyScheduledTasks[task].name }}
|
||||
{{ scheduledTasks[task].name }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
{{ dailyScheduledTasks[task].description }}
|
||||
{{ scheduledTasks[task].description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="text-sm font-medium text-zinc-400 mt-8">
|
||||
{{ $t("tasks.admin.weeklyScheduledTitle") }}
|
||||
</h2>
|
||||
<ul role="list" class="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<li
|
||||
v-for="task in weeklyTasks"
|
||||
:key="task"
|
||||
class="col-span-1 divide-y divide-gray-200 rounded-lg bg-zinc-800 border border-zinc-700 shadow-sm"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between space-x-6 p-6">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h3 class="text-sm font-medium text-zinc-100">
|
||||
{{ scheduledTasks[task].name }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
{{ scheduledTasks[task].description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -179,12 +202,12 @@ definePageMeta({
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { runningTasks, historicalTasks, dailyTasks } =
|
||||
const { runningTasks, historicalTasks, dailyTasks, weeklyTasks } =
|
||||
await $dropFetch("/api/v1/admin/task");
|
||||
|
||||
const liveRunningTasks = await Promise.all(runningTasks.map((e) => useTask(e)));
|
||||
|
||||
const dailyScheduledTasks: {
|
||||
const scheduledTasks: {
|
||||
[key in TaskGroup]: { name: string; description: string };
|
||||
} = {
|
||||
"cleanup:invitations": {
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
{{ $t("store.recentlyReleased") }}
|
||||
</h1>
|
||||
<NuxtLink class="text-blue-600 font-semibold">
|
||||
<i18n-t keypath="store.exploreMore" tag="span">
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
@ -89,7 +89,7 @@
|
||||
{{ $t("store.recentlyUpdated") }}
|
||||
</h1>
|
||||
<NuxtLink class="text-blue-600 font-semibold">
|
||||
<i18n-t keypath="store.exploreMore" tag="span">
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]);
|
||||
if (!allowed) throw createError({ statusCode: 403 });
|
||||
|
||||
const enabledAuthManagers = authManager.getAuthProviders();
|
||||
|
||||
const authData = {
|
||||
[AuthMec.Simple]: enabledAuthManagers.Simple,
|
||||
[AuthMec.OpenID]:
|
||||
|
||||
@ -30,6 +30,7 @@ export default defineEventHandler(async (h3) => {
|
||||
take: 10,
|
||||
});
|
||||
const dailyTasks = await taskHandler.dailyTasks();
|
||||
const weeklyTasks = await taskHandler.weeklyTasks();
|
||||
|
||||
return { runningTasks, historicalTasks, dailyTasks };
|
||||
return { runningTasks, historicalTasks, dailyTasks, weeklyTasks };
|
||||
});
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
const authManagers = Object.entries(enabledAuthManagers)
|
||||
.filter((e) => !!e[1])
|
||||
.map((e) => e[0]);
|
||||
|
||||
return authManagers;
|
||||
return authManager.getEnabledAuthProviders();
|
||||
});
|
||||
|
||||
@ -2,12 +2,11 @@ import { AuthMec } from "~/prisma/client";
|
||||
import type { JsonArray } from "@prisma/client/runtime/library";
|
||||
import { type } from "arktype";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import {
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import authManager, {
|
||||
checkHashArgon2,
|
||||
checkHashBcrypt,
|
||||
} from "~/server/internal/security/simple";
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
} from "~/server/internal/auth";
|
||||
|
||||
const signinValidator = type({
|
||||
username: "string",
|
||||
@ -18,10 +17,12 @@ const signinValidator = type({
|
||||
export default defineEventHandler<{
|
||||
body: typeof signinValidator.infer;
|
||||
}>(async (h3) => {
|
||||
if (!enabledAuthManagers.Simple)
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Sign in method not enabled",
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const body = signinValidator(await readBody(h3));
|
||||
@ -55,14 +56,13 @@ export default defineEventHandler<{
|
||||
if (!authMek)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid username or password.",
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
if (!authMek.user.enabled)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage:
|
||||
"Invalid or disabled account. Please contact the server administrator.",
|
||||
statusMessage: t("errors.auth.disabled"),
|
||||
});
|
||||
|
||||
// LEGACY bcrypt
|
||||
@ -72,15 +72,14 @@ export default defineEventHandler<{
|
||||
|
||||
if (!hash)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage:
|
||||
"Invalid password state. Please contact the server administrator.",
|
||||
statusCode: 500,
|
||||
statusMessage: t("errors.auth.invalidPassState"),
|
||||
});
|
||||
|
||||
if (!(await checkHashBcrypt(body.password, hash)))
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid username or password.",
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
// TODO: send user to forgot password screen or something to force them to change their password to new system
|
||||
@ -93,14 +92,13 @@ export default defineEventHandler<{
|
||||
if (!hash || typeof hash !== "string")
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage:
|
||||
"Invalid password state. Please contact the server administrator.",
|
||||
statusMessage: t("errors.auth.invalidPassState"),
|
||||
});
|
||||
|
||||
if (!(await checkHashArgon2(body.password, hash)))
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid username or password.",
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
await sessionHandler.signin(h3, authMek.userId, body.rememberMe);
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import taskHandler from "~/server/internal/tasks";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "id required in fetching invitation",
|
||||
statusMessage: t("errors.auth.inviteIdRequired"),
|
||||
});
|
||||
taskHandler.runTaskGroupByName("cleanup:invitations");
|
||||
|
||||
@ -15,7 +24,7 @@ export default defineEventHandler(async (h3) => {
|
||||
if (!invitation)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Invalid or expired invitation",
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
return invitation;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { createHashArgon2 } from "~/server/internal/security/simple";
|
||||
import authManager, { createHashArgon2 } from "~/server/internal/auth";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import { type } from "arktype";
|
||||
@ -18,13 +18,21 @@ export const CreateUserValidator = type({
|
||||
export default defineEventHandler<{
|
||||
body: typeof CreateUserValidator.infer;
|
||||
}>(async (h3) => {
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const user = await readValidatedBody(h3, CreateUserValidator);
|
||||
|
||||
const invitationId = user.invitation;
|
||||
if (!invitationId)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired invitation.",
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
const invitation = await prisma.invitation.findUnique({
|
||||
@ -33,7 +41,7 @@ export default defineEventHandler<{
|
||||
if (!invitation)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired invitation.",
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
// reuse items from invite
|
||||
@ -46,7 +54,7 @@ export default defineEventHandler<{
|
||||
if (existing > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Username already taken.",
|
||||
statusMessage: t("errors.auth.usernameTaken"),
|
||||
});
|
||||
|
||||
const userId = randomUUID();
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
import path from "path";
|
||||
import module from "module";
|
||||
import fs from "fs/promises";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
import aclManager from "~/server/internal/acls";
|
||||
|
||||
const twemojiJson = module.findPackageJSON(
|
||||
"@discordapp/twemoji",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["object:read"]);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
});
|
||||
|
||||
if (!twemojiJson)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to resolve emoji package",
|
||||
});
|
||||
|
||||
const unsafeId = getRouterParam(h3, "id");
|
||||
if (!unsafeId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
|
||||
|
||||
const svgPath = path.join(
|
||||
path.dirname(twemojiJson),
|
||||
"dist",
|
||||
"svg",
|
||||
sanitize(unsafeId),
|
||||
);
|
||||
|
||||
setHeader(
|
||||
h3,
|
||||
"Cache-Control",
|
||||
// 7 days
|
||||
"public, max-age=604800, s-maxage=604800",
|
||||
);
|
||||
setHeader(h3, "Content-Type", "image/svg+xml");
|
||||
return await fs.readFile(svgPath);
|
||||
});
|
||||
@ -4,6 +4,6 @@ export default defineEventHandler((_h3) => {
|
||||
return {
|
||||
appName: "Drop",
|
||||
version: systemConfig.getDropVersion(),
|
||||
ref: systemConfig.getGitRef(),
|
||||
gitRef: `#${systemConfig.getGitRef()}`,
|
||||
};
|
||||
});
|
||||
|
||||
62
server/internal/auth/index.ts
Normal file
62
server/internal/auth/index.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import { OIDCManager } from "./oidc";
|
||||
|
||||
class AuthManager {
|
||||
private authProviders: {
|
||||
[AuthMec.Simple]: boolean;
|
||||
[AuthMec.OpenID]: OIDCManager | undefined;
|
||||
} = {
|
||||
[AuthMec.Simple]: false,
|
||||
[AuthMec.OpenID]: undefined,
|
||||
};
|
||||
|
||||
private initFuncs: {
|
||||
[K in keyof typeof this.authProviders]: () => Promise<unknown>;
|
||||
} = {
|
||||
[AuthMec.OpenID]: OIDCManager.prototype.create,
|
||||
[AuthMec.Simple]: async () => {
|
||||
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
|
||||
return !disabled;
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
console.log("AuthManager initialized");
|
||||
}
|
||||
|
||||
async init() {
|
||||
for (const [key, init] of Object.entries(this.initFuncs)) {
|
||||
try {
|
||||
const object = await init();
|
||||
if (!object) break;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this.authProviders as any)[key] = object;
|
||||
console.log(`enabled auth: ${key}`);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
|
||||
if (!this.authProviders[AuthMec.OpenID]) {
|
||||
this.authProviders[AuthMec.Simple] = true;
|
||||
}
|
||||
}
|
||||
|
||||
getAuthProviders() {
|
||||
return this.authProviders;
|
||||
}
|
||||
|
||||
getEnabledAuthProviders() {
|
||||
const authManagers = Object.entries(this.authProviders)
|
||||
.filter((e) => !!e[1])
|
||||
.map((e) => e[0]);
|
||||
|
||||
return authManagers;
|
||||
}
|
||||
}
|
||||
|
||||
const authManager = new AuthManager();
|
||||
export default authManager;
|
||||
|
||||
export * from "./passwordHash";
|
||||
@ -1,10 +1,11 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import prisma from "../db/database";
|
||||
import prisma from "../../db/database";
|
||||
import type { User } from "~/prisma/client";
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import objectHandler from "../objects";
|
||||
import objectHandler from "../../objects";
|
||||
import type { Readable } from "stream";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import { systemConfig } from "../../config/sys-conf";
|
||||
|
||||
interface OIDCWellKnown {
|
||||
authorization_endpoint: string;
|
||||
@ -118,7 +119,7 @@ export class OIDCManager {
|
||||
|
||||
const clientId = process.env.OIDC_CLIENT_ID as string | undefined;
|
||||
const clientSecret = process.env.OIDC_CLIENT_SECRET as string | undefined;
|
||||
const externalUrl = process.env.EXTERNAL_URL as string | undefined;
|
||||
const externalUrl = systemConfig.getExternalUrl();
|
||||
|
||||
if (!clientId || !clientSecret)
|
||||
throw new Error("Missing client ID or secret for OIDC");
|
||||
@ -2,6 +2,7 @@ class SystemConfig {
|
||||
private libraryFolder = process.env.LIBRARY ?? "./.data/library";
|
||||
private dataFolder = process.env.DATA ?? "./.data/data";
|
||||
|
||||
private externalUrl = process.env.EXTERNAL_URL ?? "http://localhost:3000";
|
||||
private dropVersion;
|
||||
private gitRef;
|
||||
|
||||
@ -33,6 +34,10 @@ class SystemConfig {
|
||||
shouldCheckForUpdates() {
|
||||
return this.checkForUpdates;
|
||||
}
|
||||
|
||||
getExternalUrl() {
|
||||
return this.externalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export const systemConfig = new SystemConfig();
|
||||
|
||||
@ -9,6 +9,7 @@ import checkUpdate from "./registry/update";
|
||||
import cleanupObjects from "./registry/objects";
|
||||
import { taskGroups, type TaskGroup } from "./group";
|
||||
import prisma from "../db/database";
|
||||
import { type } from "arktype";
|
||||
|
||||
// a task that has been run
|
||||
type FinishedTask = {
|
||||
@ -45,11 +46,12 @@ class TaskHandler {
|
||||
// list of all clients currently connected to tasks
|
||||
private clientRegistry = new Map<string, PeerImpl>();
|
||||
|
||||
private scheduledTasks: TaskGroup[] = [
|
||||
private dailyScheduledTasks: TaskGroup[] = [
|
||||
"cleanup:invitations",
|
||||
"cleanup:sessions",
|
||||
"check:update",
|
||||
];
|
||||
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
|
||||
|
||||
constructor() {
|
||||
// register the cleanup invitations task
|
||||
@ -124,18 +126,22 @@ class TaskHandler {
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const progress = (progress: number) => {
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
if (!taskEntry) return;
|
||||
taskEntry.progress = progress;
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
const log = (entry: string) => {
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
if (!taskEntry) return;
|
||||
taskEntry.log.push(entry);
|
||||
// console.log(`[Task ${task.taskGroup}]: ${entry}`);
|
||||
taskEntry.log.push(msgWithTimestamp(entry));
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
const progress = (progress: number) => {
|
||||
if (progress < 0 || progress > 100) {
|
||||
console.error("Progress must be between 0 and 100", { progress });
|
||||
return;
|
||||
}
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
if (!taskEntry) return;
|
||||
taskEntry.progress = progress;
|
||||
// log(`Progress: ${progress}%`);
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
@ -288,7 +294,11 @@ class TaskHandler {
|
||||
}
|
||||
|
||||
dailyTasks() {
|
||||
return this.scheduledTasks;
|
||||
return this.dailyScheduledTasks;
|
||||
}
|
||||
|
||||
weeklyTasks() {
|
||||
return this.weeklyScheduledTasks;
|
||||
}
|
||||
|
||||
runTaskGroupByName(name: TaskGroup) {
|
||||
@ -304,7 +314,7 @@ class TaskHandler {
|
||||
* Runs all daily tasks that are scheduled to run once a day.
|
||||
*/
|
||||
async triggerDailyTasks() {
|
||||
for (const taskGroup of this.scheduledTasks) {
|
||||
for (const taskGroup of this.dailyScheduledTasks) {
|
||||
const mostRecent = await prisma.task.findFirst({
|
||||
where: {
|
||||
taskGroup,
|
||||
@ -324,6 +334,32 @@ class TaskHandler {
|
||||
}
|
||||
await this.runTaskGroupByName(taskGroup);
|
||||
}
|
||||
|
||||
// After running daily tasks, trigger weekly tasks as well
|
||||
await this.triggerWeeklyTasks();
|
||||
}
|
||||
|
||||
private async triggerWeeklyTasks() {
|
||||
for (const taskGroup of this.weeklyScheduledTasks) {
|
||||
const mostRecent = await prisma.task.findFirst({
|
||||
where: {
|
||||
taskGroup,
|
||||
},
|
||||
orderBy: {
|
||||
ended: "desc",
|
||||
},
|
||||
});
|
||||
if (mostRecent) {
|
||||
const currentTime = Date.now();
|
||||
const lastRun = mostRecent.ended.getTime();
|
||||
const difference = currentTime - lastRun;
|
||||
if (difference < 1000 * 60 * 60 * 24 * 7) {
|
||||
// If it's been less than one week
|
||||
continue; // skip
|
||||
}
|
||||
}
|
||||
await this.runTaskGroupByName(taskGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,6 +419,37 @@ interface DropTask {
|
||||
build: () => Task;
|
||||
}
|
||||
|
||||
export const TaskLog = type({
|
||||
timestamp: "string",
|
||||
message: "string",
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a log message with a timestamp in the format YYYY-MM-DD HH:mm:ss.SSS UTC
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
function msgWithTimestamp(message: string): string {
|
||||
const now = new Date();
|
||||
|
||||
const pad = (n: number, width = 2) => n.toString().padStart(width, "0");
|
||||
|
||||
const year = now.getUTCFullYear();
|
||||
const month = pad(now.getUTCMonth() + 1);
|
||||
const day = pad(now.getUTCDate());
|
||||
|
||||
const hours = pad(now.getUTCHours());
|
||||
const minutes = pad(now.getUTCMinutes());
|
||||
const seconds = pad(now.getUTCSeconds());
|
||||
const milliseconds = pad(now.getUTCMilliseconds(), 3);
|
||||
|
||||
const log: typeof TaskLog.infer = {
|
||||
timestamp: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} UTC`,
|
||||
message,
|
||||
};
|
||||
return JSON.stringify(log);
|
||||
}
|
||||
|
||||
export function defineDropTask(buildTask: BuildTask): DropTask {
|
||||
// TODO: only let one task with the same taskGroup run at the same time if specified
|
||||
|
||||
|
||||
@ -1,39 +1,5 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import { OIDCManager } from "../internal/oidc";
|
||||
|
||||
export const enabledAuthManagers: {
|
||||
[AuthMec.Simple]: boolean;
|
||||
[AuthMec.OpenID]: OIDCManager | undefined;
|
||||
} = {
|
||||
[AuthMec.Simple]: false,
|
||||
[AuthMec.OpenID]: undefined,
|
||||
};
|
||||
|
||||
const initFunctions: {
|
||||
[K in keyof typeof enabledAuthManagers]: () => Promise<unknown>;
|
||||
} = {
|
||||
[AuthMec.OpenID]: OIDCManager.prototype.create,
|
||||
[AuthMec.Simple]: async () => {
|
||||
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
|
||||
return !disabled;
|
||||
},
|
||||
};
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
for (const [key, init] of Object.entries(initFunctions)) {
|
||||
try {
|
||||
const object = await init();
|
||||
if (!object) break;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(enabledAuthManagers as any)[key] = object;
|
||||
console.log(`enabled auth: ${key}`);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
|
||||
if (!enabledAuthManagers[AuthMec.OpenID]) {
|
||||
enabledAuthManagers[AuthMec.Simple] = true;
|
||||
}
|
||||
await authManager.init();
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
defineRouteMeta({
|
||||
openAPI: {
|
||||
@ -10,6 +10,7 @@ defineRouteMeta({
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const enabledAuthManagers = authManager.getAuthProviders();
|
||||
if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin");
|
||||
|
||||
const manager = enabledAuthManagers.OpenID;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
defineRouteMeta({
|
||||
openAPI: {
|
||||
@ -11,6 +11,7 @@ defineRouteMeta({
|
||||
export default defineEventHandler((h3) => {
|
||||
const redirect = getQuery(h3).redirect?.toString();
|
||||
|
||||
const enabledAuthManagers = authManager.getAuthProviders();
|
||||
if (!enabledAuthManagers.OpenID)
|
||||
return sendRedirect(
|
||||
h3,
|
||||
|
||||
10
utils/parseTaskLog.ts
Normal file
10
utils/parseTaskLog.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { TaskLog } from "~/server/internal/tasks";
|
||||
|
||||
export function parseTaskLog(logStr: string): typeof TaskLog.infer {
|
||||
const log = JSON.parse(logStr);
|
||||
|
||||
return {
|
||||
message: log.message,
|
||||
timestamp: log.timestamp,
|
||||
};
|
||||
}
|
||||
82
yarn.lock
82
yarn.lock
@ -2938,7 +2938,7 @@ ansis@^3.17.0:
|
||||
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
|
||||
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
|
||||
|
||||
anymatch@^3.1.3:
|
||||
anymatch@^3.1.3, anymatch@~3.1.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
|
||||
@ -3116,6 +3116,11 @@ bcryptjs@*, bcryptjs@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.2.tgz#caadcca1afefe372ed6e20f86db8e8546361c1ca"
|
||||
integrity sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||
|
||||
bindings@^1.4.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
||||
@ -3162,7 +3167,7 @@ brace-expansion@^2.0.1:
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.3:
|
||||
braces@^3.0.3, braces@~3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
@ -3354,6 +3359,21 @@ cheerio@^1.0.0:
|
||||
undici "^6.19.5"
|
||||
whatwg-mimetype "^4.0.0"
|
||||
|
||||
chokidar@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^4.0.0, chokidar@^4.0.1, chokidar@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
||||
@ -4793,6 +4813,15 @@ fs-constants@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@^11.3.0:
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
|
||||
integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^8.0.1:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
||||
@ -4920,7 +4949,7 @@ github-from-package@0.0.0:
|
||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
|
||||
|
||||
glob-parent@^5.1.2:
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@ -5264,6 +5293,13 @@ is-arrayish@^0.3.1:
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-builtin-module@^3.1.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
|
||||
@ -5305,7 +5341,7 @@ is-fullwidth-code-point@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
@ -5546,6 +5582,15 @@ jsonfile@^5.0.0:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
junk@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/junk/-/junk-4.0.1.tgz#7ee31f876388c05177fe36529ee714b07b50fbed"
|
||||
@ -6468,7 +6513,7 @@ normalize-path@^2.1.1:
|
||||
dependencies:
|
||||
remove-trailing-separator "^1.0.1"
|
||||
|
||||
normalize-path@^3.0.0:
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
@ -6762,7 +6807,7 @@ p-locate@^6.0.0:
|
||||
dependencies:
|
||||
p-limit "^4.0.0"
|
||||
|
||||
p-map@^7.0.0:
|
||||
p-map@^7.0.0, p-map@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.3.tgz#7ac210a2d36f81ec28b736134810f7ba4418cdb6"
|
||||
integrity sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==
|
||||
@ -6942,7 +6987,7 @@ picocolors@^1.0.0, picocolors@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.3.1:
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
@ -7453,6 +7498,13 @@ readdirp@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
||||
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
|
||||
@ -8486,6 +8538,11 @@ universalify@^0.1.0, universalify@^0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||
|
||||
unixify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090"
|
||||
@ -8730,6 +8787,17 @@ vite-plugin-inspect@^11.1.0:
|
||||
unplugin-utils "^0.2.4"
|
||||
vite-dev-rpc "^1.0.7"
|
||||
|
||||
vite-plugin-static-copy@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-3.0.0.tgz#5d9bdf240ec25205280e48d67ab5a4c642517092"
|
||||
integrity sha512-Uki9pPUQ4ZnoMEdIFabvoh9h6Bh9Q1m3iF7BrZvoiF30reREpJh2gZb4jOnW1/uYFzyRiLCmFSkM+8hwiq1vWQ==
|
||||
dependencies:
|
||||
chokidar "^3.5.3"
|
||||
fs-extra "^11.3.0"
|
||||
p-map "^7.0.3"
|
||||
picocolors "^1.1.1"
|
||||
tinyglobby "^0.2.13"
|
||||
|
||||
vite-plugin-vue-tracer@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-vue-tracer/-/vite-plugin-vue-tracer-0.1.4.tgz#4e6c36a8f59c1b1b1fd9bceffa5d237a7687060b"
|
||||
|
||||
Reference in New Issue
Block a user