A modern web interface for Luanti (Minetest) server management with ContentDB integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
288 lines
7.4 KiB
JavaScript
288 lines
7.4 KiB
JavaScript
const bcrypt = require('bcrypt');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const sqlite3 = require('sqlite3');
|
|
const { promisify } = require('util');
|
|
|
|
class AuthManager {
|
|
constructor() {
|
|
this.dbPath = path.join(process.cwd(), 'users.db');
|
|
this.db = null;
|
|
this.saltRounds = 12;
|
|
}
|
|
|
|
async initialize() {
|
|
return new Promise((resolve, reject) => {
|
|
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
// Create users table if it doesn't exist
|
|
this.db.run(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
created_by INTEGER,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_login DATETIME,
|
|
is_active BOOLEAN DEFAULT 1,
|
|
FOREIGN KEY (created_by) REFERENCES users (id)
|
|
)
|
|
`, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async createUser(username, password, createdByUserId = null) {
|
|
if (!username || !password) {
|
|
throw new Error('Username and password are required');
|
|
}
|
|
|
|
// Check if this is not the first user and no creator is specified
|
|
const isFirstUser = await this.isFirstUser();
|
|
if (!isFirstUser && !createdByUserId) {
|
|
throw new Error('Only existing administrators can create new accounts');
|
|
}
|
|
|
|
// Validate username format
|
|
if (!/^[a-zA-Z0-9_-]{3,20}$/.test(username)) {
|
|
throw new Error('Username must be 3-20 characters, letters, numbers, underscore, or hyphen only');
|
|
}
|
|
|
|
// Validate password strength
|
|
if (password.length < 8) {
|
|
throw new Error('Password must be at least 8 characters long');
|
|
}
|
|
|
|
try {
|
|
const passwordHash = await bcrypt.hash(password, this.saltRounds);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO users (username, password_hash, created_by)
|
|
VALUES (?, ?, ?)
|
|
`);
|
|
|
|
stmt.run([username, passwordHash, createdByUserId], function(err) {
|
|
if (err) {
|
|
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
reject(new Error('Username already exists'));
|
|
} else {
|
|
reject(err);
|
|
}
|
|
return;
|
|
}
|
|
|
|
resolve({
|
|
id: this.lastID,
|
|
username: username,
|
|
created_at: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
stmt.finalize();
|
|
});
|
|
} catch (error) {
|
|
throw new Error('Failed to create user: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async authenticateUser(username, password) {
|
|
if (!username || !password) {
|
|
throw new Error('Username and password are required');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.db.get(
|
|
'SELECT * FROM users WHERE username = ? AND is_active = 1',
|
|
[username],
|
|
async (err, user) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
if (!user) {
|
|
reject(new Error('Invalid username or password'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const passwordMatch = await bcrypt.compare(password, user.password_hash);
|
|
|
|
if (!passwordMatch) {
|
|
reject(new Error('Invalid username or password'));
|
|
return;
|
|
}
|
|
|
|
// Update last login
|
|
this.db.run(
|
|
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[user.id]
|
|
);
|
|
|
|
// Return user info (without password hash)
|
|
resolve({
|
|
id: user.id,
|
|
username: user.username,
|
|
created_at: user.created_at,
|
|
last_login: user.last_login
|
|
});
|
|
} catch (bcryptError) {
|
|
reject(bcryptError);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async getUserById(id) {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.get(
|
|
'SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1',
|
|
[id],
|
|
(err, user) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(user || null);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async getUserByUsername(username) {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.get(
|
|
'SELECT id, username, created_at, last_login FROM users WHERE username = ? AND is_active = 1',
|
|
[username],
|
|
(err, user) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(user || null);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async getAllUsers() {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.all(
|
|
'SELECT id, username, created_at, last_login FROM users WHERE is_active = 1 ORDER BY created_at DESC',
|
|
[],
|
|
(err, users) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(users || []);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async deleteUser(id) {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.run(
|
|
'UPDATE users SET is_active = 0 WHERE id = ?',
|
|
[id],
|
|
function(err) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(this.changes > 0);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async changePassword(id, currentPassword, newPassword) {
|
|
if (!currentPassword || !newPassword) {
|
|
throw new Error('Current password and new password are required');
|
|
}
|
|
|
|
if (newPassword.length < 8) {
|
|
throw new Error('New password must be at least 8 characters long');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.db.get(
|
|
'SELECT password_hash FROM users WHERE id = ? AND is_active = 1',
|
|
[id],
|
|
async (err, user) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
if (!user) {
|
|
reject(new Error('User not found'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const passwordMatch = await bcrypt.compare(currentPassword, user.password_hash);
|
|
|
|
if (!passwordMatch) {
|
|
reject(new Error('Current password is incorrect'));
|
|
return;
|
|
}
|
|
|
|
const newPasswordHash = await bcrypt.hash(newPassword, this.saltRounds);
|
|
|
|
this.db.run(
|
|
'UPDATE users SET password_hash = ? WHERE id = ?',
|
|
[newPasswordHash, id],
|
|
function(err) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(this.changes > 0);
|
|
}
|
|
);
|
|
} catch (bcryptError) {
|
|
reject(bcryptError);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async isFirstUser() {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.get(
|
|
'SELECT COUNT(*) as count FROM users WHERE is_active = 1',
|
|
[],
|
|
(err, result) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(result.count === 0);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
close() {
|
|
if (this.db) {
|
|
this.db.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = AuthManager; |