151 lines
4.4 KiB
Go
151 lines
4.4 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
)
|
||
|
|
|
||
|
|
const defaultMigratorDemoCompany = "Platform Demo"
|
||
|
|
|
||
|
|
// ensureDemoUser upserts a local demo account (no Clerk), makes them platform admin,
|
||
|
|
// and binds them only to a standalone Platform Demo company (never A1 / every tenant).
|
||
|
|
func ensureDemoUser(
|
||
|
|
ctx context.Context,
|
||
|
|
pg *pgxpool.Pool,
|
||
|
|
email, password, displayName, localDemoName string,
|
||
|
|
dryRun bool,
|
||
|
|
report map[string]int,
|
||
|
|
) (*DemoReport, error) {
|
||
|
|
emailNorm := strings.ToLower(strings.TrimSpace(email))
|
||
|
|
if emailNorm == "" || password == "" {
|
||
|
|
return nil, fmt.Errorf("demo email and password required")
|
||
|
|
}
|
||
|
|
if localDemoName == "" {
|
||
|
|
localDemoName = defaultMigratorDemoCompany
|
||
|
|
}
|
||
|
|
if displayName == "" {
|
||
|
|
displayName = "Demo User"
|
||
|
|
}
|
||
|
|
out := &DemoReport{
|
||
|
|
Email: emailNorm,
|
||
|
|
PasswordSet: true,
|
||
|
|
PlatformAdmin: true,
|
||
|
|
Note: "Password documented in docs/portable-mysql-pg-migration.md (not written to report JSON).",
|
||
|
|
}
|
||
|
|
if dryRun {
|
||
|
|
report["demo_user"] = 1
|
||
|
|
out.Note = "dry-run: demo user not written"
|
||
|
|
out.PasswordSet = false
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
hash, err := auth.HashPassword(password)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
tx, err := pg.Begin(ctx)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer tx.Rollback(ctx)
|
||
|
|
|
||
|
|
var userID uuid.UUID
|
||
|
|
err = tx.QueryRow(ctx, `
|
||
|
|
INSERT INTO users (
|
||
|
|
email, name, password_hash, must_set_password,
|
||
|
|
is_platform_admin, is_active, updated_at
|
||
|
|
) VALUES ($1, $2, $3, false, true, true, now())
|
||
|
|
ON CONFLICT (email) DO UPDATE SET
|
||
|
|
name = EXCLUDED.name,
|
||
|
|
password_hash = EXCLUDED.password_hash,
|
||
|
|
must_set_password = false,
|
||
|
|
is_platform_admin = true,
|
||
|
|
is_active = true,
|
||
|
|
updated_at = now()
|
||
|
|
RETURNING id`, emailNorm, displayName, hash).Scan(&userID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("upsert demo user: %w", err)
|
||
|
|
}
|
||
|
|
out.UserID = userID.String()
|
||
|
|
|
||
|
|
demoCompanyID, demoName, err := ensureMigratorDemoCompany(ctx, tx, localDemoName)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
out.PrimaryCompany = demoCompanyID.String()
|
||
|
|
out.PrimaryName = demoName
|
||
|
|
|
||
|
|
ct, err := tx.Exec(ctx, `
|
||
|
|
INSERT INTO memberships (company_id, user_id, role, status)
|
||
|
|
VALUES ($1, $2, 'admin', 'active')
|
||
|
|
ON CONFLICT (company_id, user_id) DO UPDATE
|
||
|
|
SET role = 'admin', status = 'active', updated_at = now()`, demoCompanyID, userID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("demo membership: %w", err)
|
||
|
|
}
|
||
|
|
if _, err := tx.Exec(ctx, `
|
||
|
|
DELETE FROM memberships
|
||
|
|
WHERE user_id = $1 AND company_id <> $2`, userID, demoCompanyID); err != nil {
|
||
|
|
return nil, fmt.Errorf("remove non-demo memberships: %w", err)
|
||
|
|
}
|
||
|
|
out.Memberships = ct.RowsAffected()
|
||
|
|
|
||
|
|
if err := tx.Commit(ctx); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
report["demo_user"] = 1
|
||
|
|
report["demo_memberships"] = int(out.Memberships)
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func ensureMigratorDemoCompany(ctx context.Context, tx pgx.Tx, name string) (uuid.UUID, string, error) {
|
||
|
|
name = strings.TrimSpace(name)
|
||
|
|
if name == "" {
|
||
|
|
name = defaultMigratorDemoCompany
|
||
|
|
}
|
||
|
|
if strings.EqualFold(name, "A1 Slovenija") || strings.EqualFold(name, "A1") || strings.EqualFold(name, "Local Demo Co") {
|
||
|
|
return uuid.Nil, "", fmt.Errorf("demo company name %q collides with A1 tenant — use %q", name, defaultMigratorDemoCompany)
|
||
|
|
}
|
||
|
|
|
||
|
|
var id uuid.UUID
|
||
|
|
err := tx.QueryRow(ctx, `
|
||
|
|
SELECT c.id
|
||
|
|
FROM companies c
|
||
|
|
WHERE c.name = $1
|
||
|
|
AND COALESCE(c.legacy_company_id, '') <> $2
|
||
|
|
ORDER BY c.created_at ASC
|
||
|
|
LIMIT 1`, name, billing.A1LegacyCompanyID).Scan(&id)
|
||
|
|
if err == nil {
|
||
|
|
if _, err := tx.Exec(ctx, `INSERT INTO company_settings (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil {
|
||
|
|
return uuid.Nil, "", err
|
||
|
|
}
|
||
|
|
if _, err := tx.Exec(ctx, `INSERT INTO credit_balances (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil {
|
||
|
|
return uuid.Nil, "", err
|
||
|
|
}
|
||
|
|
return id, name, nil
|
||
|
|
}
|
||
|
|
if err != pgx.ErrNoRows {
|
||
|
|
return uuid.Nil, "", err
|
||
|
|
}
|
||
|
|
|
||
|
|
err = tx.QueryRow(ctx, `INSERT INTO companies (name) VALUES ($1) RETURNING id`, name).Scan(&id)
|
||
|
|
if err != nil {
|
||
|
|
return uuid.Nil, "", err
|
||
|
|
}
|
||
|
|
if _, err := tx.Exec(ctx, `INSERT INTO company_settings (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil {
|
||
|
|
return uuid.Nil, "", err
|
||
|
|
}
|
||
|
|
if _, err := tx.Exec(ctx, `INSERT INTO credit_balances (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil {
|
||
|
|
return uuid.Nil, "", err
|
||
|
|
}
|
||
|
|
return id, name, nil
|
||
|
|
}
|