|
|
@@ -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); });
|