Commit graph

30 commits

Author SHA1 Message Date
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
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
d467a61c22 Adding bulk approve and instruct 2026-05-09 08:39:20 +00:00
Khalim Conn-Kowlessar
1c764a77b4 fixed datetie for hs 2026-05-07 21:37:08 +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
fccf4130c8 added instruct measures approval 2026-05-06 18:00:06 +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
1767733441 add vitest unit tests for pibi-selection service
Covers: happy path (tx committed, HubSpot called with
measures_for_pibi_ordered, pushed_at stamped), empty selection,
no approval rows touched, DB failure (ok=false, no HubSpot call),
HubSpot failure (pushed_at null), correct property name assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:15:05 +00:00
Khalim Conn-Kowlessar
091388692e add pibi-selection service
Implements selectPibiMeasures (issue #254): replaces all pibi_ordered
rows for a deal, then pushes the new list to HubSpot under
measures_for_pibi_ordered via syncMeasuresFieldToHubSpot. Mirrors the
instruct-measure service; no approval rows touched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:14:12 +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
4181af6d15 add syncMeasuresFieldToHubSpot helper
Generic push for multi-value HubSpot deal properties (semicolon-separated)
with the same ECONNRESET retry pattern as pushDealPropertiesToHubSpot.
Returns a discriminated ok result so callers can stamp pushed_at only on
success. Slice 4 (PIBI selections) will reuse the same helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 18:56:19 +00:00
Khalim Conn-Kowlessar
1336d90c8d add domna survey fields to deal-properties registry
Plug `domna_survey_type` and `domna_survey_date` into
DEAL_PROPERTY_FIELDS, gated on the approver capability and reusing the
shared string-or-null + ISO date schemas. Drops the now-unused boolean
column types from the field registry. Vitest cases cover validation,
capability gating, HubSpot mapping, independent setting and clear-to-null.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 18:37:57 +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
Khalim Conn-Kowlessar
486427f73d add vitest harness with parser and getRequiredDocs unit tests 2026-05-05 11:26:02 +00:00
Khalim Conn-Kowlessar
35d0af4474 share parseMeasures helper, accept ; and , separators 2026-05-05 11:25:54 +00:00
Khalim Conn-Kowlessar
dc3bfa73d3 extract MEASURE_NAMES catalogue and MeasureName type 2026-05-05 11:25:45 +00:00
Khalim Conn-Kowlessar
9946b94ede pushing upload status to hs 2026-04-21 08:51:41 +00:00
Khalim Conn-Kowlessar
785c40f2d1 adding measure level tracking for uploaded docs 2026-04-20 15:55:45 +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
3a60a4bd91 updating approval log syncing 2026-04-20 09:03:15 +00:00
Khalim Conn-Kowlessar
ff2bfc67b2 fixed making revisions to Hubspot push up 2026-04-20 08:52:38 +00:00
Khalim Conn-Kowlessar
6056214039 recovering from merge missing files, adding Hubspot sync capabilities 2026-04-18 10:46:21 +00:00