From 85805cd2e2e0917cdcf77faa9a18ecadc9d3258a Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 11 Jan 2026 14:32:44 +0100 Subject: [PATCH] feat: add CLI command to change username --- app/server/cli/commands/change-username.ts | 78 ++++++++++++++++++++++ app/server/cli/index.ts | 2 + 2 files changed, 80 insertions(+) create mode 100644 app/server/cli/commands/change-username.ts diff --git a/app/server/cli/commands/change-username.ts b/app/server/cli/commands/change-username.ts new file mode 100644 index 00000000..8b297c47 --- /dev/null +++ b/app/server/cli/commands/change-username.ts @@ -0,0 +1,78 @@ +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 [existingUser] = await db.select().from(usersTable).where(eq(usersTable.username, newUsername)); + if (existingUser) { + throw new Error(`Username "${newUsername}" is already taken`); + } + + const usernameRegex = /^[a-z0-9_]{3,30}$/; + if (!usernameRegex.test(newUsername)) { + 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: newUsername.toLowerCase().trim() }).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) => (val.length > 0 ? true : "Username cannot be empty"), + }); + } + + 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);