diff --git a/manage-users.js b/manage-users.js
new file mode 100644
index 0000000..8f34cc7
--- /dev/null
+++ b/manage-users.js
@@ -0,0 +1,195 @@
+#!/usr/bin/env node
+import { input, password, select, confirm } from '@inquirer/prompts';
+import Database from 'better-sqlite3';
+import bcrypt from 'bcrypt';
+
+const DB_FILE = 'ac_data.db';
+
+const db = new Database(DB_FILE);
+db.exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'user',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+`);
+
+const insertUser = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)');
+const getAllUsers = db.prepare('SELECT id, username, role, created_at FROM users ORDER BY id');
+const getUserById = db.prepare('SELECT * FROM users WHERE id = ?');
+const updateUserRole = db.prepare('UPDATE users SET role = ? WHERE id = ?');
+const updateUserPassword = db.prepare('UPDATE users SET password_hash = ? WHERE id = ?');
+const deleteUser = db.prepare('DELETE FROM users WHERE id = ?');
+
+console.log('\n╔════════════════════════════════════╗');
+console.log('║ 🔐 User Manager - AC Dashboard ║');
+console.log('╚════════════════════════════════════╝\n');
+
+async function listUsers() {
+ const users = getAllUsers.all();
+ if (users.length === 0) {
+ console.log(' No users found.\n');
+ return;
+ }
+ console.log(' ID │ Username │ Role │ Created');
+ console.log(' ────┼────────────────┼────────┼─────────────────────');
+ users.forEach(u => {
+ const id = String(u.id).padStart(3);
+ const name = u.username.padEnd(14);
+ const role = u.role.padEnd(6);
+ const date = u.created_at?.slice(0, 16) || 'N/A';
+ const roleColor = u.role === 'admin' ? '\x1b[35m' : '\x1b[33m';
+ console.log(` ${id} │ ${name} │ ${roleColor}${role}\x1b[0m │ ${date}`);
+ });
+ console.log('');
+}
+
+async function createUser() {
+ const username = await input({
+ message: 'Username:',
+ validate: v => v.length >= 3 || 'Min 3 characters'
+ });
+
+ const pwd = await password({
+ message: 'Password:',
+ mask: '*',
+ validate: v => v.length >= 4 || 'Min 4 characters'
+ });
+
+ const role = await select({
+ message: 'Role:',
+ choices: [
+ { name: '👤 user', value: 'user' },
+ { name: '👑 admin', value: 'admin' }
+ ]
+ });
+
+ try {
+ const hash = await bcrypt.hash(pwd, 10);
+ insertUser.run(username, hash, role);
+ console.log(`\n✅ User "${username}" created as ${role}\n`);
+ } catch (e) {
+ if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
+ console.log(`\n❌ User "${username}" already exists\n`);
+ } else throw e;
+ }
+}
+
+async function editUser() {
+ const users = getAllUsers.all();
+ if (users.length === 0) {
+ console.log(' No users to edit.\n');
+ return;
+ }
+
+ const userId = await select({
+ message: 'Select user to edit:',
+ choices: users.map(u => ({
+ name: `${u.username} (${u.role})`,
+ value: u.id
+ }))
+ });
+
+ const action = await select({
+ message: 'What to change?',
+ choices: [
+ { name: '🔑 Change password', value: 'password' },
+ { name: '👤 Change role', value: 'role' },
+ { name: '← Back', value: 'back' }
+ ]
+ });
+
+ if (action === 'back') return;
+
+ if (action === 'password') {
+ const pwd = await password({
+ message: 'New password:',
+ mask: '*',
+ validate: v => v.length >= 4 || 'Min 4 characters'
+ });
+ const hash = await bcrypt.hash(pwd, 10);
+ updateUserPassword.run(hash, userId);
+ console.log('\n✅ Password updated\n');
+ }
+
+ if (action === 'role') {
+ const user = getUserById.get(userId);
+ const newRole = await select({
+ message: 'New role:',
+ choices: [
+ { name: '👤 user', value: 'user' },
+ { name: '👑 admin', value: 'admin' }
+ ],
+ default: user.role
+ });
+ updateUserRole.run(newRole, userId);
+ console.log(`\n✅ Role changed to ${newRole}\n`);
+ }
+}
+
+async function removeUser() {
+ const users = getAllUsers.all();
+ if (users.length === 0) {
+ console.log(' No users to delete.\n');
+ return;
+ }
+
+ const userId = await select({
+ message: 'Select user to delete:',
+ choices: users.map(u => ({
+ name: `${u.username} (${u.role})`,
+ value: u.id
+ }))
+ });
+
+ const user = getUserById.get(userId);
+ const confirmed = await confirm({
+ message: `Delete user "${user.username}"?`,
+ default: false
+ });
+
+ if (confirmed) {
+ deleteUser.run(userId);
+ console.log(`\n✅ User "${user.username}" deleted\n`);
+ } else {
+ console.log('\n❌ Cancelled\n');
+ }
+}
+
+async function main() {
+ while (true) {
+ try {
+ const action = await select({
+ message: 'What would you like to do?',
+ choices: [
+ { name: '📋 List users', value: 'list' },
+ { name: '➕ Create user', value: 'create' },
+ { name: '✏️ Edit user', value: 'edit' },
+ { name: '🗑️ Delete user', value: 'delete' },
+ { name: '🚪 Exit', value: 'exit' }
+ ]
+ });
+
+ if (action === 'exit') {
+ console.log('Bye!\n');
+ break;
+ }
+
+ if (action === 'list') await listUsers();
+ if (action === 'create') await createUser();
+ if (action === 'edit') await editUser();
+ if (action === 'delete') await removeUser();
+
+ } catch (e) {
+ if (e.name === 'ExitPromptError') {
+ console.log('\nBye!\n');
+ break;
+ }
+ throw e;
+ }
+ }
+}
+
+main();
diff --git a/package-lock.json b/package-lock.json
index 169dd71..69db587 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,14 +14,19 @@
"@babel/preset-react": "^7.28.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+ "@inquirer/prompts": "^8.1.0",
"@mui/material": "^7.3.6",
"babel-loader": "^10.0.0",
+ "bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"chart.js": "^4.5.1",
"css-loader": "^7.1.2",
"dotenv": "^16.4.5",
"express": "^5.2.1",
"html-webpack-plugin": "^5.6.5",
+ "ink": "^6.5.1",
+ "ink-text-input": "^6.0.0",
+ "jsonwebtoken": "^9.0.3",
"react": "^19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.3",
@@ -31,6 +36,19 @@
"webpack-dev-middleware": "^7.4.5"
}
},
+ "node_modules/@alcalzone/ansi-tokenize": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.2.tgz",
+ "integrity": "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -1771,6 +1789,346 @@
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
+ "node_modules/@inquirer/ansi": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.2.tgz",
+ "integrity": "sha512-SYLX05PwJVnW+WVegZt1T4Ip1qba1ik+pNJPDiqvk6zS5Y/i8PhRzLpGEtVd7sW0G8cMtkD8t4AZYhQwm8vnww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ }
+ },
+ "node_modules/@inquirer/checkbox": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.3.tgz",
+ "integrity": "sha512-xtQP2eXMFlOcAhZ4ReKP2KZvDIBb1AnCfZ81wWXG3DXLVH0f0g4obE0XDPH+ukAEMRcZT0kdX2AS1jrWGXbpxw==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.2",
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/figures": "^2.0.2",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.3.tgz",
+ "integrity": "sha512-lyEvibDFL+NA5R4xl8FUmNhmu81B+LDL9L/MpKkZlQDJZXzG8InxiqYxiAlQYa9cqLLhYqKLQwZqXmSTqCLjyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.0.tgz",
+ "integrity": "sha512-+jD/34T1pK8M5QmZD/ENhOfXdl9Zr+BrQAUc5h2anWgi7gggRq15ZbiBeLoObj0TLbdgW7TAIQRU2boMc9uOKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.2",
+ "@inquirer/figures": "^2.0.2",
+ "@inquirer/type": "^4.0.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^3.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^9.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@inquirer/editor": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.3.tgz",
+ "integrity": "sha512-wYyQo96TsAqIciP/r5D3cFeV8h4WqKQ/YOvTg5yOfP2sqEbVVpbxPpfV3LM5D0EP4zUI3EZVHyIUIllnoIa8OQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/external-editor": "^2.0.2",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/expand": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.3.tgz",
+ "integrity": "sha512-2oINvuL27ujjxd95f6K2K909uZOU2x1WiAl7Wb1X/xOtL8CgQ1kSxzykIr7u4xTkXkXOAkCuF45T588/YKee7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/external-editor": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.2.tgz",
+ "integrity": "sha512-X/fMXK7vXomRWEex1j8mnj7s1mpnTeP4CO/h2gysJhHLT2WjBnLv4ZQEGpm/kcYI8QfLZ2fgW+9kTKD+jeopLg==",
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^2.1.1",
+ "iconv-lite": "^0.7.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.2.tgz",
+ "integrity": "sha512-qXm6EVvQx/FmnSrCWCIGtMHwqeLgxABP8XgcaAoywsL0NFga9gD5kfG0gXiv80GjK9Hsoz4pgGwF/+CjygyV9A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ }
+ },
+ "node_modules/@inquirer/input": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.3.tgz",
+ "integrity": "sha512-4R0TdWl53dtp79Vs6Df2OHAtA2FVNqya1hND1f5wjHWxZJxwDMSNB1X5ADZJSsQKYAJ5JHCTO+GpJZ42mK0Otw==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/number": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.3.tgz",
+ "integrity": "sha512-TjQLe93GGo5snRlu83JxE38ZPqj5ZVggL+QqqAF2oBA5JOJoxx25GG3EGH/XN/Os5WOmKfO8iLVdCXQxXRZIMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/password": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.3.tgz",
+ "integrity": "sha512-rCozGbUMAHedTeYWEN8sgZH4lRCdgG/WinFkit6ZPsp8JaNg2T0g3QslPBS5XbpORyKP/I+xyBO81kFEvhBmjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.2",
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/prompts": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.1.0.tgz",
+ "integrity": "sha512-LsZMdKcmRNF5LyTRuZE5nWeOjganzmN3zwbtNfcs6GPh3I2TsTtF1UYZlbxVfhxd+EuUqLGs/Lm3Xt4v6Az1wA==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/checkbox": "^5.0.3",
+ "@inquirer/confirm": "^6.0.3",
+ "@inquirer/editor": "^5.0.3",
+ "@inquirer/expand": "^5.0.3",
+ "@inquirer/input": "^5.0.3",
+ "@inquirer/number": "^4.0.3",
+ "@inquirer/password": "^5.0.3",
+ "@inquirer/rawlist": "^5.1.0",
+ "@inquirer/search": "^4.0.3",
+ "@inquirer/select": "^5.0.3"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/rawlist": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.1.0.tgz",
+ "integrity": "sha512-yUCuVh0jW026Gr2tZlG3kHignxcrLKDR3KBp+eUgNz+BAdSeZk0e18yt2gyBr+giYhj/WSIHCmPDOgp1mT2niQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/search": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.0.3.tgz",
+ "integrity": "sha512-lzqVw0YwuKYetk5VwJ81Ba+dyVlhseHPx9YnRKQgwXdFS0kEavCz2gngnNhnMIxg8+j1N/rUl1t5s1npwa7bqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/figures": "^2.0.2",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/select": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.3.tgz",
+ "integrity": "sha512-M+ynbwS0ecQFDYMFrQrybA0qL8DV0snpc4kKevCCNaTpfghsRowRY7SlQBeIYNzHqXtiiz4RG9vTOeb/udew7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.2",
+ "@inquirer/core": "^11.1.0",
+ "@inquirer/figures": "^2.0.2",
+ "@inquirer/type": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.2.tgz",
+ "integrity": "sha512-cae7mzluplsjSdgFA6ACLygb5jC8alO0UUnFPyu0E7tNRPrL+q/f8VcSXp+cjZQ7l5CMpDpi2G1+IQvkOiL1Lw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -2525,6 +2883,21 @@
"ajv": "^8.8.2"
}
},
+ "node_modules/ansi-escapes": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
+ "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -2534,6 +2907,30 @@
"node": ">=8"
}
},
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/auto-bind": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
+ "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/babel-loader": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz",
@@ -2642,6 +3039,20 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/better-sqlite3": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
@@ -2763,6 +3174,12 @@
"ieee754": "^1.1.13"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2846,6 +3263,24 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
+ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
+ "license": "MIT"
+ },
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
@@ -2894,6 +3329,58 @@
"node": ">=0.10.0"
}
},
+ "node_modules/cli-boxes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
+ "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
+ "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^7.1.0",
+ "string-width": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -2917,6 +3404,18 @@
"node": ">=6"
}
},
+ "node_modules/code-excerpt": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
+ "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
+ "license": "MIT",
+ "dependencies": {
+ "convert-to-spaces": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@@ -2960,6 +3459,15 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"license": "MIT"
},
+ "node_modules/convert-to-spaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
+ "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
@@ -3270,6 +3778,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -3282,6 +3799,12 @@
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"license": "ISC"
},
+ "node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "license": "MIT"
+ },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -3334,6 +3857,18 @@
"node": ">=4"
}
},
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -3379,6 +3914,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
+ "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -3659,6 +4204,18 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3983,6 +4540,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/indent-string": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+ "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -3995,6 +4564,70 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
+ "node_modules/ink": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/ink/-/ink-6.5.1.tgz",
+ "integrity": "sha512-wF3j/DmkM8q5E+OtfdQhCRw8/0ahkc8CUTgEddxZzpEWPslu7YPL3t64MWRoI9m6upVGpfAg4ms2BBvxCdKRLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alcalzone/ansi-tokenize": "^0.2.1",
+ "ansi-escapes": "^7.2.0",
+ "ansi-styles": "^6.2.1",
+ "auto-bind": "^5.0.1",
+ "chalk": "^5.6.0",
+ "cli-boxes": "^3.0.0",
+ "cli-cursor": "^4.0.0",
+ "cli-truncate": "^5.1.1",
+ "code-excerpt": "^4.0.0",
+ "es-toolkit": "^1.39.10",
+ "indent-string": "^5.0.0",
+ "is-in-ci": "^2.0.0",
+ "patch-console": "^2.0.0",
+ "react-reconciler": "^0.33.0",
+ "signal-exit": "^3.0.7",
+ "slice-ansi": "^7.1.0",
+ "stack-utils": "^2.0.6",
+ "string-width": "^8.1.0",
+ "type-fest": "^4.27.0",
+ "widest-line": "^5.0.0",
+ "wrap-ansi": "^9.0.0",
+ "ws": "^8.18.0",
+ "yoga-layout": "~3.2.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@types/react": ">=19.0.0",
+ "react": ">=19.0.0",
+ "react-devtools-core": "^6.1.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react-devtools-core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ink-text-input": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz",
+ "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "type-fest": "^4.18.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "ink": ">=5",
+ "react": ">=18"
+ }
+ },
"node_modules/interpret": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
@@ -4034,6 +4667,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-in-ci": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz",
+ "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==",
+ "license": "MIT",
+ "bin": {
+ "is-in-ci": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -4123,6 +4786,49 @@
"node": ">=6"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -4178,6 +4884,48 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4287,6 +5035,15 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@@ -4320,6 +5077,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/mute-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
+ "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4381,6 +5147,26 @@
"node": ">=10"
}
},
+ "node_modules/node-addon-api": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
+ "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4441,6 +5227,21 @@
"wrappy": "1"
}
},
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -4539,6 +5340,15 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/patch-console": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
+ "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4934,6 +5744,21 @@
"integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
"license": "MIT"
},
+ "node_modules/react-reconciler": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz",
+ "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5110,6 +5935,22 @@
"node": ">=4"
}
},
+ "node_modules/restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -5354,6 +6195,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
@@ -5399,6 +6246,22 @@
"simple-concat": "^1.0.0"
}
},
+ "node_modules/slice-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -5436,6 +6299,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -5454,6 +6338,49 @@
"safe-buffer": "~5.2.0"
}
},
+ "node_modules/string-width": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
+ "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -5682,6 +6609,18 @@
"node": "*"
}
},
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@@ -6002,18 +6941,159 @@
"node": ">= 8"
}
},
+ "node_modules/widest-line": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
+ "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/widest-line/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/widest-line/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/widest-line/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/wildcard": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"license": "MIT"
},
+ "node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -6040,6 +7120,12 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yoga-layout": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
+ "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
+ "license": "MIT"
}
}
}
diff --git a/package.json b/package.json
index e83bfe1..f7194b2 100644
--- a/package.json
+++ b/package.json
@@ -16,14 +16,19 @@
"@babel/preset-react": "^7.28.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+ "@inquirer/prompts": "^8.1.0",
"@mui/material": "^7.3.6",
"babel-loader": "^10.0.0",
+ "bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"chart.js": "^4.5.1",
"css-loader": "^7.1.2",
"dotenv": "^16.4.5",
"express": "^5.2.1",
"html-webpack-plugin": "^5.6.5",
+ "ink": "^6.5.1",
+ "ink-text-input": "^6.0.0",
+ "jsonwebtoken": "^9.0.3",
"react": "^19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.3",
diff --git a/server.js b/server.js
index 027aef7..073b846 100644
--- a/server.js
+++ b/server.js
@@ -5,6 +5,8 @@ import path from 'path';
import { fileURLToPath } from 'url';
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
+import bcrypt from 'bcrypt';
+import jwt from 'jsonwebtoken';
import config from './webpack.config.js';
const __filename = fileURLToPath(import.meta.url);
@@ -17,6 +19,7 @@ const USER_AGENT = 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS
const POLL_INTERVAL_MS = 60000; // 60 seconds
const DB_FILE = 'ac_data.db';
const PORT = 3905;
+const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
// Device Type Mapping
const DEVICE_TYPES = {
@@ -52,11 +55,23 @@ db.exec(`
)
`);
+db.exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'user',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+`);
+
const insertStmt = db.prepare(`
INSERT INTO readings (dev_id, dev_name, port, port_name, temp_c, humidity, vpd, fan_speed, on_speed, off_speed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
+const getUserByUsername = db.prepare('SELECT * FROM users WHERE username = ?');
+
// --- AC INFINITY API LOGIC ---
let token = null;
@@ -201,7 +216,58 @@ async function poll() {
// --- EXPRESS SERVER ---
const app = express();
+app.use(express.json());
+// Auth: Login
+app.post('/api/auth/login', async (req, res) => {
+ try {
+ const { username, password } = req.body;
+ if (!username || !password) {
+ return res.status(400).json({ error: 'Username and password required' });
+ }
+
+ const user = getUserByUsername.get(username);
+ if (!user) {
+ return res.status(401).json({ error: 'Invalid credentials' });
+ }
+
+ const valid = await bcrypt.compare(password, user.password_hash);
+ if (!valid) {
+ return res.status(401).json({ error: 'Invalid credentials' });
+ }
+
+ const token = jwt.sign(
+ { id: user.id, username: user.username, role: user.role },
+ JWT_SECRET,
+ { expiresIn: '7d' }
+ );
+
+ res.json({ token, user: { username: user.username, role: user.role } });
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Auth: Get current user
+app.get('/api/auth/me', (req, res) => {
+ try {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({ error: 'No token provided' });
+ }
+
+ const token = authHeader.split(' ')[1];
+ const decoded = jwt.verify(token, JWT_SECRET);
+
+ res.json({ user: { username: decoded.username, role: decoded.role } });
+ } catch (error) {
+ if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
+ return res.status(401).json({ error: 'Invalid or expired token' });
+ }
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
// API: Devices
app.get('/api/devices', (req, res) => {
diff --git a/src/client/App.js b/src/client/App.js
index d5b505b..d4c588e 100644
--- a/src/client/App.js
+++ b/src/client/App.js
@@ -1,6 +1,9 @@
import React from 'react';
-import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box } from '@mui/material';
+import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box, Button, Chip } from '@mui/material';
import Dashboard from './Dashboard';
+import RuleManager from './RuleManager';
+import LoginDialog from './LoginDialog';
+import { AuthProvider, useAuth } from './AuthContext';
// Gruvbox Dark color palette
const gruvboxDark = {
@@ -39,22 +42,77 @@ const darkTheme = createTheme({
},
});
+function AppContent() {
+ const { user, loading, logout, isAuthenticated, isAdmin } = useAuth();
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Tischlerei Dashboard
+
+
+
+ {isAdmin && (
+
+ )}
+
+
+
+
+
+
+ {isAdmin && }
+
+
+ );
+}
+
function App() {
return (
-
-
-
-
- Tischlerei Dashboard
-
-
-
-
-
-
-
+
+
+
);
}
diff --git a/src/client/AuthContext.js b/src/client/AuthContext.js
new file mode 100644
index 0000000..50549a0
--- /dev/null
+++ b/src/client/AuthContext.js
@@ -0,0 +1,79 @@
+import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
+
+const AuthContext = createContext(null);
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ // Check for existing session on mount
+ useEffect(() => {
+ const checkAuth = async () => {
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ try {
+ const res = await fetch('api/auth/me', {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setUser(data.user);
+ } else {
+ localStorage.removeItem('authToken');
+ }
+ } catch (error) {
+ console.error('Auth check failed:', error);
+ localStorage.removeItem('authToken');
+ }
+ }
+ setLoading(false);
+ };
+ checkAuth();
+ }, []);
+
+ const login = useCallback(async (username, password) => {
+ const res = await fetch('api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password })
+ });
+
+ if (!res.ok) {
+ const data = await res.json();
+ throw new Error(data.error || 'Login failed');
+ }
+
+ const data = await res.json();
+ localStorage.setItem('authToken', data.token);
+ setUser(data.user);
+ return data.user;
+ }, []);
+
+ const logout = useCallback(() => {
+ localStorage.removeItem('authToken');
+ setUser(null);
+ }, []);
+
+ const value = {
+ user,
+ loading,
+ login,
+ logout,
+ isAuthenticated: !!user,
+ isAdmin: user?.role === 'admin'
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
diff --git a/src/client/LoginDialog.js b/src/client/LoginDialog.js
new file mode 100644
index 0000000..7f7fe05
--- /dev/null
+++ b/src/client/LoginDialog.js
@@ -0,0 +1,140 @@
+import React, { useState } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Button,
+ Alert,
+ Box,
+ CircularProgress,
+ InputAdornment,
+ IconButton,
+ Typography
+} from '@mui/material';
+import { useAuth } from './AuthContext';
+
+// Simple eye icons using unicode
+const VisibilityIcon = () => 👁;
+const VisibilityOffIcon = () => 👁🗨;
+
+export default function LoginDialog({ open }) {
+ const { login } = useAuth();
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ await login(username, password);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/client/RuleCard.js b/src/client/RuleCard.js
new file mode 100644
index 0000000..522c0ce
--- /dev/null
+++ b/src/client/RuleCard.js
@@ -0,0 +1,161 @@
+import React from 'react';
+import {
+ Box,
+ Paper,
+ Typography,
+ Switch,
+ IconButton,
+ Chip,
+ Tooltip
+} from '@mui/material';
+
+// Simple icons using unicode/emoji
+const EditIcon = () => ✏️;
+const DeleteIcon = () => 🗑️;
+
+const dayLabels = {
+ mon: 'M', tue: 'T', wed: 'W', thu: 'T', fri: 'F', sat: 'S', sun: 'S'
+};
+
+const dayOrder = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
+
+function TriggerSummary({ trigger }) {
+ if (trigger.type === 'time') {
+ const days = trigger.days || [];
+ const isEveryDay = days.length === 7;
+ const isWeekdays = days.length === 5 &&
+ ['mon', 'tue', 'wed', 'thu', 'fri'].every(d => days.includes(d));
+
+ let dayText = isEveryDay ? 'Every day' :
+ isWeekdays ? 'Weekdays' :
+ dayOrder.filter(d => days.includes(d)).map(d => dayLabels[d]).join(' ');
+
+ return (
+
+
+
+ At {trigger.time} • {dayText}
+
+
+ );
+ }
+
+ if (trigger.type === 'sensor') {
+ return (
+
+
+
+ When {trigger.sensor} {trigger.operator} {trigger.value}
+
+
+ );
+ }
+
+ return null;
+}
+
+function ActionSummary({ action }) {
+ if (action.type === 'toggle') {
+ return (
+
+
+
+ Turn {action.target} {action.state ? 'on' : 'off'}
+
+
+ );
+ }
+
+ if (action.type === 'keepOn') {
+ return (
+
+
+
+ Keep {action.target} on for {action.duration} min
+
+
+ );
+ }
+
+ return null;
+}
+
+export default function RuleCard({ rule, onEdit, onDelete, onToggle }) {
+ return (
+
+
+
+
+
+ {rule.name}
+
+ {!rule.enabled && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/client/RuleEditor.js b/src/client/RuleEditor.js
new file mode 100644
index 0000000..11c24b7
--- /dev/null
+++ b/src/client/RuleEditor.js
@@ -0,0 +1,346 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Button,
+ Box,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ ToggleButton,
+ ToggleButtonGroup,
+ Typography,
+ Divider,
+ Slider,
+ Switch,
+ FormControlLabel
+} from '@mui/material';
+
+const DAYS = [
+ { key: 'mon', label: 'Mon' },
+ { key: 'tue', label: 'Tue' },
+ { key: 'wed', label: 'Wed' },
+ { key: 'thu', label: 'Thu' },
+ { key: 'fri', label: 'Fri' },
+ { key: 'sat', label: 'Sat' },
+ { key: 'sun', label: 'Sun' }
+];
+
+const SENSORS = ['Temperature', 'Humidity', 'CO2', 'VPD', 'Light Level'];
+const OPERATORS = [
+ { value: '>', label: 'Greater than (>)' },
+ { value: '<', label: 'Less than (<)' },
+ { value: '>=', label: 'Greater or equal (≥)' },
+ { value: '<=', label: 'Less or equal (≤)' },
+ { value: '==', label: 'Equal to (=)' }
+];
+
+const OUTPUTS = [
+ 'Workshop Light',
+ 'Exhaust Fan',
+ 'Heater',
+ 'Humidifier',
+ 'All Outlets',
+ 'Grow Light',
+ 'Circulation Fan'
+];
+
+export default function RuleEditor({ open, rule, onSave, onClose }) {
+ const [name, setName] = useState('');
+ const [triggerType, setTriggerType] = useState('time');
+
+ // Time trigger state
+ const [time, setTime] = useState('08:00');
+ const [days, setDays] = useState(['mon', 'tue', 'wed', 'thu', 'fri']);
+
+ // Sensor trigger state
+ const [sensor, setSensor] = useState('Temperature');
+ const [operator, setOperator] = useState('>');
+ const [sensorValue, setSensorValue] = useState(25);
+
+ // Action state
+ const [actionType, setActionType] = useState('toggle');
+ const [target, setTarget] = useState('Workshop Light');
+ const [toggleState, setToggleState] = useState(true);
+ const [duration, setDuration] = useState(15);
+
+ // Reset form when rule changes
+ useEffect(() => {
+ if (rule) {
+ setName(rule.name);
+ setTriggerType(rule.trigger.type);
+
+ if (rule.trigger.type === 'time') {
+ setTime(rule.trigger.time);
+ setDays(rule.trigger.days || []);
+ } else {
+ setSensor(rule.trigger.sensor);
+ setOperator(rule.trigger.operator);
+ setSensorValue(rule.trigger.value);
+ }
+
+ setActionType(rule.action.type);
+ setTarget(rule.action.target);
+ if (rule.action.type === 'toggle') {
+ setToggleState(rule.action.state);
+ } else {
+ setDuration(rule.action.duration);
+ }
+ } else {
+ // Reset to defaults for new rule
+ setName('');
+ setTriggerType('time');
+ setTime('08:00');
+ setDays(['mon', 'tue', 'wed', 'thu', 'fri']);
+ setSensor('Temperature');
+ setOperator('>');
+ setSensorValue(25);
+ setActionType('toggle');
+ setTarget('Workshop Light');
+ setToggleState(true);
+ setDuration(15);
+ }
+ }, [rule, open]);
+
+ const handleDaysChange = (event, newDays) => {
+ if (newDays.length > 0) {
+ setDays(newDays);
+ }
+ };
+
+ const handleSave = () => {
+ const ruleData = {
+ name,
+ trigger: triggerType === 'time'
+ ? { type: 'time', time, days }
+ : { type: 'sensor', sensor, operator, value: sensorValue },
+ action: actionType === 'toggle'
+ ? { type: 'toggle', target, state: toggleState }
+ : { type: 'keepOn', target, duration }
+ };
+ onSave(ruleData);
+ };
+
+ const isValid = name.trim().length > 0 &&
+ (triggerType !== 'time' || days.length > 0);
+
+ return (
+
+ );
+}
diff --git a/src/client/RuleManager.js b/src/client/RuleManager.js
new file mode 100644
index 0000000..6d9aecf
--- /dev/null
+++ b/src/client/RuleManager.js
@@ -0,0 +1,173 @@
+import React, { useState } from 'react';
+import {
+ Box,
+ Typography,
+ Button,
+ Paper,
+ Divider
+} from '@mui/material';
+import RuleCard from './RuleCard';
+import RuleEditor from './RuleEditor';
+
+// Initial mock rules for demonstration
+const initialRules = [
+ {
+ id: 1,
+ name: 'Morning Light',
+ enabled: true,
+ trigger: {
+ type: 'time',
+ time: '06:30',
+ days: ['mon', 'tue', 'wed', 'thu', 'fri']
+ },
+ action: {
+ type: 'toggle',
+ target: 'Workshop Light',
+ state: true
+ }
+ },
+ {
+ id: 2,
+ name: 'High Humidity Fan',
+ enabled: true,
+ trigger: {
+ type: 'sensor',
+ sensor: 'Humidity',
+ operator: '>',
+ value: 70
+ },
+ action: {
+ type: 'keepOn',
+ target: 'Exhaust Fan',
+ duration: 15
+ }
+ },
+ {
+ id: 3,
+ name: 'Evening Shutdown',
+ enabled: false,
+ trigger: {
+ type: 'time',
+ time: '18:00',
+ days: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
+ },
+ action: {
+ type: 'toggle',
+ target: 'All Outlets',
+ state: false
+ }
+ }
+];
+
+export default function RuleManager() {
+ const [rules, setRules] = useState(initialRules);
+ const [editorOpen, setEditorOpen] = useState(false);
+ const [editingRule, setEditingRule] = useState(null);
+
+ const handleAddRule = () => {
+ setEditingRule(null);
+ setEditorOpen(true);
+ };
+
+ const handleEditRule = (rule) => {
+ setEditingRule(rule);
+ setEditorOpen(true);
+ };
+
+ const handleDeleteRule = (ruleId) => {
+ setRules(rules.filter(r => r.id !== ruleId));
+ };
+
+ const handleToggleRule = (ruleId) => {
+ setRules(rules.map(r =>
+ r.id === ruleId ? { ...r, enabled: !r.enabled } : r
+ ));
+ };
+
+ const handleSaveRule = (ruleData) => {
+ if (editingRule) {
+ // Update existing rule
+ setRules(rules.map(r =>
+ r.id === editingRule.id ? { ...r, ...ruleData } : r
+ ));
+ } else {
+ // Add new rule
+ const newRule = {
+ ...ruleData,
+ id: Math.max(0, ...rules.map(r => r.id)) + 1,
+ enabled: true
+ };
+ setRules([...rules, newRule]);
+ }
+ setEditorOpen(false);
+ setEditingRule(null);
+ };
+
+ const handleCloseEditor = () => {
+ setEditorOpen(false);
+ setEditingRule(null);
+ };
+
+ return (
+
+
+
+
+ ⚙️ Automation Rules
+
+
+ Configure triggers and actions for home automation
+
+
+
+
+
+
+
+ {rules.length === 0 ? (
+
+
+ No rules configured. Click "Add Rule" to create one.
+
+
+ ) : (
+
+ {rules.map(rule => (
+ handleEditRule(rule)}
+ onDelete={() => handleDeleteRule(rule.id)}
+ onToggle={() => handleToggleRule(rule.id)}
+ />
+ ))}
+
+ )}
+
+
+
+ );
+}