From b2098f6beb31354a0a621f5e3927a52287ac3183 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:53:57 +0100 Subject: [PATCH] feat: add CLI command to change username (#342) * feat: add CLI command to change username * ci: fix wrong folder chmod --- .github/workflows/e2e.yml | 4 +- app/server/cli/commands/change-username.ts | 86 ++++++++++++++++++++++ app/server/cli/index.ts | 2 + 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/server/cli/commands/change-username.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 747d5888..9bb4f948 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -56,8 +56,8 @@ jobs: timeout 30s bash -c 'until curl -f http://localhost:4096/healthcheck; do echo "Waiting for server..." && sleep 2; done' continue-on-error: false - - name: Make data directory writable - run: sudo chmod -R 777 data + - name: Make playwright directory writable + run: sudo chmod -R 777 playwright - name: Run Playwright tests run: bun run test:e2e diff --git a/app/server/cli/commands/change-username.ts b/app/server/cli/commands/change-username.ts new file mode 100644 index 00000000..7e13bb61 --- /dev/null +++ b/app/server/cli/commands/change-username.ts @@ -0,0 +1,86 @@ +import { input, select } from "@inquirer/prompts"; +import { Command } from "commander"; +import { eq } from "drizzle-orm"; +import { toMessage } from "~/server/utils/errors"; +import { db } from "../../db/db"; +import { sessionsTable, usersTable } from "../../db/schema"; + +const listUsers = () => { + return db.select({ id: usersTable.id, username: usersTable.username }).from(usersTable); +}; + +const changeUsername = async (oldUsername: string, newUsername: string) => { + const [user] = await db.select().from(usersTable).where(eq(usersTable.username, oldUsername)); + + if (!user) { + throw new Error(`User "${oldUsername}" not found`); + } + + const normalizedUsername = newUsername.toLowerCase().trim(); + + const [existingUser] = await db.select().from(usersTable).where(eq(usersTable.username, normalizedUsername)); + if (existingUser) { + throw new Error(`Username "${newUsername}" is already taken`); + } + + const usernameRegex = /^[a-z0-9_]{3,30}$/; + if (!usernameRegex.test(normalizedUsername)) { + throw new Error( + `Invalid username "${newUsername}". Usernames must be 3-30 characters long and can only contain lowercase letters, numbers, and underscores.`, + ); + } + + await db.transaction(async (tx) => { + await tx.update(usersTable).set({ username: normalizedUsername }).where(eq(usersTable.id, user.id)); + await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)); + }); +}; + +export const changeUsernameCommand = new Command("change-username") + .description("Change username for a user") + .option("-u, --username ", "Current username of the account") + .option("-n, --new-username ", "New username for the account") + .action(async (options) => { + console.info("\n👤 Zerobyte Change Username\n"); + + let username = options.username; + let newUsername = options.newUsername; + + try { + if (!username) { + const users = await listUsers(); + + if (users.length === 0) { + console.error("No users found in the database."); + return; + } + + username = await select({ + message: "Select a user to change username for:", + choices: users.map((u) => ({ + name: u.username, + value: u.username, + })), + }); + } + + if (!newUsername) { + newUsername = await input({ + message: "Enter the new username:", + validate: (val) => { + const usernameRegex = /^[a-z0-9_]{3,30}$/; + return usernameRegex.test(val) + ? true + : "Username must be 3-30 characters and contain only lowercase letters, numbers, or underscores"; + }, + }); + newUsername = newUsername.toLowerCase().trim(); + } + + await changeUsername(username, newUsername); + console.info(`\n✅ Username for "${username}" has been changed to "${newUsername}" successfully.`); + } catch (error) { + console.error(`\n❌ Error: ${toMessage(error)}`); + process.exit(1); + } + }); diff --git a/app/server/cli/index.ts b/app/server/cli/index.ts index 74a6d074..fe54cd99 100644 --- a/app/server/cli/index.ts +++ b/app/server/cli/index.ts @@ -1,4 +1,5 @@ import { Command } from "commander"; +import { changeUsernameCommand } from "./commands/change-username"; import { disable2FACommand } from "./commands/disable-2fa"; import { resetPasswordCommand } from "./commands/reset-password"; @@ -7,6 +8,7 @@ const program = new Command(); program.name("zerobyte").description("Zerobyte CLI - Backup automation tool built on top of Restic").version("1.0.0"); program.addCommand(resetPasswordCommand); program.addCommand(disable2FACommand); +program.addCommand(changeUsernameCommand); export async function runCLI(argv: string[]): Promise { const args = argv.slice(2);