feat: add InstanceSetting model and related database migration; refactor DataManager for instance settings management

This commit is contained in:
Space-Banane
2026-07-07 20:56:22 +02:00
parent a707f22d16
commit fbf4588586
7 changed files with 98 additions and 166 deletions
@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE `InstanceSetting` (
`type` ENUM('instance_uuid', 'registration_disabled', 'link_lock', 'guest_access_disabled', 'external_access_disabled', 'disabled_images', 'link_status') NOT NULL,
`value` TEXT NOT NULL,
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`type`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+16
View File
@@ -11,6 +11,22 @@ enum UserRole {
User
}
enum InstanceSettingType {
instance_uuid
registration_disabled
link_lock
guest_access_disabled
external_access_disabled
disabled_images
link_status
}
model InstanceSetting {
type InstanceSettingType @id
value String @db.Text
updatedAt DateTime @updatedAt
}
model User {
id Int @id @default(autoincrement())
displayName String @db.VarChar(128)
+3 -14
View File
@@ -1,13 +1,12 @@
import { env } from "./lib/env"; // must be first — loads dotenv and validates
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { Server } from "rjweb-server";
import { Runtime } from "@rjweb/runtime-node";
import { network } from "@rjweb/utils";
import { existsSync } from "node:fs";
import { executeFunction } from "./lib/Runner";
import { performGitPull } from "./lib/GitOps";
import { getUUID, prevDirectory } from "./lib/DataManager";
import { getUUID } from "./lib/DataManager";
import { prisma } from "./lib/db";
import { join } from "path";
import { logger } from "./lib/logger";
import { corsMiddleware, initCorsDomains } from "./lib/middlewares/cors";
@@ -41,13 +40,7 @@ export const API_KEY_HEADER = "x-access-key";
export const INSTANCE_SECRET = env.INSTANCE_SECRET;
export const API_URL = env.REACT_APP_API_URL;
const _adapter = new PrismaMariaDb(env.DATABASE_URL);
export const prisma = new PrismaClient({
adapter: _adapter,
log: ["info", "error", "warn"],
errorFormat: "pretty",
transactionOptions: { timeout: 30000, maxWait: 20000 },
});
export { prisma };
const CORS_DOMAINS = env.CORS_URLS.split(",");
CORS_DOMAINS.push(URL);
@@ -61,14 +54,10 @@ if (env.NODE_ENV !== "test") {
logger.info(`Reachable on ${env.PORT}; For example: ${env.REACT_APP_API_URL}`);
}
const dataPath = join(prevDirectory, ".data");
const uiBuildPath = join(__dirname, "../../UI/build");
const uiIndexPath = join(uiBuildPath, "index.html");
const hasUiBuild = existsSync(uiBuildPath);
const hasUiIndex = existsSync(uiIndexPath);
if (env.NODE_ENV !== "test") {
logger.debug(`DataManager: Using data directory at ${dataPath}`);
}
export const server = new Server(
Runtime,
+58 -148
View File
@@ -1,206 +1,116 @@
// Manages the .data directory, which contains config and info files for cross platform communication
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { InstanceSettingType } from "@prisma/client";
import { prisma } from "./db";
import { createLogger } from "./logger";
const log = createLogger("DataManager");
/* Files and what they contain:
- .uuid: A unique identifier for this instance. Generated on the first run and stored in the .data directory.
- .linked: A JSON file indicating whether this instance is linked to a global user account.
*/
// We are always in the Backend/dist directory, so we need to go up two levels to get to the .data directory
// But when we are in a container, we cant really
// export const prevDirectory = env.CI ? "../" : "../../";
export const prevDirectory = "../../"; // For now, as we should just mount the containers .data directory to the host's /app/.data directory
async function ensureDataDirectory() {
try {
if (!existsSync(prevDirectory + ".data")) {
mkdirSync(prevDirectory + ".data");
}
} catch (err) {
log.error({ err }, "Error ensuring .data directory exists");
throw new Error("Failed to ensure .data directory exists");
}
async function getSetting(type: InstanceSettingType): Promise<string | null> {
const row = await prisma.instanceSetting.findUnique({ where: { type } });
return row?.value ?? null;
}
async function setupUUID() {
await ensureDataDirectory();
if (existsSync(prevDirectory + ".data/.uuid")) {
return { error: "UUID already exists" };
}
const newUUID = crypto.randomUUID();
try {
writeFileSync(prevDirectory + ".data/.uuid", newUUID, "utf-8");
return { uuid: newUUID };
} catch (err) {
log.error({ err }, "Error writing .uuid file");
return { error: "Failed to create UUID" };
}
async function setSetting(type: InstanceSettingType, value: string): Promise<void> {
await prisma.instanceSetting.upsert({
where: { type },
create: { type, value },
update: { value },
});
}
export async function getUUID() {
await ensureDataDirectory();
export async function getUUID(): Promise<string | null> {
const val = await getSetting("instance_uuid");
if (val !== null) return val;
const uuid = crypto.randomUUID();
try {
const uuid = readFileSync(prevDirectory + ".data/.uuid", "utf-8");
await setSetting("instance_uuid", uuid);
log.info({ uuid }, "Generated new instance UUID");
return uuid;
} catch (err) {
log.warn({ err }, "Could not read .uuid file, attempting to create one");
const setupResult = await setupUUID();
if (setupResult.error) {
log.error({ error: setupResult.error }, "Error setting up UUID");
return null;
}
log.info({ uuid: setupResult.uuid }, "Successfully created new UUID");
return setupResult.uuid;
log.error({ err }, "Failed to persist instance UUID");
return null;
}
}
type LinkStatusLinked = { linked: true; global_user_email: string };
type LinkStatusUnlinked = { linked: false };
export type LinkStatus = LinkStatusLinked | LinkStatusUnlinked;
export type LinkStatus =
| { linked: true; global_user_email: string }
| { linked: false };
export async function getLinkStatus(): Promise<LinkStatus> {
await ensureDataDirectory();
try {
const raw = readFileSync(prevDirectory + ".data/.linked", "utf-8");
const parsed = JSON.parse(raw) as LinkStatus;
return parsed;
} catch {
return { linked: false };
const val = await getSetting("link_status");
if (val !== null) {
try {
return JSON.parse(val) as LinkStatus;
} catch {
return { linked: false };
}
}
return { linked: false };
}
export async function setLinkStatus(status: LinkStatus): Promise<void> {
await ensureDataDirectory();
try {
writeFileSync(prevDirectory + ".data/.linked", JSON.stringify(status), "utf-8");
} catch (err) {
log.error({ err }, "Error writing .linked file");
throw new Error("Failed to write link status");
}
await setSetting("link_status", JSON.stringify(status));
}
export async function getData() {
await ensureDataDirectory();
const uuid = await getUUID();
const linkStatus = await getLinkStatus();
return {
uuid: uuid,
linkStatus: linkStatus,
};
return { uuid, linkStatus };
}
export async function getLinkLock(): Promise<boolean> {
await ensureDataDirectory();
try {
const raw = readFileSync(prevDirectory + ".data/.linkLock", "utf-8");
return JSON.parse(raw) === true;
} catch {
return false;
}
const val = await getSetting("link_lock");
if (val !== null) return val === "true";
return false;
}
export async function setLinkLock(locked: boolean): Promise<void> {
await ensureDataDirectory();
try {
writeFileSync(prevDirectory + ".data/.linkLock", JSON.stringify(locked), "utf-8");
} catch (err) {
log.error({ err }, "Error writing .linkLock file");
throw new Error("Failed to write link lock status");
}
await setSetting("link_lock", String(locked));
}
export async function getRegistrationDisabled(): Promise<boolean> {
await ensureDataDirectory();
try {
const raw = readFileSync(prevDirectory + ".data/.registrationDisabled", "utf-8");
return JSON.parse(raw) === true;
} catch {
return false;
}
const val = await getSetting("registration_disabled");
if (val !== null) return val === "true";
return false;
}
export async function setRegistrationDisabled(disabled: boolean): Promise<void> {
await ensureDataDirectory();
try {
writeFileSync(prevDirectory + ".data/.registrationDisabled", JSON.stringify(disabled), "utf-8");
} catch (err) {
log.error({ err }, "Error writing .registrationDisabled file");
throw new Error("Failed to write registration disabled status");
}
await setSetting("registration_disabled", String(disabled));
}
export async function getGuestAccessDisabled(): Promise<boolean> {
await ensureDataDirectory();
try {
const raw = readFileSync(prevDirectory + ".data/.guestAccessDisabled", "utf-8");
return JSON.parse(raw) === true;
} catch {
return false;
}
const val = await getSetting("guest_access_disabled");
if (val !== null) return val === "true";
return false;
}
export async function setGuestAccessDisabled(disabled: boolean): Promise<void> {
await ensureDataDirectory();
try {
writeFileSync(prevDirectory + ".data/.guestAccessDisabled", JSON.stringify(disabled), "utf-8");
} catch (err) {
log.error({ err }, "Error writing .guestAccessDisabled file");
throw new Error("Failed to write guest access disabled status");
}
await setSetting("guest_access_disabled", String(disabled));
}
export async function getExternalAccessDisabled(): Promise<boolean> {
await ensureDataDirectory();
try {
const raw = readFileSync(prevDirectory + ".data/.externalAccessDisabled", "utf-8");
return JSON.parse(raw) === true;
} catch {
return false;
}
const val = await getSetting("external_access_disabled");
if (val !== null) return val === "true";
return false;
}
export async function setExternalAccessDisabled(disabled: boolean): Promise<void> {
await ensureDataDirectory();
try {
writeFileSync(prevDirectory + ".data/.externalAccessDisabled", JSON.stringify(disabled), "utf-8");
} catch (err) {
log.error({ err }, "Error writing .externalAccessDisabled file");
throw new Error("Failed to write external access disabled status");
}
await setSetting("external_access_disabled", String(disabled));
}
export async function getDisabledImages(): Promise<string[]> {
await ensureDataDirectory();
try {
const raw = readFileSync(prevDirectory + ".data/.disabledImages", "utf-8");
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
const val = await getSetting("disabled_images");
if (val !== null) {
try {
const parsed = JSON.parse(val);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
return [];
}
export async function setDisabledImages(images: string[]): Promise<void> {
await ensureDataDirectory();
try {
writeFileSync(prevDirectory + ".data/.disabledImages", JSON.stringify(images), "utf-8");
} catch (err) {
log.error({ err }, "Error writing .disabledImages file");
throw new Error("Failed to write disabled images list");
}
await setSetting("disabled_images", JSON.stringify(images));
}
+2 -3
View File
@@ -1,4 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { prisma as defaultPrisma } from "./db";
export class StorageServiceError extends Error {
constructor(
@@ -65,9 +66,7 @@ export class FunctionStorageService {
return this.db;
}
// Resolve lazily to avoid importing the Prisma singleton while index.ts
// is still initializing its exports.
return require("..").prisma as StoragePrisma;
return defaultPrisma;
}
async createStorage(userId: number, name: string, purpose: string) {
+11
View File
@@ -0,0 +1,11 @@
import { env } from "./env";
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
const _adapter = new PrismaMariaDb(env.DATABASE_URL);
export const prisma = new PrismaClient({
adapter: _adapter,
log: ["info", "error", "warn"],
errorFormat: "pretty",
transactionOptions: { timeout: 30000, maxWait: 20000 },
});
-1
View File
@@ -13,7 +13,6 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./shsf_data:/opt/shsf_data
- ./.data:/app/.data
# To use a local MariaDB database, uncomment the following service and update your .env DATABASE_URL to use 'db' as the host:
# database: