Commit graph

1169 commits

Author SHA1 Message Date
Jun-te Kim
aecff4bdbf migration table 2026-05-27 19:19:39 +00:00
Khalim Conn-Kowlessar
f3887a215c Polish user-access UX: shadcn confirm dialog + toast feedback
Two small UX upgrades on the portfolio user-access page:

- Replace native confirm() prompts (for "Remove user" and "Revoke
  invitation") with a shadcn-based ConfirmDialog. Shows the email of
  the user/invitation being acted on, disables buttons while the
  mutation is in flight, and matches the visual language of the rest
  of the app. New ConfirmDialog component is generic and reusable.

- Toast on every mutation outcome (invite/remove/revoke, success and
  failure), using the existing useToast hook. Invite success now
  surfaces "Invitation sent — We've emailed <email>…" so admins get
  immediate feedback that the email was dispatched, instead of just
  the form silently clearing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:29:36 +00:00
Khalim Conn-Kowlessar
66cc71d228 Fix collaborators-not-iterable crash; rename misspelled colloborators route
Crash root cause: UsersPermissionsCard and CapabilitiesCard both used the
TanStack Query key ["portfolioUsers", id], but a previous change made
UsersPermissionsCard's fetcher return { users, currentUser } while
CapabilitiesCard's still returned an array. TanStack Query dedupes by
key — whichever component mounted first won the cache; the other read an
incompatible shape and crashed on `for (const c of collaborators)`.

Fix: extracted a single shared fetcher (collaboratorsClient.ts) so both
components import the same fetchCollaborators function and consume the
same response shape. CapabilitiesCard projects data?.users ?? []; the
shared cache stays consistent regardless of mount order.

Also rename the route folder from "colloborators" to "collaborators"
(spelling fix). Used git mv so history follows. Fetch URLs, console
error labels, and the route doc comment updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:12:00 +00:00
Khalim Conn-Kowlessar
171c586db1 Added database migration files 2026-05-27 16:54:58 +00:00
Khalim Conn-Kowlessar
7cde991871 Merge branch 'main' of https://github.com/Hestia-Homes/assessment-model into bug/portfolio-invitations 2026-05-27 16:53:36 +00:00
Jun-te Kim
27dcc218ce visual improvement on groups 2026-05-27 16:49:57 +00:00
Khalim Conn-Kowlessar
616302a5c7 Gate user-access page behind admin privilege; allow admin role assignment
Adds a portfolio-privilege concept (creator > admin > domna employee >
write > read > none) and gates all user-access mutations + the pending-
invitations view behind it. Plus opens the role dropdown to include
"admin" so creators/admins/Domna can promote and demote.

Privilege model:
  - Portfolio creator: full admin powers (cannot be removed/demoted)
  - Portfolio admin: full admin powers via explicit membership role
  - Domna employee (email ends @domna.homes, case-insensitive):
    implicit admin across all portfolios, even if not a member —
    intended for customer-support / internal-tooling needs
  - Anyone else (read/write/none): no admin powers

Backend:
  - New pure-function helpers in src/app/lib/portfolioAdmin.ts —
    isDomnaEmail() and canAdminister(privilege), with 6 tests covering
    case-insensitivity and look-alike domain rejection
  - New server helper resolvePortfolioPrivilege() that reads
    portfolioUsers + checks the email, returning the highest privilege
  - New denyIfNotAdmin(portfolioId, session) one-liner that returns a
    401/403 NextResponse or null; used at the top of every mutating
    route handler to keep the guard out of the way
  - POST/PUT/DELETE on /colloborators and GET/DELETE on /invitations
    are now all gated. Non-admin callers get 403.
  - GET /colloborators now requires auth and returns
    `{ users, currentUser: { privilege } }` so the UI knows which
    actions to expose without an extra round-trip

Frontend:
  - ROLE_OPTIONS extended to ["read", "write", "admin"]. RoleDropdown
    takes allowAdminPromotion?: boolean to keep the basic dropdown
    unchanged where promotion isn't allowed.
  - UsersPermissionsCard derives isAdmin = canAdminister(privilege)
    from the API response. Invite section, role-change dropdown,
    Remove button, and the entire Pending Invitations section are now
    rendered only when isAdmin. Non-admins see a read-only members
    table.
  - The invitations useQuery is disabled when !isAdmin, avoiding
    guaranteed-403 network calls.

Defensive note: the UI gating is for UX; the backend guard is the
security boundary. A non-admin who hand-crafts a POST still gets 403.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:40:34 +00:00
Jun-te Kim
7086a63c1e project db addition and live tracker update 2026-05-27 16:27:48 +00:00
Khalim Conn-Kowlessar
07acf4d93d Add pending-invitations admin UI and wire member removal
Adds two pieces of user-access management on the portfolio settings page
that were previously stubbed or missing:

- The existing "Remove" button was wired to console.log only. It now
  calls a DELETE on /colloborators with optimistic cache update and
  rollback on failure. The route refuses to remove the portfolio
  creator and 404s if the membership isn't in the URL's portfolio.

- A new "Pending invitations" section in UsersPermissionsCard lists
  invitees who haven't signed in yet, backed by a new
  /api/portfolio/[id]/invitations endpoint (GET + DELETE). Admins can
  revoke a pending invitation; revoking deletes the row so the invitee
  no longer auto-joins on sign-in. Inviting a new email shows up here
  immediately because the invite mutation invalidates both query keys.

Both new mutations use optimistic updates with rollback, and disable
only the in-flight row (mutation.variables === currentId) so the rest
of the table stays interactive. No useEffect, TanStack Query throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:27:41 +00:00
Khalim Conn-Kowlessar
7e9193313b added missing email and email test files 2026-05-27 16:19:14 +00:00
Khalim Conn-Kowlessar
c921db7d9c initial implementation for portfolio invitations. A user can send an invitation to a user and they will receive an invitation email 2026-05-27 16:18:21 +00:00
Jun-te Kim
5ea7e00fbe
Merge pull request #282 from Hestia-Homes/bug/blocked-magic-links
Bug/blocked magic links
2026-05-27 16:21:53 +01:00
Khalim Conn-Kowlessar
e57e4ab750 changing email to test 2026-05-27 15:01:26 +00:00
Khalim Conn-Kowlessar
4f176a482f changing ses config to test values 2026-05-27 14:54:59 +00:00
Khalim Conn-Kowlessar
47efd1bd5a Drop useEffect from VerifyCodeForm; replace tick-based countdown
The per-second countdown was the only thing pulling useEffect into this
component, in violation of the project rule (CLAUDE.md: avoid useEffect,
prefer event handlers). The hook was driving a tick purely so we could
show "Resend in 28s" / "27s" / ... — none of which is load-bearing.

Replace it with a single setTimeout fired from the resend event handler
that flips resendStatus from "cooldown" back to "idle" after 30 seconds.
The button stays disabled with "Code sent — wait a moment" instead of
showing a live countdown. Same blocking signal, no hook needed.

While here, split the single status enum into verifyStatus + resendStatus
so the verify button no longer wrongly disables during a resend cooldown
(latent bug from the previous shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:50:05 +00:00
Khalim Conn-Kowlessar
ee506425fd Drop link from email body; fix paste + stale-resend-notice UX bugs
Email body now contains the 6-digit code only. The /verify/[token] route
and the EmailProvider link callback are intentionally left wired (just
not advertised in the email) so reverting to a link-bearing template is
a content-only change if the all-code variant doesn't improve
deliverability for the Atkins-style blocked recipients.

Hypothesis being tested: corporate gateway URL scanners are part of why
some emails got silently quarantined, and a short transactional body
without an auth-token URL clears more filters.

Two small UX bugs surfaced in preview testing also fixed here:

- Paste of "482 911" (with the visual space from the email's formatted
  code) was dropping a digit. Root cause: maxLength=6 on the input
  truncated the 7-char paste before our \D-stripping ran. Drop the
  attribute and let the existing .slice(0, 6) after stripping handle
  the bound. Pasting the formatted code now works.

- After requesting a resend and then typing into the code field, the
  green "we've sent a new code" notice would re-appear as soon as the
  previous error message cleared. handleChange now clears the resend
  notice on the next keystroke, alongside the error clear it already
  did.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:46:33 +00:00
Khalim Conn-Kowlessar
d042606955 Add 6-digit code sign-in as primary, magic link as fast-path fallback
Same email now contains a 6-digit code and a magic link, both backed by a
single verificationToken row. After submitting their email, the user
lands on /auth/verify-code with a single-input form (inputmode=numeric,
autocomplete=one-time-code, auto-submit on 6 digits or paste) and can
either type the code or use the link from the email. Either path
consumes the same row — single-use, replace-on-resend.

This is the structural fix for the silent-quarantine pattern observed
with Atkins and Sustainable Building UK: corporate gateways are happier
with short transactional content than long opaque token URLs, and a
code can't be broken by SafeLinks-style URL rewriting. The link path is
preserved so users whose email gets through unmangled keep one-click UX.

Security:
  - Codes are 6-digit, crypto.randomInt-generated, stored as sha256
    hashed against NEXTAUTH_SECRET on the same row as the link token
  - 5-attempt lockout per code (attempts column); 6th attempt with the
    correct code still fails
  - Per-email send rate limit: 5/hour fixed window (authRateLimits
    table); 6th send returns an error
  - Code + link share a 10-minute window (maxAge dropped from 1h)
  - Resending replaces any prior token rows for the identifier so only
    the latest send is ever live

Implementation:
  - verificationCode.ts holds generateCode + hashCode + the pure
    evaluateCodeAttempt decision tree; 9 unit tests cover every branch
    of the verification outcome (no-such-row, expired, locked-out, ok,
    wrong-with-newAttempts, locked-out-still-rejects-correct-code)
  - sendVerificationRequest now hashes the URL token the same way
    /verify/[token]/page.tsx does, applies the rate limit + records the
    code + replaces older rows in two transactions
  - CredentialsProvider (id: "email-code") calls evaluateCodeAttempt
    inside a transaction, handles all 5 outcomes, creates the user on
    first successful code (parity with the magic-link callback path)
  - oauthId backfill in the signIn callback is now guarded on
    account.type === "oauth" so the credentials flow doesn't pollute
    oauthProvider with "email-code"
  - Migration is additive: code_hash nullable, attempts default 0; new
    authRateLimits table is independent. In-flight tokens at deploy time
    keep working via the link path.

Vercel preview deployment is the test surface; a Mailpit + Cypress E2E
loop is intentionally deferred per the lean-setup plan in docs/wip/
auth-email-code-fallback-plan.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:16:47 +00:00
Jun-te Kim
ed14ac79eb group filter to allow batches 2026-05-27 13:57:10 +00:00
Khalim Conn-Kowlessar
9c569f5584 Add SES observability foundation for email auth (PR 1 of code-fallback)
Wires the X-SES-CONFIGURATION-SET header on outbound auth emails so SES
bounce/delivery events flow through dev-ses-config to the dev-ses-events
SNS topic. Replaces the fire-and-forget "EMAIL MAGIC LINK SENT" log
(which fired before the SMTP transaction and swallowed downstream errors)
with structured EMAIL_MAGIC_LINK_SUCCESS/_FAILURE logs carrying the
Nodemailer messageId, so app-side sends are now correlatable with SES
events.

Motivated by the Atkins / Sustainable Building UK silent-quarantine
incidents where we couldn't tell whether SES had even tried to send.

Plan doc at docs/wip/auth-email-code-fallback-plan.md tracks the
broader email-code-fallback design that PR 2 will implement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:15:25 +00:00
Jun-te Kim
54ad998032 additional batches 2026-05-27 10:28:19 +00:00
Jun-te Kim
f205524fe9 more hubspot deal data 2026-05-27 10:26:39 +00:00
Jun-te Kim
6c7c43fb89 roof 2026-05-26 16:07:26 +00:00
Jun-te Kim
35156111d0 built type 2026-05-26 15:04:02 +00:00
Jun-te Kim
9612b1fd4b new wall types 2026-05-26 14:32:57 +00:00
Jun-te Kim
f0d53d4e86 some comments to understand the sql more 2026-05-26 10:25:42 +00:00
Jun-te Kim
b8be604ab3 migration scripts 2026-05-26 10:23:50 +00:00
Jun-te Kim
1df047a84a landlord overrides 2026-05-26 10:21:50 +00:00
Daniel Roth
e22833041d Correct required file type count in tests
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
2026-05-20 09:00:32 +00:00
Daniel Roth
c533bb3e80 specify certain variables as survey 2026-05-19 10:35:15 +00:00
Daniel Roth
d755299043 Correctly separate install document categories 2026-05-19 10:32:18 +00:00
Daniel Roth
d292bb53c1 coordination and design docs not listed under install 2026-05-19 10:30:59 +00:00
Daniel Roth
f99374a16f list coordination and design documents in their own sections 2026-05-19 08:30:57 +00:00
Daniel Roth
61be9a477c pas significance file is not expected 2026-05-19 08:17:17 +00:00
KhalimCK
20f6aff62e
Merge pull request #266 from Hestia-Homes/feature/pm-ui-ux
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
Feature/pm UI ux
2026-05-18 10:00:40 +01:00
Daniel Roth
57d425f848 migration files 2026-05-18 08:23:38 +00:00
Daniel Roth
20648f30e0 add coordination_hub file source 2026-05-18 08:17:28 +00:00
Jun-te Kim
604e0014fc deploy to main 2026-05-15 11:34:14 +00:00
Jun-te Kim
41891a4540 job ordering 2026-05-15 10:13:09 +00:00
Jun-te Kim
cd47aef985 order by job started and not job updated time 2026-05-15 10:10:00 +00:00
Daniel Roth
578e146aec migration files 2026-05-13 13:51:26 +00:00
Daniel Roth
452a2cd61d add new coordination and design file types 2026-05-13 13:51:02 +00:00
Daniel Roth
6b1627d03c migration files 2026-05-13 10:03:43 +00:00
Daniel Roth
00b0cc2a45 add uploaded_file_id fk to magic_plan_plan 2026-05-13 10:03:07 +00:00
Jun-te Kim
6f9fabb622
Merge pull request #243 from Hestia-Homes/feature/onbarding_of_addresses
Some checks are pending
Test Suite / unit-tests (push) Waiting to run
Feature/onbarding of addresses
2026-05-12 18:26:40 +01:00
Jun-te Kim
10b3d81bc2 added enum from hubspot source 2026-05-12 16:18:42 +00:00
Daniel Roth
4b62b12f15 add migration files 2026-05-12 15:42:27 +00:00
Daniel Roth
8f87ea0c96 magic_plan_uid column is unique 2026-05-12 15:41:43 +00:00
Jun-te Kim
b387bc24f8 got rid of get server
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
2026-05-12 13:54:18 +00:00
Khalim Conn-Kowlessar
1345f36d8d Add test coverage for transforms.ts stage classification logic
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
49 tests across all exported functions: resolveDisplayStage (STAGE_ID_MAP
lookups, AFTER_ASSESSMENT sub-classification, POST_DESIGN precedence chain,
RA ISSUE overrides), classifyDeals, computeDampMouldRisk, computeFunnelStages,
computeProjectProgress (Queries exclusion from percentages), computeOutcomeSlices,
and computeLiveTrackerData (__ALL__ synthetic project behaviour).
2026-05-12 13:29:00 +00:00
Khalim Conn-Kowlessar
7841e4a556 xtracted duplicated removal state logic into a shared module.
Created removalState.ts with two functions: deriveEffectiveRemovalState (single DB row → EffectiveRemovalState) and computeRemovalStatusByDeal (ordered rows → RemovalStatusByDeal map, owns deduplication).
Created removalState.test.ts with 11 tests covering all 6 (type, status) combinations and the map-building edge cases (empty input, deduplication, none entries excluded).
live/page.tsx: replaced a 14-line seen-set loop with a single computeRemovalStatusByDeal(removalRows) call.
live/[dealId]/page.tsx: replaced an if/else block with deriveEffectiveRemovalState(removalRows[0]).
2026-05-12 13:12:14 +00:00