Repository navigation
Expand file tree
/
Copy pathadmin.js
More file actions
86 lines (77 loc) · 3.65 KB
/
Copy pathadmin.js
File metadata and controls
86 lines (77 loc) · 3.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Takes Umami's default admin login out of play before the server is reachable.
//
// Umami's first migration creates one user, admin, with the password "umami". On a public URL
// that is an open door: anyone who has read Umami's docs can sign in, read every site's
// analytics and change the tracking setup. So on every boot, after migrations and before
// Umami's server starts:
//
// - With UMAMI_ADMIN_PASSWORD set, the admin user gets that password (unless it already has
// it). While the variable is set it wins: a password changed in Umami's settings is put
// back on the next boot, so remove the variable once you would rather manage the password
// in Umami. It is also the way back in if the generated password is lost.
// - Otherwise, if the admin user still has the password "umami", it is replaced with a random
// one that is printed to the logs once. Later boots find a password that is no longer
// "umami" and leave it alone.
//
// The user is found by the fixed id Umami's migration gives it, so renaming it in Umami makes
// no difference. A deleted admin stays deleted.
'use strict';
const crypto = require('node:crypto');
const bcrypt = require('bcryptjs');
const { Client } = require('pg');
// From Umami's prisma/migrations/01_init/migration.sql.
const SEEDED_ADMIN_ID = '41e2b680-648e-4b09-bcd7-3e2b10c06264';
const DEFAULT_PASSWORD = 'umami';
// Umami's own minimum length, and the bcrypt cost it hashes with (src/lib/password.ts).
const MIN_LENGTH = 8;
const ROUNDS = 10;
async function secureAdminLogin(connectionString) {
const client = new Client({ connectionString, connectionTimeoutMillis: 10_000 });
await client.connect();
try {
const { rows } = await client.query(
'SELECT username, password FROM "user" WHERE user_id = $1 AND deleted_at IS NULL',
[SEEDED_ADMIN_ID],
);
if (rows.length === 0) return;
const { username, password: hash } = rows[0];
const wanted = process.env.UMAMI_ADMIN_PASSWORD;
if (wanted && wanted.length >= MIN_LENGTH) {
// Compared first, not rewritten every boot: Umami ties sessions to the password hash,
// so a fresh hash on each restart would sign everyone out.
if (!bcrypt.compareSync(wanted, hash) && (await setPassword(client, hash, wanted))) {
console.log(`The Umami password for "${username}" was set from UMAMI_ADMIN_PASSWORD.`);
}
return;
}
if (wanted) {
console.warn(`UMAMI_ADMIN_PASSWORD is shorter than ${MIN_LENGTH} characters, so it was ignored.`);
}
if (!bcrypt.compareSync(DEFAULT_PASSWORD, hash)) return;
const generated = crypto.randomBytes(15).toString('base64url'); // 20 characters
if (await setPassword(client, hash, generated)) {
const rule = '-'.repeat(72);
console.log([
rule,
'Umami admin login. It is shown once, so save it now:',
` username: ${username}`,
` password: ${generated}`,
'Change it after signing in: Settings, Profile, Change password.',
'Lost it? Set UMAMI_ADMIN_PASSWORD on the Environment tab and redeploy.',
rule,
].join('\n'));
}
} finally {
await client.end().catch(() => {});
}
}
// Only replaces the hash that was just read. Two instances booting at once cannot both change
// it and print two different passwords: the second update matches nothing.
async function setPassword(client, currentHash, password) {
const { rowCount } = await client.query(
'UPDATE "user" SET password = $1, updated_at = now() WHERE user_id = $2 AND password = $3',
[bcrypt.hashSync(password, ROUNDS), SEEDED_ADMIN_ID, currentHash],
);
return rowCount === 1;
}
module.exports = { secureAdminLogin };