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
|
* Registration status
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
disabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -4155,7 +4155,7 @@ export type GetRegistrationStatusResponse = GetRegistrationStatusResponses[keyof
|
||||||
|
|
||||||
export type SetRegistrationStatusData = {
|
export type SetRegistrationStatusData = {
|
||||||
body?: {
|
body?: {
|
||||||
disabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
path?: never;
|
path?: never;
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -4167,7 +4167,7 @@ export type SetRegistrationStatusResponses = {
|
||||||
* Registration status updated
|
* Registration status updated
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
disabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
|
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">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="disable-registrations" className="text-base">
|
<Label htmlFor="enable-registrations" className="text-base">
|
||||||
Disable new user registrations
|
Enable new user registrations
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground max-w-2xl">
|
<p className="text-sm text-muted-foreground max-w-2xl">When enabled, new users can sign up</p>
|
||||||
When enabled, new users cannot sign up. Only administrators can create accounts manually.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="disable-registrations"
|
id="enable-registrations"
|
||||||
checked={registrationStatusQuery.data?.disabled ?? false}
|
checked={registrationStatusQuery.data?.enabled ?? false}
|
||||||
onCheckedChange={(checked) => updateRegistrationStatusMutation.mutate({ body: { disabled: checked } })}
|
onCheckedChange={(checked) => updateRegistrationStatusMutation.mutate({ body: { enabled: checked } })}
|
||||||
disabled={registrationStatusQuery.isLoading || updateRegistrationStatusMutation.isPending}
|
disabled={registrationStatusQuery.isLoading || updateRegistrationStatusMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import { logger } from "~/server/utils/logger";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { appMetadataTable } from "~/server/db/schema";
|
import { appMetadataTable } from "~/server/db/schema";
|
||||||
import { ForbiddenError } from "http-errors-enhanced";
|
import { ForbiddenError } from "http-errors-enhanced";
|
||||||
|
import { REGISTRATION_ENABLED_KEY } from "~/client/lib/constants";
|
||||||
export const REGISTRATION_DISABLED_KEY = "registrationsDisabled";
|
|
||||||
|
|
||||||
export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => {
|
export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => {
|
||||||
const { path } = ctx;
|
const { path } = ctx;
|
||||||
|
|
@ -16,11 +15,11 @@ export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.query.appMetadataTable.findFirst({
|
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) {
|
if (result?.value !== "true" && existingUser) {
|
||||||
logger.info("User registration attempt blocked: registrations are disabled.");
|
logger.info("User registration attempt blocked: registrations are not enabled.");
|
||||||
throw new ForbiddenError("User registrations are currently disabled. Please contact an administrator for access.");
|
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 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);
|
return c.json<UpdateInfoDto>(updates, 200);
|
||||||
})
|
})
|
||||||
.get("/registration-status", getRegistrationStatusDto, async (c) => {
|
.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(
|
.put(
|
||||||
"/registration-status",
|
"/registration-status",
|
||||||
|
|
@ -57,9 +57,9 @@ export const systemController = new Hono()
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json");
|
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(
|
.post(
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,8 @@ export const downloadResticPasswordBodySchema = type({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadResticPasswordDto = describeRoute({
|
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"],
|
tags: ["System"],
|
||||||
operationId: "downloadResticPassword",
|
operationId: "downloadResticPassword",
|
||||||
responses: {
|
responses: {
|
||||||
|
|
@ -81,13 +82,13 @@ export const downloadResticPasswordDto = describeRoute({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registrationStatusResponse = type({
|
export const registrationStatusResponse = type({
|
||||||
disabled: "boolean",
|
enabled: "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RegistrationStatusDto = typeof registrationStatusResponse.infer;
|
export type RegistrationStatusDto = typeof registrationStatusResponse.infer;
|
||||||
|
|
||||||
export const registrationStatusBody = type({
|
export const registrationStatusBody = type({
|
||||||
disabled: "boolean",
|
enabled: "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getRegistrationStatusDto = describeRoute({
|
export const getRegistrationStatusDto = describeRoute({
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import { logger } from "~/server/utils/logger";
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { appMetadataTable } from "../../db/schema";
|
import { appMetadataTable } from "../../db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { REGISTRATION_ENABLED_KEY } from "~/client/lib/constants";
|
||||||
|
|
||||||
const CACHE_TTL = 60 * 60;
|
const CACHE_TTL = 60 * 60;
|
||||||
const REGISTRATION_DISABLED_KEY = "registrationsDisabled";
|
|
||||||
|
|
||||||
const getSystemInfo = async () => {
|
const getSystemInfo = async () => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -89,28 +89,28 @@ const getUpdates = async (): Promise<UpdateInfoDto> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRegistrationDisabled = async () => {
|
const isRegistrationEnabled = async () => {
|
||||||
const result = await db.query.appMetadataTable.findFirst({
|
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";
|
return result?.value === "true";
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRegistrationDisabled = async (disabled: boolean) => {
|
const setRegistrationEnabled = async (enabled: boolean) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(appMetadataTable)
|
.insert(appMetadataTable)
|
||||||
.values({ key: REGISTRATION_DISABLED_KEY, value: JSON.stringify(disabled), createdAt: now, updatedAt: now })
|
.values({ key: REGISTRATION_ENABLED_KEY, value: JSON.stringify(enabled), createdAt: now, updatedAt: now })
|
||||||
.onConflictDoUpdate({ target: appMetadataTable.key, set: { value: JSON.stringify(disabled), 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 = {
|
export const systemService = {
|
||||||
getSystemInfo,
|
getSystemInfo,
|
||||||
getUpdates,
|
getUpdates,
|
||||||
isRegistrationDisabled,
|
isRegistrationEnabled,
|
||||||
setRegistrationDisabled,
|
setRegistrationEnabled,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { config, getAppSecret } from "../core/config";
|
import { config } from "../core/config";
|
||||||
import { isNodeJSErrnoException } from "./fs";
|
import { isNodeJSErrnoException } from "./fs";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
|
@ -96,10 +96,8 @@ const encrypt = async (data: string) => {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = getAppSecret();
|
|
||||||
|
|
||||||
const salt = crypto.randomBytes(16);
|
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 iv = crypto.randomBytes(12);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||||
|
|
@ -118,13 +116,11 @@ const decrypt = async (encryptedData: string) => {
|
||||||
return encryptedData;
|
return encryptedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = getAppSecret();
|
|
||||||
|
|
||||||
const parts = encryptedData.split(":").slice(1); // Remove prefix
|
const parts = encryptedData.split(":").slice(1); // Remove prefix
|
||||||
const saltHex = parts.shift() as string;
|
const saltHex = parts.shift() as string;
|
||||||
const salt = Buffer.from(saltHex, "hex");
|
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 iv = Buffer.from(parts.shift() as string, "hex");
|
||||||
const encrypted = Buffer.from(parts.shift() as string, "hex");
|
const encrypted = Buffer.from(parts.shift() as string, "hex");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue