Public Access
122 lines
4.1 KiB
JavaScript
122 lines
4.1 KiB
JavaScript
import db from './db.js';
|
|
import config from './config.js';
|
|
import { hashPassword, randomPassword } from './auth.js';
|
|
import { nowIso } from './util.js';
|
|
|
|
const LOCKOUT_FAILS = 6;
|
|
const LOCKOUT_MINUTES = 15;
|
|
|
|
export function domainAllowed(email) {
|
|
const allowed = config.admin.allowedDomains;
|
|
if (!allowed.length) return true;
|
|
const domain = String(email).split('@')[1]?.toLowerCase() || '';
|
|
return allowed.some((d) => domain === d || domain.endsWith(`.${d}`));
|
|
}
|
|
|
|
export function domainRuleText() {
|
|
const allowed = config.admin.allowedDomains;
|
|
if (!allowed.length) return null;
|
|
return allowed.map((d) => `@${d}`).join(' or ');
|
|
}
|
|
|
|
export function findByEmail(email) {
|
|
return db
|
|
.prepare('SELECT * FROM admin_users WHERE email = ?')
|
|
.get(String(email).trim().toLowerCase());
|
|
}
|
|
|
|
export function findById(id) {
|
|
return db.prepare('SELECT * FROM admin_users WHERE id = ?').get(id);
|
|
}
|
|
|
|
export function countActive() {
|
|
return db.prepare('SELECT COUNT(*) AS n FROM admin_users WHERE active = 1').get().n;
|
|
}
|
|
|
|
export function shape(user) {
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
siteId: user.site_id,
|
|
twoFactorOn: Boolean(user.totp_enabled),
|
|
mustChangePassword: Boolean(user.must_change_password),
|
|
active: Boolean(user.active),
|
|
lastLoginAt: user.last_login_at,
|
|
createdAt: user.created_at,
|
|
};
|
|
}
|
|
|
|
export function createUser({ email, name, password, role = 'admin', siteId = null, mustChange = true }) {
|
|
const clean = String(email || '').trim().toLowerCase();
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(clean)) throw new Error('Enter a valid email address.');
|
|
if (!domainAllowed(clean)) {
|
|
throw new Error(`Admin accounts must use an ${domainRuleText()} address.`);
|
|
}
|
|
if (findByEmail(clean)) throw new Error('An account already uses that email address.');
|
|
|
|
const temp = password || randomPassword();
|
|
const info = db
|
|
.prepare(
|
|
`INSERT INTO admin_users (email, name, password_hash, role, site_id, must_change_password)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
)
|
|
.run(clean, String(name || '').trim() || null, hashPassword(temp), role, siteId, mustChange ? 1 : 0);
|
|
|
|
return { user: findById(info.lastInsertRowid), temporaryPassword: password ? null : temp };
|
|
}
|
|
|
|
/* ------------------------------------------------------------- lockout */
|
|
|
|
export function lockState(email) {
|
|
const row = db.prepare('SELECT * FROM login_attempts WHERE email = ?').get(email);
|
|
if (row?.locked_until && row.locked_until > nowIso()) return row;
|
|
return null;
|
|
}
|
|
|
|
export function noteFailure(email) {
|
|
const row = db.prepare('SELECT * FROM login_attempts WHERE email = ?').get(email);
|
|
const fails = (row?.fails || 0) + 1;
|
|
const lockedUntil =
|
|
fails >= LOCKOUT_FAILS ? new Date(Date.now() + LOCKOUT_MINUTES * 60000).toISOString() : null;
|
|
db.prepare(
|
|
`INSERT INTO login_attempts (email, fails, locked_until) VALUES (?, ?, ?)
|
|
ON CONFLICT(email) DO UPDATE SET fails = excluded.fails, locked_until = excluded.locked_until`
|
|
).run(email, fails, lockedUntil);
|
|
return { fails, lockedUntil };
|
|
}
|
|
|
|
export function clearFailures(email) {
|
|
db.prepare('DELETE FROM login_attempts WHERE email = ?').run(email);
|
|
}
|
|
|
|
/* ----------------------------------------------------------- bootstrap */
|
|
|
|
/** Creates the very first admin account from the environment, once. */
|
|
export function bootstrap() {
|
|
if (countActive() > 0) return;
|
|
|
|
const { bootstrapEmail, bootstrapPassword } = config.admin;
|
|
if (!bootstrapEmail || !bootstrapPassword) {
|
|
console.warn(
|
|
'[users] No admin accounts exist yet. Set ADMIN_BOOTSTRAP_EMAIL and ADMIN_BOOTSTRAP_PASSWORD\n' +
|
|
' in .env and restart to create the first one.'
|
|
);
|
|
return;
|
|
}
|
|
try {
|
|
createUser({
|
|
email: bootstrapEmail,
|
|
name: 'First admin',
|
|
password: bootstrapPassword,
|
|
role: 'owner',
|
|
mustChange: true,
|
|
});
|
|
console.log(`[users] created the first admin account: ${bootstrapEmail}`);
|
|
console.log('[users] you will be asked to set a new password at first sign in');
|
|
} catch (err) {
|
|
console.error('[users] could not create the first admin account:', err.message);
|
|
}
|
|
}
|