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.
Background
Section titled “Background”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;1. Rotate the admin credentials
Section titled “1. Rotate the admin credentials”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).
-
Change the email. Admin → Profile / Account Settings (the exact path is in your
vendor-capabilities.md § Admin menus; commonly/user/profileon Jetstream apps). Change from the vendor’sexample.comaddress 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.
- ✅ The admin email is a real inbox you control, not the vendor’s
-
Change the password. Same page → Change Password. Use a generated 20+ char password. Save it to your password manager and
credentials.mdbefore 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”.
-
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.
-
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 incredentials.md.
- ✅ For each result: delete demo/test identities (
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=…". Ifop vault listdoesn’t show your vault andop vault getreturns403 Forbidden, a service account lacks per-vault access — grant it in the web UI (Integrations → service account → Vaults → Add), or fall back tocredentials.md. credentials.md(fallback): edit with Python, notsed/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.
| Environment | When | Tracking entry | Entropy — and why |
|---|---|---|---|
| Local dev | Phase 3, or earliest Phase 6 touch | Superadmin (Local) | 12 chars, pronounceable — typed often |
| Staging | This task | Superadmin (Staging) | 24 chars, random — never typed |
| Production | Phase 12, after first deploy + installer | Superadmin (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.
-
Generate the password, load it to the clipboard via Python (no shell expansion of `$ ! “).
Terminal window python3 - <<'PY'import secrets, pathlib, subprocessalphabet = "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.)
2. Generate the brand asset kit
Section titled “2. Generate the brand asset kit”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:
| Step | Output | Cost |
|---|---|---|
| Source logos | 3 SVGs (square / horizontal / vertical) | ~$0.24 |
| Derive variants | 12 SVGs (light / dark / mono / inverted × 3) | $0 (pure SVG) |
| Favicons | 16→1024 PNGs + .ico + site.webmanifest | $0 |
| Hero / feature imagery | per-scene PNGs | ~$0.039 each |
| Icons | MIT-licensed (Lucide via CDN) | $0 |
| Deploy | copies to vendor paths per asset-deployment-map.json | $0 |
Two human approval gates are built in — the agent pauses, a person reviews, then responds.
-
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>, orabort.
- ✅ The 3 source logos and first 3 hero images were each answered
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.
3. Upload identity & branding
Section titled “3. Upload identity & branding”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).
-
Fill app identity in
Settings → App.Admin field Brand-profile source Notes App name Identity → App nameExact capitalization — shows everywhere Tagline Identity → Tagline≤80 chars — mobile truncates Timezone App settings → TimezoneIANA, e.g. Asia/DubaiDefault currency App settings → CurrencyISO 4217 — also drives Stripe Default locale App settings → Localee.g. en_AE,ar_AEFirst day of week App settings → First daySunday ME/US, Monday EU/Asia - ✅ App name, tagline, timezone, currency, locale, and first-day are set from the brand profile.
-
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.
-
-
Collect every asset variant before uploading, then upload in the typical Froiden-family order.
Asset Light Dark Size Format Main logo (wide) ✅ ✅ ~400×100 SVG / transparent PNG Logo square / icon ✅ ✅ 512×512 SVG / PNG Login logo ✅ ✅ ~300×80 transparent PNG Email header logo ✅ — ~600×150 PNG (SVG fails in email) Invoice / PDF logo ✅ — 300×100 PNG 300 DPI Favicon one — 32×32 ICO / SVG Apple touch icon one — 180×180 PNG, no transparency PWA icon one — 512×512 PNG OG / social preview one — 1200×630 PNG / 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.
-
Reuse the path
deploy.phpalready 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_BINis executable and prints the versioncomposer.jsonrequires.
- ✅
-
If
deploy.phphas 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.
-
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_BINis recorded inCLAUDE.local.md.
- ✅
5. Audit & replace demo content
Section titled “5. Audit & replace demo content”Apply the same three-source protocol from page 2, scoped to content.
-
Inventory demo content from seeders and content tables.
Terminal window # Codebase — seedersgrep -rn "Faker\|demo\|seed" database/seeders/ 2>/dev/null | head -30# DB schema — content tablesmysql -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.mdrecords Category | Table | Current state | Decision (replace/remove/defer) | Target source.
- ✅ A table in
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;-
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/nullgrep -rn "@else" resources/views/ | head -30 # blade fallbacksgrep -rn "@lang('landing\|@lang('home" resources/views/ | head -30cat 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.
-
Replace fake testimonials with
updateOrCreate, keyed on the vendor’s exact primary key (usuallyid, orlanguage_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
curlcount is 0. If it still matches, the blade@elsefallback is rendering the fake name — hide that block with an attributed comment wrapper ({{-- [PROJECT] Phase 6 — hidden pending real content --}}).
- ✅ The fake testimonial is replaced (not deleted); the
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.
-
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
updateOrCreatekey didn’t match the observer’s — fix the key and re-run.
- ✅ 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
6. Audit tracking files for infra-path drift
Section titled “6. Audit tracking files for infra-path drift”-
Run the drift scan — pull path and alias references, then cross-check against
deploy.phpand~/.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 -ugrep -E "deploy_path|domains/" deploy.php 2>/dev/null | head -10grep -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.
-
Resolve each finding by type, then commit the fixes.
Drift type Action Wrong absolute path Fix immediately — it will mislead debugging Stale credentials reference Update to match the rotation tracking Stale domain name Fix immediately Outdated vendor gotcha Update the entry; link the vendor changelog Dead external link Update to current URL, or archive via archive.org - ✅ Every drift finding is fixed and committed.
Checklist
Section titled “Checklist”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:imageall verified in incognito (light + dark, desktop + mobile). - 🤖 PHP binary resolved —
STAGING_PHP_BINresolved and persisted toCLAUDE.local.md. - 🔀 Demo content handled — inventoried; observer + blade-
@elsetraps scanned; fake testimonials replaced (not deleted); replacement verified to survive an observer trigger. - 🤖 Staging clean —
curlof the staging landing page returns 0 demo markers; deferred blocks logged for Phase 9.