From 43115695de09a152193168afd300b45a7505d9d0 Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:30:06 -0400 Subject: [PATCH 01/22] feat: set lang in html head --- components/UserHeader/SelectLang.vue | 16 +++++++++++++++- layouts/admin.vue | 23 ++++++++++++----------- layouts/default.vue | 9 +++++++-- nuxt.config.ts | 4 +++- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/components/UserHeader/SelectLang.vue b/components/UserHeader/SelectLang.vue index b3793415..f0714c10 100644 --- a/components/UserHeader/SelectLang.vue +++ b/components/UserHeader/SelectLang.vue @@ -1,9 +1,23 @@ diff --git a/layouts/default.vue b/layouts/default.vue index 11303ead..3abced86 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -13,13 +13,18 @@ diff --git a/nuxt.config.ts b/nuxt.config.ts index 5cc18fe3..f54d4799 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -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"], @@ -56,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: { diff --git a/package.json b/package.json index cf0062ce..84c99403 100644 --- a/package.json +++ b/package.json @@ -46,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", diff --git a/server/api/v1/emojis/[id]/index.get.ts b/server/api/v1/emojis/[id]/index.get.ts deleted file mode 100644 index 1dd3a727..00000000 --- a/server/api/v1/emojis/[id]/index.get.ts +++ /dev/null @@ -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); -}); diff --git a/yarn.lock b/yarn.lock index 18ba6cbb..789f2551 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2951,7 +2951,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== @@ -3129,6 +3129,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" @@ -3175,7 +3180,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== @@ -3367,6 +3372,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" @@ -4806,6 +4826,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" @@ -4933,7 +4962,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== @@ -5277,6 +5306,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" @@ -5318,7 +5354,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== @@ -5559,6 +5595,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" @@ -6481,7 +6526,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== @@ -6776,7 +6821,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== @@ -6956,7 +7001,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== @@ -7467,6 +7512,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" @@ -8493,6 +8545,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" @@ -8737,6 +8794,17 @@ vite-plugin-inspect@^11.0.1: 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.3: version "0.1.3" resolved "https://registry.yarnpkg.com/vite-plugin-vue-tracer/-/vite-plugin-vue-tracer-0.1.3.tgz#6639050fd946aa911f89efd80120fea6e49fd830" From 36b4cdd706af7c25625ee4be8ffd6078649ec71f Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 7 Jun 2025 10:45:48 -0400 Subject: [PATCH 07/22] chore: refactor auth manager --- server/api/v1/admin/auth/index.get.ts | 4 +- server/api/v1/auth/index.get.ts | 8 +-- server/api/v1/auth/signin/simple.post.ts | 9 ++- server/api/v1/auth/signup/simple.post.ts | 2 +- server/internal/auth/index.ts | 62 +++++++++++++++++++ .../simple.ts => auth/passwordHash.ts} | 0 server/plugins/04.auth-init.ts | 38 +----------- server/routes/auth/callback/oidc.get.ts | 3 +- server/routes/auth/oidc.get.ts | 3 +- 9 files changed, 78 insertions(+), 51 deletions(-) create mode 100644 server/internal/auth/index.ts rename server/internal/{security/simple.ts => auth/passwordHash.ts} (100%) diff --git a/server/api/v1/admin/auth/index.get.ts b/server/api/v1/admin/auth/index.get.ts index 75ffc2c2..e1d9da53 100644 --- a/server/api/v1/admin/auth/index.get.ts +++ b/server/api/v1/admin/auth/index.get.ts @@ -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]: diff --git a/server/api/v1/auth/index.get.ts b/server/api/v1/auth/index.get.ts index 1bf368d9..1582f1b3 100644 --- a/server/api/v1/auth/index.get.ts +++ b/server/api/v1/auth/index.get.ts @@ -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(); }); diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts index f184ca81..5e08da9a 100644 --- a/server/api/v1/auth/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -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,7 +17,7 @@ const signinValidator = type({ export default defineEventHandler<{ body: typeof signinValidator.infer; }>(async (h3) => { - if (!enabledAuthManagers.Simple) + if (!authManager.getAuthProviders().Simple) throw createError({ statusCode: 403, statusMessage: "Sign in method not enabled", diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index 5541ec33..48f2dd96 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -1,6 +1,6 @@ import { AuthMec } from "~/prisma/client"; import prisma from "~/server/internal/db/database"; -import { createHashArgon2 } from "~/server/internal/security/simple"; +import { createHashArgon2 } from "~/server/internal/auth"; import * as jdenticon from "jdenticon"; import objectHandler from "~/server/internal/objects"; import { type } from "arktype"; diff --git a/server/internal/auth/index.ts b/server/internal/auth/index.ts new file mode 100644 index 00000000..332586df --- /dev/null +++ b/server/internal/auth/index.ts @@ -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; + } = { + [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"; diff --git a/server/internal/security/simple.ts b/server/internal/auth/passwordHash.ts similarity index 100% rename from server/internal/security/simple.ts rename to server/internal/auth/passwordHash.ts diff --git a/server/plugins/04.auth-init.ts b/server/plugins/04.auth-init.ts index 1c0bc24a..a2f3e5bc 100644 --- a/server/plugins/04.auth-init.ts +++ b/server/plugins/04.auth-init.ts @@ -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; -} = { - [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(); }); diff --git a/server/routes/auth/callback/oidc.get.ts b/server/routes/auth/callback/oidc.get.ts index 65c56c16..5aba8fd9 100644 --- a/server/routes/auth/callback/oidc.get.ts +++ b/server/routes/auth/callback/oidc.get.ts @@ -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; diff --git a/server/routes/auth/oidc.get.ts b/server/routes/auth/oidc.get.ts index db79fa23..8ba7c741 100644 --- a/server/routes/auth/oidc.get.ts +++ b/server/routes/auth/oidc.get.ts @@ -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, From bbc8a4a7b64ad51c56b0821d7e943a850a30e57b Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 7 Jun 2025 10:50:44 -0400 Subject: [PATCH 08/22] feat: disable invitations if simple auth disabled --- server/api/v1/auth/signup/simple.get.ts | 7 +++++++ server/api/v1/auth/signup/simple.post.ts | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/api/v1/auth/signup/simple.get.ts b/server/api/v1/auth/signup/simple.get.ts index ea920986..14dfcef4 100644 --- a/server/api/v1/auth/signup/simple.get.ts +++ b/server/api/v1/auth/signup/simple.get.ts @@ -1,7 +1,14 @@ import prisma from "~/server/internal/db/database"; import taskHandler from "~/server/internal/tasks"; +import authManager from "~/server/internal/auth"; export default defineEventHandler(async (h3) => { + if (!authManager.getAuthProviders().Simple) + throw createError({ + statusCode: 403, + statusMessage: "Sign in method not enabled", + }); + const query = getQuery(h3); const id = query.id?.toString(); if (!id) diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index 48f2dd96..89084e11 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -1,6 +1,6 @@ import { AuthMec } from "~/prisma/client"; import prisma from "~/server/internal/db/database"; -import { createHashArgon2 } from "~/server/internal/auth"; +import authManager, { createHashArgon2 } from "~/server/internal/auth"; import * as jdenticon from "jdenticon"; import objectHandler from "~/server/internal/objects"; import { type } from "arktype"; @@ -17,6 +17,12 @@ const userValidator = type({ export default defineEventHandler<{ body: typeof userValidator.infer; }>(async (h3) => { + if (!authManager.getAuthProviders().Simple) + throw createError({ + statusCode: 403, + statusMessage: "Sign in method not enabled", + }); + const body = await readBody(h3); const invitationId = body.invitation; From 99d87a3752951c00a0cc2b1349f48b239a3ece33 Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:09:02 -0400 Subject: [PATCH 09/22] feat: add drop version to footer --- components/UserFooter.vue | 20 ++++++++++++++++++++ i18n/locales/en_us.json | 29 ++--------------------------- server/api/v1/index.get.ts | 2 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/components/UserFooter.vue b/components/UserFooter.vue index 87a5fd60..d453b34a 100644 --- a/components/UserFooter.vue +++ b/components/UserFooter.vue @@ -2,6 +2,7 @@
+
@@ -21,6 +22,8 @@
+ +
@@ -83,6 +86,21 @@
+ +
+

