refactor: enable user registration vs disabling it
This commit is contained in:
parent
343c00cb33
commit
b35fdae5cd
9 changed files with 34 additions and 51 deletions
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
|
||||
export const REGISTRATION_ENABLED_KEY = "registrations_enabled";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue