fix
This commit is contained in:
@@ -11,7 +11,9 @@
|
|||||||
// -password 'choose-a-strong-password' \
|
// -password 'choose-a-strong-password' \
|
||||||
// -confirm
|
// -confirm
|
||||||
//
|
//
|
||||||
// Env aliases: PLATFORM_ADMIN_EMAIL, PLATFORM_ADMIN_PASSWORD, DATABASE_URL.
|
// Env aliases: none for credentials — pass -email / -password (or use
|
||||||
|
// `npm run seed:platform-admin -- --email … --password …`). DATABASE_URL may
|
||||||
|
// come from the shell or monorepo-root .env via config.LoadDotEnv.
|
||||||
//
|
//
|
||||||
// -promote-only grants admin on an existing user without changing the password
|
// -promote-only grants admin on an existing user without changing the password
|
||||||
// (-password is ignored). Always requires -confirm.
|
// (-password is ignored). Always requires -confirm.
|
||||||
@@ -28,15 +30,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
config.LoadDotEnv()
|
||||||
|
|
||||||
postgresURL := flag.String("postgres", os.Getenv("DATABASE_URL"), "Postgres URL")
|
postgresURL := flag.String("postgres", os.Getenv("DATABASE_URL"), "Postgres URL")
|
||||||
email := flag.String("email", os.Getenv("PLATFORM_ADMIN_EMAIL"), "Platform admin email")
|
email := flag.String("email", "", "Platform admin email (required; do not put in .env)")
|
||||||
password := flag.String("password", os.Getenv("PLATFORM_ADMIN_PASSWORD"), "Password (min 8; ignored with -promote-only)")
|
password := flag.String("password", "", "Password (min 8; ignored with -promote-only; do not put in .env)")
|
||||||
name := flag.String("name", "", "Display name (defaults to email local-part)")
|
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")
|
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")
|
confirm := flag.Bool("confirm", false, "Required: acknowledge this writes is_platform_admin + staff_role=admin")
|
||||||
@@ -93,11 +98,11 @@ func parseOptions(postgresURL, email, password, name string, promoteOnly, confir
|
|||||||
}
|
}
|
||||||
pg := strings.TrimSpace(postgresURL)
|
pg := strings.TrimSpace(postgresURL)
|
||||||
if pg == "" {
|
if pg == "" {
|
||||||
return options{}, fmt.Errorf("-postgres / DATABASE_URL is required")
|
return options{}, fmt.Errorf("-postgres / DATABASE_URL is required (export it, pass -postgres, or put DATABASE_URL in monorepo-root .env — the API loads .env, your shell may not)")
|
||||||
}
|
}
|
||||||
emailNorm := strings.ToLower(strings.TrimSpace(email))
|
emailNorm := strings.ToLower(strings.TrimSpace(email))
|
||||||
if emailNorm == "" || !strings.Contains(emailNorm, "@") {
|
if emailNorm == "" || !strings.Contains(emailNorm, "@") {
|
||||||
return options{}, fmt.Errorf("-email / PLATFORM_ADMIN_EMAIL is required")
|
return options{}, fmt.Errorf("-email is required (pass on the CLI; do not store admin credentials in .env)")
|
||||||
}
|
}
|
||||||
if err := rejectLocalDemoAccount(emailNorm, appEnv); err != nil {
|
if err := rejectLocalDemoAccount(emailNorm, appEnv); err != nil {
|
||||||
return options{}, err
|
return options{}, err
|
||||||
@@ -122,7 +127,7 @@ func parseOptions(postgresURL, email, password, name string, promoteOnly, confir
|
|||||||
}
|
}
|
||||||
pass := password // keep as provided (do not trim interior spaces)
|
pass := password // keep as provided (do not trim interior spaces)
|
||||||
if len(pass) < 8 {
|
if len(pass) < 8 {
|
||||||
return options{}, fmt.Errorf("-password / PLATFORM_ADMIN_PASSWORD must be at least 8 characters")
|
return options{}, fmt.Errorf("-password must be at least 8 characters (pass on the CLI; do not store in .env)")
|
||||||
}
|
}
|
||||||
if err := rejectDemoPassword(pass, appEnv); err != nil {
|
if err := rejectDemoPassword(pass, appEnv); err != nil {
|
||||||
return options{}, err
|
return options{}, err
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestParseOptionsRequiresPostgres(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := parseOptions("", "a@b.com", "password1", "", false, true, "development")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "DATABASE_URL") {
|
||||||
|
t.Fatalf("err = %v, want DATABASE_URL required", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseOptionsRequiresConfirm(t *testing.T) {
|
func TestParseOptionsRequiresConfirm(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, err := parseOptions("postgres://x", "a@b.com", "password1", "", false, false, "development")
|
_, err := parseOptions("postgres://x", "a@b.com", "password1", "", false, false, "development")
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoadDotEnv loads the monorepo-root .env into the process environment.
|
||||||
|
// Existing variables are never overridden. Safe for CLI tools that only need
|
||||||
|
// DATABASE_URL without a full config.Load (production often has no .env file).
|
||||||
|
func LoadDotEnv() {
|
||||||
|
loadDotEnv()
|
||||||
|
}
|
||||||
|
|
||||||
// loadDotEnv loads the monorepo-root .env into the process environment.
|
// loadDotEnv loads the monorepo-root .env into the process environment.
|
||||||
// Existing variables (including empty ones set by tests) are never overridden.
|
// Existing variables (including empty ones set by tests) are never overridden.
|
||||||
// Missing file is a no-op — production typically injects env without a file.
|
// Missing file is a no-op — production typically injects env without a file.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"db:wait": "node scripts/wait-postgres.mjs",
|
"db:wait": "node scripts/wait-postgres.mjs",
|
||||||
"migrate": "node scripts/migrate.mjs",
|
"migrate": "node scripts/migrate.mjs",
|
||||||
"seed": "node scripts/seed-local.mjs",
|
"seed": "node scripts/seed-local.mjs",
|
||||||
|
"seed:platform-admin": "node scripts/seed-platform-admin.mjs",
|
||||||
"health": "node scripts/health.mjs",
|
"health": "node scripts/health.mjs",
|
||||||
"cutover:deploy-check": "node scripts/cutover-deploy-check.mjs",
|
"cutover:deploy-check": "node scripts/cutover-deploy-check.mjs",
|
||||||
"cutover:deploy-check:code": "node scripts/cutover-deploy-check.mjs --skip-goose --skip-readyz",
|
"cutover:deploy-check:code": "node scripts/cutover-deploy-check.mjs --skip-goose --skip-readyz",
|
||||||
|
|||||||
+10
-5
@@ -34,14 +34,19 @@ function run(args) {
|
|||||||
if (r.status !== 0) process.exit(r.status ?? 1);
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`==> set-password ${adminEmail} (platform admin if present)`);
|
console.log(`==> seed-platform-admin ${adminEmail}`);
|
||||||
run([
|
run([
|
||||||
"run",
|
"run",
|
||||||
"./cmd/migrator",
|
"./cmd/seed-platform-admin",
|
||||||
"-postgres",
|
"-postgres",
|
||||||
env.DATABASE_URL,
|
env.DATABASE_URL,
|
||||||
"-set-password",
|
"-email",
|
||||||
`${adminEmail}:${adminPass}`,
|
adminEmail,
|
||||||
|
"-password",
|
||||||
|
adminPass,
|
||||||
|
"-name",
|
||||||
|
"Local Admin",
|
||||||
|
"-confirm",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -63,7 +68,7 @@ run([
|
|||||||
console.log("");
|
console.log("");
|
||||||
console.log("Local accounts:");
|
console.log("Local accounts:");
|
||||||
console.log(` demo: ${demoEmail} / ${demoPass} (also demo@descrybe.test)`);
|
console.log(` demo: ${demoEmail} / ${demoPass} (also demo@descrybe.test)`);
|
||||||
console.log(` admin: ${adminEmail} / ${adminPass} (platform admin when that user exists)`);
|
console.log(` admin: ${adminEmail} / ${adminPass} (platform admin)`);
|
||||||
console.log(" plan: Enterprise — Local Demo Co");
|
console.log(" plan: Enterprise — Local Demo Co");
|
||||||
console.log(`Web: http://localhost:${DEV_WEB_PORT}/login`);
|
console.log(`Web: http://localhost:${DEV_WEB_PORT}/login`);
|
||||||
console.log(`API: http://localhost:${DEV_API_PORT}`);
|
console.log(`API: http://localhost:${DEV_API_PORT}`);
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Upsert a platform admin (is_platform_admin + staff_role=admin).
|
||||||
|
* Loads monorepo-root `.env` only for DATABASE_URL (same as the API).
|
||||||
|
* Email/password are CLI flags only — never read from .env.
|
||||||
|
*
|
||||||
|
* Usage (from repo root):
|
||||||
|
* npm run seed:platform-admin -- --email you@example.com --password '…'
|
||||||
|
* npm run seed:platform-admin -- --email you@example.com --promote-only
|
||||||
|
*
|
||||||
|
* Optional: --name "Display Name"
|
||||||
|
*/
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
import { detectHostProfile, withGoLowMem } from "./lowmem-env.mjs";
|
||||||
|
import { loadRootEnv, repoRoot } from "./root-env.mjs";
|
||||||
|
|
||||||
|
const env = withGoLowMem(loadRootEnv(process.env), detectHostProfile());
|
||||||
|
|
||||||
|
function usage(msg) {
|
||||||
|
if (msg) console.error(msg);
|
||||||
|
console.error(`Usage:
|
||||||
|
npm run seed:platform-admin -- --email you@example.com --password 'strong-password'
|
||||||
|
npm run seed:platform-admin -- --email you@example.com --promote-only
|
||||||
|
npm run seed:platform-admin -- --email you@example.com --password '…' --name 'Ops'
|
||||||
|
|
||||||
|
Email/password must be passed on the CLI (not stored in .env).
|
||||||
|
DATABASE_URL comes from the shell or monorepo-root .env.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse `--key value` / `--flag` from argv after npm's `--`. */
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const out = { email: "", password: "", name: "", promoteOnly: false };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === "--promote-only") {
|
||||||
|
out.promoteOnly = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a === "--email" || a === "-email") {
|
||||||
|
out.email = String(argv[++i] || "").trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a === "--password" || a === "-password") {
|
||||||
|
out.password = String(argv[++i] || "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a === "--name" || a === "-name") {
|
||||||
|
out.name = String(argv[++i] || "").trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a === "--help" || a === "-h") usage();
|
||||||
|
usage(`Unknown argument: ${a}`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = parseArgs(process.argv.slice(2));
|
||||||
|
|
||||||
|
if (!env.DATABASE_URL) {
|
||||||
|
usage("DATABASE_URL is required (shell or monorepo-root .env)");
|
||||||
|
}
|
||||||
|
if (!opts.email || !opts.email.includes("@")) {
|
||||||
|
usage("--email is required");
|
||||||
|
}
|
||||||
|
if (!opts.promoteOnly && opts.password.length < 8) {
|
||||||
|
usage("--password is required (min 8 characters), or pass --promote-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiDir = path.join(repoRoot, "apps", "api");
|
||||||
|
const args = [
|
||||||
|
"run",
|
||||||
|
"./cmd/seed-platform-admin",
|
||||||
|
"-postgres",
|
||||||
|
env.DATABASE_URL,
|
||||||
|
"-email",
|
||||||
|
opts.email,
|
||||||
|
"-confirm",
|
||||||
|
];
|
||||||
|
if (opts.name) {
|
||||||
|
args.push("-name", opts.name);
|
||||||
|
}
|
||||||
|
if (opts.promoteOnly) {
|
||||||
|
args.push("-promote-only");
|
||||||
|
} else {
|
||||||
|
args.push("-password", opts.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`==> seed-platform-admin ${opts.email}`);
|
||||||
|
const r = spawnSync("go", args, {
|
||||||
|
cwd: apiDir,
|
||||||
|
env,
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
process.exit(r.status ?? 1);
|
||||||
Reference in New Issue
Block a user