chore: final cleanup
This commit is contained in:
parent
b29500bcf4
commit
bffed92c84
10 changed files with 53 additions and 50 deletions
|
|
@ -1,6 +1,6 @@
|
|||
SERVER_IP=localhost
|
||||
DATABASE_URL=./data/zerobyte.db
|
||||
RESTIC_PASS_FILE=./data/restic.pass
|
||||
RESTIC_CACHE_DIR=./data/restic/cache
|
||||
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
|
||||
ZEROBYTE_VOLUMES_DIR=./data/volumes
|
||||
APP_SECRET=<openssl rand -hex 32>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
DATABASE_URL=:memory:
|
||||
ZEROBYTE_APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69
|
||||
APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69
|
||||
|
|
|
|||
|
|
@ -185,7 +185,6 @@ Zerobyte is a wrapper around Restic for backup operations. Key integration point
|
|||
|
||||
- `buildRepoUrl()` - Constructs repository URLs for different backends
|
||||
- `buildEnv()` - Sets environment variables (credentials, cache dir)
|
||||
- `ensurePassfile()` - Manages encryption password file
|
||||
- Type-safe parsing of Restic JSON output using ArkType schemas
|
||||
|
||||
**Rclone Integration** (`app/server/modules/repositories/`):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { eq } from "drizzle-orm";
|
|||
import { config } from "../server/core/config";
|
||||
import { db } from "../server/db/db";
|
||||
import { cryptoUtils } from "../server/utils/crypto";
|
||||
import { organization as organizationTable, member } from "../server/db/schema";
|
||||
import { organization as organizationTable, member, usersTable } from "../server/db/schema";
|
||||
import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user";
|
||||
|
||||
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
||||
|
|
@ -55,25 +55,31 @@ const createBetterAuth = (secret: string) =>
|
|||
resticPassword: await cryptoUtils.sealSecret(resticPassword),
|
||||
};
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const orgId = Bun.randomUUIDv7();
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
const orgId = Bun.randomUUIDv7();
|
||||
|
||||
await tx.insert(organizationTable).values({
|
||||
name: `${user.name}'s Workspace`,
|
||||
slug: slug,
|
||||
id: orgId,
|
||||
createdAt: new Date(),
|
||||
metadata,
|
||||
});
|
||||
await tx.insert(organizationTable).values({
|
||||
name: `${user.name}'s Workspace`,
|
||||
slug: slug,
|
||||
id: orgId,
|
||||
createdAt: new Date(),
|
||||
metadata,
|
||||
});
|
||||
|
||||
await tx.insert(member).values({
|
||||
id: Bun.randomUUIDv7(),
|
||||
userId: user.id,
|
||||
role: "owner",
|
||||
organizationId: orgId,
|
||||
createdAt: new Date(),
|
||||
await tx.insert(member).values({
|
||||
id: Bun.randomUUIDv7(),
|
||||
userId: user.id,
|
||||
role: "owner",
|
||||
organizationId: orgId,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
await db.delete(usersTable).where(eq(usersTable.id, user.id));
|
||||
|
||||
throw new Error(`Failed to create organization for user ${user.id}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const envSchema = type({
|
|||
APP_VERSION: "string = 'dev'",
|
||||
TRUSTED_ORIGINS: "string?",
|
||||
DISABLE_RATE_LIMITING: 'string = "false"',
|
||||
ZEROBYTE_APP_SECRET: "32 <= string <= 256",
|
||||
APP_SECRET: "32 <= string <= 256",
|
||||
}).pipe((s) => ({
|
||||
__prod__: s.NODE_ENV === "production",
|
||||
environment: s.NODE_ENV,
|
||||
|
|
@ -46,25 +46,25 @@ const envSchema = type({
|
|||
appVersion: s.APP_VERSION,
|
||||
trustedOrigins: s.TRUSTED_ORIGINS?.split(",").map((origin) => origin.trim()),
|
||||
disableRateLimiting: s.DISABLE_RATE_LIMITING === "true",
|
||||
appSecret: s.ZEROBYTE_APP_SECRET,
|
||||
appSecret: s.APP_SECRET,
|
||||
}));
|
||||
|
||||
const parseConfig = (env: unknown) => {
|
||||
const result = envSchema(env);
|
||||
|
||||
if (result instanceof type.errors) {
|
||||
if (!process.env.ZEROBYTE_APP_SECRET) {
|
||||
if (!process.env.APP_SECRET) {
|
||||
const errorMessage = [
|
||||
"",
|
||||
"================================================================================",
|
||||
"ZEROBYTE_APP_SECRET is not configured.",
|
||||
"APP_SECRET is not configured.",
|
||||
"",
|
||||
"This secret is required for encrypting sensitive data in the database.",
|
||||
"",
|
||||
"To generate a new secret, run:",
|
||||
" openssl rand -hex 32",
|
||||
"",
|
||||
"Then set the ZEROBYTE_APP_SECRET environment variable with the generated value.",
|
||||
"Then set the APP_SECRET environment variable with the generated value.",
|
||||
"",
|
||||
"IMPORTANT: Store this secret securely and back it up. If lost, encrypted data",
|
||||
"in the database will be unrecoverable.",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { RESTIC_PASS_FILE } from "~/server/core/constants";
|
|||
*
|
||||
* This migration performs two critical tasks:
|
||||
* 1. Assigns unique restic passwords to each organization (using the legacy password for existing orgs)
|
||||
* 2. Re-keys all encrypted secrets from the legacy restic passfile to use the new ZEROBYTE_APP_SECRET
|
||||
* 2. Re-keys all encrypted secrets from the legacy restic passfile to use the new APP_SECRET
|
||||
*
|
||||
* This allows per-organization encryption key isolation while ensuring
|
||||
* database encryption is decoupled from restic repository passwords.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { and, eq, or } from "drizzle-orm";
|
|||
import { db } from "../../db/db";
|
||||
import { backupSchedulesTable, volumesTable } from "../../db/schema";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { restic } from "../../utils/restic";
|
||||
import { volumeService } from "../volumes/volume.service";
|
||||
import { CleanupDanglingMountsJob } from "../../jobs/cleanup-dangling";
|
||||
import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
||||
|
|
@ -55,10 +54,6 @@ export const startup = async () => {
|
|||
await Scheduler.start();
|
||||
await Scheduler.clear();
|
||||
|
||||
await restic.ensurePassfile().catch((err) => {
|
||||
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
|
||||
});
|
||||
|
||||
await initAuth().catch((err) => {
|
||||
logger.error(`Error initializing auth: ${toMessage(err)}`);
|
||||
throw err;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,9 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||
}
|
||||
|
||||
const errorMessage = toMessage(error);
|
||||
await db.delete(repositoriesTable).where(and(eq(repositoriesTable.id, id), eq(repositoriesTable.organizationId, organizationId)));
|
||||
await db
|
||||
.delete(repositoriesTable)
|
||||
.where(and(eq(repositoriesTable.id, id), eq(repositoriesTable.organizationId, organizationId)));
|
||||
|
||||
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
|
||||
};
|
||||
|
|
@ -155,7 +157,9 @@ const deleteRepository = async (id: string) => {
|
|||
|
||||
await db
|
||||
.delete(repositoriesTable)
|
||||
.where(and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)));
|
||||
.where(
|
||||
and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)),
|
||||
);
|
||||
|
||||
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||
cache.delByPrefix(`ls:${repository.id}:`);
|
||||
|
|
@ -330,7 +334,9 @@ const checkHealth = async (repositoryId: string) => {
|
|||
lastChecked: Date.now(),
|
||||
lastError: error,
|
||||
})
|
||||
.where(and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)));
|
||||
.where(
|
||||
and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)),
|
||||
);
|
||||
|
||||
return { lastError: error };
|
||||
} finally {
|
||||
|
|
@ -420,7 +426,9 @@ const doctorRepository = async (id: string) => {
|
|||
lastChecked: Date.now(),
|
||||
lastError: doctorError,
|
||||
})
|
||||
.where(and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)));
|
||||
.where(
|
||||
and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)),
|
||||
);
|
||||
|
||||
return {
|
||||
success: doctorSucceeded,
|
||||
|
|
@ -466,7 +474,11 @@ const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
|
|||
}
|
||||
};
|
||||
|
||||
const tagSnapshots = async (id: string, snapshotIds: string[], tags: { add?: string[]; remove?: string[]; set?: string[] }) => {
|
||||
const tagSnapshots = async (
|
||||
id: string,
|
||||
snapshotIds: string[],
|
||||
tags: { add?: string[]; remove?: string[]; set?: string[] },
|
||||
) => {
|
||||
const organizationId = getOrganizationId();
|
||||
const repository = await findRepository(id);
|
||||
|
||||
|
|
|
|||
|
|
@ -64,17 +64,6 @@ const snapshotInfoSchema = type({
|
|||
}).optional(),
|
||||
});
|
||||
|
||||
const ensurePassfile = async () => {
|
||||
await fs.mkdir(path.dirname(RESTIC_PASS_FILE), { recursive: true });
|
||||
|
||||
try {
|
||||
await fs.access(RESTIC_PASS_FILE);
|
||||
} catch {
|
||||
logger.info("Restic passfile not found, creating a new one...");
|
||||
await fs.writeFile(RESTIC_PASS_FILE, crypto.randomBytes(32).toString("hex"), { mode: 0o600 });
|
||||
}
|
||||
};
|
||||
|
||||
export const buildRepoUrl = (config: RepositoryConfig): string => {
|
||||
switch (config.backend) {
|
||||
case "local":
|
||||
|
|
@ -239,8 +228,6 @@ export const buildEnv = async (config: RepositoryConfig, organizationId: string)
|
|||
};
|
||||
|
||||
const init = async (config: RepositoryConfig, organizationId: string) => {
|
||||
await ensurePassfile();
|
||||
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
|
||||
logger.info(`Initializing restic repository at ${repoUrl}...`);
|
||||
|
|
@ -956,7 +943,6 @@ export const addCommonArgs = (
|
|||
};
|
||||
|
||||
export const restic = {
|
||||
ensurePassfile,
|
||||
init,
|
||||
backup,
|
||||
restore,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ services:
|
|||
- SYS_ADMIN
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b
|
||||
ports:
|
||||
- "4096:4096"
|
||||
volumes:
|
||||
|
|
@ -21,6 +22,7 @@ services:
|
|||
- ~/.config/rclone:/root/.config/rclone
|
||||
|
||||
zerobyte-prod:
|
||||
# image: ghcr.io/nicotsx/zerobyte:v0.22.0
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
|
@ -35,6 +37,8 @@ services:
|
|||
- SYS_ADMIN
|
||||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
|
|
@ -49,6 +53,7 @@ services:
|
|||
restart: unless-stopped
|
||||
environment:
|
||||
- DISABLE_RATE_LIMITING=true
|
||||
- APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
cap_add:
|
||||
|
|
|
|||
Loading…
Reference in a new issue