Skip to content
prod e051e98
Browse

3 · Credentials & branding

Objective — lock the account before anything public ships: rotate the default admin login, upload a coherent brand kit, resolve the server PHP binary, then strip the vendor’s demo content — because the default creds are a security hole and fake testimonials are a legal one.

This page is the MUST core of Phase 6. It does four things, in this order, and the order is not negotiable: rotate the admin credentials, produce the brand asset kit, resolve the server PHP binary, then strip the vendor’s demo content. Skipping the rotation or the demo-content audit are both ship-blockers — the first is a security hole, the second is a legal one. Everything here reads from the brand profile you built on the previous page. If _CUSTOMIZATIONS.md → ## Project Brand Profile is incomplete, stop and finish it — these tasks are “read the row, apply the value”, not “decide the value now”.

flowchart LR
R["1 · Rotate creds<br/>(security first)"] --> A["2 · Brand assets<br/>(generate kit)"]
A --> U["3 · Upload identity<br/>(config branding)"]
U --> P["4 · PHP binary<br/>(SSH prereq)"]
P --> D["5 · Demo content<br/>(replace, don't delete)"]
classDef must fill:#0d7b6e22,stroke:#0d7b6e;
classDef warn fill:#f4a26122,stroke:#f4a261;
class R,U,D must;
class A,P warn;

The email/password change happens in the running app’s profile page — a person logging into the panel. The agent can audit and store secrets, but the panel form itself is human-driven (or a clipboard-bridged Playwright pass).

  1. Change the email. Admin → Profile / Account Settings (the exact path is in your vendor-capabilities.md § Admin menus; commonly /user/profile on Jetstream apps). Change from the vendor’s example.com address to a real inbox you control — you need it for password resets.

    • ✅ The admin email is a real inbox you control, not the vendor’s example.com.
  2. Change the password. Same page → Change Password. Use a generated 20+ char password. Save it to your password manager and credentials.md before you submit.

    • ✅ A 20+ char password is set and already saved to the vault before submit.

The installer seeds demo users independently in every database (local, staging, production) — audit each one. Pick whichever query path your environment supports; the only invalid choice is “skip it because I have no SQL client”.

  1. List every user via tinker (the universal fallback — works on any machine where the app runs).

    Terminal window
    php artisan tinker --execute="\
    App\Models\User::select('id','name','email')->get()->each(function(\$u){
    echo \$u->id.'|'.\$u->email.'|'.\$u->name.PHP_EOL;
    });"
    # Expected: one id|email|name line per user
    • ✅ Every user row prints, so demo/test identities are visible.
  2. List users on staging / production over SSH (respects the User model + soft-deletes).

    Terminal window
    dep ssh staging "cd ~/domains/staging.[DOMAIN]/deploy/current && \
    php artisan tinker --execute=\"
    \App\Models\User::select('id','name','email')->get()->each(function(\$u){
    echo \$u->id.'|'.\$u->email.'|'.\$u->name.PHP_EOL;
    });
    \" 2>&1 | grep -v '^$'"
    # Expected: the same id|email|name listing, from the remote DB
    • ✅ For each result: delete demo/test identities (demo@example.com, Emma Holden, etc.), rotate the password on any account you keep, and record the action in credentials.md.

Store the new secret correctly — two valid homes, never leaving a rotated password only in /tmp/ or the clipboard:

  • 1Password (preferred): op item edit "superadmin" --vault "[PROJECT]-Local" "username=…" "password=…". If op vault list doesn’t show your vault and op vault get returns 403 Forbidden, a service account lacks per-vault access — grant it in the web UI (Integrations → service account → Vaults → Add), or fall back to credentials.md.
  • credentials.md (fallback): edit with Python, not sed/echo — passwords contain $ / + & ! that break shell escaping. Mark old defaults as “rotated”, don’t delete them — the audit trail shows which environments still need work.

Default credentials live in the database, not the code, so every environment with its own DB needs its own rotation — local included.

EnvironmentWhenTracking entryEntropy — and why
Local devPhase 3, or earliest Phase 6 touchSuperadmin (Local)12 chars, pronounceable — typed often
StagingThis taskSuperadmin (Staging)24 chars, random — never typed
ProductionPhase 12, after first deploy + installerSuperadmin (Production)24 chars, random — stored in the vault

If you copy the staging DB → production at deploy time, production inherits the staging password and needs no separate rotation. Document which path you took.

