first commit
This commit is contained in:
103
scripts/add-mqtt-tables.js
Normal file
103
scripts/add-mqtt-tables.js
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Migration script to add MQTT credentials and ACL tables
|
||||
* This extends the existing database with MQTT provisioning capabilities
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const dataDir = path.join(__dirname, '..', 'data');
|
||||
const dbPath = path.join(dataDir, 'database.sqlite');
|
||||
|
||||
// Check if database exists
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
console.error('❌ Database not found. Run npm run db:init:app first');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Open database
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
console.log('Starting MQTT tables migration...\n');
|
||||
|
||||
// Create mqtt_credentials table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS mqtt_credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL UNIQUE,
|
||||
mqtt_username TEXT NOT NULL UNIQUE,
|
||||
mqtt_password_hash TEXT NOT NULL,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE,
|
||||
CHECK (enabled IN (0, 1))
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created mqtt_credentials table');
|
||||
|
||||
// Create mqtt_acl_rules table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS mqtt_acl_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
topic_pattern TEXT NOT NULL,
|
||||
permission TEXT NOT NULL CHECK(permission IN ('read', 'write', 'readwrite')),
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created mqtt_acl_rules table');
|
||||
|
||||
// Create indexes for performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_mqtt_credentials_device
|
||||
ON mqtt_credentials(device_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mqtt_credentials_username
|
||||
ON mqtt_credentials(mqtt_username);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mqtt_acl_device
|
||||
ON mqtt_acl_rules(device_id);
|
||||
`);
|
||||
console.log('✓ Created indexes');
|
||||
|
||||
// Create mqtt_sync_status table to track pending changes
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS mqtt_sync_status (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
pending_changes INTEGER DEFAULT 0,
|
||||
last_sync_at TEXT,
|
||||
last_sync_status TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created mqtt_sync_status table');
|
||||
|
||||
// Initialize sync status
|
||||
const syncStatus = db.prepare('SELECT * FROM mqtt_sync_status WHERE id = 1').get();
|
||||
if (!syncStatus) {
|
||||
db.prepare(`
|
||||
INSERT INTO mqtt_sync_status (id, pending_changes, last_sync_status)
|
||||
VALUES (1, 0, 'never_synced')
|
||||
`).run();
|
||||
console.log('✓ Initialized sync status');
|
||||
} else {
|
||||
console.log('✓ Sync status already exists');
|
||||
}
|
||||
|
||||
// Get stats
|
||||
const mqttCredsCount = db.prepare('SELECT COUNT(*) as count FROM mqtt_credentials').get();
|
||||
const aclRulesCount = db.prepare('SELECT COUNT(*) as count FROM mqtt_acl_rules').get();
|
||||
|
||||
console.log(`\n✓ MQTT tables migration completed successfully!`);
|
||||
console.log(` MQTT Credentials: ${mqttCredsCount.count}`);
|
||||
console.log(` ACL Rules: ${aclRulesCount.count}`);
|
||||
|
||||
db.close();
|
||||
49
scripts/add-parent-user-column.js
Normal file
49
scripts/add-parent-user-column.js
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Add parent_user_id column to User table
|
||||
* This enables parent-child relationship for ADMIN -> VIEWER hierarchy
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dataDir = path.join(__dirname, '..', 'data');
|
||||
const dbPath = path.join(dataDir, 'database.sqlite');
|
||||
|
||||
console.log('Adding parent_user_id column to User table...');
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
try {
|
||||
// Check if column already exists
|
||||
const tableInfo = db.prepare("PRAGMA table_info(User)").all();
|
||||
const hasParentColumn = tableInfo.some(col => col.name === 'parent_user_id');
|
||||
|
||||
if (hasParentColumn) {
|
||||
console.log('⚠ Column parent_user_id already exists, skipping...');
|
||||
} else {
|
||||
// Add parent_user_id column
|
||||
db.exec(`
|
||||
ALTER TABLE User ADD COLUMN parent_user_id TEXT;
|
||||
`);
|
||||
console.log('✓ Added parent_user_id column');
|
||||
|
||||
// Create index for faster lookups
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_user_parent ON User(parent_user_id);
|
||||
`);
|
||||
console.log('✓ Created index on parent_user_id');
|
||||
|
||||
// Add foreign key constraint check
|
||||
// Note: SQLite doesn't enforce foreign keys on ALTER TABLE,
|
||||
// but we'll add the constraint in the application logic
|
||||
console.log('✓ Parent-child relationship enabled');
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log('\n✓ Migration completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during migration:', error);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
68
scripts/add-test-location.js
Normal file
68
scripts/add-test-location.js
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Add a test location via command line
|
||||
* Usage: node scripts/add-test-location.js <username> <lat> <lon>
|
||||
* Example: node scripts/add-test-location.js 10 48.1351 11.582
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 3) {
|
||||
console.error('Usage: node scripts/add-test-location.js <username> <lat> <lon> [speed] [battery]');
|
||||
console.error('Example: node scripts/add-test-location.js 10 48.1351 11.582 25 85');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [username, lat, lon, speed, battery] = args;
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO Location (
|
||||
latitude, longitude, timestamp, user_id,
|
||||
username, display_time, battery, speed, chat_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString();
|
||||
const displayTime = now.toLocaleString('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
const result = stmt.run(
|
||||
parseFloat(lat),
|
||||
parseFloat(lon),
|
||||
timestamp,
|
||||
0,
|
||||
username,
|
||||
displayTime,
|
||||
battery ? parseInt(battery) : null,
|
||||
speed ? parseFloat(speed) : null,
|
||||
0
|
||||
);
|
||||
|
||||
console.log('✓ Test location added successfully!');
|
||||
console.log(` Username: ${username}`);
|
||||
console.log(` Coordinates: ${lat}, ${lon}`);
|
||||
console.log(` Timestamp: ${timestamp}`);
|
||||
if (speed) console.log(` Speed: ${speed} km/h`);
|
||||
if (battery) console.log(` Battery: ${battery}%`);
|
||||
console.log(` ID: ${result.lastInsertRowid}`);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
51
scripts/check-admin.js
Normal file
51
scripts/check-admin.js
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Check admin user and test password verification
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Get admin user
|
||||
const user = db.prepare('SELECT * FROM User WHERE username = ?').get('admin');
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ Admin user not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Admin user found:');
|
||||
console.log(' ID:', user.id);
|
||||
console.log(' Username:', user.username);
|
||||
console.log(' Email:', user.email);
|
||||
console.log(' Role:', user.role);
|
||||
console.log(' Password Hash:', user.passwordHash.substring(0, 20) + '...');
|
||||
|
||||
// Test password verification
|
||||
const testPassword = 'admin123';
|
||||
console.log('\nTesting password verification...');
|
||||
console.log(' Test password:', testPassword);
|
||||
|
||||
try {
|
||||
const isValid = bcrypt.compareSync(testPassword, user.passwordHash);
|
||||
console.log(' Result:', isValid ? '✅ VALID' : '❌ INVALID');
|
||||
|
||||
if (!isValid) {
|
||||
console.log('\n⚠️ Password verification failed!');
|
||||
console.log('Recreating admin user with fresh hash...');
|
||||
|
||||
const newHash = bcrypt.hashSync(testPassword, 10);
|
||||
db.prepare('UPDATE User SET passwordHash = ? WHERE username = ?').run(newHash, 'admin');
|
||||
|
||||
console.log('✅ Admin password reset successfully');
|
||||
console.log('Try logging in again with: admin / admin123');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' Error:', error.message);
|
||||
}
|
||||
|
||||
db.close();
|
||||
63
scripts/check-user-password.js
Normal file
63
scripts/check-user-password.js
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Check user password hash in database
|
||||
* Usage: node scripts/check-user-password.js <username>
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
|
||||
async function checkUserPassword(username) {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
try {
|
||||
const user = db.prepare('SELECT * FROM User WHERE username = ?').get(username);
|
||||
|
||||
if (!user) {
|
||||
console.error(`❌ User "${username}" not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n✓ User found: ${user.username}`);
|
||||
console.log(` ID: ${user.id}`);
|
||||
console.log(` Email: ${user.email || 'N/A'}`);
|
||||
console.log(` Role: ${user.role}`);
|
||||
console.log(` Created: ${user.createdAt}`);
|
||||
console.log(` Updated: ${user.updatedAt}`);
|
||||
console.log(` Last Login: ${user.lastLoginAt || 'Never'}`);
|
||||
console.log(`\n Password Hash: ${user.passwordHash.substring(0, 60)}...`);
|
||||
console.log(` Hash starts with: ${user.passwordHash.substring(0, 7)}`);
|
||||
|
||||
// Check if it's a valid bcrypt hash
|
||||
const isBcrypt = user.passwordHash.startsWith('$2a$') ||
|
||||
user.passwordHash.startsWith('$2b$') ||
|
||||
user.passwordHash.startsWith('$2y$');
|
||||
|
||||
if (isBcrypt) {
|
||||
console.log(` ✓ Hash format: Valid bcrypt hash`);
|
||||
|
||||
// Extract rounds
|
||||
const rounds = parseInt(user.passwordHash.split('$')[2]);
|
||||
console.log(` ✓ Bcrypt rounds: ${rounds}`);
|
||||
} else {
|
||||
console.log(` ❌ Hash format: NOT a valid bcrypt hash!`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
const username = process.argv[2];
|
||||
if (!username) {
|
||||
console.error('Usage: node scripts/check-user-password.js <username>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
checkUserPassword(username);
|
||||
76
scripts/cleanup-old-locations.js
Normal file
76
scripts/cleanup-old-locations.js
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Cleanup old location data from locations.sqlite
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/cleanup-old-locations.js [hours]
|
||||
*
|
||||
* Examples:
|
||||
* node scripts/cleanup-old-locations.js 168 # Delete older than 7 days
|
||||
* node scripts/cleanup-old-locations.js 720 # Delete older than 30 days
|
||||
*
|
||||
* Default: Deletes data older than 7 days (168 hours)
|
||||
*
|
||||
* You can run this as a cron job:
|
||||
* 0 2 * * * cd /path/to/poc-app && node scripts/cleanup-old-locations.js >> /var/log/location-cleanup.log 2>&1
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
|
||||
const DEFAULT_RETENTION_HOURS = 168; // 7 days
|
||||
|
||||
// Get retention period from command line or use default
|
||||
const retentionHours = process.argv[2]
|
||||
? parseInt(process.argv[2], 10)
|
||||
: DEFAULT_RETENTION_HOURS;
|
||||
|
||||
if (isNaN(retentionHours) || retentionHours <= 0) {
|
||||
console.error('Error: Invalid retention hours. Must be a positive number.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Get stats before cleanup
|
||||
const beforeCount = db.prepare('SELECT COUNT(*) as count FROM Location').get();
|
||||
const beforeSize = db.prepare("SELECT page_count * page_size / 1024 as sizeKB FROM pragma_page_count(), pragma_page_size()").get();
|
||||
|
||||
console.log(`\n🗑️ Location Data Cleanup`);
|
||||
console.log(`================================`);
|
||||
console.log(`Database: ${dbPath}`);
|
||||
console.log(`Retention: ${retentionHours} hours (${Math.round(retentionHours / 24)} days)`);
|
||||
console.log(`\nBefore cleanup:`);
|
||||
console.log(` Records: ${beforeCount.count}`);
|
||||
console.log(` Size: ${Math.round(beforeSize.sizeKB)} KB`);
|
||||
|
||||
// Delete old records
|
||||
const result = db.prepare(`
|
||||
DELETE FROM Location
|
||||
WHERE timestamp < datetime('now', '-' || ? || ' hours')
|
||||
`).run(retentionHours);
|
||||
|
||||
// Optimize database (reclaim space)
|
||||
db.exec('VACUUM');
|
||||
db.exec('ANALYZE');
|
||||
|
||||
// Get stats after cleanup
|
||||
const afterCount = db.prepare('SELECT COUNT(*) as count FROM Location').get();
|
||||
const afterSize = db.prepare("SELECT page_count * page_size / 1024 as sizeKB FROM pragma_page_count(), pragma_page_size()").get();
|
||||
|
||||
console.log(`\nAfter cleanup:`);
|
||||
console.log(` Records: ${afterCount.count}`);
|
||||
console.log(` Size: ${Math.round(afterSize.sizeKB)} KB`);
|
||||
console.log(`\nResult:`);
|
||||
console.log(` ✓ Deleted ${result.changes} old records`);
|
||||
console.log(` ✓ Freed ${Math.round(beforeSize.sizeKB - afterSize.sizeKB)} KB`);
|
||||
|
||||
db.close();
|
||||
console.log(`\n✓ Cleanup completed successfully\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n❌ Cleanup failed:`, error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
147
scripts/init-database.js
Normal file
147
scripts/init-database.js
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Initialize database.sqlite with User and Device tables
|
||||
* This creates the schema for authentication and device management
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const dataDir = path.join(__dirname, '..', 'data');
|
||||
const dbPath = path.join(dataDir, 'database.sqlite');
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
console.log('✓ Created data directory');
|
||||
}
|
||||
|
||||
// Create database
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Enable WAL mode for better concurrency
|
||||
db.pragma('journal_mode = WAL');
|
||||
console.log('✓ Enabled WAL mode');
|
||||
|
||||
// Create User table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS User (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT,
|
||||
passwordHash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'VIEWER',
|
||||
createdAt TEXT DEFAULT (datetime('now')),
|
||||
updatedAt TEXT DEFAULT (datetime('now')),
|
||||
lastLoginAt TEXT,
|
||||
|
||||
CHECK (role IN ('ADMIN', 'VIEWER'))
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created User table');
|
||||
|
||||
// Create Device table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS Device (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
ownerId TEXT,
|
||||
isActive INTEGER DEFAULT 1,
|
||||
description TEXT,
|
||||
icon TEXT,
|
||||
createdAt TEXT DEFAULT (datetime('now')),
|
||||
updatedAt TEXT DEFAULT (datetime('now')),
|
||||
|
||||
FOREIGN KEY (ownerId) REFERENCES User(id) ON DELETE SET NULL,
|
||||
CHECK (isActive IN (0, 1))
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created Device table');
|
||||
|
||||
// Create indexes
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_user_username ON User(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_device_owner ON Device(ownerId);
|
||||
CREATE INDEX IF NOT EXISTS idx_device_active ON Device(isActive);
|
||||
`);
|
||||
console.log('✓ Created indexes');
|
||||
|
||||
// Create Settings table for app configuration
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created settings table');
|
||||
|
||||
// Create password reset tokens table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created password_reset_tokens table');
|
||||
|
||||
// Create index for performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_reset_tokens_user_id
|
||||
ON password_reset_tokens(user_id);
|
||||
`);
|
||||
console.log('✓ Created password reset tokens index');
|
||||
|
||||
// Check if admin user exists
|
||||
const existingAdmin = db.prepare('SELECT * FROM User WHERE username = ?').get('admin');
|
||||
|
||||
if (!existingAdmin) {
|
||||
// Create default admin user
|
||||
const passwordHash = bcrypt.hashSync('admin123', 10);
|
||||
db.prepare(`
|
||||
INSERT INTO User (id, username, email, passwordHash, role)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run('admin-001', 'admin', 'admin@example.com', passwordHash, 'ADMIN');
|
||||
console.log('✓ Created default admin user (username: admin, password: admin123)');
|
||||
} else {
|
||||
console.log('✓ Admin user already exists');
|
||||
}
|
||||
|
||||
// Check if default devices exist
|
||||
const deviceCount = db.prepare('SELECT COUNT(*) as count FROM Device').get();
|
||||
|
||||
if (deviceCount.count === 0) {
|
||||
// Create default devices
|
||||
db.prepare(`
|
||||
INSERT INTO Device (id, name, color, ownerId, isActive, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run('10', 'Device A', '#e74c3c', null, 1, 'Default OwnTracks device');
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO Device (id, name, color, ownerId, isActive, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run('11', 'Device B', '#3498db', null, 1, 'Default OwnTracks device');
|
||||
|
||||
console.log('✓ Created default devices (10, 11)');
|
||||
} else {
|
||||
console.log(`✓ Devices already exist (${deviceCount.count} devices)`);
|
||||
}
|
||||
|
||||
// Get stats
|
||||
const userCount = db.prepare('SELECT COUNT(*) as count FROM User').get();
|
||||
const activeDeviceCount = db.prepare('SELECT COUNT(*) as count FROM Device WHERE isActive = 1').get();
|
||||
|
||||
console.log(`\n✓ Database initialized successfully!`);
|
||||
console.log(` Path: ${dbPath}`);
|
||||
console.log(` Users: ${userCount.count}`);
|
||||
console.log(` Active Devices: ${activeDeviceCount.count}`);
|
||||
console.log(` WAL mode: enabled`);
|
||||
|
||||
db.close();
|
||||
79
scripts/init-locations-db.js
Normal file
79
scripts/init-locations-db.js
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Initialize locations.sqlite database
|
||||
* This creates the schema for location tracking data
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const dataDir = path.join(__dirname, '..', 'data');
|
||||
const dbPath = path.join(dataDir, 'locations.sqlite');
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
console.log('✓ Created data directory');
|
||||
}
|
||||
|
||||
// Create database
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Enable WAL mode for better concurrency and crash resistance
|
||||
db.pragma('journal_mode = WAL');
|
||||
console.log('✓ Enabled WAL mode');
|
||||
|
||||
// Create Location table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS Location (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
user_id INTEGER DEFAULT 0,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
username TEXT,
|
||||
marker_label TEXT,
|
||||
display_time TEXT,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
battery INTEGER,
|
||||
speed REAL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
|
||||
-- Index for fast filtering by timestamp and device
|
||||
CHECK (latitude >= -90 AND latitude <= 90),
|
||||
CHECK (longitude >= -180 AND longitude <= 180)
|
||||
);
|
||||
`);
|
||||
console.log('✓ Created Location table');
|
||||
|
||||
// Create indexes for performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_location_timestamp
|
||||
ON Location(timestamp DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_location_username
|
||||
ON Location(username);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_location_user_id
|
||||
ON Location(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_location_composite
|
||||
ON Location(user_id, username, timestamp DESC);
|
||||
|
||||
-- Prevent duplicates: unique combination of timestamp, username, and coordinates
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_location_unique
|
||||
ON Location(timestamp, username, latitude, longitude);
|
||||
`);
|
||||
console.log('✓ Created indexes (including unique constraint)');
|
||||
|
||||
// Get stats
|
||||
const count = db.prepare('SELECT COUNT(*) as count FROM Location').get();
|
||||
console.log(`\n✓ Database initialized successfully!`);
|
||||
console.log(` Path: ${dbPath}`);
|
||||
console.log(` Records: ${count.count}`);
|
||||
console.log(` WAL mode: enabled`);
|
||||
|
||||
db.close();
|
||||
75
scripts/migrate-device-ownership.js
Normal file
75
scripts/migrate-device-ownership.js
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Migrate devices with NULL ownerId to admin user
|
||||
* Usage: node scripts/migrate-device-ownership.js
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
|
||||
function migrateDeviceOwnership() {
|
||||
const db = new Database(dbPath);
|
||||
|
||||
try {
|
||||
// Find first admin user
|
||||
const adminUser = db.prepare(`
|
||||
SELECT * FROM User
|
||||
WHERE role = 'ADMIN'
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT 1
|
||||
`).get();
|
||||
|
||||
if (!adminUser) {
|
||||
console.error('❌ No admin user found in database');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✓ Admin user found: ${adminUser.username} (${adminUser.id})`);
|
||||
|
||||
// Find devices with NULL ownerId
|
||||
const devicesWithoutOwner = db.prepare(`
|
||||
SELECT * FROM Device
|
||||
WHERE ownerId IS NULL
|
||||
`).all();
|
||||
|
||||
console.log(`\n📊 Found ${devicesWithoutOwner.length} devices without owner`);
|
||||
|
||||
if (devicesWithoutOwner.length === 0) {
|
||||
console.log('✓ All devices already have an owner');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update devices to assign to admin
|
||||
const updateStmt = db.prepare(`
|
||||
UPDATE Device
|
||||
SET ownerId = ?, updatedAt = datetime('now')
|
||||
WHERE ownerId IS NULL
|
||||
`);
|
||||
|
||||
const result = updateStmt.run(adminUser.id);
|
||||
|
||||
console.log(`\n✅ Updated ${result.changes} devices`);
|
||||
console.log(` Assigned to: ${adminUser.username} (${adminUser.id})`);
|
||||
|
||||
// Show updated devices
|
||||
const updatedDevices = db.prepare(`
|
||||
SELECT id, name, ownerId FROM Device
|
||||
WHERE ownerId = ?
|
||||
`).all(adminUser.id);
|
||||
|
||||
console.log('\n📋 Devices now owned by admin:');
|
||||
updatedDevices.forEach(device => {
|
||||
console.log(` - ${device.id}: ${device.name}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrateDeviceOwnership();
|
||||
39
scripts/remove-duplicates.js
Normal file
39
scripts/remove-duplicates.js
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Remove duplicate locations from database
|
||||
* Keeps the oldest entry for each unique (timestamp, username, lat, lon) combination
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log('🔍 Checking for duplicates...\n');
|
||||
|
||||
// Get count before
|
||||
const beforeCount = db.prepare('SELECT COUNT(*) as count FROM Location').get();
|
||||
console.log(`Total locations before: ${beforeCount.count}`);
|
||||
|
||||
// Find and delete duplicates, keeping the oldest entry (lowest id)
|
||||
const result = db.prepare(`
|
||||
DELETE FROM Location
|
||||
WHERE id NOT IN (
|
||||
SELECT MIN(id)
|
||||
FROM Location
|
||||
GROUP BY timestamp, username, latitude, longitude
|
||||
)
|
||||
`).run();
|
||||
|
||||
console.log(`\n✓ Deleted ${result.changes} duplicate records`);
|
||||
|
||||
// Get count after
|
||||
const afterCount = db.prepare('SELECT COUNT(*) as count FROM Location').get();
|
||||
console.log(`Total locations after: ${afterCount.count}`);
|
||||
|
||||
// Optimize database
|
||||
db.exec('VACUUM');
|
||||
console.log('✓ Database optimized\n');
|
||||
|
||||
db.close();
|
||||
48
scripts/reset-admin.js
Normal file
48
scripts/reset-admin.js
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Reset admin user to default state
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log('🔄 Resetting admin user...\n');
|
||||
|
||||
// Delete all existing admin users
|
||||
const deleted = db.prepare('DELETE FROM User WHERE username = ?').run('admin');
|
||||
console.log(`Deleted ${deleted.changes} existing admin user(s)`);
|
||||
|
||||
// Create fresh admin user
|
||||
const passwordHash = bcrypt.hashSync('admin123', 10);
|
||||
db.prepare(`
|
||||
INSERT INTO User (id, username, email, passwordHash, role)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run('admin-001', 'admin', 'admin@example.com', passwordHash, 'ADMIN');
|
||||
|
||||
console.log('✅ Created fresh admin user\n');
|
||||
|
||||
// Verify
|
||||
const user = db.prepare('SELECT * FROM User WHERE username = ?').get('admin');
|
||||
console.log('Admin user details:');
|
||||
console.log(' ID:', user.id);
|
||||
console.log(' Username:', user.username);
|
||||
console.log(' Email:', user.email);
|
||||
console.log(' Role:', user.role);
|
||||
console.log(' Password Hash:', user.passwordHash.substring(0, 30) + '...');
|
||||
|
||||
// Test password
|
||||
const isValid = bcrypt.compareSync('admin123', user.passwordHash);
|
||||
console.log('\n✅ Password verification:', isValid ? 'PASS' : 'FAIL');
|
||||
|
||||
if (isValid) {
|
||||
console.log('\n🎉 Admin user reset complete!');
|
||||
console.log('Login with:');
|
||||
console.log(' Username: admin');
|
||||
console.log(' Password: admin123');
|
||||
}
|
||||
|
||||
db.close();
|
||||
22
scripts/reset-joachim-password.js
Normal file
22
scripts/reset-joachim-password.js
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const newPassword = 'joachim123';
|
||||
const passwordHash = bcrypt.hashSync(newPassword, 10);
|
||||
|
||||
const result = db.prepare('UPDATE User SET passwordHash = ? WHERE username = ?').run(passwordHash, 'joachim');
|
||||
|
||||
if (result.changes > 0) {
|
||||
console.log('✓ Password reset successfully for user "joachim"');
|
||||
console.log(` New password: ${newPassword}`);
|
||||
} else {
|
||||
console.log('❌ User "joachim" not found');
|
||||
}
|
||||
|
||||
db.close();
|
||||
19
scripts/show-schema.js
Normal file
19
scripts/show-schema.js
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log('📋 Database Schema:\n');
|
||||
|
||||
// Get all tables
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
||||
|
||||
tables.forEach(table => {
|
||||
console.log(`\n━━━ Table: ${table.name} ━━━`);
|
||||
const schema = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`).get(table.name);
|
||||
console.log(schema.sql);
|
||||
});
|
||||
|
||||
db.close();
|
||||
89
scripts/test-device-access.js
Normal file
89
scripts/test-device-access.js
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test device access control after security fix
|
||||
* Tests that users can only see devices they own
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Import the getAllowedDeviceIds logic
|
||||
function getAllowedDeviceIds(userId, role, username) {
|
||||
try {
|
||||
// Super admin (username === "admin") can see ALL devices
|
||||
if (username === 'admin') {
|
||||
const allDevices = db.prepare('SELECT id FROM Device WHERE isActive = 1').all();
|
||||
return allDevices.map(d => d.id);
|
||||
}
|
||||
|
||||
// VIEWER users see their parent user's devices
|
||||
if (role === 'VIEWER') {
|
||||
const user = db.prepare('SELECT parent_user_id FROM User WHERE id = ?').get(userId);
|
||||
if (user?.parent_user_id) {
|
||||
const devices = db.prepare('SELECT id FROM Device WHERE ownerId = ? AND isActive = 1').all(user.parent_user_id);
|
||||
return devices.map(d => d.id);
|
||||
}
|
||||
// If VIEWER has no parent, return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
// Regular ADMIN users see only their own devices
|
||||
if (role === 'ADMIN') {
|
||||
const devices = db.prepare('SELECT id FROM Device WHERE ownerId = ? AND isActive = 1').all(userId);
|
||||
return devices.map(d => d.id);
|
||||
}
|
||||
|
||||
// Default: no access
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error in getAllowedDeviceIds:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== Device Access Control Test ===\n');
|
||||
|
||||
// Get all users
|
||||
const users = db.prepare('SELECT id, username, role, parent_user_id FROM User').all();
|
||||
|
||||
// Get all devices
|
||||
const allDevices = db.prepare('SELECT id, name, ownerId FROM Device WHERE isActive = 1').all();
|
||||
|
||||
console.log('All devices in system:');
|
||||
allDevices.forEach(d => {
|
||||
console.log(` - Device ${d.id} (${d.name}) owned by: ${d.ownerId}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Test each user
|
||||
users.forEach(user => {
|
||||
const allowedDevices = getAllowedDeviceIds(user.id, user.role, user.username);
|
||||
|
||||
console.log(`User: ${user.username} (${user.role})`);
|
||||
console.log(` ID: ${user.id}`);
|
||||
if (user.parent_user_id) {
|
||||
const parent = users.find(u => u.id === user.parent_user_id);
|
||||
console.log(` Parent: ${parent?.username || 'unknown'}`);
|
||||
}
|
||||
console.log(` Can see devices: ${allowedDevices.length > 0 ? allowedDevices.join(', ') : 'NONE'}`);
|
||||
|
||||
// Show device names
|
||||
if (allowedDevices.length > 0) {
|
||||
allowedDevices.forEach(deviceId => {
|
||||
const device = allDevices.find(d => d.id === deviceId);
|
||||
console.log(` - ${deviceId}: ${device?.name || 'unknown'}`);
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
|
||||
console.log('=== Expected Results ===');
|
||||
console.log('✓ admin: Should see ALL devices (10, 11, 12, 15)');
|
||||
console.log('✓ joachim: Should see only devices 12, 15 (owned by joachim)');
|
||||
console.log('✓ hummel: Should see devices 12, 15 (parent joachim\'s devices)');
|
||||
console.log('✓ joachiminfo: Should see NO devices (doesn\'t own any)');
|
||||
|
||||
db.close();
|
||||
25
scripts/test-joachim-password.js
Normal file
25
scripts/test-joachim-password.js
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const joachim = db.prepare('SELECT * FROM User WHERE username = ?').get('joachim');
|
||||
|
||||
if (!joachim) {
|
||||
console.log('❌ User "joachim" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Testing passwords for joachim:');
|
||||
|
||||
const passwords = ['joachim123', 'joachim', 'password', 'admin123'];
|
||||
for (const pwd of passwords) {
|
||||
const match = bcrypt.compareSync(pwd, joachim.passwordHash);
|
||||
console.log(` '${pwd}': ${match ? '✓ MATCH' : '✗ no match'}`);
|
||||
}
|
||||
|
||||
db.close();
|
||||
55
scripts/test-password.js
Normal file
55
scripts/test-password.js
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test if a password matches a user's stored hash
|
||||
* Usage: node scripts/test-password.js <username> <password>
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
|
||||
async function testPassword(username, password) {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
try {
|
||||
const user = db.prepare('SELECT * FROM User WHERE username = ?').get(username);
|
||||
|
||||
if (!user) {
|
||||
console.error(`❌ User "${username}" not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n✓ User found: ${user.username}`);
|
||||
console.log(` Testing password...`);
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.passwordHash);
|
||||
|
||||
if (isValid) {
|
||||
console.log(` ✅ Password is CORRECT!`);
|
||||
} else {
|
||||
console.log(` ❌ Password is INCORRECT!`);
|
||||
console.log(`\n Debug info:`);
|
||||
console.log(` - Password provided: "${password}"`);
|
||||
console.log(` - Password length: ${password.length}`);
|
||||
console.log(` - Hash in DB: ${user.passwordHash.substring(0, 60)}...`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
const username = process.argv[2];
|
||||
const password = process.argv[3];
|
||||
|
||||
if (!username || !password) {
|
||||
console.error('Usage: node scripts/test-password.js <username> <password>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
testPassword(username, password);
|
||||
58
scripts/test-smtp.js
Executable file
58
scripts/test-smtp.js
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test SMTP configuration and email sending
|
||||
* Usage: node scripts/test-smtp.js your-email@example.com
|
||||
*/
|
||||
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
const { emailService } = require('../lib/email-service.ts');
|
||||
|
||||
const testEmail = process.argv[2];
|
||||
|
||||
if (!testEmail) {
|
||||
console.error('Usage: node scripts/test-smtp.js your-email@example.com');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function testSMTP() {
|
||||
console.log('Testing SMTP configuration...\n');
|
||||
|
||||
try {
|
||||
// Test connection
|
||||
console.log('1. Testing SMTP connection...');
|
||||
const connected = await emailService.testConnection();
|
||||
if (connected) {
|
||||
console.log('✓ SMTP connection successful\n');
|
||||
} else {
|
||||
console.error('✗ SMTP connection failed\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test welcome email
|
||||
console.log('2. Sending test welcome email...');
|
||||
await emailService.sendWelcomeEmail({
|
||||
email: testEmail,
|
||||
username: 'Test User',
|
||||
loginUrl: 'http://localhost:3000/login',
|
||||
temporaryPassword: 'TempPass123!',
|
||||
});
|
||||
console.log('✓ Welcome email sent\n');
|
||||
|
||||
// Test password reset email
|
||||
console.log('3. Sending test password reset email...');
|
||||
await emailService.sendPasswordResetEmail({
|
||||
email: testEmail,
|
||||
username: 'Test User',
|
||||
resetUrl: 'http://localhost:3000/reset-password?token=test-token-123',
|
||||
expiresIn: '1 hour',
|
||||
});
|
||||
console.log('✓ Password reset email sent\n');
|
||||
|
||||
console.log('All tests passed! Check your inbox at:', testEmail);
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testSMTP();
|
||||
68
scripts/test-time-filter.js
Executable file
68
scripts/test-time-filter.js
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test time filter logic to debug why old locations are still visible
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
console.log('=== Current Time ===');
|
||||
const nowJS = new Date();
|
||||
console.log(`JavaScript Date.now(): ${nowJS.toISOString()}`);
|
||||
console.log(`Local time (Europe/Berlin): ${nowJS.toLocaleString('de-DE', { timeZone: 'Europe/Berlin' })}`);
|
||||
|
||||
console.log('\n=== Time Filter Test (1 Hour) ===');
|
||||
const timeRangeHours = 1;
|
||||
const cutoffTime = new Date(Date.now() - timeRangeHours * 60 * 60 * 1000).toISOString();
|
||||
console.log(`Cutoff time (${timeRangeHours}h ago): ${cutoffTime}`);
|
||||
console.log(`Cutoff local: ${new Date(cutoffTime).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' })}`);
|
||||
|
||||
console.log('\n=== All Locations (Last 10) ===');
|
||||
const allLocations = db.prepare(`
|
||||
SELECT timestamp, username, display_time
|
||||
FROM Location
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 10
|
||||
`).all();
|
||||
|
||||
allLocations.forEach((loc, idx) => {
|
||||
const locDate = new Date(loc.timestamp);
|
||||
const ageHours = (Date.now() - locDate.getTime()) / (1000 * 60 * 60);
|
||||
const shouldShow = loc.timestamp >= cutoffTime ? '✅ SHOW' : '❌ HIDE';
|
||||
console.log(`${idx + 1}. ${shouldShow} | ${loc.username} | ${loc.display_time} | Age: ${ageHours.toFixed(1)}h`);
|
||||
});
|
||||
|
||||
console.log('\n=== Filtered Locations (1 Hour) ===');
|
||||
const filteredLocations = db.prepare(`
|
||||
SELECT timestamp, username, display_time
|
||||
FROM Location
|
||||
WHERE timestamp >= ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 10
|
||||
`).all(cutoffTime);
|
||||
|
||||
console.log(`Found ${filteredLocations.length} locations within last ${timeRangeHours} hour(s)`);
|
||||
filteredLocations.forEach((loc, idx) => {
|
||||
console.log(`${idx + 1}. ${loc.username} | ${loc.display_time}`);
|
||||
});
|
||||
|
||||
console.log('\n=== OLD SQLite Method (datetime now) ===');
|
||||
const oldMethod = db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM Location
|
||||
WHERE timestamp >= datetime('now', '-1 hours')
|
||||
`).get();
|
||||
console.log(`OLD method (SQLite datetime): ${oldMethod.count} locations`);
|
||||
|
||||
console.log('\n=== NEW JavaScript Method ===');
|
||||
const newMethod = db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM Location
|
||||
WHERE timestamp >= ?
|
||||
`).get(cutoffTime);
|
||||
console.log(`NEW method (JS Date): ${newMethod.count} locations`);
|
||||
|
||||
db.close();
|
||||
101
scripts/test-user-visibility.js
Normal file
101
scripts/test-user-visibility.js
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify user visibility restrictions
|
||||
* - Admin can see all users including "admin"
|
||||
* - Non-admin users (like "joachim") cannot see the "admin" user
|
||||
*/
|
||||
|
||||
const baseUrl = 'http://localhost:3001';
|
||||
|
||||
async function login(username, password) {
|
||||
const response = await fetch(`${baseUrl}/api/auth/signin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const cookies = response.headers.get('set-cookie');
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function getUsers(cookies) {
|
||||
const response = await fetch(`${baseUrl}/api/users`, {
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`Failed to get users: ${error.error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.users;
|
||||
}
|
||||
|
||||
async function testAdminUser() {
|
||||
console.log('\n🔍 Testing with "admin" user:');
|
||||
console.log('================================');
|
||||
|
||||
try {
|
||||
const cookies = await login('admin', 'admin123');
|
||||
const users = await getUsers(cookies);
|
||||
|
||||
console.log(`✓ Admin can see ${users.length} user(s):`);
|
||||
users.forEach(user => {
|
||||
console.log(` - ${user.username} (${user.role})`);
|
||||
});
|
||||
|
||||
const hasAdminUser = users.some(u => u.username === 'admin');
|
||||
if (hasAdminUser) {
|
||||
console.log('✓ Admin can see the "admin" user ✓');
|
||||
} else {
|
||||
console.log('✗ FAIL: Admin cannot see the "admin" user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`✗ FAIL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function testJoachimUser() {
|
||||
console.log('\n🔍 Testing with "joachim" user:');
|
||||
console.log('================================');
|
||||
|
||||
try {
|
||||
const cookies = await login('joachim', 'joachim123');
|
||||
const users = await getUsers(cookies);
|
||||
|
||||
console.log(`✓ Joachim can see ${users.length} user(s):`);
|
||||
users.forEach(user => {
|
||||
console.log(` - ${user.username} (${user.role})`);
|
||||
});
|
||||
|
||||
const hasAdminUser = users.some(u => u.username === 'admin');
|
||||
if (!hasAdminUser) {
|
||||
console.log('✓ Joachim cannot see the "admin" user ✓');
|
||||
} else {
|
||||
console.log('✗ FAIL: Joachim can see the "admin" user (should be hidden)');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`✗ FAIL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Testing User Visibility Restrictions');
|
||||
console.log('=====================================\n');
|
||||
console.log('Expected behavior:');
|
||||
console.log(' - Admin user can see all users including "admin"');
|
||||
console.log(' - Non-admin users (joachim) cannot see "admin" user\n');
|
||||
|
||||
await testAdminUser();
|
||||
await testJoachimUser();
|
||||
|
||||
console.log('\n✓ Test completed!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
33
scripts/update-acl-permission.js
Normal file
33
scripts/update-acl-permission.js
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Update ACL rule permission to readwrite
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
try {
|
||||
// Update ACL rule 11 to readwrite
|
||||
db.prepare('UPDATE mqtt_acl_rules SET permission = ? WHERE id = ?').run('readwrite', 11);
|
||||
|
||||
// Mark pending changes
|
||||
db.prepare(`UPDATE mqtt_sync_status
|
||||
SET pending_changes = pending_changes + 1,
|
||||
updated_at = datetime('now')
|
||||
WHERE id = 1`).run();
|
||||
|
||||
console.log('✓ ACL rule updated to readwrite');
|
||||
|
||||
const updated = db.prepare('SELECT * FROM mqtt_acl_rules WHERE id = ?').get(11);
|
||||
console.log('\nUpdated rule:');
|
||||
console.log(JSON.stringify(updated, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
Reference in New Issue
Block a user