When the database has no users, the UI redirects to /setup and prompts for creation of the first admin account. The setup endpoints are self-disabling — once any user exists, POST /setup/admin returns 403 and the frontend redirects /setup back to /login. The backend uses an atomic transaction to prevent race conditions on concurrent requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
69 lines
1.5 KiB
Go
69 lines
1.5 KiB
Go
package setup
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
)
|
|
|
|
var ErrSetupAlreadyDone = errors.New("setup already completed")
|
|
|
|
// Storer is the interface for setup-related DB operations.
|
|
type Storer interface {
|
|
NeedsSetup(ctx context.Context) (bool, error)
|
|
CreateAdmin(ctx context.Context, name, email, hashedPassword string) (int64, error)
|
|
}
|
|
|
|
type Store struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewStore(db *sql.DB) *Store {
|
|
return &Store{db: db}
|
|
}
|
|
|
|
// NeedsSetup returns true when the volunteers table has zero rows.
|
|
func (s *Store) NeedsSetup(ctx context.Context) (bool, error) {
|
|
var count int
|
|
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM volunteers`).Scan(&count)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count == 0, nil
|
|
}
|
|
|
|
// CreateAdmin atomically checks that no users exist and inserts the first admin.
|
|
func (s *Store) CreateAdmin(ctx context.Context, name, email, hashedPassword string) (int64, error) {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
var count int
|
|
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM volunteers`).Scan(&count); err != nil {
|
|
return 0, err
|
|
}
|
|
if count > 0 {
|
|
return 0, ErrSetupAlreadyDone
|
|
}
|
|
|
|
res, err := tx.ExecContext(ctx,
|
|
`INSERT INTO volunteers (name, email, password, role, active, operational_roles) VALUES (?, ?, ?, 'admin', 1, '')`,
|
|
name, email, hashedPassword,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
id, err := res.LastInsertId()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return 0, err
|
|
}
|
|
return id, nil
|
|
}
|