cli.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const readline = require('readline');
  4. const bcrypt = require('bcryptjs');
  5. const { initDatabase } = require('./src/database');
  6. const { User } = require('./src/models');
  7. const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  8. function ask(question) {
  9. return new Promise(resolve => rl.question(question, resolve));
  10. }
  11. function askHidden(question) {
  12. return new Promise(resolve => {
  13. process.stdout.write(question);
  14. const stdin = process.stdin;
  15. const wasRaw = stdin.isRaw;
  16. stdin.setRawMode(true);
  17. stdin.resume();
  18. stdin.setEncoding('utf8');
  19. let input = '';
  20. const onData = ch => {
  21. if (ch === '\r' || ch === '\n') {
  22. stdin.setRawMode(wasRaw || false);
  23. stdin.pause();
  24. stdin.removeListener('data', onData);
  25. process.stdout.write('\n');
  26. resolve(input);
  27. } else if (ch === '\u0003') {
  28. process.stdout.write('\n');
  29. process.exit();
  30. } else if (ch === '\u007f' || ch === '\b') {
  31. if (input.length > 0) input = input.slice(0, -1);
  32. } else {
  33. input += ch;
  34. }
  35. };
  36. stdin.on('data', onData);
  37. });
  38. }
  39. function formatDate(d) {
  40. return d ? new Date(d).toISOString().slice(0, 10) : '-';
  41. }
  42. function printUsers(users) {
  43. console.log('');
  44. const header = ` ${'ID'.padEnd(4)} ${'Login'.padEnd(20)} ${'Email'.padEnd(28)} ${'Active'.padEnd(6)} ${'Admin'.padEnd(5)} Created`;
  45. console.log(header);
  46. console.log('-'.repeat(header.length));
  47. for (const u of users) {
  48. console.log(
  49. ` ${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)}`
  50. );
  51. }
  52. console.log('');
  53. }
  54. async function listUsers() {
  55. const users = await User.findAll({ order: [['id', 'ASC']] });
  56. if (users.length === 0) {
  57. console.log('No users found.');
  58. } else {
  59. printUsers(users);
  60. }
  61. }
  62. async function findUser(prompt) {
  63. const input = (await ask(prompt)).trim();
  64. if (!input) return null;
  65. const byId = /^\d+$/.test(input);
  66. const user = byId
  67. ? await User.findByPk(parseInt(input))
  68. : await User.findOne({ where: { login: input } });
  69. if (!user) { console.log('User not found.'); return null; }
  70. return user;
  71. }
  72. async function activateDeactivate() {
  73. await listUsers();
  74. const user = await findUser('Enter user ID or login: ');
  75. if (!user) return;
  76. const current = user.isActive ? 'active' : 'inactive';
  77. const action = user.isActive ? 'deactivate' : 'activate';
  78. const confirm = (await ask(`User "${user.login}" is ${current}. ${action}? [y/N] `)).trim().toLowerCase();
  79. if (confirm !== 'y') { console.log('Cancelled.'); return; }
  80. await user.update({ isActive: !user.isActive });
  81. console.log(`User "${user.login}" is now ${user.isActive ? 'active' : 'inactive'}.`);
  82. }
  83. async function changePassword() {
  84. await listUsers();
  85. const user = await findUser('Enter user ID or login: ');
  86. if (!user) return;
  87. const pw1 = await askHidden(`New password for "${user.login}": `);
  88. if (!pw1) { console.log('Password cannot be empty.'); return; }
  89. const pw2 = await askHidden('Confirm password: ');
  90. if (pw1 !== pw2) { console.log('Passwords do not match.'); return; }
  91. const hash = await bcrypt.hash(pw1, 10);
  92. await user.update({ passwordHash: hash });
  93. console.log(`Password updated for "${user.login}".`);
  94. }
  95. async function toggleAdmin() {
  96. await listUsers();
  97. const user = await findUser('Enter user ID or login: ');
  98. if (!user) return;
  99. const current = user.isAdmin ? 'admin' : 'regular user';
  100. const action = user.isAdmin ? 'remove admin rights from' : 'grant admin rights to';
  101. const confirm = (await ask(`User "${user.login}" is ${current}. ${action}? [y/N] `)).trim().toLowerCase();
  102. if (confirm !== 'y') { console.log('Cancelled.'); return; }
  103. await user.update({ isAdmin: !user.isAdmin });
  104. console.log(`User "${user.login}" is now ${user.isAdmin ? 'an admin' : 'a regular user'}.`);
  105. }
  106. async function createUser() {
  107. const login = (await ask('Login: ')).trim();
  108. if (!login) { console.log('Login cannot be empty.'); return; }
  109. const existing = await User.findOne({ where: { login } });
  110. if (existing) { console.log('Login already taken.'); return; }
  111. const email = (await ask('Email (optional): ')).trim() || null;
  112. const pw = await askHidden('Password: ');
  113. if (!pw) { console.log('Password cannot be empty.'); return; }
  114. const pw2 = await askHidden('Confirm password: ');
  115. if (pw !== pw2) { console.log('Passwords do not match.'); return; }
  116. const isAdmin = (await ask('Admin? [y/N] ')).trim().toLowerCase() === 'y';
  117. const isActive = (await ask('Active? [Y/n] ')).trim().toLowerCase() !== 'n';
  118. const hash = await bcrypt.hash(pw, 10);
  119. const user = await User.create({ login, email, passwordHash: hash, isAdmin, isActive });
  120. console.log(`User "${user.login}" created (id=${user.id}).`);
  121. }
  122. async function deleteUser() {
  123. await listUsers();
  124. const user = await findUser('Enter user ID or login to delete: ');
  125. if (!user) return;
  126. const confirm = (await ask(`Delete user "${user.login}" and all their data? [y/N] `)).trim().toLowerCase();
  127. if (confirm !== 'y') { console.log('Cancelled.'); return; }
  128. await user.destroy();
  129. console.log(`User "${user.login}" deleted.`);
  130. }
  131. const MENU = [
  132. { key: '1', label: 'List users', fn: listUsers },
  133. { key: '2', label: 'Activate / deactivate', fn: activateDeactivate },
  134. { key: '3', label: 'Change password', fn: changePassword },
  135. { key: '4', label: 'Toggle admin', fn: toggleAdmin },
  136. { key: '5', label: 'Create user', fn: createUser },
  137. { key: '6', label: 'Delete user', fn: deleteUser },
  138. { key: 'q', label: 'Quit', fn: null },
  139. ];
  140. function printMenu() {
  141. console.log('\n=== GPX-Vis User Manager ===');
  142. for (const item of MENU) {
  143. console.log(` [${item.key}] ${item.label}`);
  144. }
  145. }
  146. async function main() {
  147. await initDatabase();
  148. console.log('Connected to database.');
  149. while (true) {
  150. printMenu();
  151. const choice = (await ask('> ')).trim().toLowerCase();
  152. const item = MENU.find(m => m.key === choice);
  153. if (!item) { console.log('Invalid choice.'); continue; }
  154. if (!item.fn) break;
  155. try {
  156. await item.fn();
  157. } catch (e) {
  158. console.error('Error:', e.message);
  159. }
  160. }
  161. rl.close();
  162. process.exit(0);
  163. }
  164. main().catch(e => { console.error(e); process.exit(1); });