diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8af13558..d390d3db 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -135,7 +135,7 @@ "sessionTimeoutDesc": "Session duration for Remember Me (seconds)", "metrics": "Metrics", "metricsPassword": "Password", - "metricsPasswordDesc": "Bearer Password for the metrics endpoint (argon2 hash)", + "metricsPasswordDesc": "Bearer Password for the metrics endpoint (password or argon2 hash)", "json": "JSON", "jsonDesc": "Route for metrics in JSON format", "prometheus": "Prometheus", diff --git a/src/package.json b/src/package.json index 4fba784c..57429a0d 100644 --- a/src/package.json +++ b/src/package.json @@ -23,6 +23,7 @@ "@libsql/client": "^0.15.1", "@nuxtjs/i18n": "^9.4.0", "@nuxtjs/tailwindcss": "^6.13.2", + "@phc/format": "^1.0.0", "@pinia/nuxt": "^0.10.1", "@tailwindcss/forms": "^0.5.10", "apexcharts": "^4.5.0", @@ -51,6 +52,7 @@ "devDependencies": { "@nuxt/eslint": "1.3.0", "@types/debug": "^4.1.12", + "@types/phc__format": "^1.0.1", "@types/qrcode": "^1.5.5", "@types/semver": "^7.7.0", "drizzle-kit": "^0.30.6", diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml index 40a8f4e5..de609fdd 100644 --- a/src/pnpm-lock.yaml +++ b/src/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@nuxtjs/tailwindcss': specifier: ^6.13.2 version: 6.13.2(magicast@0.3.5) + '@phc/format': + specifier: ^1.0.0 + version: 1.0.0 '@pinia/nuxt': specifier: ^0.10.1 version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))) @@ -102,6 +105,9 @@ importers: '@types/debug': specifier: ^4.1.12 version: 4.1.12 + '@types/phc__format': + specifier: ^1.0.1 + version: 1.0.1 '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 @@ -1602,11 +1608,11 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tanstack/virtual-core@3.13.5': - resolution: {integrity: sha512-gMLNylxhJdUlfRR1G3U9rtuwUh2IjdrrniJIDcekVJN3/3i+bluvdMi3+eodnxzJq5nKnxnigo9h0lIpaqV6HQ==} + '@tanstack/virtual-core@3.13.6': + resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==} - '@tanstack/vue-virtual@3.13.5': - resolution: {integrity: sha512-1hhUA6CUjmKc5JDyKLcYOV6mI631FaKKxXh77Ja4UtIy6EOofYaLPk8vVgvK6vLMUSfHR2vI3ZpPY9ibyX60SA==} + '@tanstack/vue-virtual@3.13.6': + resolution: {integrity: sha512-GYdZ3SJBQPzgxhuCE2fvpiH46qzHiVx5XzBSdtESgiqh4poj8UgckjGWYEhxaBbcVt1oLzh1m3Ql4TyH32TOzQ==} peerDependencies: vue: ^2.7.0 || ^3.0.0 @@ -1641,6 +1647,9 @@ packages: '@types/parse-path@7.0.3': resolution: {integrity: sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==} + '@types/phc__format@1.0.1': + resolution: {integrity: sha512-hoAQFKcP3voXk/ZEl3jrvS63o/HYLszq4nA2mqjytaSEHEy3j3t0gSFtPLnfKtX34k/xfath7etOoGw5ukoqXQ==} + '@types/qrcode@1.5.5': resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} @@ -6664,11 +6673,11 @@ snapshots: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.17 - '@tanstack/virtual-core@3.13.5': {} + '@tanstack/virtual-core@3.13.6': {} - '@tanstack/vue-virtual@3.13.5(vue@3.5.13(typescript@5.8.2))': + '@tanstack/vue-virtual@3.13.6(vue@3.5.13(typescript@5.8.2))': dependencies: - '@tanstack/virtual-core': 3.13.5 + '@tanstack/virtual-core': 3.13.6 vue: 3.5.13(typescript@5.8.2) '@trysound/sax@0.2.0': {} @@ -6698,6 +6707,10 @@ snapshots: '@types/parse-path@7.0.3': {} + '@types/phc__format@1.0.1': + dependencies: + '@types/node': 22.13.14 + '@types/qrcode@1.5.5': dependencies: '@types/node': 22.13.14 @@ -9509,7 +9522,7 @@ snapshots: '@floating-ui/vue': 1.1.6(vue@3.5.13(typescript@5.8.2)) '@internationalized/date': 3.7.0 '@internationalized/number': 3.6.0 - '@tanstack/vue-virtual': 3.13.5(vue@3.5.13(typescript@5.8.2)) + '@tanstack/vue-virtual': 3.13.6(vue@3.5.13(typescript@5.8.2)) '@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.8.2)) '@vueuse/shared': 10.11.1(vue@3.5.13(typescript@5.8.2)) aria-hidden: 1.2.4 diff --git a/src/server/database/repositories/general/service.ts b/src/server/database/repositories/general/service.ts index d63a05b7..ef629fd8 100644 --- a/src/server/database/repositories/general/service.ts +++ b/src/server/database/repositories/general/service.ts @@ -107,7 +107,15 @@ export class GeneralService { }; } - update(data: GeneralUpdateType) { + async update(data: GeneralUpdateType) { + // only hash the password if it is not already hashed + if ( + data.metricsPassword !== null && + !isValidPasswordHash(data.metricsPassword) + ) { + data.metricsPassword = await hashPassword(data.metricsPassword); + } + return this.#db.update(general).set(data).execute(); } diff --git a/src/server/database/repositories/general/types.ts b/src/server/database/repositories/general/types.ts index 351caacf..e16bc4c6 100644 --- a/src/server/database/repositories/general/types.ts +++ b/src/server/database/repositories/general/types.ts @@ -11,7 +11,6 @@ const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') }); const metricsPassword = z .string({ message: t('zod.general.metricsPassword') }) .min(1, { message: t('zod.general.metricsPassword') }) - // TODO?: validate argon2 regex .nullable(); export const GeneralUpdateSchema = z.object({ diff --git a/src/server/utils/password.ts b/src/server/utils/password.ts index c7dee33e..11ce37e4 100644 --- a/src/server/utils/password.ts +++ b/src/server/utils/password.ts @@ -1,4 +1,5 @@ import argon2 from 'argon2'; +import { deserialize } from '@phc/format'; /** * Checks if `password` matches the hash. @@ -16,3 +17,21 @@ export function isPasswordValid( export async function hashPassword(password: string): Promise { return argon2.hash(password); } + +/** + * Checks if the password hash is valid. + * This only checks if the hash is a valid PHC formatted string using argon2. + */ +export function isValidPasswordHash(hash: string): boolean { + try { + const obj = deserialize(hash); + + if (obj.id !== 'argon2i' && obj.id !== 'argon2d' && obj.id !== 'argon2id') { + return false; + } + + return true; + } catch { + return false; + } +}