Browse Source

Add interactive CLI for user management

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
k4be 2 weeks ago
parent
commit
aed4ee7c7c
1 changed files with 184 additions and 0 deletions
  1. 184 0
      gpx-vis-backend/cli.js

+ 184 - 0
gpx-vis-backend/cli.js

@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+'use strict';
+
+const readline = require('readline');
+const bcrypt = require('bcryptjs');
+
+const { initDatabase } = require('./src/database');
+const { User } = require('./src/models');
+
+const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+
+function ask(question) {
+  return new Promise(resolve => rl.question(question, resolve));
+}
+
+function askHidden(question) {
+  return new Promise(resolve => {
+    process.stdout.write(question);
+    const stdin = process.stdin;
+    const wasRaw = stdin.isRaw;
+    stdin.setRawMode(true);
+    stdin.resume();
+    stdin.setEncoding('utf8');
+    let input = '';
+    const onData = ch => {
+      if (ch === '\r' || ch === '\n') {
+        stdin.setRawMode(wasRaw || false);
+        stdin.pause();
+        stdin.removeListener('data', onData);
+        process.stdout.write('\n');
+        resolve(input);
+      } else if (ch === '\u0003') {
+        process.stdout.write('\n');
+        process.exit();
+      } else if (ch === '\u007f' || ch === '\b') {
+        if (input.length > 0) input = input.slice(0, -1);
+      } else {
+        input += ch;
+      }
+    };
+    stdin.on('data', onData);
+  });
+}
+
+function formatDate(d) {
+  return d ? new Date(d).toISOString().slice(0, 10) : '-';
+}
+
+function printUsers(users) {
+  console.log('');
+  const header = `  ${'ID'.padEnd(4)}  ${'Login'.padEnd(20)}  ${'Email'.padEnd(28)}  ${'Active'.padEnd(6)}  ${'Admin'.padEnd(5)}  Created`;
+  console.log(header);
+  console.log('-'.repeat(header.length));
+  for (const u of users) {
+    console.log(
+      `  ${String(u.id).padEnd(4)}  ${u.login.padEnd(20)}  ${(u.email || '').padEnd(28)}  ${(u.isActive ? 'yes' : 'no').padEnd(6)}  ${(u.isAdmin ? 'yes' : 'no').padEnd(5)}  ${formatDate(u.createdAt)}`
+    );
+  }
+  console.log('');
+}
+
+async function listUsers() {
+  const users = await User.findAll({ order: [['id', 'ASC']] });
+  if (users.length === 0) {
+    console.log('No users found.');
+  } else {
+    printUsers(users);
+  }
+}
+
+async function findUser(prompt) {
+  const input = (await ask(prompt)).trim();
+  if (!input) return null;
+  const byId = /^\d+$/.test(input);
+  const user = byId
+    ? await User.findByPk(parseInt(input))
+    : await User.findOne({ where: { login: input } });
+  if (!user) { console.log('User not found.'); return null; }
+  return user;
+}
+
+async function activateDeactivate() {
+  await listUsers();
+  const user = await findUser('Enter user ID or login: ');
+  if (!user) return;
+  const current = user.isActive ? 'active' : 'inactive';
+  const action = user.isActive ? 'deactivate' : 'activate';
+  const confirm = (await ask(`User "${user.login}" is ${current}. ${action}? [y/N] `)).trim().toLowerCase();
+  if (confirm !== 'y') { console.log('Cancelled.'); return; }
+  await user.update({ isActive: !user.isActive });
+  console.log(`User "${user.login}" is now ${user.isActive ? 'active' : 'inactive'}.`);
+}
+
+async function changePassword() {
+  await listUsers();
+  const user = await findUser('Enter user ID or login: ');
+  if (!user) return;
+  const pw1 = await askHidden(`New password for "${user.login}": `);
+  if (!pw1) { console.log('Password cannot be empty.'); return; }
+  const pw2 = await askHidden('Confirm password: ');
+  if (pw1 !== pw2) { console.log('Passwords do not match.'); return; }
+  const hash = await bcrypt.hash(pw1, 10);
+  await user.update({ passwordHash: hash });
+  console.log(`Password updated for "${user.login}".`);
+}
+
+async function toggleAdmin() {
+  await listUsers();
+  const user = await findUser('Enter user ID or login: ');
+  if (!user) return;
+  const current = user.isAdmin ? 'admin' : 'regular user';
+  const action = user.isAdmin ? 'remove admin rights from' : 'grant admin rights to';
+  const confirm = (await ask(`User "${user.login}" is ${current}. ${action}? [y/N] `)).trim().toLowerCase();
+  if (confirm !== 'y') { console.log('Cancelled.'); return; }
+  await user.update({ isAdmin: !user.isAdmin });
+  console.log(`User "${user.login}" is now ${user.isAdmin ? 'an admin' : 'a regular user'}.`);
+}
+
+async function createUser() {
+  const login = (await ask('Login: ')).trim();
+  if (!login) { console.log('Login cannot be empty.'); return; }
+  const existing = await User.findOne({ where: { login } });
+  if (existing) { console.log('Login already taken.'); return; }
+  const email = (await ask('Email (optional): ')).trim() || null;
+  const pw = await askHidden('Password: ');
+  if (!pw) { console.log('Password cannot be empty.'); return; }
+  const pw2 = await askHidden('Confirm password: ');
+  if (pw !== pw2) { console.log('Passwords do not match.'); return; }
+  const isAdmin = (await ask('Admin? [y/N] ')).trim().toLowerCase() === 'y';
+  const isActive = (await ask('Active? [Y/n] ')).trim().toLowerCase() !== 'n';
+  const hash = await bcrypt.hash(pw, 10);
+  const user = await User.create({ login, email, passwordHash: hash, isAdmin, isActive });
+  console.log(`User "${user.login}" created (id=${user.id}).`);
+}
+
+async function deleteUser() {
+  await listUsers();
+  const user = await findUser('Enter user ID or login to delete: ');
+  if (!user) return;
+  const confirm = (await ask(`Delete user "${user.login}" and all their data? [y/N] `)).trim().toLowerCase();
+  if (confirm !== 'y') { console.log('Cancelled.'); return; }
+  await user.destroy();
+  console.log(`User "${user.login}" deleted.`);
+}
+
+const MENU = [
+  { key: '1', label: 'List users',              fn: listUsers },
+  { key: '2', label: 'Activate / deactivate',   fn: activateDeactivate },
+  { key: '3', label: 'Change password',          fn: changePassword },
+  { key: '4', label: 'Toggle admin',             fn: toggleAdmin },
+  { key: '5', label: 'Create user',              fn: createUser },
+  { key: '6', label: 'Delete user',              fn: deleteUser },
+  { key: 'q', label: 'Quit',                     fn: null },
+];
+
+function printMenu() {
+  console.log('\n=== GPX-Vis User Manager ===');
+  for (const item of MENU) {
+    console.log(`  [${item.key}] ${item.label}`);
+  }
+}
+
+async function main() {
+  await initDatabase();
+  console.log('Connected to database.');
+
+  while (true) {
+    printMenu();
+    const choice = (await ask('> ')).trim().toLowerCase();
+    const item = MENU.find(m => m.key === choice);
+    if (!item) { console.log('Invalid choice.'); continue; }
+    if (!item.fn) break;
+    try {
+      await item.fn();
+    } catch (e) {
+      console.error('Error:', e.message);
+    }
+  }
+
+  rl.close();
+  process.exit(0);
+}
+
+main().catch(e => { console.error(e); process.exit(1); });