+ + + + +

+
@@ -92,6 +110,8 @@ import { IconsDiscordLogo, IconsGithubLogo } from "#components"; const { t } = useI18n(); +const versionInfo = await $dropFetch("/api/v1"); + const navigation = { games: [ { name: t("store.recentlyAdded"), href: "#" }, diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index 0abe06cb..128c608a 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -201,7 +201,8 @@ "discord": "Discord", "github": "GitHub" }, - "topSellers": "Top Sellers" + "topSellers": "Top Sellers", + "version": "Drop {version} {gitRef}" }, "header": { "admin": { @@ -218,17 +219,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 +235,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 +331,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 +387,18 @@ "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", "viewTask": "View {arrow}", - "back": "{arrow} Back to Tasks" } }, diff --git a/server/api/v1/index.get.ts b/server/api/v1/index.get.ts index 415f80e4..6d278f63 100644 --- a/server/api/v1/index.get.ts +++ b/server/api/v1/index.get.ts @@ -4,6 +4,6 @@ export default defineEventHandler((_h3) => { return { appName: "Drop", version: systemConfig.getDropVersion(), - ref: `#${systemConfig.getGitRef()}`, + gitRef: `#${systemConfig.getGitRef()}`, }; }); From 5f34967b15ca3c357642c2dd2e27897c0234830a Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:32:30 -0400 Subject: [PATCH 10/22] feat: translate auth endpoints --- .vscode/settings.json | 3 ++- i18n/locales/en_us.json | 11 +++++++++++ server/api/v1/auth/signin/simple.post.ts | 21 ++++++++++----------- server/api/v1/auth/signup/simple.get.ts | 8 +++++--- server/api/v1/auth/signup/simple.post.ts | 10 ++++++---- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a7c41eb5..76d084c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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"] } } diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index 128c608a..ad155ed1 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -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.", diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts index 5e08da9a..4e0c6baa 100644 --- a/server/api/v1/auth/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -17,10 +17,12 @@ const signinValidator = type({ export default defineEventHandler<{ body: typeof signinValidator.infer; }>(async (h3) => { + 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)); @@ -54,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 @@ -71,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 @@ -92,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); diff --git a/server/api/v1/auth/signup/simple.get.ts b/server/api/v1/auth/signup/simple.get.ts index 14dfcef4..ad6c4969 100644 --- a/server/api/v1/auth/signup/simple.get.ts +++ b/server/api/v1/auth/signup/simple.get.ts @@ -3,10 +3,12 @@ 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: "Sign in method not enabled", + statusMessage: t("errors.auth.method.signinDisabled"), }); const query = getQuery(h3); @@ -14,7 +16,7 @@ export default defineEventHandler(async (h3) => { if (!id) throw createError({ statusCode: 400, - statusMessage: "id required in fetching invitation", + statusMessage: t("errors.auth.inviteIdRequired"), }); taskHandler.runTaskGroupByName("cleanup:invitations"); @@ -22,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; diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index 89084e11..ac93c145 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -17,10 +17,12 @@ const userValidator = type({ export default defineEventHandler<{ body: typeof userValidator.infer; }>(async (h3) => { + 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 = await readBody(h3); @@ -29,7 +31,7 @@ export default defineEventHandler<{ if (!invitationId) throw createError({ statusCode: 401, - statusMessage: "Invalid or expired invitation.", + statusMessage: t("errors.auth.invalidInvite"), }); const invitation = await prisma.invitation.findUnique({ @@ -38,7 +40,7 @@ export default defineEventHandler<{ if (!invitation) throw createError({ statusCode: 401, - statusMessage: "Invalid or expired invitation.", + statusMessage: t("errors.auth.invalidInvite"), }); const user = userValidator(body); @@ -62,7 +64,7 @@ export default defineEventHandler<{ if (existing > 0) throw createError({ statusCode: 400, - statusMessage: "Username already taken.", + statusMessage: t("errors.auth.usernameTaken"), }); const userId = randomUUID(); From cdaba484f41862f13673d6013db1e1e198604cc1 Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:38:48 -0400 Subject: [PATCH 11/22] chore: move oidc module --- server/internal/auth/index.ts | 2 +- server/internal/{ => auth}/oidc/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename server/internal/{ => auth}/oidc/index.ts (99%) diff --git a/server/internal/auth/index.ts b/server/internal/auth/index.ts index 332586df..8942bf1e 100644 --- a/server/internal/auth/index.ts +++ b/server/internal/auth/index.ts @@ -1,5 +1,5 @@ import { AuthMec } from "~/prisma/client"; -import { OIDCManager } from "../oidc"; +import { OIDCManager } from "./oidc"; class AuthManager { private authProviders: { diff --git a/server/internal/oidc/index.ts b/server/internal/auth/oidc/index.ts similarity index 99% rename from server/internal/oidc/index.ts rename to server/internal/auth/oidc/index.ts index ec5f43e0..9320fe32 100644 --- a/server/internal/oidc/index.ts +++ b/server/internal/auth/oidc/index.ts @@ -1,8 +1,8 @@ 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"; From 11042ab3f777b45da9882e9f65f695bc10fd4f1e Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:57:33 -0400 Subject: [PATCH 12/22] feat: add weekly tasks enabled object cleanup as weekly task --- i18n/locales/en_us.json | 1 + pages/admin/task/index.vue | 31 +++++++++++++++++++--- server/api/v1/admin/task/index.get.ts | 3 ++- server/internal/tasks/index.ts | 37 ++++++++++++++++++++++++--- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index ad155ed1..d90e9e88 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -409,6 +409,7 @@ "noTasksRunning": "No tasks currently running", "completedTasksTitle": "Completed tasks", "dailyScheduledTitle": "Daily scheduled tasks", + "weeklyScheduledTitle": "Weekly scheduled tasks", "viewTask": "View {arrow}", "back": "{arrow} Back to Tasks" } diff --git a/pages/admin/task/index.vue b/pages/admin/task/index.vue index 6a9dbfd4..1ca7b035 100644 --- a/pages/admin/task/index.vue +++ b/pages/admin/task/index.vue @@ -151,11 +151,34 @@

- {{ dailyScheduledTasks[task].name }} + {{ scheduledTasks[task].name }}

- {{ dailyScheduledTasks[task].description }} + {{ scheduledTasks[task].description }} +

+
+ + + +

+ {{ $t("tasks.admin.weeklyScheduledTitle") }} +

+