2026-03-05 00:04:26 -06:00
import { Database } from "bun:sqlite" ;
import { resolve } from "path" ;
import { logger } from "./logger" ;
import { ensureDataDir } from "./paths" ;
import { getConfig } from "./config" ;
const dataDir = ensureDataDir ();
const dbPath = resolve ( dataDir , "overlord.db" );
const db = new Database ( dbPath );
export type UserRole = "admin" | "operator" | "viewer" ;
export interface User {
id : number ;
username : string ;
password_hash : string ;
role : UserRole ;
created_at : number ;
last_login : number | null ;
created_by : string | null ;
must_change_password : number ;
}
export interface UserInfo {
id : number ;
username : string ;
role : UserRole ;
created_at : number ;
last_login : number | null ;
created_by : string | null ;
}
db . exec ( `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'operator', 'viewer')),
created_at INTEGER NOT NULL,
last_login INTEGER,
created_by TEXT,
must_change_password INTEGER DEFAULT 0
)
` );
try {
db . exec (
`ALTER TABLE users ADD COLUMN must_change_password INTEGER DEFAULT 0` ,
);
logger . info ( "[users] Added must_change_password column to existing database" );
} catch ( err : any ) {
if ( ! err . message ? . includes ( "duplicate column name" )) {
logger . error ( "[users] Migration error:" , err );
}
}
const userCount = db . prepare ( "SELECT COUNT(*) as count FROM users" ). get () as {
count : number ;
};
if ( userCount . count === 0 ) {
const config = getConfig ();
const initialUsername = ( config . auth . username || "admin" ). trim () || "admin" ;
const initialPassword = config . auth . password ;
logger . info ( "[users] No users found, creating default admin account" );
const defaultPassword = await Bun . password . hash ( initialPassword , {
algorithm : "bcrypt" ,
cost : 10 ,
});
db . prepare (
"INSERT INTO users (username, password_hash, role, created_at, created_by, must_change_password) VALUES (?, ?, ?, ?, ?, ?)" ,
). run ( initialUsername , defaultPassword , "admin" , Date . now (), "system" , 1 );
const createdUser = db
. prepare ( "SELECT * FROM users WHERE username = ?" )
. get ( initialUsername ) as User | undefined ;
logger . info (
"[users] Default admin created with must_change_password =" ,
createdUser ? . must_change_password ,
);
logger . info ( `[users] Initial admin account created (username: ${ initialUsername } )` );
2026-03-06 11:55:16 -06:00
logger . warn (
`[users] Bootstrap login credentials -> username: ${ initialUsername } | password: ${ initialPassword } ` ,
);
2026-03-05 00:04:26 -06:00
logger . warn (
2026-03-06 14:45:15 -06:00
"[users] SECURITY WARNING: Rotate the bootstrap password after first login. Default bootstrap credentials are admin/admin unless overridden by configuration." ,
2026-03-05 00:04:26 -06:00
);
}
export function getUserById ( id : number ) : User | null {
const user = db . prepare ( "SELECT * FROM users WHERE id = ?" ). get ( id ) as
| User
| undefined ;
return user || null ;
}
export function getUserByUsername ( username : string ) : User | null {
const user = db
. prepare ( "SELECT * FROM users WHERE username = ?" )
. get ( username ) as User | undefined ;
return user || null ;
}
export function listUsers () : UserInfo [] {
const users = db
. prepare (
"SELECT id, username, role, created_at, last_login, created_by FROM users ORDER BY created_at DESC" ,
)
. all () as UserInfo [];
return users ;
}
export async function createUser (
username : string ,
password : string ,
role : UserRole ,
createdBy : string ,
) : Promise < { success : boolean ; error? : string ; userId? : number } > {
if ( ! username || username . length < 3 || username . length > 32 ) {
return {
success : false ,
error : "Username must be between 3 and 32 characters" ,
};
}
if ( ! /^[a-zA-Z0-9_-]+$/ . test ( username )) {
return {
success : false ,
error :
"Username can only contain letters, numbers, hyphens, and underscores" ,
};
}
if ( ! password || password . length < 6 ) {
return { success : false , error : "Password must be at least 6 characters" };
}
const existing = getUserByUsername ( username );
if ( existing ) {
return { success : false , error : "Username already exists" };
}
try {
const passwordHash = await Bun . password . hash ( password , {
algorithm : "bcrypt" ,
cost : 10 ,
});
const result = db
. prepare (
"INSERT INTO users (username, password_hash, role, created_at, created_by) VALUES (?, ?, ?, ?, ?)" ,
)
. run ( username , passwordHash , role , Date . now (), createdBy );
return { success : true , userId : result.lastInsertRowid as number };
} catch ( err : any ) {
logger . error ( "[users] Create user error:" , err );
return { success : false , error : err.message || "Failed to create user" };
}
}
export async function updateUserPassword (
userId : number ,
newPassword : string ,
) : Promise < { success : boolean ; error? : string } > {
if ( ! newPassword || newPassword . length < 6 ) {
return { success : false , error : "Password must be at least 6 characters" };
}
try {
const passwordHash = await Bun . password . hash ( newPassword , {
algorithm : "bcrypt" ,
cost : 10 ,
});
db . prepare (
"UPDATE users SET password_hash = ?, must_change_password = 0 WHERE id = ?" ,
). run ( passwordHash , userId );
return { success : true };
} catch ( err : any ) {
console . error ( "[users] Update password error:" , err );
return {
success : false ,
error : err.message || "Failed to update password" ,
};
}
}
export function updateUserRole (
userId : number ,
newRole : UserRole ,
) : { success : boolean ; error? : string } {
try {
db . prepare ( "UPDATE users SET role = ? WHERE id = ?" ). run ( newRole , userId );
return { success : true };
} catch ( err : any ) {
console . error ( "[users] Update role error:" , err );
return { success : false , error : err.message || "Failed to update role" };
}
}
export function deleteUser ( userId : number ) : {
success : boolean ;
error? : string ;
} {
const admins = db
. prepare ( "SELECT COUNT(*) as count FROM users WHERE role = 'admin'" )
. get () as { count : number };
const user = getUserById ( userId );
if ( user ? . role === "admin" && admins . count <= 1 ) {
return { success : false , error : "Cannot delete the last admin user" };
}
try {
db . prepare ( "DELETE FROM users WHERE id = ?" ). run ( userId );
return { success : true };
} catch ( err : any ) {
console . error ( "[users] Delete user error:" , err );
return { success : false , error : err.message || "Failed to delete user" };
}
}
export function updateLastLogin ( userId : number ) : void {
db . prepare ( "UPDATE users SET last_login = ? WHERE id = ?" ). run (
Date . now (),
userId ,
);
}
export async function verifyPassword (
username : string ,
password : string ,
) : Promise < User | null > {
const user = getUserByUsername ( username );
if ( ! user ) return null ;
const isValid = await Bun . password . verify ( password , user . password_hash );
if ( ! isValid ) return null ;
updateLastLogin ( user . id );
return user ;
}
export function canManageUsers ( role : UserRole ) : boolean {
return role === "admin" ;
}
export function canControlClients ( role : UserRole ) : boolean {
return role === "admin" || role === "operator" ;
}
export function canViewClients ( role : UserRole ) : boolean {
return true ;
}
export function canViewAuditLogs ( role : UserRole ) : boolean {
return role === "admin" ;
}
export function hasPermission ( role : UserRole , permission : string ) : boolean {
switch ( permission ) {
case "users:manage" :
return canManageUsers ( role );
case "clients:control" :
return canControlClients ( role );
case "clients:view" :
return canViewClients ( role );
case "audit:view" :
return canViewAuditLogs ( role );
default :
return false ;
}
}