feat: add InstanceSetting model and related database migration; refactor DataManager for instance settings management
This commit is contained in:
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user