Files
descrybe/apps/api/cmd/seed-platform-admin/main.go
T

223 lines
6.3 KiB
Go
Raw Normal View History

2026-08-13 21:11:09 +02:00
// Command seed-platform-admin upserts the first (or additional) platform admin
// for greenfield / production deploys where no legacy admin_users cutover ran
// and seed-demo must not be used.
//
// Usage:
//
// cd apps/api
// go run ./cmd/seed-platform-admin \
// -postgres "$env:DATABASE_URL" \
// -email you@example.com \
// -password 'choose-a-strong-password' \
// -confirm
//
// Env aliases: PLATFORM_ADMIN_EMAIL, PLATFORM_ADMIN_PASSWORD, DATABASE_URL.
//
// -promote-only grants admin on an existing user without changing the password
// (-password is ignored). Always requires -confirm.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
postgresURL := flag.String("postgres", os.Getenv("DATABASE_URL"), "Postgres URL")
email := flag.String("email", os.Getenv("PLATFORM_ADMIN_EMAIL"), "Platform admin email")
password := flag.String("password", os.Getenv("PLATFORM_ADMIN_PASSWORD"), "Password (min 8; ignored with -promote-only)")
name := flag.String("name", "", "Display name (defaults to email local-part)")
promoteOnly := flag.Bool("promote-only", false, "Grant platform admin on an existing user; do not set password")
confirm := flag.Bool("confirm", false, "Required: acknowledge this writes is_platform_admin + staff_role=admin")
flag.Parse()
opts, err := parseOptions(*postgresURL, *email, *password, *name, *promoteOnly, *confirm, os.Getenv("APP_ENV"))
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
pg, err := pgxpool.New(ctx, opts.PostgresURL)
if err != nil {
log.Fatalf("postgres: %v", err)
}
defer pg.Close()
userID, created, err := upsertPlatformAdmin(ctx, pg, opts)
if err != nil {
log.Fatal(err)
}
action := "updated"
if created {
action = "created"
}
fmt.Printf("platform admin %s\n", action)
fmt.Printf(" id: %s\n", userID)
fmt.Printf(" email: %s\n", opts.Email)
fmt.Printf(" is_platform_admin: true\n")
fmt.Printf(" staff_role: %s\n", auth.StaffRoleAdmin)
if opts.PromoteOnly {
fmt.Println(" password: unchanged (-promote-only)")
} else {
fmt.Println(" password: set (argon2id)")
}
fmt.Println()
fmt.Println("Sign in on the web app, then open /admin.")
}
type options struct {
PostgresURL string
Email string
Password string
Name string
PromoteOnly bool
}
func parseOptions(postgresURL, email, password, name string, promoteOnly, confirm bool, appEnv string) (options, error) {
if !confirm {
return options{}, fmt.Errorf("-confirm is required (refuses silent privilege grants)")
}
pg := strings.TrimSpace(postgresURL)
if pg == "" {
return options{}, fmt.Errorf("-postgres / DATABASE_URL is required")
}
emailNorm := strings.ToLower(strings.TrimSpace(email))
if emailNorm == "" || !strings.Contains(emailNorm, "@") {
return options{}, fmt.Errorf("-email / PLATFORM_ADMIN_EMAIL is required")
}
if err := rejectLocalDemoAccount(emailNorm, appEnv); err != nil {
return options{}, err
}
display := strings.TrimSpace(name)
if display == "" {
display = emailNorm
if i := strings.IndexByte(display, '@'); i > 0 {
display = display[:i]
}
}
opts := options{
PostgresURL: pg,
Email: emailNorm,
Name: display,
PromoteOnly: promoteOnly,
}
if promoteOnly {
return opts, nil
}
pass := password // keep as provided (do not trim interior spaces)
if len(pass) < 8 {
return options{}, fmt.Errorf("-password / PLATFORM_ADMIN_PASSWORD must be at least 8 characters")
}
if err := rejectDemoPassword(pass, appEnv); err != nil {
return options{}, err
}
opts.Password = pass
return opts, nil
}
func rejectLocalDemoAccount(email, appEnv string) error {
if !isProductionEnv(appEnv) {
return nil
}
if strings.HasSuffix(email, ".local") || strings.HasSuffix(email, "@descrybe.test") {
return fmt.Errorf("refusing demo/local emails in production APP_ENV=%q", strings.TrimSpace(appEnv))
}
return nil
}
func rejectDemoPassword(password, appEnv string) error {
if !isProductionEnv(appEnv) {
return nil
}
if password == "DemoPass123!" {
return fmt.Errorf("refusing seed-demo password in production")
}
return nil
}
func isProductionEnv(appEnv string) bool {
switch strings.ToLower(strings.TrimSpace(appEnv)) {
case "production", "prod":
return true
default:
return false
}
}
func upsertPlatformAdmin(ctx context.Context, pg *pgxpool.Pool, opts options) (uuid.UUID, bool, error) {
if opts.PromoteOnly {
var userID uuid.UUID
err := pg.QueryRow(ctx, `
UPDATE users
SET is_platform_admin = true,
staff_role = $2,
is_active = true,
updated_at = now()
WHERE email = $1
RETURNING id`, opts.Email, auth.StaffRoleAdmin).Scan(&userID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return uuid.Nil, false, fmt.Errorf("user %q not found (-promote-only requires an existing account)", opts.Email)
}
return uuid.Nil, false, fmt.Errorf("promote: %w", err)
}
return userID, false, nil
}
hash, err := auth.HashPassword(opts.Password)
if err != nil {
return uuid.Nil, false, fmt.Errorf("hash password: %w", err)
}
tx, err := pg.Begin(ctx)
if err != nil {
return uuid.Nil, false, fmt.Errorf("begin: %w", err)
}
defer tx.Rollback(ctx)
var existed bool
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)`, opts.Email).Scan(&existed)
if err != nil {
return uuid.Nil, false, fmt.Errorf("lookup: %w", err)
}
var userID uuid.UUID
err = tx.QueryRow(ctx, `
INSERT INTO users (
email, name, password_hash, must_set_password,
is_platform_admin, staff_role, is_active, updated_at
) VALUES ($1, $2, $3, false, true, $4, true, now())
ON CONFLICT (email) DO UPDATE SET
name = EXCLUDED.name,
password_hash = EXCLUDED.password_hash,
must_set_password = false,
is_platform_admin = true,
staff_role = EXCLUDED.staff_role,
is_active = true,
updated_at = now()
RETURNING id`, opts.Email, opts.Name, hash, auth.StaffRoleAdmin).Scan(&userID)
if err != nil {
return uuid.Nil, false, fmt.Errorf("upsert user: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return uuid.Nil, false, fmt.Errorf("commit: %w", err)
}
return userID, !existed, nil
}