diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index f2666606..ebb93f62 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -4147,7 +4147,7 @@ export type GetRegistrationStatusResponses = { * Registration status */ 200: { - disabled: boolean; + enabled: boolean; }; }; @@ -4155,7 +4155,7 @@ export type GetRegistrationStatusResponse = GetRegistrationStatusResponses[keyof export type SetRegistrationStatusData = { body?: { - disabled: boolean; + enabled: boolean; }; path?: never; query?: never; @@ -4167,7 +4167,7 @@ export type SetRegistrationStatusResponses = { * Registration status updated */ 200: { - disabled: boolean; + enabled: boolean; }; }; diff --git a/app/client/lib/constants.ts b/app/client/lib/constants.ts index 801c5768..fc2c499a 100644 --- a/app/client/lib/constants.ts +++ b/app/client/lib/constants.ts @@ -1 +1,2 @@ export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories"; +export const REGISTRATION_ENABLED_KEY = "registrations_enabled"; diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index ae949f8e..45731e60 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -182,17 +182,15 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
-
updateRegistrationStatusMutation.mutate({ body: { disabled: checked } })} + id="enable-registrations" + checked={registrationStatusQuery.data?.enabled ?? false} + onCheckedChange={(checked) => updateRegistrationStatusMutation.mutate({ body: { enabled: checked } })} disabled={registrationStatusQuery.isLoading || updateRegistrationStatusMutation.isPending} />
diff --git a/app/lib/auth-middlewares/only-one-user.ts b/app/lib/auth-middlewares/only-one-user.ts index af5e1a82..9d69cd9e 100644 --- a/app/lib/auth-middlewares/only-one-user.ts +++ b/app/lib/auth-middlewares/only-one-user.ts @@ -4,8 +4,7 @@ import { logger } from "~/server/utils/logger"; import { eq } from "drizzle-orm"; import { appMetadataTable } from "~/server/db/schema"; import { ForbiddenError } from "http-errors-enhanced"; - -export const REGISTRATION_DISABLED_KEY = "registrationsDisabled"; +import { REGISTRATION_ENABLED_KEY } from "~/client/lib/constants"; export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => { const { path } = ctx; @@ -16,11 +15,11 @@ export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => { } const result = await db.query.appMetadataTable.findFirst({ - where: eq(appMetadataTable.key, REGISTRATION_DISABLED_KEY), + where: eq(appMetadataTable.key, REGISTRATION_ENABLED_KEY), }); - if (result?.value === "true" && existingUser) { - logger.info("User registration attempt blocked: registrations are disabled."); + if (result?.value !== "true" && existingUser) { + logger.info("User registration attempt blocked: registrations are not enabled."); throw new ForbiddenError("User registrations are currently disabled. Please contact an administrator for access."); } }; diff --git a/app/server/core/config.ts b/app/server/core/config.ts index d3532c43..a7b0895e 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -82,15 +82,3 @@ const parseConfig = (env: unknown) => { }; export const config = parseConfig(process.env); - -export const getAppSecret = (): string => { - if (!config.appSecret) { - throw new Error("ZEROBYTE_APP_SECRET is not configured"); - } - - if (config.appSecret.length < 32) { - throw new Error("ZEROBYTE_APP_SECRET must be at least 32 characters long"); - } - - return config.appSecret; -}; diff --git a/app/server/modules/system/system.controller.ts b/app/server/modules/system/system.controller.ts index 4f9b2e1a..d83a4d20 100644 --- a/app/server/modules/system/system.controller.ts +++ b/app/server/modules/system/system.controller.ts @@ -45,9 +45,9 @@ export const systemController = new Hono() return c.json(updates, 200); }) .get("/registration-status", getRegistrationStatusDto, async (c) => { - const disabled = await systemService.isRegistrationDisabled(); + const enabled = await systemService.isRegistrationEnabled(); - return c.json({ disabled }, 200); + return c.json({ enabled }, 200); }) .put( "/registration-status", @@ -57,9 +57,9 @@ export const systemController = new Hono() async (c) => { const body = c.req.valid("json"); - await systemService.setRegistrationDisabled(body.disabled); + await systemService.setRegistrationEnabled(body.enabled); - return c.json({ disabled: body.disabled }, 200); + return c.json({ enabled: body.enabled }, 200); }, ) .post( diff --git a/app/server/modules/system/system.dto.ts b/app/server/modules/system/system.dto.ts index 061e9406..3a148488 100644 --- a/app/server/modules/system/system.dto.ts +++ b/app/server/modules/system/system.dto.ts @@ -65,7 +65,8 @@ export const downloadResticPasswordBodySchema = type({ }); export const downloadResticPasswordDto = describeRoute({ - description: "Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.", + description: + "Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.", tags: ["System"], operationId: "downloadResticPassword", responses: { @@ -81,13 +82,13 @@ export const downloadResticPasswordDto = describeRoute({ }); export const registrationStatusResponse = type({ - disabled: "boolean", + enabled: "boolean", }); export type RegistrationStatusDto = typeof registrationStatusResponse.infer; export const registrationStatusBody = type({ - disabled: "boolean", + enabled: "boolean", }); export const getRegistrationStatusDto = describeRoute({ diff --git a/app/server/modules/system/system.service.ts b/app/server/modules/system/system.service.ts index aca5ba41..2549024e 100644 --- a/app/server/modules/system/system.service.ts +++ b/app/server/modules/system/system.service.ts @@ -7,9 +7,9 @@ import { logger } from "~/server/utils/logger"; import { db } from "../../db/db"; import { appMetadataTable } from "../../db/schema"; import { eq } from "drizzle-orm"; +import { REGISTRATION_ENABLED_KEY } from "~/client/lib/constants"; const CACHE_TTL = 60 * 60; -const REGISTRATION_DISABLED_KEY = "registrationsDisabled"; const getSystemInfo = async () => { return { @@ -89,28 +89,28 @@ const getUpdates = async (): Promise => { } }; -const isRegistrationDisabled = async () => { +const isRegistrationEnabled = async () => { const result = await db.query.appMetadataTable.findFirst({ - where: eq(appMetadataTable.key, REGISTRATION_DISABLED_KEY), + where: eq(appMetadataTable.key, REGISTRATION_ENABLED_KEY), }); return result?.value === "true"; }; -const setRegistrationDisabled = async (disabled: boolean) => { +const setRegistrationEnabled = async (enabled: boolean) => { const now = Date.now(); await db .insert(appMetadataTable) - .values({ key: REGISTRATION_DISABLED_KEY, value: JSON.stringify(disabled), createdAt: now, updatedAt: now }) - .onConflictDoUpdate({ target: appMetadataTable.key, set: { value: JSON.stringify(disabled), updatedAt: now } }); + .values({ key: REGISTRATION_ENABLED_KEY, value: JSON.stringify(enabled), createdAt: now, updatedAt: now }) + .onConflictDoUpdate({ target: appMetadataTable.key, set: { value: JSON.stringify(enabled), updatedAt: now } }); - logger.info(`Registration disabled set to: ${disabled}`); + logger.info(`Registration enabled set to: ${enabled}`); }; export const systemService = { getSystemInfo, getUpdates, - isRegistrationDisabled, - setRegistrationDisabled, + isRegistrationEnabled, + setRegistrationEnabled, }; diff --git a/app/server/utils/crypto.ts b/app/server/utils/crypto.ts index cea8c126..aed32a24 100644 --- a/app/server/utils/crypto.ts +++ b/app/server/utils/crypto.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { config, getAppSecret } from "../core/config"; +import { config } from "../core/config"; import { isNodeJSErrnoException } from "./fs"; import { promisify } from "node:util"; @@ -96,10 +96,8 @@ const encrypt = async (data: string) => { return data; } - const secret = getAppSecret(); - const salt = crypto.randomBytes(16); - const key = crypto.pbkdf2Sync(secret, salt, 100000, keyLength, "sha256"); + const key = crypto.pbkdf2Sync(config.appSecret, salt, 100000, keyLength, "sha256"); const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv(algorithm, key, iv); @@ -118,13 +116,11 @@ const decrypt = async (encryptedData: string) => { return encryptedData; } - const secret = getAppSecret(); - const parts = encryptedData.split(":").slice(1); // Remove prefix const saltHex = parts.shift() as string; const salt = Buffer.from(saltHex, "hex"); - const key = crypto.pbkdf2Sync(secret, salt, 100000, keyLength, "sha256"); + const key = crypto.pbkdf2Sync(config.appSecret, salt, 100000, keyLength, "sha256"); const iv = Buffer.from(parts.shift() as string, "hex"); const encrypted = Buffer.from(parts.shift() as string, "hex");