package db import ( "crypto/rand" "database/sql" "encoding/hex" "errors" "time" ) const adminUserColumns = `id, username, password_hash, must_change_password, must_change_username, totp_secret, totp_enabled, is_global_admin, created_by, created_at` func scanAdminUser(row *sql.Row) (*AdminUser, error) { var u AdminUser var createdAt string var createdBy sql.NullInt64 if err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.MustChangePassword, &u.MustChangeUsername, &u.TOTPSecret, &u.TOTPEnabled, &u.IsGlobalAdmin, &createdBy, &createdAt); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } u.CreatedAt, _ = parseTime(createdAt) if createdBy.Valid { u.CreatedBy = &createdBy.Int64 } return &u, nil } func (d *DB) CountAdminUsers() (int, error) { var n int err := d.QueryRow(`SELECT COUNT(*) FROM esrv_admin_users`).Scan(&n) return n, err } // DefaultAdminUsername/Password are the seeded first-run credentials — the admin is // forced to change both before they can use the rest of the dashboard (see // AdminUser.MustChangePassword and the login flow). const ( DefaultAdminUsername = "admin" DefaultAdminPassword = "Password123!" ) // SeedDefaultAdminIfEmpty creates the default admin account on a brand-new install // (no admin users yet at all) with must_change_password AND must_change_username set, // so the well-known default credentials (username "admin") can never be left in place // silently. This is the one and only place must_change_username is ever set — every // other admin (delegated, or a global admin created via the delegation flow) picks // their own username up front and only needs to set their own password. func (d *DB) SeedDefaultAdminIfEmpty() error { n, err := d.CountAdminUsers() if err != nil { return err } if n > 0 { return nil } hash, err := HashPassword(DefaultAdminPassword) if err != nil { return err } _, err = d.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, must_change_username, is_global_admin) VALUES (?, ?, 1, 1, 1)`, DefaultAdminUsername, hash) return err } // CreateAdminUser inserts a new global-admin account (full access, no domain // restriction) — used by the delegation flow when a global admin grants another user // global access. mustChangePassword should be true so the admin who set the initial // password isn't the only one who knows it; must_change_username is always false // here, since the account was created with the username the new admin will actually // use (see SeedDefaultAdminIfEmpty for the one exception). func (d *DB) CreateAdminUser(username, passwordHash string, mustChangePassword bool) (int64, error) { res, err := d.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, must_change_username, is_global_admin) VALUES (?, ?, ?, 0, 1)`, username, passwordHash, mustChangePassword) if err != nil { return 0, err } return res.LastInsertId() } // CreateScopedAdminUser inserts a new domain-scoped admin (delegated access), owned by // createdBy, and grants it access to exactly domainIDs — mirrors the delegation flow: // a scoped admin can create other scoped admins limited to domains within their own. func (d *DB) CreateScopedAdminUser(username, passwordHash string, createdBy int64, domainIDs []int64) (int64, error) { tx, err := d.Begin() if err != nil { return 0, err } defer tx.Rollback() res, err := tx.Exec(`INSERT INTO esrv_admin_users (username, password_hash, must_change_password, must_change_username, is_global_admin, created_by) VALUES (?, ?, 1, 0, 0, ?)`, username, passwordHash, createdBy) if err != nil { return 0, err } id, err := res.LastInsertId() if err != nil { return 0, err } for _, domainID := range domainIDs { if _, err := tx.Exec(`INSERT INTO esrv_admin_domain_access (admin_user_id, domain_id) VALUES (?, ?)`, id, domainID); err != nil { return 0, err } } return id, tx.Commit() } // ListAllAdminUsers returns every admin account — for a global admin's user-management // view. func (d *DB) ListAllAdminUsers() ([]AdminUser, error) { rows, err := d.Query(`SELECT ` + adminUserColumns + ` FROM esrv_admin_users ORDER BY username`) if err != nil { return nil, err } return scanAdminUsers(rows) } // ListScopedAdminUsers returns every non-global admin. Combined with AccessibleDomainIDs // per user, this lets the caller compute "which of these can I (a scoped admin) // manage" — the subset check happens in Go since the admin counts here are always // small (a handful of delegated accounts, not enterprise scale). func (d *DB) ListScopedAdminUsers() ([]AdminUser, error) { rows, err := d.Query(`SELECT ` + adminUserColumns + ` FROM esrv_admin_users WHERE is_global_admin = 0 ORDER BY username`) if err != nil { return nil, err } return scanAdminUsers(rows) } func scanAdminUsers(rows *sql.Rows) ([]AdminUser, error) { defer rows.Close() var out []AdminUser for rows.Next() { var u AdminUser var createdAt string var createdBy sql.NullInt64 if err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.MustChangePassword, &u.MustChangeUsername, &u.TOTPSecret, &u.TOTPEnabled, &u.IsGlobalAdmin, &createdBy, &createdAt); err != nil { return nil, err } u.CreatedAt, _ = parseTime(createdAt) if createdBy.Valid { u.CreatedBy = &createdBy.Int64 } out = append(out, u) } return out, rows.Err() } // AccessibleDomainIDs returns the domains a scoped admin can see/manage. Meaningless // for a global admin (they can access everything regardless of this table). func (d *DB) AccessibleDomainIDs(userID int64) ([]int64, error) { rows, err := d.Query(`SELECT domain_id FROM esrv_admin_domain_access WHERE admin_user_id = ?`, userID) if err != nil { return nil, err } defer rows.Close() var out []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() } // GrantDomainAccess mirrors auto-assigning a newly-created domain to the scoped admin // who created it. func (d *DB) GrantDomainAccess(userID, domainID int64) error { _, err := d.Exec(`INSERT OR IGNORE INTO esrv_admin_domain_access (admin_user_id, domain_id) VALUES (?, ?)`, userID, domainID) return err } // SetAdminDomainAccess replaces a scoped admin's entire domain assignment set. func (d *DB) SetAdminDomainAccess(userID int64, domainIDs []int64) error { tx, err := d.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(`DELETE FROM esrv_admin_domain_access WHERE admin_user_id = ?`, userID); err != nil { return err } for _, id := range domainIDs { if _, err := tx.Exec(`INSERT INTO esrv_admin_domain_access (admin_user_id, domain_id) VALUES (?, ?)`, userID, id); err != nil { return err } } return tx.Commit() } // DeleteAdminUser removes an admin account and everything tied to it. func (d *DB) DeleteAdminUser(id int64) error { tx, err := d.Begin() if err != nil { return err } defer tx.Rollback() for _, stmt := range []string{ `DELETE FROM esrv_admin_domain_access WHERE admin_user_id = ?`, `DELETE FROM esrv_admin_sessions WHERE user_id = ?`, `DELETE FROM esrv_webauthn_credentials WHERE user_id = ?`, `DELETE FROM esrv_admin_users WHERE id = ?`, } { if _, err := tx.Exec(stmt, id); err != nil { return err } } return tx.Commit() } func (d *DB) GetAdminUserByUsername(username string) (*AdminUser, error) { row := d.QueryRow(`SELECT `+adminUserColumns+` FROM esrv_admin_users WHERE lower(username) = lower(?)`, username) return scanAdminUser(row) } func (d *DB) GetAdminUserByID(id int64) (*AdminUser, error) { row := d.QueryRow(`SELECT `+adminUserColumns+` FROM esrv_admin_users WHERE id = ?`, id) return scanAdminUser(row) } // UpdateAdminCredentials mirrors the forced first-login change for the seeded default // admin: new username, password hash, and clears must_change_password/ // must_change_username in one step. func (d *DB) UpdateAdminCredentials(id int64, username, passwordHash string) error { _, err := d.Exec(`UPDATE esrv_admin_users SET username = ?, password_hash = ?, must_change_password = 0, must_change_username = 0 WHERE id = ?`, username, passwordHash, id) return err } // UpdateAdminPasswordClearMustChange mirrors the forced first-login change for a // delegated admin: password hash only (the username was already chosen when the // account was created), clearing must_change_password. func (d *DB) UpdateAdminPasswordClearMustChange(id int64, passwordHash string) error { _, err := d.Exec(`UPDATE esrv_admin_users SET password_hash = ?, must_change_password = 0, must_change_username = 0 WHERE id = ?`, passwordHash, id) return err } func (d *DB) UpdateAdminPassword(id int64, passwordHash string) error { _, err := d.Exec(`UPDATE esrv_admin_users SET password_hash = ? WHERE id = ?`, passwordHash, id) return err } func (d *DB) SetAdminTOTPSecret(id int64, secret string, enabled bool) error { _, err := d.Exec(`UPDATE esrv_admin_users SET totp_secret = ?, totp_enabled = ? WHERE id = ?`, secret, enabled, id) return err } func (d *DB) DisableAdminTOTP(id int64) error { _, err := d.Exec(`UPDATE esrv_admin_users SET totp_secret = '', totp_enabled = 0 WHERE id = ?`, id) return err } // ResetAdminMFA clears every second factor an admin has enrolled — TOTP and every // registered passkey — e.g. after a lost device, so they can re-enroll from scratch. // Distinct from DisableAdminTOTP (TOTP only, self-service from /account): this is the // admin-management action a manager takes on someone else's account (see // adminWithManageAccess's delegation rule for who's allowed to). func (d *DB) ResetAdminMFA(id int64) error { tx, err := d.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(`UPDATE esrv_admin_users SET totp_secret = '', totp_enabled = 0 WHERE id = ?`, id); err != nil { return err } if _, err := tx.Exec(`DELETE FROM esrv_webauthn_credentials WHERE user_id = ?`, id); err != nil { return err } return tx.Commit() } // --- Sessions --- func newSessionToken() string { b := make([]byte, 32) rand.Read(b) return hex.EncodeToString(b) } // CreateSession mirrors starting a new login session; mfaVerified should be true only // when the account has no second factor enabled (nothing left to verify) or the second // factor was just satisfied. func (d *DB) CreateSession(userID int64, mfaVerified bool, ttl time.Duration) (string, error) { token := newSessionToken() _, err := d.Exec(`INSERT INTO esrv_admin_sessions (token, user_id, mfa_verified, expires_at) VALUES (?, ?, ?, ?)`, token, userID, mfaVerified, time.Now().Add(ttl)) if err != nil { return "", err } return token, nil } func (d *DB) GetSession(token string) (*AdminSession, error) { row := d.QueryRow(`SELECT token, user_id, mfa_verified, created_at, expires_at FROM esrv_admin_sessions WHERE token = ?`, token) var s AdminSession var createdAt, expiresAt string if err := row.Scan(&s.Token, &s.UserID, &s.MFAVerified, &createdAt, &expiresAt); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } s.CreatedAt, _ = parseTime(createdAt) s.ExpiresAt, _ = parseTime(expiresAt) return &s, nil } func (d *DB) MarkSessionMFAVerified(token string) error { _, err := d.Exec(`UPDATE esrv_admin_sessions SET mfa_verified = 1 WHERE token = ?`, token) return err } func (d *DB) DeleteSession(token string) error { _, err := d.Exec(`DELETE FROM esrv_admin_sessions WHERE token = ?`, token) return err } // DeleteExpiredSessions is a lightweight best-effort sweep, called opportunistically // rather than on a schedule — this admin UI has at most a handful of sessions ever. func (d *DB) DeleteExpiredSessions() error { _, err := d.Exec(`DELETE FROM esrv_admin_sessions WHERE expires_at < ?`, time.Now()) return err } // --- WebAuthn credentials --- func (d *DB) ListWebAuthnCredentials(userID int64) ([]WebAuthnCredential, error) { rows, err := d.Query(`SELECT id, user_id, name, credential_id, credential_data, created_at FROM esrv_webauthn_credentials WHERE user_id = ? ORDER BY created_at`, userID) if err != nil { return nil, err } defer rows.Close() var out []WebAuthnCredential for rows.Next() { var c WebAuthnCredential var createdAt string if err := rows.Scan(&c.ID, &c.UserID, &c.Name, &c.CredentialID, &c.CredentialData, &createdAt); err != nil { return nil, err } c.CreatedAt, _ = parseTime(createdAt) out = append(out, c) } return out, rows.Err() } func (d *DB) CreateWebAuthnCredential(userID int64, name, credentialID, credentialData string) error { _, err := d.Exec(`INSERT INTO esrv_webauthn_credentials (user_id, name, credential_id, credential_data) VALUES (?, ?, ?, ?)`, userID, name, credentialID, credentialData) return err } func (d *DB) DeleteWebAuthnCredential(id, userID int64) error { _, err := d.Exec(`DELETE FROM esrv_webauthn_credentials WHERE id = ? AND user_id = ?`, id, userID) return err } func (d *DB) CountWebAuthnCredentials(userID int64) (int, error) { var n int err := d.QueryRow(`SELECT COUNT(*) FROM esrv_webauthn_credentials WHERE user_id = ?`, userID).Scan(&n) return n, err }