feat: full frontend redesign + applicant portal#7
Open
GravityDarkLab wants to merge 86 commits into
Open
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.
Rows had a varying number of action buttons, so flex-wrap pushed extra buttons onto a second line and made row heights inconsistent. The actions container now stays on a single row and scrolls horizontally when it overflows the column's max width.
The footer hardcoded 2025. Interpolate the year from Date.now() so it stays correct without future edits.
The run card now opens with an animated band in the style of the public pages' LifeBackground: two gold particle streams flow in from either side and converge on a beating heart. While a run is in flight the heart rate and flow surge, sparks fly where the streams meet, and a cycling status line narrates the phases; on completion the heart pops with expanding rings and a spray of sparks, and the result card enters with a sprung check mark and staggered stat reveals. All motion honors prefers-reduced-motion and pauses while the tab is hidden.
The run CTA was the stark black primary button and the labels read as generic grey. The CTA now uses the public pages' cta-gold gradient pill, the algorithm label gets the uppercase tracked treatment used elsewhere, and the last-run line moves into a bordered footer row with a small clock icon.
The native select was the one control the theme couldn't reach. The three algorithms are now selectable cards with the hint inline, a gold Recommended badge, and a selection dot — keyboard-accessible via a visually hidden radio group.
Sign out floated as a lone small pill between two dividers while its neighbors both had headings. It now gets a Session heading, a short note, an icon, and a full-width button — same rhythm as the other sections, while staying separated from the destructive deactivate zone below.
…L, env validation)
…-option warning The @config directive for the legacy JS config triggered Tailwind's internal PostCSS reprocessing without a from path, which Vite's dev server warned about on every CSS request. Moving the theme overrides into @theme/@Keyframes in index.css removes the @config directive (and the now-unused autoprefixer, which Tailwind v4 already covers).
The portal matches endpoint now batch-loads each partner's public
answers and attaches them as partnerProfile on the applicant match
view. Consent checkboxes are filtered out, and instagram_handle is
excluded as defense in depth (identity is stored encrypted in a
separate collection and never reaches the answers field).
The MatchCard expand toggle now also opens an "About {alias}"
section rendering the raw answers, shown above the score breakdown
on proposed, in-progress (initiator) and dating cards.
…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.
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