Skip to content
prod 352bb92
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.

Steps at a glance:

  1. Rotate the admin credentials — The email/password change happens in the running app’s profile page — a person logging into the panel.
  2. Generate the brand asset kit — If your project has the image-gen toolchain wired (brand profile + deployment map from project setup), the pipeline produces everything section 3 of this page and the landing page need:.
  3. Upload identity & branding — Most CodeCanyon apps cluster these under one Settings → App / Settings → Branding tab. The human signs in once; the agent drives non-secret uploads/fields from the existing session when browser automation is available.
  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.
  5. Audit & replace demo content — Apply the same three-source protocol from page 2, scoped to content.
  6. Audit tracking files for infra-path drift — scan tracking files for hardcoded server paths, so branding work does not preserve stale staging or host-specific assumptions.

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 Zaj-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, prepare non-secret context, and verify non-secret evidence, but the login/password-change form itself is human-driven. Do not use a clipboard-bridged Playwright pass for browser password fields.

  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 the selected non-production environment / production over SSH (respects the User model + soft-deletes).

    Terminal window
    DEPLOY_TARGET="<selected-nonproduction-deploy-target>" # from Zaj-PROJECT.md, e.g. staging-primary/qa/uat/client-demo
    DEPLOY_PATH="~/domains/<DOMAIN>/deploy"
    SSH_ALIAS="<non-prod-alias-from-Zaj-PROJECT>"
    ssh "$SSH_ALIAS" "cd $DEPLOY_PATH/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
Selected non-productionThis taskSuperadmin (<env-key>), e.g. Superadmin (Staging)24 chars, random — never typed
ProductionPhase 12, after first deploy + installerSuperadmin (Production)24 chars, random — stored in the vault

If you copy a verified non-production DB → production at deploy time, production inherits that non-production admin password and needs no separate rotation. Document which path you took.

For AI-assisted rotation, do not use a clipboard bridge, browser_type, or browser_evaluate to fill password fields. The human owns the whole browser password-change transaction; the agent prepares/checks everything around it.

  1. Generate or choose the replacement password without printing it.

    Terminal window
    # 1Password path (preferred): human generates/saves the value in the vault UI.
    # Agent verification is non-secret only:
    op item get "Superadmin (<env-key>)" --vault "[PROJECT]-Local" --fields label=username >/dev/null \
    && echo "✅ vault item exists (secret hidden)" || echo "❌ missing vault item"
    # Expected: vault item exists; no password value is printed
    • ✅ The replacement password exists in the vault or approved fallback, and no value was printed.

Then the human signs in, opens Profile / Account Settings, enters the current password and the replacement password, and submits the change. The agent resumes after the human reports the save or after the browser session redirects.

After saving, verify it took without asking the agent to type passwords. Capture non-secret evidence: the stored password hash changed from the pre-rotation hash, Hash::check("123456", ...) returns false for vendor defaults, and any auth-required page redirects to /login after logout/session invalidation. The human can perform one fresh sign-in to confirm the new credential works; the agent resumes after that session exists and records the evidence. (Jetstream logout is POST-only; GET /logout returns 405.)

If your project has the image-gen toolchain wired (brand profile + deployment map from project setup), 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/1-Info/Brand/assets/ (same tree as Phase 8 · Brand kit); keep machine-readable inputs in Admin-Local/1-Project/1-Info/Brand/brand-profile.json.

Most CodeCanyon apps cluster these under one Settings → App / Settings → Branding tab. These are panel uploads and field edits: the human supplies the logged-in session and any subjective approval, then the agent should drive non-secret fields/uploads when browser automation is available.

  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 Zaj-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)
    DEPLOY_TARGET="<selected-nonproduction-deploy-target>" # from Zaj-PROJECT.md, e.g. staging-primary/qa/uat/client-demo
    SSH_ALIAS="<non-prod-alias-from-Zaj-PROJECT>"
    ssh "$SSH_ALIAS" "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
    ssh "$SSH_ALIAS" '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 "NON_PROD_PHP_BIN" CLAUDE.local.md 2>/dev/null || \
    printf '\n## Server PHP binaries (Phase 6 prerequisite)\n- NON_PROD_ENV_KEY: <selected environment key from Zaj-PROJECT.md>\n- NON_PROD_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)
    • NON_PROD_PHP_BIN is recorded in CLAUDE.local.md with the matching non-production environment key.

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 Zaj-CUSTOMIZATIONS.md records Category | Table | Current state | Decision (replace/remove/track post-launch) | 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). These writes overwrite live content rows — snapshot the content tables first so a wrong key is reversible (mariadb-dump …reviews testimonials > backups/content-pre-edit-$(date +%F).sql; Herd Pro ships MariaDB, so the tool is mariadb-dump).

    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();
    "
    NON_PROD_URL="https://<non-production-host>" # from Zaj-PROJECT.md, e.g. staging-primary/qa/uat/client-demo
    curl -sS "$NON_PROD_URL" | 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 capture the Phase 9 content task in Zaj-BACKLOG.md with the current placeholder, owner, trigger, and acceptance criterion. For more than a few rows, persist a script at Admin-Local/1-Project/5-Scripts/1-Setup/Content/DemoContentCleanup/ (with --dry-run + rollback) rather than nesting tinker inside SSH.

  1. Verify the replacement survives an observer trigger.

    Terminal window
    NON_PROD_URL="https://<non-production-host>" # from Zaj-PROJECT.md
    curl -sS "$NON_PROD_URL" | 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-Vault/credentials.md \
    Zaj-PROJECT.md \
    Zaj-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 by the agent when browser automation is available; logo, favicon, login logo, invoice logo, and og:image all verified in incognito (light + dark, desktop + mobile).
  • 🤖 PHP binary resolvedNON_PROD_PHP_BIN resolved and persisted to CLAUDE.local.md with the selected environment key.
  • 🔀 Demo content handled — inventoried; observer + blade-@else traps scanned; fake testimonials replaced (not deleted); replacement verified to survive an observer trigger.
  • 🤖 Non-production cleancurl of the selected non-production landing page returns 0 demo markers; any remaining non-fake content blocks have Zaj-BACKLOG.md rows targeting Phase 9 with owner, trigger, current placeholder, and acceptance criterion.