138 lines
4.3 KiB
Go
138 lines
4.3 KiB
Go
// Command seed-a1-teammate upserts a second A1 company member (not the owner)
|
|
// with must_set_password=true and prints a one-time accept-invite URL.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./cmd/seed-a1-teammate -postgres "$DATABASE_URL"
|
|
// go run ./cmd/seed-a1-teammate -email a1-other@descrybe.local -role member
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/mail"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func main() {
|
|
postgresURL := flag.String("postgres", os.Getenv("DATABASE_URL"), "Postgres URL")
|
|
email := flag.String("email", "a1-other@descrybe.local", "Teammate email to upsert")
|
|
name := flag.String("name", "A1 teammate", "Display name")
|
|
role := flag.String("role", "member", "Membership role: member|admin")
|
|
webOrigin := flag.String("web-origin", firstNonEmpty(os.Getenv("WEB_ORIGIN"), "http://localhost:28472"), "Web origin for accept-invite URL")
|
|
flag.Parse()
|
|
|
|
if strings.TrimSpace(*postgresURL) == "" {
|
|
log.Fatal("-postgres / DATABASE_URL is required")
|
|
}
|
|
emailNorm := strings.ToLower(strings.TrimSpace(*email))
|
|
if emailNorm == "" {
|
|
log.Fatal("-email is required")
|
|
}
|
|
if emailNorm == "a1-primary@descrybe.local" {
|
|
log.Fatal("-email must not be a1-primary@descrybe.local (that user remains the company owner)")
|
|
}
|
|
memRole := auth.NormalizeMembershipRole(*role)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
pg, err := pgxpool.New(ctx, *postgresURL)
|
|
if err != nil {
|
|
log.Fatalf("postgres: %v", err)
|
|
}
|
|
defer pg.Close()
|
|
|
|
var companyID uuid.UUID
|
|
var companyName string
|
|
err = pg.QueryRow(ctx, `
|
|
SELECT id, name FROM companies
|
|
WHERE legacy_company_id = $1
|
|
ORDER BY created_at ASC
|
|
LIMIT 1`, billing.A1LegacyCompanyID).Scan(&companyID, &companyName)
|
|
if err != nil {
|
|
log.Fatalf("find A1 company (legacy_company_id=%s): %v", billing.A1LegacyCompanyID, err)
|
|
}
|
|
|
|
tx, err := pg.Begin(ctx)
|
|
if err != nil {
|
|
log.Fatalf("begin: %v", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
var userID uuid.UUID
|
|
err = tx.QueryRow(ctx, `
|
|
INSERT INTO users (email, name, must_set_password, is_platform_admin, is_active, updated_at)
|
|
VALUES ($1, $2, true, false, true, now())
|
|
ON CONFLICT (email) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
must_set_password = true,
|
|
password_hash = NULL,
|
|
is_active = true,
|
|
updated_at = now()
|
|
RETURNING id`, emailNorm, strings.TrimSpace(*name)).Scan(&userID)
|
|
if err != nil {
|
|
log.Fatalf("upsert user: %v", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO memberships (company_id, user_id, role, status)
|
|
VALUES ($1, $2, $3, 'active')
|
|
ON CONFLICT (company_id, user_id) DO UPDATE SET
|
|
role = EXCLUDED.role,
|
|
status = 'active',
|
|
updated_at = now()`, companyID, userID, memRole)
|
|
if err != nil {
|
|
log.Fatalf("upsert membership: %v", err)
|
|
}
|
|
|
|
// Ensure a1-primary remains owner when present.
|
|
_, _ = tx.Exec(ctx, `
|
|
UPDATE companies c
|
|
SET owner_user_id = u.id, updated_at = now()
|
|
FROM users u
|
|
INNER JOIN memberships m ON m.user_id = u.id AND m.status = 'active'
|
|
WHERE c.id = m.company_id
|
|
AND c.id = $1
|
|
AND lower(u.email) = 'a1-primary@descrybe.local'`, companyID)
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
log.Fatalf("commit: %v", err)
|
|
}
|
|
|
|
svc := &auth.Service{Pool: pg}
|
|
inv, err := svc.ReissueSetPasswordInvite(ctx, userID, 0)
|
|
if err != nil {
|
|
log.Fatalf("issue set-password invite: %v", err)
|
|
}
|
|
acceptURL := mail.AcceptInviteURL(strings.TrimSpace(*webOrigin), inv.Token)
|
|
|
|
fmt.Printf("A1 teammate ready\n")
|
|
fmt.Printf(" company: %s (%s)\n", companyName, companyID)
|
|
fmt.Printf(" email: %s\n", emailNorm)
|
|
fmt.Printf(" user_id: %s\n", userID)
|
|
fmt.Printf(" role: %s\n", memRole)
|
|
fmt.Printf(" expires_at: %s\n", inv.ExpiresAt.UTC().Format(time.RFC3339))
|
|
fmt.Printf(" accept_url: %s\n", acceptURL)
|
|
fmt.Printf("\nShare accept_url with the teammate (or Admin → Users → Send set-password for this user).\n")
|
|
fmt.Printf("While impersonating a1-primary, Settings → Team → Invite also returns a copyable link when SMTP is off.\n")
|
|
}
|
|
|
|
func firstNonEmpty(vals ...string) string {
|
|
for _, v := range vals {
|
|
if strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|