package store import ( "database/sql" "errors" "fmt" "time" ) // ErrNoAdmin is returned by GetAdmin when primary setup has not happened yet. var ErrNoAdmin = errors.New("no administrator account") // Admin is the single panel administrator (spec 7.6.1). type Admin struct { Username string PasswordHash string CreatedAt time.Time } // AdminExists reports whether the administrator account has been created. This // doubles as the "primary setup complete" flag: once true, the /setup route is // permanently gone (spec 7.6.1). func (s *Store) AdminExists() (bool, error) { var n int if err := s.db.QueryRow("SELECT COUNT(*) FROM admin").Scan(&n); err != nil { return false, fmt.Errorf("count admin: %w", err) } return n > 0, nil } // CreateAdmin inserts the administrator row. It fails if one already exists, // which — combined with the id=1 constraint — makes admin creation one-shot // even under a race between two setup submissions. func (s *Store) CreateAdmin(username, passwordHash string) error { _, err := s.db.Exec( "INSERT INTO admin (id, username, password_hash, created_at) VALUES (1, ?, ?, ?)", username, passwordHash, time.Now().UTC().Format(time.RFC3339), ) if err != nil { return fmt.Errorf("create admin: %w", err) } return nil } // UpdateAdmin replaces the administrator's username and password hash. It // touches only the admin row (id = 1): panel credentials are unrelated to the // SASL logins applications authenticate with, which live in their own table. // ErrNoAdmin is returned if setup has not happened yet, so a change can never // silently create an account. func (s *Store) UpdateAdmin(username, passwordHash string) error { res, err := s.db.Exec( "UPDATE admin SET username = ?, password_hash = ? WHERE id = 1", username, passwordHash, ) if err != nil { return fmt.Errorf("update admin: %w", err) } n, err := res.RowsAffected() if err != nil { return fmt.Errorf("update admin: %w", err) } if n == 0 { return ErrNoAdmin } return nil } // GetAdmin returns the administrator account, or ErrNoAdmin if setup is pending. func (s *Store) GetAdmin() (Admin, error) { var ( a Admin createdAt string ) err := s.db.QueryRow("SELECT username, password_hash, created_at FROM admin WHERE id = 1"). Scan(&a.Username, &a.PasswordHash, &createdAt) if errors.Is(err, sql.ErrNoRows) { return Admin{}, ErrNoAdmin } if err != nil { return Admin{}, fmt.Errorf("get admin: %w", err) } a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) return a, nil }