refactor: enable user registration vs disabling it

This commit is contained in:
Nicolas Meienberger 2026-01-20 21:21:45 +01:00
parent 343c00cb33
commit b35fdae5cd
9 changed files with 34 additions and 51 deletions

View file

@ -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;
};
};

View file

@ -1 +1,2 @@
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
export const REGISTRATION_ENABLED_KEY = "registrations_enabled";

View file

@ -182,17 +182,15 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="disable-registrations" className="text-base">
Disable new user registrations
<Label htmlFor="enable-registrations" className="text-base">
Enable new user registrations
</Label>
<p className="text-sm text-muted-foreground max-w-2xl">
When enabled, new users cannot sign up. Only administrators can create accounts manually.
</p>
<p className="text-sm text-muted-foreground max-w-2xl">When enabled, new users can sign up</p>
</div>
<Switch
id="disable-registrations"
checked={registrationStatusQuery.data?.disabled ?? false}
onCheckedChange={(checked) => 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}
/>
</div>

View file

@ -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.");
}
};

View file

@ -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;
};

View file

@ -45,9 +45,9 @@ export const systemController = new Hono()
return c.json<UpdateInfoDto>(updates, 200);
})
.get("/registration-status", getRegistrationStatusDto, async (c) => {
const disabled = await systemService.isRegistrationDisabled();
const enabled = await systemService.isRegistrationEnabled();
return c.json<RegistrationStatusDto>({ disabled }, 200);
return c.json<RegistrationStatusDto>({ 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<RegistrationStatusDto>({ disabled: body.disabled }, 200);
return c.json<RegistrationStatusDto>({ enabled: body.enabled }, 200);
},
)
.post(

View file

@ -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({

View file

@ -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<UpdateInfoDto> => {
}
};
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,
};

View file

@ -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");