Heartbeat and Online Presence
This commit is contained in:
@@ -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}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user