Compare commits
3 Commits
0d81e62ac4
...
dc22aa0ff0
| Author | SHA1 | Date | |
|---|---|---|---|
| dc22aa0ff0 | |||
| e50a2150d5 | |||
| 3a3844b68e |
@@ -4,11 +4,15 @@
|
||||
|
||||
**Last updated:** 2026-05-12
|
||||
|
||||
**Active task:** Phase O cutover for self-serve signup. All code blockers are closed on `main` (PR #164 `3f04911`, PR #165 `ba45cfe`). **Currently blocked on Stripe live-mode activation — root cause is EIN, not code.** User does not yet have an EIN for ResolutionFlow, LLC; Stripe requires a tax ID for live-mode activation. Applying via IRS.gov on 2026-05-13. Mailing-address decision made 2026-05-12: user will enter home address into the Stripe business profile temporarily so live-mode isn't blocked on the P.O. Box; the **public-facing** mailing-address `TODO` in `ContactPage.tsx` and `PoliciesPage.tsx` stays "available on request" until the P.O. Box is set up (do NOT put the home address on the public site). Stripe address can be updated to the P.O. Box later without re-verification. Apex DNS at Namecheap is still missing (separate user-side issue tracked below); only matters once Stripe runs its site-verification step, which happens after the business-profile fields are accepted. Nothing on the code side blocks live-mode flip.
|
||||
**Active task:** Phase O cutover for self-serve signup. All code blockers are closed on `main` (PR #164 `3f04911`, PR #165 `ba45cfe`, PR #167 `e50a215`). **Currently blocked on Stripe live-mode activation — root cause is EIN, not code.** User does not yet have an EIN for ResolutionFlow, LLC; Stripe requires a tax ID for live-mode activation. Applying via IRS.gov on 2026-05-13. Mailing-address decision made 2026-05-12: user will enter home address into the Stripe business profile temporarily so live-mode isn't blocked on the P.O. Box; the **public-facing** mailing-address `TODO` in `ContactPage.tsx` and `PoliciesPage.tsx` stays "available on request" until the P.O. Box is set up (do NOT put the home address on the public site). Stripe address can be updated to the P.O. Box later without re-verification. Apex DNS at Namecheap is still missing (separate user-side issue tracked below); only matters once Stripe runs its site-verification step, which happens after the business-profile fields are accepted. Nothing on the code side blocks live-mode flip.
|
||||
|
||||
**Bug pending capture (2026-05-12):** User reported finding a bug during the session but did not provide details — planning to send a screenshot via the VS Code extension GUI in the next session. Ask for the screenshot at session start, then triage. No further context yet.
|
||||
|
||||
## Where this session ended
|
||||
|
||||
PR #165 squash-merged (`ba45cfe feat(legal): add /policies, /contact, /promotions pages + MarketingFooter (#165)`):
|
||||
PR #167 merged (`e50a215 Merge pull request '...create_site_admin.py...'`). Bootstrap script for the site-wide super-admin: `backend/scripts/create_site_admin.py`. Idempotent — creates or promotes a `super_admin` on any env. Reads `--email`, optionally `--send-reset` (mails the reset link) or `--print-reset` (prints to stdout) or `--promote-only`. Uses `ADMIN_DATABASE_URL` for BYPASSRLS. Verified end-to-end against the deployed Railway backend container by the user via `railway ssh` ("we're good now" confirmation, 2026-05-12 evening). Intended prod invocation when Stripe/EIN clears: `railway run python -m scripts.create_site_admin --email michael@resolutionflow.com --send-reset` from inside the backend container shell (not local Windows — local Python lacks the dep tree).
|
||||
|
||||
Earlier in the session, PR #165 squash-merged (`ba45cfe feat(legal): add /policies, /contact, /promotions pages + MarketingFooter (#165)`):
|
||||
|
||||
- **New pages**, all SPA, matching existing `/privacy` and `/terms` pattern: `/policies` (consolidated Customer Policies — customer service contact, return/refund/dispute policy, cancellation, U.S. legal and export restrictions, promotional terms; anchor IDs per subsection), `/contact` (phone **(470) 949-4131**, support/sales/billing/security inboxes, response-time SLAs), `/promotions` (stub stating no promotions currently active — satisfies Policies §6.2 cross-ref).
|
||||
- **`MarketingFooter`** (`frontend/src/components/common/MarketingFooter.tsx`) extracted from inline landing footer and mounted on `/landing`, `/pricing`, `/contact-sales`. Reuses existing `landing-footer*` CSS — must be rendered inside a `.landing-page` wrapper (documented in a JSX comment) because `--lp-*` vars are scoped there. All four legal links (Privacy / Terms / Policies / Contact) are now reachable from every marketing surface.
|
||||
@@ -23,17 +27,22 @@ Single alembic head: `4ce3e594cb87` (no schema changes in this PR).
|
||||
|
||||
## Resume point
|
||||
|
||||
**Phase O manual ops** — entirely user-side, gated on the apex DNS fix below:
|
||||
**First thing next session:** ask the user for the bug screenshot (mentioned at end of 2026-05-12 session — they were planning to send it via VS Code extension GUI). Triage that before resuming Phase O work.
|
||||
|
||||
1. **Stripe Dashboard live-mode:**
|
||||
After that — **Phase O manual ops, all user-side, all gated on EIN landing first:**
|
||||
|
||||
1. **EIN application** (user, 2026-05-13 via IRS.gov). Without this, Stripe live-mode can't activate.
|
||||
2. **Stripe Dashboard live-mode** (once EIN is in hand):
|
||||
- 3 Products (Starter, Pro, Enterprise). Monthly Prices for Starter ($19.99) + Pro ($29.99). No Prices on Enterprise (sales-led).
|
||||
- Customer Portal with plan-switching disabled.
|
||||
- Webhook at `https://api.resolutionflow.com/api/v1/webhooks/stripe` with 5 events. Save live signing secret.
|
||||
- **Business profile fields**: Customer service URL `https://resolutionflow.com/contact`. Refund/cancellation policy URL `https://resolutionflow.com/policies`. Terms `https://resolutionflow.com/terms`. Privacy `https://resolutionflow.com/privacy`. Phone `(470) 949-4131`. Mailing address per Stripe form (not required on website).
|
||||
2. **Railway prod env**: `STRIPE_SECRET_KEY=sk_live_...`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PUBLISHABLE_KEY` + `VITE_STRIPE_PUBLISHABLE_KEY` (frontend redeploy required — Vite bake-at-build, Lesson 60), `OAUTH_REDIRECT_BASE=https://resolutionflow.com`, `SELF_SERVE_ENABLED=false` (still false at this point), `INTERNAL_TESTER_EMAILS=<allowlist>`, prod Google + Microsoft OAuth credentials.
|
||||
3. **Sync against prod**: `railway run python -m scripts.sync_stripe_plan_ids`. Verify `plan_billing` rows have `sk_live_*` price IDs.
|
||||
4. **Internal validation (Task 46)**: 9 scenarios with internal testers whose emails match `INTERNAL_TESTER_EMAILS`.
|
||||
5. **Flag flip (Task 47)**: email pilots, set `SELF_SERVE_ENABLED=true` + `VITE_SELF_SERVE_ENABLED=true` (frontend redeploy). PostHog signup-funnel dashboard + Sentry alert at >1/hour Stripe webhook errors.
|
||||
- **Business profile fields**: Customer service URL `https://resolutionflow.com/contact`. Refund/cancellation policy URL `https://resolutionflow.com/policies`. Terms `https://resolutionflow.com/terms`. Privacy `https://resolutionflow.com/privacy`. Phone `(470) 949-4131`. Mailing address = user's home address temporarily (private Stripe field; will swap to P.O. Box later without re-verification). EIN = the newly-issued tax ID.
|
||||
3. **Apex DNS fix at Namecheap** (re-add `@` ALIAS → `c9g7uku8.up.railway.app`, or re-add apex as a Railway custom domain). Becomes the next blocker once Stripe runs its site-verification step.
|
||||
4. **Railway prod env**: `STRIPE_SECRET_KEY=sk_live_...`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PUBLISHABLE_KEY` + `VITE_STRIPE_PUBLISHABLE_KEY` (frontend redeploy required — Vite bake-at-build, Lesson 60), `OAUTH_REDIRECT_BASE=https://resolutionflow.com`, `SELF_SERVE_ENABLED=false` (still false at this point), `INTERNAL_TESTER_EMAILS=<allowlist>`, prod Google + Microsoft OAuth credentials.
|
||||
5. **Bootstrap prod super-admin** via the new `create_site_admin.py` script (PR #167, on main as `e50a215`). Run from inside the backend container shell (not local Windows): `railway ssh --service=<backend-service-name>` then `python -m scripts.create_site_admin --email michael@resolutionflow.com --send-reset`. Reset email arrives at `michael@resolutionflow.com`, user clicks the link, sets a password, logs in as super_admin. Idempotent — safe to re-run.
|
||||
6. **Sync Stripe → DB**: `railway run python -m scripts.sync_stripe_plan_ids` (or via `railway ssh` for the same reason as #5). Verify `plan_billing` rows have `sk_live_*` price IDs.
|
||||
7. **Internal validation (Phase O Task 46)**: 9 scenarios with internal testers whose emails match `INTERNAL_TESTER_EMAILS`.
|
||||
8. **Flag flip (Task 47)**: email pilots, set `SELF_SERVE_ENABLED=true` + `VITE_SELF_SERVE_ENABLED=true` (frontend redeploy). PostHog signup-funnel dashboard + Sentry alert at >1/hour Stripe webhook errors.
|
||||
|
||||
## Open issues from prior session (non-code, user-side)
|
||||
|
||||
@@ -46,5 +55,6 @@ Single alembic head: `4ce3e594cb87` (no schema changes in this PR).
|
||||
- `INTERNAL_TESTER_EMAILS` parsed comma-separated → normalized lowercase list. Anonymous callers always see the global flag — allowlist never leaks via unauthenticated request content (regression test enforces).
|
||||
- Office-hours design doc at `~/.gstack/projects/chihlasm-resolutionflow/abc-feat-self-serve-signup-phase-2-design-20260507-112020.md` (documentation-builder thesis). NOT yet adopted as roadmap — gated on 3 cold calls with external Directors of Onboarding.
|
||||
- Mailing address fill-in: search for `TODO: replace with full mailing address` in `frontend/src/pages/ContactPage.tsx` and `frontend/src/pages/PoliciesPage.tsx` (one each) once P.O. Box is purchased.
|
||||
- `backend/scripts/create_site_admin.py` is the durable site-admin bootstrap tool — use for first prod admin, recovery, or promoting future teammates. Idempotent. Three modes: `--send-reset`, `--print-reset`, `--promote-only`. Run from inside the deployed backend container via `railway ssh`, not from local Windows (local Python lacks the dep tree).
|
||||
- Bot-crawlability of legal pages: still SPA-rendered. Stripe didn't enforce content scraping last time (issue turned out to be DNS). If a future vendor review flags it, pre-render with `vite-plugin-prerender-spa` (~half day).
|
||||
- Frontend env additions for cutover: `VITE_SELF_SERVE_ENABLED`, `VITE_GOOGLE_CLIENT_ID`, `VITE_MS_CLIENT_ID`, `VITE_OAUTH_REDIRECT_BASE`, `VITE_CALENDLY_URL`, `VITE_STRIPE_PUBLISHABLE_KEY`.
|
||||
|
||||
@@ -12,6 +12,29 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-12 ~06:30 UTC — Claude — PR #167 (site-admin bootstrap script) merged; bug pending capture
|
||||
|
||||
**Accomplished:**
|
||||
|
||||
- User reported being unable to log into prod with `admin@resolutionflow.example.com` — that's the dev seed email (`.example.com` is a documentation TLD), only present in dev. Prod has no admin user at all because `seed_test_users.py` doesn't run in prod, self-serve is still gated, and even when it flips on signup creates `owner` roles not `super_admin`.
|
||||
- Designed and built `backend/scripts/create_site_admin.py` — idempotent CLI script for creating or promoting a site-wide super-admin on any environment. Three modes: `--send-reset` (mails reset link), `--print-reset` (stdout reset link), `--promote-only` (promote existing user without creating). Creates an `Account` first, then a `User` with `is_super_admin=true`, `account_role='owner'`, `email_verified_at` stamped at creation, `password_hash=NULL` (forces the reset flow on first login). Uses `ADMIN_DATABASE_URL` (BYPASSRLS) — required because `users` is RLS-enabled and the script has no tenant context at bootstrap. Reset token mints via existing `create_password_reset_token` helper, hashes JTI into `password_reset_tokens` row matching the `/auth/password/forgot` shape.
|
||||
- Smoke-tested all three paths in the dev container before pushing: fresh create on a new email (Account + User + reset URL emitted), idempotent re-run on same email (SKIP message + new reset URL), `--promote-only` on a user with `password_hash=NULL` (promotes + issues reset). Cleaned up the dev test row + account afterwards.
|
||||
- Initial bug: had `used: false` in the `password_reset_tokens` INSERT — actual column is `used_at` (nullable timestamp, NULL means "not used"). Fixed before pushing.
|
||||
- PR #167 opened, CI green, squash-merged into main as `e50a215`. Remote branch `feat/site-admin-script` auto-deleted.
|
||||
- User confirmed end-to-end success on prod via `railway ssh --service=<backend>` then `python -m scripts.create_site_admin ...` ("we're good now"). Specific service name not captured. First prod super-admin row now exists in the prod DB.
|
||||
- Stripe live-mode activation block traced to EIN, not code (user does not yet have an EIN for ResolutionFlow, LLC). Applying via IRS.gov 2026-05-13. Mailing-address decision: home address into Stripe's **private** business profile temporarily so live-mode isn't blocked on the P.O. Box; public `ContactPage`/`PoliciesPage` stays "available on request". Stripe accepts address update later without re-verification.
|
||||
- PR #166 (docs handoff for PR #164/#165 merges + EIN decision) still open from earlier in this same session — was never merged. This entry rebases the docs branch onto current main (which now includes PR #167) and adds the PR #167 narrative + bug-pending state so a fresh session has the full picture in one merge.
|
||||
- User reported finding a bug in a UI surface but did not provide details — planning to send a screenshot via the VS Code extension GUI in the next session (CLI is unreliable for them). Next session: ask for the screenshot at session start, then triage.
|
||||
|
||||
**Left for next session:**
|
||||
|
||||
- Get the bug screenshot from the user, triage, fix or scope.
|
||||
- Otherwise everything that was on the prior entry's left-for-next-session still stands: EIN application Tuesday 2026-05-13, then Stripe live-mode setup, apex DNS at Namecheap, Railway prod env vars, internal validation, flag flip.
|
||||
|
||||
**Files touched (all merged to main via PR #167 squash `e50a215`):** `backend/scripts/create_site_admin.py` (new, ~270 lines including docstring). Plus `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md` on `docs/handoff-pr-165-merge` (PR #166, awaiting merge).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-12 05:30 UTC — Claude — PR #164 + #165 merged; Stripe activation reported blocked
|
||||
|
||||
**Accomplished:**
|
||||
|
||||
274
backend/scripts/create_site_admin.py
Normal file
274
backend/scripts/create_site_admin.py
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create or promote a site-wide super-admin user on any environment.
|
||||
|
||||
Designed for the prod bootstrap case where no admin exists yet and self-serve
|
||||
signup is gated, so there is no way to obtain admin access through the UI.
|
||||
Also safe to use as a recovery tool: if an admin email exists already, the
|
||||
script just promotes them to `is_super_admin=True` instead of duplicating.
|
||||
|
||||
Usage:
|
||||
|
||||
# Bootstrap a fresh super-admin and email a password-reset link:
|
||||
python -m scripts.create_site_admin --email michael@resolutionflow.com --send-reset
|
||||
|
||||
# Same but emit the reset URL on stdout instead of sending email (useful
|
||||
# if email infra is not configured yet or if you want to bypass the inbox):
|
||||
python -m scripts.create_site_admin --email michael@resolutionflow.com --print-reset
|
||||
|
||||
# Promote an existing user (no reset needed if they already have a password):
|
||||
python -m scripts.create_site_admin --email michael@resolutionflow.com --promote-only
|
||||
|
||||
The script is idempotent. Running it twice on the same email is safe.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.email import EmailService
|
||||
from app.core.security import (
|
||||
create_password_reset_token,
|
||||
decode_token,
|
||||
hash_token,
|
||||
)
|
||||
|
||||
|
||||
def _display_code() -> str:
|
||||
return "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||
|
||||
|
||||
async def _find_user(conn: AsyncConnection, email: str):
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"SELECT id, account_id, is_super_admin, password_hash "
|
||||
"FROM users WHERE email = :email"
|
||||
),
|
||||
{"email": email},
|
||||
)
|
||||
return result.first()
|
||||
|
||||
|
||||
async def _create_user_and_account(
|
||||
conn: AsyncConnection,
|
||||
email: str,
|
||||
name: str,
|
||||
account_name: str,
|
||||
now: datetime,
|
||||
) -> uuid.UUID:
|
||||
"""Create a new Account and a new super-admin User as its owner.
|
||||
|
||||
Mirrors the shape used by seed_test_users.py for the super-admin row,
|
||||
minus the shared dev password — this bootstrap user gets no password
|
||||
until the reset flow runs.
|
||||
"""
|
||||
account_id = uuid.uuid4()
|
||||
user_id = uuid.uuid4()
|
||||
|
||||
await conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO accounts (id, name, display_code, created_at, updated_at)
|
||||
VALUES (:id, :name, :code, :now, :now)
|
||||
"""
|
||||
),
|
||||
{"id": account_id, "name": account_name, "code": _display_code(), "now": now},
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (
|
||||
id, email, password_hash, name, role, is_super_admin,
|
||||
is_team_admin, is_active, account_id, account_role,
|
||||
created_at, email_verified_at
|
||||
)
|
||||
VALUES (
|
||||
:id, :email, NULL, :name, 'engineer', true,
|
||||
false, true, :account_id, 'owner',
|
||||
:now, :now
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"account_id": account_id,
|
||||
"now": now,
|
||||
},
|
||||
)
|
||||
await conn.execute(
|
||||
text("UPDATE accounts SET owner_id = :uid WHERE id = :aid"),
|
||||
{"uid": user_id, "aid": account_id},
|
||||
)
|
||||
return user_id
|
||||
|
||||
|
||||
async def _promote_existing(conn: AsyncConnection, user_id: uuid.UUID, now: datetime) -> None:
|
||||
"""Promote an existing user to super-admin and backfill verification."""
|
||||
await conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE users
|
||||
SET is_super_admin = true,
|
||||
email_verified_at = COALESCE(email_verified_at, :now),
|
||||
is_active = true
|
||||
WHERE id = :uid
|
||||
"""
|
||||
),
|
||||
{"uid": user_id, "now": now},
|
||||
)
|
||||
|
||||
|
||||
async def _issue_reset_link(
|
||||
conn: AsyncConnection, user_id: uuid.UUID, send_email: bool, email: str
|
||||
) -> Optional[str]:
|
||||
"""Generate a password-reset token, persist its hash, and return the URL.
|
||||
|
||||
Mirrors /auth/password/forgot. We commit the token row directly because
|
||||
the script owns its own transaction (not the API request lifecycle).
|
||||
"""
|
||||
raw_token = create_password_reset_token(str(user_id))
|
||||
payload = decode_token(raw_token)
|
||||
if not payload or not payload.get("jti"):
|
||||
return None
|
||||
|
||||
await conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO password_reset_tokens
|
||||
(id, token_hash, user_id, expires_at, created_at)
|
||||
VALUES (:id, :token_hash, :user_id, :expires_at, :created_at)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"token_hash": hash_token(payload["jti"]),
|
||||
"user_id": user_id,
|
||||
"expires_at": datetime.fromtimestamp(payload["exp"], tz=timezone.utc),
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
},
|
||||
)
|
||||
|
||||
reset_url = f"{settings.FRONTEND_URL}/reset-password?token={raw_token}"
|
||||
|
||||
if send_email:
|
||||
# Best-effort. If email infra is misconfigured, the caller still
|
||||
# has --print-reset as a fallback.
|
||||
try:
|
||||
await EmailService.send_password_reset_email(
|
||||
to_email=email, reset_url=reset_url
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f" [WARN] Email send failed: {exc}")
|
||||
print(f" [WARN] Use the printed URL below as a fallback.")
|
||||
|
||||
return reset_url
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace) -> int:
|
||||
email = args.email.strip().lower()
|
||||
name = args.name or email.split("@", 1)[0].title()
|
||||
account_name = args.account_name or "ResolutionFlow Admin"
|
||||
|
||||
admin_url = getattr(settings, "ADMIN_DATABASE_URL", None) or settings.DATABASE_URL
|
||||
engine = create_async_engine(admin_url, echo=False)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
existing = await _find_user(conn, email)
|
||||
|
||||
if existing is None:
|
||||
if args.promote_only:
|
||||
print(f"[ERROR] --promote-only set but no user with email {email!r} exists.")
|
||||
return 1
|
||||
user_id = await _create_user_and_account(
|
||||
conn, email, name, account_name, now
|
||||
)
|
||||
print(f" [OK] Created super-admin user {email} (id={user_id})")
|
||||
else:
|
||||
user_id = existing.id
|
||||
if existing.is_super_admin:
|
||||
print(f" [SKIP] {email} already exists and is super-admin (id={user_id})")
|
||||
else:
|
||||
await _promote_existing(conn, user_id, now)
|
||||
print(f" [OK] Promoted {email} to super-admin (id={user_id})")
|
||||
|
||||
# Skip reset issuance for --promote-only when the user already
|
||||
# has a password (they can just log in with their existing creds).
|
||||
should_issue_reset = not args.promote_only or (
|
||||
existing is not None and existing.password_hash is None
|
||||
)
|
||||
|
||||
if should_issue_reset:
|
||||
reset_url = await _issue_reset_link(
|
||||
conn, user_id, send_email=args.send_reset, email=email
|
||||
)
|
||||
if reset_url is None:
|
||||
print(" [ERROR] Failed to mint password-reset token.")
|
||||
return 2
|
||||
|
||||
print()
|
||||
if args.send_reset:
|
||||
print(f" [OK] Password-reset email sent to {email}")
|
||||
print(f" [INFO] Reset link (also emailed) — copy if email is delayed:")
|
||||
if args.print_reset or not args.send_reset:
|
||||
print(f" {reset_url}")
|
||||
else:
|
||||
print(f" {reset_url}")
|
||||
print()
|
||||
print(" This link expires per PASSWORD_RESET_TOKEN_EXPIRE in settings.")
|
||||
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(
|
||||
prog="create_site_admin",
|
||||
description="Create or promote a site-wide super-admin user.",
|
||||
)
|
||||
p.add_argument("--email", required=True, help="Email of the admin (will be normalized to lowercase).")
|
||||
p.add_argument("--name", help="Display name. Defaults to the local part of the email, title-cased.")
|
||||
p.add_argument(
|
||||
"--account-name",
|
||||
help="Account name to create alongside a brand-new user. Ignored if the user already exists. Defaults to 'ResolutionFlow Admin'.",
|
||||
)
|
||||
mode = p.add_mutually_exclusive_group()
|
||||
mode.add_argument(
|
||||
"--send-reset",
|
||||
action="store_true",
|
||||
help="Send a password-reset email to the admin. The reset URL is also printed to stdout as a fallback.",
|
||||
)
|
||||
mode.add_argument(
|
||||
"--print-reset",
|
||||
action="store_true",
|
||||
help="Mint a reset token and print the URL to stdout WITHOUT sending email. Use when email infra is not configured.",
|
||||
)
|
||||
mode.add_argument(
|
||||
"--promote-only",
|
||||
action="store_true",
|
||||
help="Only promote an existing user to super-admin. Will NOT create a new user, and will NOT issue a reset link unless the existing user has no password.",
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n[*] ResolutionFlow — Site Admin Bootstrap")
|
||||
print("=" * 60)
|
||||
args = _build_parser().parse_args()
|
||||
sys.exit(asyncio.run(main(args)))
|
||||
Reference in New Issue
Block a user