176ba187c89635cece68450de730195a3cd437b0
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
582f13fa99 |
Entry screen is the app: phone frame with a live deck inside
The homepage was a marketing brochure -- a hero paragraph, a "three steps" explainer and a tag list. It described the gesture in prose while the actual Tinder deck sat behind /deck/[jobId], reachable only after signing in AND posting a job. Nobody opening the app ever saw the product. Now / renders a phone illustration with the real app running inside it: the same <Deck>, the same drag physics, real verified pros. On a phone the bezel collapses and the deck simply fills the viewport -- drawing a picture of a phone on a phone is absurd, and it would eat the width the cards need. - Removed the fixed app bar and bottom tab bar. AppShell now renders the screen title as an in-flow h1; the bar owned the only h1 on every screen, so dropping it silently would have left every page headingless. The /jobs "post" action moved from bar chrome into the content, since the tab bar was its only other route there. - getShowcaseDeck(): a deck with no job behind it. getDeck is job-scoped (joins jobs for category and location, anti-joins swipes), which an anonymous visitor has none of, so this centres on the launch city. Eligibility rules are copied verbatim -- nobody may appear in the shop window who could not appear on a real deck. - deck.showcase: the only public procedure in the router. list and swipe stay behind clientProcedure. It reads nothing about the caller and writes nothing, so a right swipe on the entry screen is purely local. No real tradesperson is contacted until a job is posted. - <Deck> filled a hardcoded 560px desktop box; it now fills its container. The card counter moved out from between the two action buttons so the thumb zone holds nothing but the two controls. Verified against the seeded database: 22 eligible pros returned, and all three seeded traps excluded for the right reason -- Pau Ribas (22km out, 5km radius), Unverified Ulla (pending), Away Arnau (not accepting). 7 new integration tests cover exactly that. Also corrects a label I had written as "Plumbers near you" -- the showcase deck is not category-filtered and shows every trade. Includes concurrent edits to the mobile shell, ui/ primitives and DESIGN.md made outside this session. typecheck, lint, build clean; 141 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c617bc9687 |
M1 security: close the password backdoor, apply re-review, pin E.164
Acts on an adversarial review of the M1 auth and authorization code. Five findings fixed; the rest recorded in SECURITY-FINDINGS.md as the M1 exit criteria rather than left in a tool transcript. - auth: serve /phone-number/request-password-reset, /phone-number/ reset-password and /sign-in/phone-number as 404. better-auth's phoneNumber() registers all three unconditionally -- they are NOT gated on emailAndPassword.enabled:false. Left live they form a silent second credential path: request-password-reset stores an OTP and sends no SMS (sendPasswordResetOTP was never configured, so the owner is never told), reset-password mints a bcrypt credential row, and sign-in/phone-number then accepts it forever with no OTP. The OTP gate still applies, so this is not remote unauthenticated takeover -- it converts one momentary OTP compromise into permanent access the victim cannot see or rotate. - auth: drop bearer(). It accepts the plaintext sessions.token column as an Authorization credential, making any single leaked row a replayable login. The mobile client it was added for is hypothetical. - auth: pin E.164 via phoneNumberValidator, and add toE164/isE164 to @linkder/shared. phone is UNIQUE and bans are per-account, so "+34600111222" and "0034600111222" being separately storable meant one handset could hold two accounts and a ban was escapable by retyping. 15 tests. - auth: NEXT_PUBLIC_APP_URL now throws in production instead of falling back to localhost, which was silently dropping Secure and the __Secure- prefix from the production session cookie. - pro.upsertProfile: actually apply requiresReReview. It was computed, returned to the client and never acted on, so a verified plumber could become a verified electrician in another city by ignoring a response flag. Now demotes to pending in the same transaction and audits it. Trade changes count as material (they did not before) -- the licence is per-trade. Needed a verified -> pending edge in VERIFICATION_GRAPH, which did not exist. Removed two untracked scratch repro files. The impersonation repro depended on bearer() for transport and no longer applies as written; the underlying finding (resolveSession drops impersonatedBy, so admin actions are audited as the victim) is open and documented. typecheck, lint, build clean; 127 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |