feat: full frontend redesign + applicant portal#7
Merged
Conversation
- ApplicantStatus: applied/matched/dating/inactive (was active/inactive/matched/withdrawn) - MatchStatus: proposed/in_progress/dating/success/failed/declined/expired (was proposed/contacted/matched/failed) - Add deleteApplicant and deleteMatch aliases in API client - Expose role in AuthContext and AuthState - Update all pages and tests to use new status values
Replace the password-based success page with a magic link display. The API client now types magicToken in the submit response; Apply.tsx passes it as a URL param; Success.tsx shows the profile link with copy-to-clipboard and .txt download actions and a one-time-view warning. All four locale files updated with the new i18n keys.
…identity - Two-column layout (left: profile card, right: status stepper + match history) - Status stepper with completed/current/future visual states - Role-gated identity reveal: super_admin sees reveal button, admin sees locked card - Add useOptionalAuth to AuthContext for components that run outside AuthProvider - Match history card only rendered when matches exist - Reset matches state on navigation to prevent stale data between applicants
…ocalStorage Bearer JWT storage in localStorage is a conscious design decision for the applicant portal (vs. HttpOnly cookies used by the admin panel). Mark both setPassword and profileLogin storage sites with lgtm suppression comments.
…pOnly session cookie - Backend: setPassword and login now call setCookie() with httpOnly:true, sameSite:Lax, secure in production; no token returned in response body - Backend: deactivate clears the session cookie via deleteCookie() - Backend: requireApplicant reads ons_applicant_session cookie first, falls back to Authorization header for API clients - Frontend: profile.client.ts uses credentials:'include'; all localStorage usage removed - Frontend: ProfileLoginPage and ProfileDashboard no longer touch localStorage - Tests: ProfileDashboard beforeEach no longer needs a fake JWT in storage Fixes CodeQL alert js/clear-text-storage-sensitive-data (CWE-312).
login and set-password now return no token in the body; tests verify the ons_applicant_session Set-Cookie header is present instead.
…ion cookie - Add ApplicantCookieAuth security scheme (ons_applicant_session cookie) - Keep ApplicantBearerAuth as fallback scheme for API clients - All profile endpoints list cookie auth first, bearer as fallback - login/set-password 200 responses: remove token field, add Set-Cookie header - ProfileLoginResponse schema: remove token property - deactivate: note that session cookie is cleared - Update top-level auth description to reflect cookie-first approach
… page URL - Add X-Submission-Key to CORS allowHeaders so browser form submission is not blocked by preflight - Add default empty-string values for all string fields in Apply.tsx to prevent uncontrolled→controlled React warnings and Zod validation failures - Fix Success page magic link URL from /profile?token=... to /profile/login?token=... so the token is not lost on redirect
profileRequest now returns the parsed body instead of unwrapping
body.data, since not all endpoints follow that shape. Only treat 401s
from the auth middleware itself ("Unauthorized" / "Invalid or expired
token") as a session-expired event — other 401s (e.g. wrong current
password) are business-logic errors and should surface as such.
…e accepting The target of a contact request now sees the initiator's Instagram handle, the partner profile and the score breakdown on the "wants to meet you" card before accepting or declining. The handle is attached as partnerInstagram on the match view for in_progress and dating matches only (never while proposed), with an audit log entry per decryption — consistent with the existing design where the initiator already sees the target's handle at contact time. This also fixes initiator/dating cards losing the revealed handle on page reload, since the handle previously lived only in session state from the contact response.
…longer clips it The desktop table wrapper uses overflow-hidden for its rounded corners, which cut off the status menu on the last rows. The menu now renders via createPortal with fixed positioning from the trigger's bounding rect, and closes on outside click, scroll, or resize.
…y layer - revealIdentityById() in identity.service decrypts and writes the audit log in one place; raw resolveIdentityById is documented as repeat-view only, and identityExistsById allows pre-flight checks without decrypting - admin getApplicantIdentity logs RESOLVE_IDENTITY via the central function instead of the controller hand-rolling it - drop the LIST_APPLICANTS audit action: listing applicants exposes no sensitive data and flooded the log - MatchDoc.identityViewLoggedFor tracks which applicants already had a reveal logged so repeat matches-page loads don't write duplicates
…ile tab API: - GET/PUT /api/v1/profile/answers — same Zod field rules as submission, derived from the submit schema with instagram_handle and disclaimer_agreed omitted; .strict() rejects them and unknown keys (422) - updateMyAnswers merges over stored answers so non-editable keys survive, and refreshes embeddings in the background like a fresh submission - profile.service also adopts the centralized revealIdentityById and the once-per-match audit dedup for matches-page reveals Frontend: - Matches | My profile tab bar on the portal dashboard (hidden for inactive accounts) - EditProfileForm reuses the apply-wizard step components (Step2-4) plus the final slider/date fields as stacked cards, with a locked Instagram row pointing applicants to the admins and a sticky save bar gated on dirty state - i18n keys for en/de/fr/ar
… edits Applicants now enter a date of birth instead of a raw age, with age derived consistently on both API and frontend via a shared helper. birth_date and gender_identity are shown read-only in the profile editor and rejected by the answers-update API — only an admin can change them. Partners continue to see a derived age, never the exact birth date. Also fixes the floating save bar hiding the Save button from assistive tech when the form is clean.
- Smoke test payload helpers now send birth_date instead of the removed age field, so /form/submit no longer 422s - injectBCMatch fails fast with a clear error if setup didn't populate applicant B/C IDs, instead of a non-null-assertion crash - DeletionCountdown resets cancelLoading in a finally and closes the confirm dialog on a successful cancel - Fix stale "Bearer JWT auth" comment on the applicant portal routes (it's an HttpOnly session cookie)
Update the three top-level READMEs to reflect the current app: the /profile applicant portal (session-cookie auth, edit answers, mutual identity reveal), admin match management endpoints, i18n, and the birth_date question (replacing the old age field in the form-steps table). Also fixes a stale frontend dev port and test count.
…rithms cosine and embedding-cosine duplicated the same REL_TYPE_ENCODING table, type coercion helpers (str/bool/num), vector builder, and cosine similarity math. Move the shared pieces into matching/scorers/numeric.scorer.ts so both algorithms import one implementation instead of two copies.
admin and applicant auth middleware each reimplemented JWT signing, verification, expiry-to-seconds conversion, and bearer/cookie token extraction. Move the shared logic into middleware/jwt.util.ts and have both middlewares consume it. applicant.auth.middleware now also exports APPLICANT_COOKIE_MAX_AGE so the profile controller can stop maintaining its own copy of the cookie lifetime.
…alidation
- Split match.service.ts's status-transition/match-view kernel into a new
match-state.service.ts (toMatchView, assertMatchTransition,
expireConflictingMatches, transitionApplicantStatus,
applyMatchStatusSideEffects, DELETION_GRACE_MS), shared by
match.service, admin.service, profile.service, and the matching job.
- Add errorResponse() (utils/error-response.ts) and a ValidatedContext
type alias (utils/validated-context.ts), and use them across the
admin/matching/form/match/profile controllers — replacing ad-hoc
`(err as {statusCode?:number})` casts, the local matchErrorResponse
helper, and `c.req.valid('json' as never) as T` double-casts.
- Move the inline patch-match schema into validators/match.validator.ts
and export input types from the admin/profile validators for the new
ValidatedContext handlers.
- Convert remaining plain Error throws (engine.ts, form.service.ts,
admin.service.ts) to AppError with correct status codes, and extract
escapeRegex (utils/regex.ts) shared by the admin/match search filters.
- Fix requestContact to reveal the partner's identity before expiring the
actor's other matches, rolling the claim back to "proposed" if the
reveal fails instead of leaving it stuck in_progress.
- Fix cancelAccountDeletion to restore "dating" status when the applicant
still has a dating match, instead of always "applied".
- Fix reportOutcome to also expire the partner's other proposed/in_progress
matches on a successful outcome.
- Rename AuditContext.adminId to actorId since the field also carries
applicant ids for self-service actions.
- Update tests for the new module boundaries and AppError status codes.
The mutual-reveal description still described the old flow where both sides had to accept before handles were decrypted. Update it to match the current behavior: the initiator sees the partner's handle immediately on contact request, and the partner sees it as soon as they view that match.
Both admin/types.ts and api/profile.client.ts declared their own copies of ApplicantStatus and MatchStatus. Move them to types/status.ts and re-export from both call sites.
…e UI - Add missing translation keys (admin nav/login/dashboard/applicants, portal dashboard/success, common.dismiss) across en/fr/ar/de and replace the remaining hardcoded English strings in AdminLayout, Toast, Dashboard, ApplicantDetail, Applicants, Success, and ProfileDashboard. - Move useTimeAgo from admin/utils into lib/ so both the admin panel and the profile portal can use it. - Extract shared admin table primitives (Th, PageButton, TrashIcon) into admin/components/Table.tsx, and a shared useStatusLabels hook used by both Matches and Dashboard. - Extract AuthPageShell for ProfileLoginPage's four near-identical layout wrappers, and replace duplicated spinner SVGs with the shared Spinner component in InviteGate and MatchCard. - Switch Applicants to deactivateApplicant and drop the dead deleteApplicant/deleteMatch aliases from the admin API client. - Fix InviteGate's hardcoded hover color and Textarea's ml-1 (now ms-1) so it respects RTL layouts. - Stop passing the magic token through the /success and /profile/login URLs — pass it via router state instead.
…e fixture - Add unit coverage for transitionApplicantStatus and applyMatchStatusSideEffects (dating/success/failed/no-op branches), closing most of the match-state.service.ts gap. - Rework the accept->success leg of match-flow.smoke.ts to use a 4th applicant (D) instead of reusing A, since contacting B in the A-B flow now expires A's other proposed matches via the exclusive-contact side effect.
…entions Adds a Product section (elevator pitch for new sessions), Error handling & status transitions, Design system (frontend) — covering the token system, dark mode, brand identity, shared UI primitives, and i18n/RTL rules — and a Conventions section with Conventional Commits style and the no-Co-Authored-By rule. Also fills in the Applicant portal bullet and the magicToken/plainPassword contract note that were missing from the Frontend section.
CORS middleware in 4.12.23 reflects any Origin with credentials when the origin option is unset. Fixed in 4.12.25. Regenerates standalone api/bun.lock via bun run lockfiles.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Admin panel & applicant portal redesign
#C9A96E), new CSS design tokens inindex.cssapi/profile.client.tscovering all portal endpoints (login, password, profile, matches, contact, outcomes, deactivation)Exclusive contact flow
applied→matchedAdmin recovery & data-lifecycle fixes (manual QA follow-ups)
super_admincan regenerate an applicant's magic link fromApplicantDetail(audit-logged asREGENERATE_MAGIC_LINK). The applicant's password is cleared so the new link takes them through first-login set-password again. The raw token is shown once with a copy button.deletionScheduledAt(180-day grace period) instead of just deactivating. A new "Scheduled for deletion" tab on the Applicants page shows pending deletions with their deletion date; the default "All" tab excludes them.in_progress) contact are excluded from new proposals/candidates, so a contacted applicant doesn't get fresh matches mid-conversation.ALLOWED_ORIGINSaccepts comma- or semicolon-separated values (trimmed, trailing slashes stripped); pinned Node 22-24 LTS for frontend tooling via.nvmrc/.node-version/engines.Test Plan
bun run test— 344 API tests + 183 frontend tests passbun run typecheck— clean on both workspaces/success?alias=X&token=Yshows magic link, copy + download work/profile?token=abc→ first-login set-password flow/profilewith session cookie → dashboard with match listREGENERATE_MAGIC_LINKfirstLogin: true(password reset confirmed)in_progress