feat: add CLI command to change username (#342)
* feat: add CLI command to change username * ci: fix wrong folder chmod
This commit is contained in:
parent
52280be3c2
commit
9754e366c1
3 changed files with 90 additions and 2 deletions
4
.github/workflows/e2e.yml
vendored
4
.github/workflows/e2e.yml
vendored
|
|
@ -56,8 +56,8 @@ jobs:
|
||||||
timeout 30s bash -c 'until curl -f http://localhost:4096/healthcheck; do echo "Waiting for server..." && sleep 2; done'
|
timeout 30s bash -c 'until curl -f http://localhost:4096/healthcheck; do echo "Waiting for server..." && sleep 2; done'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
|
||||||
- name: Make data directory writable
|
- name: Make playwright directory writable
|
||||||
run: sudo chmod -R 777 data
|
run: sudo chmod -R 777 playwright
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: bun run test:e2e
|
run: bun run test:e2e
|
||||||
|
|
|
||||||
86
app/server/cli/commands/change-username.ts
Normal file
86
app/server/cli/commands/change-username.ts
Normal file
|
|
@ -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 <username>", "Current username of the account")
|
||||||
|
.option("-n, --new-username <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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
import { changeUsernameCommand } from "./commands/change-username";
|
||||||
import { disable2FACommand } from "./commands/disable-2fa";
|
import { disable2FACommand } from "./commands/disable-2fa";
|
||||||
import { resetPasswordCommand } from "./commands/reset-password";
|
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.name("zerobyte").description("Zerobyte CLI - Backup automation tool built on top of Restic").version("1.0.0");
|
||||||
program.addCommand(resetPasswordCommand);
|
program.addCommand(resetPasswordCommand);
|
||||||
program.addCommand(disable2FACommand);
|
program.addCommand(disable2FACommand);
|
||||||
|
program.addCommand(changeUsernameCommand);
|
||||||
|
|
||||||
export async function runCLI(argv: string[]): Promise<boolean> {
|
export async function runCLI(argv: string[]): Promise<boolean> {
|
||||||
const args = argv.slice(2);
|
const args = argv.slice(2);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue