Commit graph

232 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
Khalim Conn-Kowlessar
d467a61c22 Adding bulk approve and instruct 2026-05-09 08:39:20 +00:00
Khalim Conn-Kowlessar
5eced11db1 reverse order of activity log 2026-05-07 21:09:28 +00:00
Khalim Conn-Kowlessar
5f0617b691 adding multi organisation connect 2026-05-07 12:32:20 +00:00
Khalim Conn-Kowlessar
a046ed4a5c working on pibi ui 2026-05-06 23:04:45 +00:00
Khalim Conn-Kowlessar
19139b6253 Updated survey request UI for Devon County Council 2026-05-06 20:37:30 +00:00
Khalim Conn-Kowlessar
2b05abb185 Adding tests for modified approvals route 2026-05-06 19:10:47 +00:00
Khalim Conn-Kowlessar
fccf4130c8 added instruct measures approval 2026-05-06 18:00:06 +00:00
Jun-te Kim
7ece33b7b6 removed finalise code as its needed only in the final new process 2026-05-06 16:05:24 +00:00
Jun-te Kim
4f43d32309 Merge remote-tracking branch 'origin' into feature/onbarding_of_addresses 2026-05-06 15:50:35 +00:00
Jun-te Kim
8170601efb refactored with improve-code-archievture and grill-with-docs 2026-05-06 15:49:34 +00:00
Khalim Conn-Kowlessar
bf85787936 re-designing UI for PM' 2026-05-06 14:23:54 +00:00
Khalim Conn-Kowlessar
4734eeed07 updating live tracking new features UI 2026-05-05 20:43:19 +00:00
Khalim Conn-Kowlessar
1e5eb09c3e add pibi-measures route
Adds POST /api/portfolio/[portfolioId]/pibi-measures (approver-gated)
that accepts { dealId, measureNames[] } and delegates to
selectPibiMeasures. Also adds a GET handler that returns the current
pibi_ordered selection, approved measures, and instructed measures for
a deal — used by the drawer's PIBI selector to pre-populate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:14:20 +00:00
Khalim Conn-Kowlessar
7de92cf5ea add instruct-measure service, route and soft-warning helper
Approver-only flow that records an instructed measure in
user_defined_deal_measures, auto-creates a deal_measure_approvals row +
event in a single tx, then pushes the instructed list to HubSpot under
instructed_measures. When the deal has no proposed measures and no prior
approvals, also pushes the new measure as proposed_measures so the deal
has a coherent starting point. Soft-warning helper surfaces the
out-of-order case for the drawer (and later slices).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 18:56:48 +00:00
Khalim Conn-Kowlessar
e020b3fd83 add halted state fields and approver capability gate
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 18:24:43 +00:00
Khalim Conn-Kowlessar
b8befe56fe add deal-properties PATCH endpoint and update service 2026-05-05 14:33:59 +00:00
Jun-te Kim
a7eef6db85 get rid of useeffect 2026-04-27 16:00:33 +00:00
Jun-te Kim
c3cc123a6f save working progress 2026-04-24 15:57:30 +00:00
Jun-te Kim
9f8ea85fed Merge remote-tracking branch 'origin/main' into feature/onbarding_of_addresses 2026-04-24 11:16:46 +00:00
Jun-te Kim
4cdb21fbbc save current changes 2026-04-23 12:01:17 +00:00
Khalim Conn-Kowlessar
afdc8ec703 commenting unintuitive sql query 2026-04-22 10:10:34 +00:00
Jun-te Kim
bee57a56b5 address2uprn onboaridng poc 2026-04-21 20:24:26 +00:00
Khalim Conn-Kowlessar
4bbd973b6b added filtering to filter on lodged rating rather than modelled 2026-04-21 17:47:21 +00:00
Khalim Conn-Kowlessar
c7a5780877 implemeting document search on deal id instead of uprn 2026-04-20 18:58:24 +00:00
Jun-te Kim
119a800995 save latest changes and pulled from main 2026-04-20 15:59:11 +00:00
Khalim Conn-Kowlessar
785c40f2d1 adding measure level tracking for uploaded docs 2026-04-20 15:55:45 +00:00
Jun-te Kim
776c88409c added backlog 2026-04-20 14:58:52 +00:00
Khalim Conn-Kowlessar
680935e1fa Improving upload logging 2026-04-20 14:04:06 +00:00
Khalim Conn-Kowlessar
254125ddb7 adding batch push up when removal requests are made 2026-04-20 12:21:00 +00:00
Khalim Conn-Kowlessar
09931cb878 added in addition requests 2026-04-20 11:14:52 +00:00
Khalim Conn-Kowlessar
20e5eaff9a updating format of text when creating logs for document uploads 2026-04-20 09:10:57 +00:00
Khalim Conn-Kowlessar
ff2bfc67b2 fixed making revisions to Hubspot push up 2026-04-20 08:52:38 +00:00
Jun-te Kim
b81a1aaf61 added trigger of sqs 2026-04-18 18:55:19 +00:00
Khalim Conn-Kowlessar
6056214039 recovering from merge missing files, adding Hubspot sync capabilities 2026-04-18 10:46:21 +00:00
Khalim Conn-Kowlessar
9b9b9e2848 merging the madness 2026-04-17 20:48:15 +00:00
Khalim Conn-Kowlessar
9774a52207 rolling back migration 2026-04-17 19:38:26 +00:00
Khalim Conn-Kowlessar
f04fb8f026 updated install documents ui 2026-04-17 18:49:10 +00:00
Khalim Conn-Kowlessar
881364b562 migrated db and building out upload doc ui and approval log 2026-04-17 00:01:08 +00:00
Khalim Conn-Kowlessar
c6bd99c980 adding missing files 2026-04-16 21:13:35 +00:00