For AI-assisted rotation, use the clipboard bridge so the password never becomes a tool parameter (passing it as a browser_type argument leaks it into the tool-call log and transcript). The password lives in a 600-mode file, is loaded into the OS clipboard, and is read by in-browser JS.

  1. Generate the password, load it to the clipboard via Python (no shell expansion of `$ ! “).

    Terminal window
    python3 - <<'PY'
    import secrets, pathlib, subprocess
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    pwd = "".join(secrets.choice(alphabet) for _ in range(24))
    pathlib.Path("/tmp/new_admin_pass.txt").write_text(pwd + "\n")
    subprocess.run(["pbcopy"], input=pwd, text=True, check=True)
    print(f"loaded ({len(pwd)} chars)")
    PY
    # Expected: "loaded (24 chars)" — the value is never printed
    • ✅ The clipboard holds exactly 24 characters; only the length was printed.

Then in Playwright’s browser_evaluate (not browser_type), read navigator.clipboard.readText(), set the three password fields via the prototype value setter (so Vue/React/Livewire bindings fire), and return only lengths + a match boolean — never the value.

After saving, clear everything: printf '' | pbcopy and shred -uz /tmp/new_admin_pass.txt. Then verify it actually took — don’t trust in-page success text (Jetstream’s only signal is the fields emptying). The reliable check: log out, confirm old credentials fail, confirm new credentials work. A successful change invalidates the session, so navigating to any auth-required page should redirect to /login — that redirect is the proof. If the dashboard still loads, the change did not work. (Jetstream logout is POST-only; GET /logout returns 405.)

If your project has the image-gen toolchain wired (brand-profile + deployment-map from Phase 1B), the pipeline produces everything section 3 of this page and the landing page need:

StepOutputCost
Source logos3 SVGs (square / horizontal / vertical)~$0.24
Derive variants12 SVGs (light / dark / mono / inverted × 3)$0 (pure SVG)
Favicons16→1024 PNGs + .ico + site.webmanifest$0
Hero / feature imageryper-scene PNGs~$0.039 each
IconsMIT-licensed (Lucide via CDN)$0
Deploycopies to vendor paths per asset-deployment-map.json$0

Two human approval gates are built in — the agent pauses, a person reviews, then responds.

  1. Pass both human approval gates before the pipeline deploys assets.

    • ✅ The 3 source logos and first 3 hero images were each answered approve, regenerate: <feedback>, or abort.

No toolchain? Resize a single master with ImageMagick (magick "$MASTER" -resize ${size}x${size} …) into the sizes the panel asks for, build the multi-size favicon with -define icon:auto-resize=16,32,48,64, and make the Apple touch icon at 180×180 with no transparency (-background white -alpha remove). Commit the kit under Admin-Local/1-Project/0-Brand/assets/ (same tree as Phase 8 · Brand kit); keep machine-readable inputs in Admin-Local/1-Project/0-Brand/brand-profile.json.

Most CodeCanyon apps cluster these under one Settings → App / Settings → Branding tab. These are panel uploads and field edits — a person clicking through the admin UI (or a Playwright pass driving it).

  1. Fill app identity in Settings → App.

    Admin fieldBrand-profile sourceNotes
    App nameIdentity → App nameExact capitalization — shows everywhere
    TaglineIdentity → Tagline≤80 chars — mobile truncates
    TimezoneApp settings → TimezoneIANA, e.g. Asia/Dubai
    Default currencyApp settings → CurrencyISO 4217 — also drives Stripe
    Default localeApp settings → Localee.g. en_AE, ar_AE
    First day of weekApp settings → First daySunday ME/US, Monday EU/Asia
    • ✅ App name, tagline, timezone, currency, locale, and first-day are set from the brand profile.
  2. Fill company information (feeds invoices, PDF exports, email footers).

    • Required: legal entity name (matches business registration), company address (use registered, not residential).

    • Recommended: support email (support@[domain]), website URL.

    • Optional: support phone, registration number, VAT/Tax ID, founding year.

    • ✅ Legal entity name and address are real (no placeholder); a grep-able REPLACE_ME_* marks any required-but-unknown field.

  3. Collect every asset variant before uploading, then upload in the typical Froiden-family order.

    AssetLightDarkSizeFormat
    Main logo (wide)~400×100SVG / transparent PNG
    Logo square / icon512×512SVG / PNG
    Login logo~300×80transparent PNG
    Email header logo~600×150PNG (SVG fails in email)
    Invoice / PDF logo300×100PNG 300 DPI
    Faviconone32×32ICO / SVG
    Apple touch iconone180×180PNG, no transparency
    PWA iconone512×512PNG
    OG / social previewone1200×630PNG / JPG

    Typical upload order: main logo light → dark → favicon → login logo (often a separate sub-tab) → email logo (Settings → Email) → app/PWA icon → invoice logo (Settings → Finance). After each upload click Save, refresh, and confirm the preview updates — some panels silently reject non-PNG or >500KB files.

    • ✅ Every variant uploaded; each save refreshed and confirmed (no silent reject).

Verify in incognito at desktop + mobile breakpoints: logo in header (light + dark), favicon in tab, logo on /login, logo on a sample invoice, and the og:image meta tag in the landing page <head>. If the app uses a service worker, a new favicon may stay cached up to 24h — bump the PWA manifest version and note it in _CUSTOMIZATIONS.md so you don’t chase a ghost.

4. Resolve the server PHP binary (SSH prerequisite)

Section titled “4. Resolve the server PHP binary (SSH prerequisite)”

Resolve it once and persist it so every later page (theme/error publishing, SMTP test, payment config) reuses it.

  1. Reuse the path deploy.php already captured, and verify it runs the required version.

    Terminal window
    PHP_BIN=$(grep "set('bin/php'" deploy.php | grep -oE "/[^'\"]+/php" | head -1)
    dep ssh staging "test -x $PHP_BIN && $PHP_BIN -v | head -1" # expect the version composer.json requires
    # Expected: the PHP version line composer.json requires
    • $PHP_BIN is executable and prints the version composer.json requires.
  2. If deploy.php has none, probe the known CloudLinux / Hostinger paths.

    Terminal window
    dep ssh staging 'for p in /opt/alt/php83/usr/bin/php /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/php83 /usr/bin/php; do
    [ -x "$p" ] && echo " $p → $($p -v 2>/dev/null | head -1)"; done'
    # Expected: each existing binary path with its version
    • ✅ A correct PHP binary path is identified.
  3. Persist it for future sessions/phases.

    Terminal window
    grep -q "STAGING_PHP_BIN" CLAUDE.local.md 2>/dev/null || \
    printf '\n## Server PHP binaries (Phase 6 prerequisite)\n- STAGING_PHP_BIN: %s\n- PRODUCTION_PHP_BIN: [fill in when Phase 12 runs]\n' \
    "$PHP_BIN" >> CLAUDE.local.md
    # Expected: the binary path is written to CLAUDE.local.md (idempotent)
    • STAGING_PHP_BIN is recorded in CLAUDE.local.md.

Apply the same three-source protocol from page 2, scoped to content.

  1. Inventory demo content from seeders and content tables.

    Terminal window
    # Codebase — seeders
    grep -rn "Faker\|demo\|seed" database/seeders/ 2>/dev/null | head -30
    # DB schema — content tables
    mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e \
    "SHOW TABLES LIKE '%review%'; SHOW TABLES LIKE '%testimonial%'; SHOW TABLES LIKE '%faq%'; SHOW TABLES LIKE '%feature%'; SHOW TABLES LIKE '%price%';"
    # Expected: seeder hits + the content tables (reviews/testimonials/faqs/features/prices)
    • ✅ A table in _CUSTOMIZATIONS.md records Category | Table | Current state | Decision (replace/remove/defer) | Target source.

Deleting demo rows is almost never enough — two mechanisms silently undo it:

flowchart TD
Del["You DELETE the<br/>demo testimonial row"] --> T1{"Observer bound to<br/>language / tenant / setting?"}
T1 -->|Yes| Re["Next activation re-inserts<br/>the fake row"]
Del --> T2{"Blade @else falls back<br/>to @lang('landing.*')?"}
T2 -->|Yes| Lang["Page renders the SAME<br/>fake name from lang/*.php"]
Re --> Fix["✅ REPLACE via updateOrCreate<br/>on the vendor's exact key"]
Lang --> Fix2["✅ ALSO hide the @else block<br/>(3-file deviation pattern)"]
classDef bad fill:#e6394622,stroke:#e63946;
classDef good fill:#0d7b6e22,stroke:#0d7b6e;
class Re,Lang bad;
class Fix,Fix2 good;
  1. Scan for both mechanisms before touching anything.

    Terminal window
    ls app/Observers/ 2>/dev/null # observer re-seeding (Froiden family)
    grep -rn "firstOrCreate\|updateOrCreate" app/Observers/*.php 2>/dev/null
    grep -rn "@else" resources/views/ | head -30 # blade fallbacks
    grep -rn "@lang('landing\|@lang('home" resources/views/ | head -30
    cat lang/en/landing.php 2>/dev/null | head -50 # hardcoded demo strings
    # Expected: any re-seeding observers + blade @else fallbacks + hardcoded lang strings
    • ✅ Each category is classified as DB-only, blade-fallback-only, or both in a new “Source classification” column.
  2. Replace fake testimonials with updateOrCreate, keyed on the vendor’s exact primary key (usually id, or language_code + slot_index).

    Terminal window
    php artisan tinker --execute="
    \App\Models\FrontReviewSetting::updateOrCreate(
    ['id' => 1],
    ['reviewer_name' => '[PROJECT] beta customer (pending permission)',
    'content' => '[Real testimonial pending — replace or hide the section]']
    );
    echo 'rows: ' . \App\Models\FrontReviewSetting::count();
    "
    curl -sS https://staging.[DOMAIN] | grep -c "Michael Davis\|Greenview\|Lakeside" # expect 0
    # Expected: a rows count, then 0 fake-name matches in the rendered page
    • ✅ The fake testimonial is replaced (not deleted); the curl count is 0. If it still matches, the blade @else fallback is rendering the fake name — hide that block with an attributed comment wrapper ({{-- [PROJECT] Phase 6 — hidden pending real content --}}).

For each remaining category (hero, features, FAQs): replace now (read the Livewire form → read the model → updateOrCreate for bulk, or drive the admin form via Playwright if there are post-save hooks → screenshot-verify → document the UI path), or defer to Phase 9 by logging it in the task ledger with the current placeholder. For more than a few rows, persist a script at Admin-Local/1-Project/4-Scripts/Content/DemoContentCleanup/ (with --dry-run + rollback) rather than nesting tinker inside SSH.

  1. Verify the replacement survives an observer trigger.

    Terminal window
    curl -sS https://staging.[DOMAIN] | grep -iE "lorem ipsum|john doe|jane smith|faker" # expect nothing
    # Expected: no output — no demo markers remain
    • ✅ No demo markers render. Then fire the seeding observer once (activate/deactivate a language, or create/delete a tenant) and re-render — your replacement still holds. If a new demo row appears alongside yours, your updateOrCreate key didn’t match the observer’s — fix the key and re-run.

6. Audit tracking files for infra-path drift

Section titled “6. Audit tracking files for infra-path drift”
  1. Run the drift scan — pull path and alias references, then cross-check against deploy.php and ~/.ssh/config.

    Terminal window
    grep -rhnE '/home/[a-zA-Z0-9_-]+/domains/[^ "`]+|~/domains/[^ "`]+|/opt/alt/[^ "`]+' \
    CLAUDE.md CLAUDE.local.md \
    Admin-Local/1-Project/2-ProjectVault/credentials.md \
    Admin-Local/1-Project/1-ProjectInfo/ProjectCard.md \
    _CUSTOMIZATIONS.md DECISIONS.md 2>/dev/null | sort -u
    grep -E "deploy_path|domains/" deploy.php 2>/dev/null | head -10
    grep -B1 -A5 "Host.*[Ss]taging\|Host.*[Pp]roduction" ~/.ssh/config 2>/dev/null
    # Expected: tracked references next to the deploy.php / ssh-config truth
    • ✅ Tracking-file references are visible next to authoritative values.
  2. Resolve each finding by type, then commit the fixes.

    Drift typeAction
    Wrong absolute pathFix immediately — it will mislead debugging
    Stale credentials referenceUpdate to match the rotation tracking
    Stale domain nameFix immediately
    Outdated vendor gotchaUpdate the entry; link the vendor changelog
    Dead external linkUpdate to current URL, or archive via archive.org
    • ✅ Every drift finding is fixed and committed.

Do not mark this step done until every box below is checked.

  • 👤 Creds rotated — admin email + password rotated on every environment with a DB; old credentials confirmed dead; new secret stored in 1Password or credentials.md (header fixed if plaintext).
  • 🔀 Users audited — demo/test users deleted, kept users re-passworded, audit logged in credentials.md.
  • 👤 Branding uploaded — brand asset kit generated/collected and uploaded; logo, favicon, login logo, invoice logo, and og:image all verified in incognito (light + dark, desktop + mobile).
  • 🤖 PHP binary resolvedSTAGING_PHP_BIN resolved and persisted to CLAUDE.local.md.
  • 🔀 Demo content handled — inventoried; observer + blade-@else traps scanned; fake testimonials replaced (not deleted); replacement verified to survive an observer trigger.
  • 🤖 Staging cleancurl of the staging landing page returns 0 demo markers; deferred blocks logged for Phase 9.