Skip to content

Commit 9efe8cf

Browse files
committed
feat: capture User-Agent on register/login and display hashed password + UA in admin panel
1 parent 4416856 commit 9efe8cf

4 files changed

Lines changed: 31 additions & 7 deletions

File tree

config/migrate.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const tables = {
2525
{ name: 'email_change_token', def: 'VARCHAR(64) DEFAULT NULL' },
2626
{ name: 'email_change_code', def: 'VARCHAR(10) DEFAULT NULL' },
2727
{ name: 'email_change_expires', def: 'DATETIME DEFAULT NULL' },
28+
{ name: 'user_agent', def: 'VARCHAR(512) DEFAULT NULL' },
2829
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
2930
],
3031
},
@@ -45,6 +46,7 @@ const tables = {
4546
{ name: 'id', def: 'INT AUTO_INCREMENT PRIMARY KEY' },
4647
{ name: 'user_id', def: 'INT NOT NULL' },
4748
{ name: 'ip_address', def: 'VARCHAR(45) NOT NULL' },
49+
{ name: 'user_agent', def: 'VARCHAR(512) DEFAULT NULL' },
4850
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
4951
],
5052
},

public/js/admin.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,13 +1265,21 @@ async function renderAdminUserDetail(userId) {
12651265
<div class="detail-item"><span class="detail-label">Ptero ID</span><span class="detail-value" style="font-family:monospace">${u.ptero_user_id || 'N/A'}</span></div>
12661266
<div class="detail-item"><span class="detail-label">API Key Set</span><span class="detail-value">${u.ptero_client_api_key ? 'Yes' : 'No'}</span></div>
12671267
<div class="detail-item"><span class="detail-label">Created</span><span class="detail-value">${formatDateWithTooltip(u.created_at)}</span></div>
1268+
<div class="detail-item"><span class="detail-label">Password Hash</span><span class="detail-value" style="font-family:monospace;word-break:break-all;font-size:0.8rem">${u.password_hash ? escapeHtml(u.password_hash) : 'N/A'}</span></div>
1269+
<div class="detail-item"><span class="detail-label">User-Agent</span><span class="detail-value" style="word-break:break-all;font-size:0.8rem">${u.user_agent ? escapeHtml(u.user_agent) : 'N/A'}</span></div>
12681270
</div>
12691271
</div>
12701272
<div class="card">
12711273
<h2 class="card-title" style="margin-bottom:16px">IP Addresses</h2>
12721274
${data.ips && data.ips.length > 0 ? ahtml`
1273-
<div style="display:flex;flex-wrap:wrap;gap:8px">
1274-
${data.ips.map(ip => ahtml`<span class="server-detail-tag" style="font-family:monospace">${ip.ip_address}</span>`).join('')}
1275+
<div style="display:flex;flex-direction:column;gap:10px">
1276+
${data.ips.map(ip => ahtml`
1277+
<div style="border:1px solid var(--border);border-radius:8px;padding:10px 12px;background:var(--bg-secondary)">
1278+
<div style="font-family:monospace;font-size:0.9rem">${escapeHtml(ip.ip_address)}</div>
1279+
<div style="color:var(--text-secondary);font-size:0.75rem;margin-top:4px">${formatDateWithTooltip(ip.created_at)}</div>
1280+
<div style="color:var(--text-secondary);font-size:0.78rem;margin-top:4px;word-break:break-all">${ip.user_agent ? escapeHtml(ip.user_agent) : 'N/A'}</div>
1281+
</div>
1282+
`).join('')}
12751283
</div>
12761284
` : '<p style="color:var(--text-secondary)">No IPs recorded.</p>'}
12771285
</div>

routes/admin.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ router.post('/login', adminLoginLimiter, async (req, res) => {
7878
}
7979

8080
const ip = getClientIp(req);
81+
const userAgent = (req.headers['user-agent'] || 'unknown').toString().slice(0, 512);
8182
const delay = getAdminLoginDelay(ip);
8283
if (delay > 0) {
8384
await new Promise(r => setTimeout(r, delay));
@@ -112,6 +113,10 @@ router.post('/login', adminLoginLimiter, async (req, res) => {
112113

113114
recordAdminLoginAttempt(ip, true);
114115

116+
await query('UPDATE users SET user_agent = ? WHERE id = ?', [userAgent, user.id]).catch(err => {
117+
console.error('Failed to update user_agent:', err.message);
118+
});
119+
115120
const token = jwt.sign(
116121
{ userId: user.id, email: user.email, username: user.username, pteroId: user.ptero_user_id, isAdmin: true, restricted: false, tokenVersion: user.token_version },
117122
JWT_SECRET,
@@ -457,7 +462,7 @@ router.get('/users/:id', authenticateToken, requireAdmin, async (req, res) => {
457462
}
458463

459464
const users = await query(
460-
'SELECT id, email, username, is_admin, restricted, auth_restricted, ptero_user_id, ptero_client_api_key, created_at FROM users WHERE id = ?',
465+
'SELECT id, email, username, is_admin, restricted, auth_restricted, ptero_user_id, ptero_client_api_key, password_hash, user_agent, created_at FROM users WHERE id = ?',
461466
[userId]
462467
);
463468
if (users.length === 0) {
@@ -484,7 +489,7 @@ router.get('/users/:id', authenticateToken, requireAdmin, async (req, res) => {
484489
}
485490

486491
const ips = await query(
487-
'SELECT ip_address, created_at FROM user_ips WHERE user_id = ? ORDER BY created_at DESC',
492+
'SELECT ip_address, user_agent, created_at FROM user_ips WHERE user_id = ? ORDER BY created_at DESC',
488493
[userId]
489494
);
490495

routes/auth.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ router.post('/register', async (req, res) => {
151151
const { email, username, password, capToken, rgpdConsent } = req.body;
152152

153153
const ip = getClientIp(req);
154+
const userAgent = (req.headers['user-agent'] || 'unknown').toString().slice(0, 512);
154155

155156
if (!email || !username || !password) {
156157
return res.status(400).json({ error: 'Email, username and password are required' });
@@ -219,12 +220,12 @@ router.post('/register', async (req, res) => {
219220
createdPteroUserId = pteroUser.id;
220221

221222
const insertResult = await query(
222-
'INSERT INTO users (email, username, password_hash, ptero_user_id, ptero_uuid, first_name, last_name, password_set) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
223-
[email, username, passwordHash, pteroUser.id, pteroUser.uuid, username, 'User']
223+
'INSERT INTO users (email, username, password_hash, ptero_user_id, ptero_uuid, first_name, last_name, password_set, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)',
224+
[email, username, passwordHash, pteroUser.id, pteroUser.uuid, username, 'User', userAgent]
224225
);
225226
const localUserId = insertResult.insertId;
226227

227-
await query('INSERT INTO user_ips (user_id, ip_address) VALUES (?, ?)', [localUserId, ip]).catch(err => {
228+
await query('INSERT INTO user_ips (user_id, ip_address, user_agent) VALUES (?, ?, ?)', [localUserId, ip, userAgent]).catch(err => {
228229
console.error('Failed to log IP:', err.message);
229230
});
230231

@@ -387,6 +388,7 @@ router.post('/login', async (req, res) => {
387388
}
388389

389390
const ip = getClientIp(req);
391+
const userAgent = (req.headers['user-agent'] || 'unknown').toString().slice(0, 512);
390392

391393
// VPN / Proxy detection — checked first for security
392394
if (await isVpnOrProxy(ip)) {
@@ -451,6 +453,13 @@ router.post('/login', async (req, res) => {
451453

452454
recordLoginAttempt(ip, true);
453455

456+
await query('UPDATE users SET user_agent = ? WHERE id = ?', [userAgent, user.id]).catch(err => {
457+
console.error('Failed to update user_agent:', err.message);
458+
});
459+
await query('INSERT INTO user_ips (user_id, ip_address, user_agent) VALUES (?, ?, ?)', [user.id, ip, userAgent]).catch(err => {
460+
console.error('Failed to log login IP:', err.message);
461+
});
462+
454463
res.json({
455464
token,
456465
user: {

0 commit comments

Comments
 (0)