Heartbeat and Online Presence

This commit is contained in:
Patrick
2026-06-14 17:01:55 +02:00
parent b1ed1b52e2
commit 7c19961e00
+79 -32
View File
@@ -7,6 +7,22 @@ const { open } = require('sqlite');
const JWT_SECRET = 'super-secret-key-change-me'; const JWT_SECRET = 'super-secret-key-change-me';
const DB_FILE = './users.db'; const DB_FILE = './users.db';
const onlineUsers = new Map();
const ONLINE_TIMEOUT = 30000;
function verifyToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
const token = authHeader.split(' ')[1];
try {
return jwt.verify(token, JWT_SECRET);
} catch (err) {
return null;
}
}
async function initDb() { async function initDb() {
const db = await open({ const db = await open({
filename: DB_FILE, filename: DB_FILE,
@@ -14,8 +30,8 @@ async function initDb() {
}); });
await db.exec(` await db.exec(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY, username TEXT PRIMARY KEY,
passwordHash TEXT passwordHash TEXT
) )
`); `);
return db; return db;
@@ -32,15 +48,12 @@ async function initDb() {
console.error('Please provide a username: npm run airclientauth -- create <user>'); console.error('Please provide a username: npm run airclientauth -- create <user>');
process.exit(1); process.exit(1);
} }
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]); const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
if (user) { if (user) {
console.error('User already exists.'); console.error('User already exists.');
process.exit(1); process.exit(1);
} }
await db.run('INSERT INTO users (username, passwordHash) VALUES (?, ?)', [username, null]); await db.run('INSERT INTO users (username, passwordHash) VALUES (?, ?)', [username, null]);
console.log(`User '${username}' created successfully.`); console.log(`User '${username}' created successfully.`);
console.log(`Waiting for first login to set the password.`); console.log(`Waiting for first login to set the password.`);
process.exit(0); process.exit(0);
@@ -55,70 +68,104 @@ async function initDb() {
app.post('/login', async (req, res) => { app.post('/login', async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (!username) { if (!username) {
return res.status(400).json({ error: 'Username is required' }); return res.status(400).json({ error: 'Username is required' });
} }
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]); const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
if (!user) { if (!user) {
return res.status(404).json({ error: 'User not found' }); return res.status(404).json({ error: 'User not found' });
} }
if (!user.passwordHash) { if (!user.passwordHash) {
if (password) { if (password) {
const salt = await bcrypt.genSalt(10); const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt); const hash = await bcrypt.hash(password, salt);
await db.run('UPDATE users SET passwordHash = ? WHERE username = ?', [hash, username]); await db.run('UPDATE users SET passwordHash = ? WHERE username = ?', [hash, username]);
const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' }); const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' });
return res.json({ return res.json({
status: 'success', status: 'success',
message: 'Password set successfully. Logged in.', message: 'Password set successfully. Logged in.',
token token
}); });
} else { } else {
return res.status(403).json({ return res.status(403).json({
status: 'require_password', status: 'require_password',
message: 'First login requires setting a password.' message: 'First login requires setting a password.'
}); });
} }
} else { } else {
if (!password) { if (!password) {
return res.status(400).json({ error: 'Password is required' }); return res.status(400).json({ error: 'Password is required' });
} }
const isMatch = await bcrypt.compare(password, user.passwordHash); const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) { if (!isMatch) {
return res.status(401).json({ error: 'Invalid password' }); return res.status(401).json({ error: 'Invalid password' });
} }
const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' }); const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' });
return res.json({ return res.json({
status: 'success', status: 'success',
message: 'Logged in successfully', message: 'Logged in successfully',
token token
}); });
} }
}); });
app.get('/verify', (req, res) => { app.get('/verify', (req, res) => {
const authHeader = req.headers.authorization; const decoded = verifyToken(req);
if (!authHeader || !authHeader.startsWith('Bearer ')) { if (!decoded) {
return res.status(401).json({ valid: false, error: 'No token provided' }); return res.status(401).json({ valid: false, error: 'Invalid or missing token' });
} }
return res.json({ valid: true, user: decoded.username });
});
const token = authHeader.split(' ')[1]; app.post('/online', (req, res) => {
try { const decoded = verifyToken(req);
const decoded = jwt.verify(token, JWT_SECRET); if (!decoded) {
return res.json({ valid: true, user: decoded.username }); return res.status(401).json({ error: 'Invalid or missing token' });
} catch (err) { }
return res.status(401).json({ valid: false, error: 'Invalid or expired token' }); const { username } = decoded;
onlineUsers.set(username, Date.now());
return res.json({ status: 'online', count: onlineUsers.size });
});
app.post('/ping', (req, res) => {
const decoded = verifyToken(req);
if (!decoded) {
return res.status(401).json({ error: 'Invalid or missing token' });
}
const { username } = decoded;
if (onlineUsers.has(username)) {
onlineUsers.set(username, Date.now());
return res.json({ status: 'alive' });
} else {
onlineUsers.set(username, Date.now());
return res.json({ status: 'reconnected' });
} }
}); });
app.post('/offline', (req, res) => {
const decoded = verifyToken(req);
if (!decoded) {
return res.status(401).json({ error: 'Invalid or missing token' });
}
const { username } = decoded;
onlineUsers.delete(username);
return res.json({ status: 'offline', count: onlineUsers.size });
});
app.get('/playercount', (req, res) => {
return res.json({ count: onlineUsers.size });
});
setInterval(() => {
const now = Date.now();
for (const [username, lastPing] of onlineUsers.entries()) {
if (now - lastPing > ONLINE_TIMEOUT) {
onlineUsers.delete(username);
console.log(`User '${username}' timed out (offline)`);
}
}
}, 10000);
const PORT = process.env.PORT || 3001; const PORT = process.env.PORT || 3001;
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);