From d76734634556543f82604263359b17cb443a5944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?= Date: Mon, 1 Dec 2025 12:29:30 +0100 Subject: [PATCH] admin passwordHash import support --- README.md | 19 +++++++++++++++++-- app/server/modules/lifecycle/startup.ts | 23 ++++++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a40507bd..30b9fc28 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ Secrets/credentials in the config file can reference environment variables using ##### Admin Setup (Automated) -- **Example:** +- **Example (new instance):** ```json { "admin": { @@ -346,11 +346,26 @@ Secrets/credentials in the config file can reference environment variables using } } ``` + +- **Example (migration from another instance):** + ```json + { + "admin": { + "username": "admin", + "passwordHash": "$argon2id$v=19$m=19456,t=2,p=1$...", + "recoveryKey": "${RECOVERY_KEY}" + } + } + ``` + - **Fields:** - `username`: Admin username to create on first startup - - `password`: Admin password (can use `${ENV_VAR}`) + - `password`: Admin password for new instances (can use `${ENV_VAR}`) + - `passwordHash`: Pre-hashed password for migration (exported from another instance) - `recoveryKey`: Optional recovery key (can use `${ENV_VAR}`) - if provided, the UI prompt to download recovery key will be skipped +> **Note:** Use either `password` OR `passwordHash`, not both. The `passwordHash` option is useful when migrating from another Zerobyte instance using an exported config with `includePasswordHash=true`. + **On first startup, Zerobyte will automatically create the admin user from the config file.** > **⚠️ About the Recovery Key** diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts index 77538b00..432ec81c 100644 --- a/app/server/modules/lifecycle/startup.ts +++ b/app/server/modules/lifecycle/startup.ts @@ -176,17 +176,30 @@ export const startup = async () => { try { const { authService } = await import("../auth/auth.service"); - if (configFileAdmin && configFileAdmin.username && configFileAdmin.password) { + if (configFileAdmin && configFileAdmin.username && (configFileAdmin.password || configFileAdmin.passwordHash)) { const hasUsers = await authService.hasUsers(); if (!hasUsers) { - const { user } = await authService.register(configFileAdmin.username, configFileAdmin.password); - logger.info(`Admin user '${configFileAdmin.username}' created from config.`); + let userId: number; + if (configFileAdmin.passwordHash) { + // Import with existing password hash (migration from another instance) + const [user] = await db.insert(usersTable).values({ + username: configFileAdmin.username, + passwordHash: configFileAdmin.passwordHash, + }).returning(); + userId = user.id; + logger.info(`Admin user '${configFileAdmin.username}' imported with password hash from config.`); + } else { + // Create new user with plaintext password + const { user } = await authService.register(configFileAdmin.username, configFileAdmin.password); + userId = user.id; + logger.info(`Admin user '${configFileAdmin.username}' created from config.`); + } if (configFileAdmin.recoveryKey) { - await db.update(usersTable).set({ hasDownloadedResticPassword: true }).where(eq(usersTable.id, user.id)); + await db.update(usersTable).set({ hasDownloadedResticPassword: true }).where(eq(usersTable.id, userId)); } } } else { - logger.warn("Admin config missing required fields (username, password). Skipping automated admin setup."); + logger.warn("Admin config missing required fields (username, password or passwordHash). Skipping automated admin setup."); } } catch (err) { const e = err instanceof Error ? err : new Error(String(err));