first commit

This commit is contained in:
2025-11-24 16:30:37 +00:00
commit 843e93a274
114 changed files with 25585 additions and 0 deletions

103
scripts/add-mqtt-tables.js Normal file
View 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();

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

View 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
View 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();

View 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);

View 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
View 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();

View 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();

View 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();

View 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
View 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();

View 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
View 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();

View 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();

View 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
View 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
View 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
View 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();

View 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);

View 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();
}