DataTalksClub / DataTalksClub/website

Add verified member-profile onboarding and cut over account settings

Open
#248 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

accessibility auth data-migration enhancement frontend integration P0 security testing
Dominant language
Python
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Parent epic: #108

Product outcome

On top of the accepted accounts-owned MemberProfile foundation, give an authenticated durable member one accessible create/edit/resume journey at /accounts/profile/ and one session-authenticated self API at GET/PATCH /api/v1/me/profile. Preserve verified ownership, safe local intent/resume, and existing-account access; show a non-blocking incomplete-profile prompt; and move every profile-covered write owned by /accounts/settings/ and its timezone adapter through the shared accounts service with exact legacy projections and dual-read evidence.

This is Phase B adapter cutover. It does not declare MemberProfile globally canonical and does not remove a legacy column.

Normative authority

  • _docs/PROCESS.md — role-separated lifecycle, versioned verification, independent screenshots and PM acceptance.
  • _docs/architecture/app-boundaries.mdaccounts owns MemberProfile, its mutation service and temporary CustomUser projections; content.Person stays separate.
  • _docs/specs/01-platform-architecture.md, “Member profile version 1” and “Member signup, profile, Slack, and course registration” — exact fields, completion, verified ownership and resume behavior.
  • _docs/specs/03-github-content-and-people.md — no Person inference, link, write, publication or authority side effect.
  • _docs/specs/06-studio-and-admin-api.md, “Member self service” — exact HTML/self routes, API field allowlist, CSRF and revision contract.
  • _docs/specs/07-security-privacy-operations.md — identity, path-only intent, private/no-store/noindex handling, redaction, safe links and existing-account access.
  • _docs/specs/09-migration-rollout-roadmap.md, “Member-profile expand and contract” — Phase-B projections and Phase-C exit/rollback boundary.
  • _docs/specs/10-verification-strategy.md, “Member onboarding and Slack” plus security/browser sections — parity, identity, concurrency, privacy and responsive evidence.
  • _docs/specs/open-decisions.md, resolved decision 19 — one private account-owned MemberProfile with member-confirmed reuse.
  • #247 — exact MemberProfile schema/service, country catalog, revisions, confirmation, compatibility projection and activation/rollback contract.
  • #234 — normalized, collision-safe eligible-survivor identity resolution.

Dependencies and start gate

Depends on accepted and integrated #247. #247 itself depends on #224, #231, #234 and #51; #248 must not bypass those dependencies or implement a substitute identity/country/migration contract.

Before engineering starts, record the accepted merge SHA for #247, the final accounts migration leaf, and the integrated #234 merge SHA in the engineer handoff. This issue is groomed but BLOCKED until #247 is closed by the normal tester/PM/merge/on-call lifecycle and all of its named dependencies are integrated.

#108 is the parent coordination/HUMAN record, not a separately implementable dependency. #49 is not a dependency because this child creates no Slack grant, delivery or email intent.

Exact routes and response policy

HTML
  • GET/POST /accounts/profile/, route name account_profile, is the only full member-profile create/edit/resume page.
  • GET/POST /accounts/settings/, existing route name account_settings, remains the account overview, display/email preferences and course-enrollment page. Its profile-covered fields use the shared service; its existing unrelated settings behavior remains.
  • Unauthenticated HTML requests use the existing safe account login flow and a local next; they never disclose whether a profile/account exists.
  • An authenticated but unavailable identity (inactive, quarantined, ambiguous, broken absorbed alias or survivor mismatch) receives one generic 403 HTML state: “Your profile is not available. Contact support if this continues.” No email, state, candidate, profile ID or matching detail is rendered.
  • A missing foundation row for an otherwise eligible preserved account is an operational invariant failure: return one generic private 503 state and record only the safe category profile_missing. Do not create a row in a GET adapter.

Both HTML routes and every success/error/redirect response in this flow are Cache-Control: private, no-store, X-Robots-Tag: noindex, nofollow, excluded from sitemap/search/public serializers, and assigned the private zero-TTL edge class. Referrer policy is no stricter than the application security baseline and must not permit profile data to enter a downstream URL. No profile value appears in a page title, URL, query string or fragment.

Self API
  • GET /api/v1/me/profile and PATCH /api/v1/me/profile, route name api_me_profile; no trailing-slash compatibility alias is introduced.
  • Session authentication only. Bearer/API principals are rejected. Unauthenticated requests return generic JSON 401; identity-unavailable returns generic 403; a missing invariant row returns generic 503. None enumerates another user or candidate.
  • Responses use JSON, the same private/no-store/noindex/zero-TTL policy, and Vary: Cookie. GET is side-effect free.
  • PATCH requires Content-Type: application/json, a valid session CSRF token, and a strong current If-Match value copied from GET: "member-profile:<profile UUID>:<decimal revision>". GET and successful PATCH return the same value in ETag.
  • Missing If-Match returns 428 precondition_required; malformed or foreign-profile tags return generic 400 invalid_precondition; a valid old tag returns 409 stale_revision. No stale request changes either table. A wildcard, weak tag, multiple tag list, profile/revision in the JSON body, or cross-account tag is rejected.
  • CSRF denial is generic 403; unsupported method is 405; malformed JSON/type/unknown field is 400 invalid_request. There is no CORS credential widening.

Successful GET/PATCH JSON contains exactly:

profile_id
country
work_status
organization
professional_role
seniority
about
ambitions
why_joined
github_url
linkedin_url
website_url
certificate_name
preferred_timezone
required_fields
missing_fields
completion_version
completed_at
revision

profile_id is the owner’s UUID string. All editable scalar values are strings; optional values use "", not JSON null. Country and choice values are the exact stable codes from #247. required_fields is the fixed ordered list country, work_status, professional_role, seniority, about, ambitions, why_joined; missing_fields is the ordered subset still blank after trimming. completion_version and revision are JSON integers. completed_at is JSON null or UTC RFC3339 with an explicit Z. Do not serialize email, username/user ID, normalized email, identity state, derived region, member-confirmed revision, audit data, legacy representations, Person data, Slack state/secret, or support metadata.

PATCH is partial: omitted allowlisted keys are preserved; supplied keys replace the whole scalar after shared validation/trimming. JSON null, arrays, objects, booleans and numbers are invalid for scalar fields. The writable allowlist is exactly the 13 profile fields above from country through preferred_timezone. PATCH success returns 200 with the complete new representation and ETag. Exact duplicate content is still an explicit member confirmation/mutation under #247 and increments revision once; a replay with the old tag conflicts rather than applying twice.

All API errors use the bounded shape { "code": <safe code>, "message": <generic safe text>, "field_errors": {<allowlisted field>: [<stable code>]}}. Omit field_errors when empty. Field errors contain codes, never submitted/stored values or identity evidence. Add this session/CSRF route to the generated OpenAPI contract with redacted placeholders only; it is not part of /api/v1/admin/.

HTML field and submission contract

/accounts/profile/ renders all 13 writable fields. It uses:

  • a region-grouped native country <select> whose option values are exact #247 alpha-2 codes, including XK;
  • native <select> controls whose values are the exact work-status, professional-role and seniority codes from #247;
  • text inputs for organization, certificate name and the three URLs;
  • textareas for about, ambitions and why joined;
  • the existing IANA timezone select;
  • one hidden decimal revision bound to the rendered profile.

Labels present human-readable wording; stored choice codes do not change. The page identifies required versus optional fields, explains that the private member profile is not a public profile, and does not display the signed-in email. A no-JavaScript POST sends the 13 fields plus revision and CSRF to the same service. Missing/malformed/foreign revision is a form error; stale revision returns the page with HTTP 409, a focused error summary, preserved submitted values, and a “Review the current profile and try again” message. Ordinary invalid/incomplete saves return 200 with field errors and preserved safe values. A successful incomplete or complete save uses PRG and a private success status.

An incomplete member may save partial valid values. Verified ownership is required only to transition to completion version 1; an unverified owner’s otherwise complete submit returns the stable generic profile-level error “Verify your email before completing your profile,” keeps the profile incomplete, and changes no revision/confirmation/completion timestamp. Expired or replayed verification never completes a profile. First completion, later edit, support-correction reconfirmation, duplicate submit and rollback follow #247 exactly. Once complete, clearing a required value is rejected without mutation; optional values may be cleared to "".

Every validation rule, result code and mutation is supplied by the #247 accounts service. HTML forms do not reimplement identity normalization, choice, country, URL, timezone, revision or completion logic.

Verified ownership and non-enumeration

  • Resolve the authenticated actor through #234/#247. An active or legacy eligible durable survivor may read/save its own row; a valid absorbed source session resolves only to its single eligible survivor and never mutates the absorbed source profile.
  • Completion additionally requires a matching verified allauth EmailAddress for that survivor exactly as #247 defines. A provider’s raw/unverified email, notification address or request-supplied email is never ownership evidence.
  • Collision, multiple verified claims, quarantine, inactive user, missing/broken/cyclic alias, unavailable survivor and account/profile mismatch fail closed before mutation.
  • There is no profile UUID route, owner selector, hidden user/email field or support override. Cross-object and mass-assignment attempts receive the same generic denial/invalid shape and reveal no existence/state difference.
  • Signup/social forms continue to collect no profile field before verified provider/account ownership. This issue does not replace or broaden the existing login/signup/recovery implementation.

Safe intent and resume contract

The destination is session-server-side state, never a profile/model field, client cookie value, hidden arbitrary URL, email, token or provider payload.

  • Store at most one normalized local path only under a dedicated session key. Strip/reject every query and fragment. Maximum UTF-8 length is 512 bytes.
  • The only destination this child itself may create is exact /accounts/settings/. The future Slack and active-course-registration owners may call the same helper only after their canonical route exists and their own issue validates the exact resolved route/object. This issue does not add those routes or accept a generic /courses/... prefix.
  • Reject absolute/protocol-relative URLs, encoded or repeated-encoded traversal/separators, controls, whitespace variants, backslashes, account transition/callback/logout paths, /accounts/profile/ self-loops, unknown routes, inactive campaign destinations and any path whose resolved route is not explicitly registered by its owning adapter. Rejection stores nothing and falls back to /accounts/settings/.
  • Before login/social return may redirect to profile, the safe local candidate is validated and stored server-side; generated /accounts/profile/ URLs contain no next. Successful completion atomically consumes the current session destination once and redirects there. Incomplete/invalid/stale submits retain it. Logout/session flush removes it.
  • Refresh, back and duplicate POST resume from the durable profile’s first missing required field and never create another profile. In a second browser the member resumes from the same durable missing-field state; because the first browser’s session destination is intentionally not transferable, successful completion safely falls back to /accounts/settings/ unless the member starts a new explicitly registered local intent there. No cross-session token or PII-bearing resume link is introduced.

An incomplete profile never blocks login, logout, recovery, /accounts/settings/, dashboards, enrollments, history or existing course access. This child adds no global forced redirect middleware.

Incomplete-profile prompt

For an authenticated eligible account with completion version 0, /accounts/settings/ shows one dismissible-in-layout but not permanently stateful informational banner near the page heading:

  • heading: Complete your member profile
  • body: Add the remaining details once so future community and course sign-ups can reuse them.
  • action: Continue profile linking to /accounts/profile/

The prompt is rendered server-side and works without JavaScript. It is absent for complete profiles and unavailable identities. It does not redirect, disable settings, hide courses, expose which field is missing, or appear on public pages. No analytics event includes the member/profile ID or values.

/accounts/settings/ compatibility adapter

The existing page, navigation route, course list, dark-mode toggle, email-preference behavior and empty states remain. Profile-covered writes no longer call CustomUser.save() directly.

The settings profile form preserves these legacy request names and representations, translating them once at the accounts adapter boundary:

/accounts/settings/ field MemberProfile field Settings representation
certificate_name certificate_name trimmed string
country country exact canonical #247 legacy country name; blank or unknown is a field error, never fuzzy-matched
registration_role professional_role exact adopted role code mapped by #247
github_url github_url string
linkedin_url linkedin_url string
personal_website_url website_url string
about_me about string
preferred_timezone preferred_timezone IANA string

The form renders all eight mapped controls and a hidden profile_revision; omitted mapped POST keys preserve current values rather than clearing them. work_status, organization, seniority, ambitions and why_joined are not writable by this legacy adapter; the page links to /accounts/profile/ for the complete editor. dark_mode remains an unrelated account preference and is never passed to the profile service. Email preferences, course enrollment display/profile flags and course-owned records remain outside the profile mutation.

The existing POST /accounts/settings/timezone/ adapter also calls the accounts service, requires CSRF plus current profile_revision, and returns the resulting revision so the page updates its hidden state. Stale writes return 409 and do not overwrite. The no-JavaScript settings form remains sufficient to update timezone. Existing account-toggle and email-preference endpoints do not acquire a profile revision because they do not mutate MemberProfile.

Every successful settings/profile-covered mutation performs, in one transaction, the #247 MemberProfile mutation and exact compatibility projection:

  • certificate_nameCustomUser.certificate_name;
  • alpha-2 country → canonical #247 legacy name in CustomUser.country and its exact derived label in CustomUser.region;
  • professional_roleCustomUser.registration_role;
  • github_url, linkedin_url, website_url, about, preferred_timezoneCustomUser.github_url, linkedin_url, personal_website_url, about_me, preferred_timezone.

Legacy nullable text/URL columns receive their established blank-compatible value; no new-only profile value is put into an invented legacy column. A projection error or post-write mismatch rolls back the whole mutation. Adapter reads use MemberProfile for the adapted fields and compute a legacy projection comparison. Evidence records only bounded field-category mismatch counts, completion version and safe outcome—never values, email, IDs, URLs, country or free text. Tests fail if these owned adapters write a projected CustomUser column outside the service.

Rollout, activation and rollback

This issue introduces one reversible adapter flag/configuration boundary covering the profile HTML/self API and settings profile/timezone adapters.

  • Cutover enabled: the routes and owned adapters behave as specified above and maintain exact legacy projections.
  • Application rollback/cutover disabled: /accounts/settings/ returns to the pre-cutover legacy writer/reader for its existing fields; the new profile HTML and self API are unavailable with a generic private 404. New-only MemberProfile values remain inert. Because every successful cutover write projected all legacy-covered values in the same transaction, rollback loses no covered settings value.
  • Changing the flag performs no data rewrite, email, network call or completion mutation. Re-enable resumes from the preserved MemberProfile row/revision and first missing field.

This child must not set the global Phase-C canonical-read flag, drop/rename a CustomUser column, stop projections, or claim the rollback window complete. Phase C remains blocked until the #247 exit gate is evidenced: all HTML, self API, Studio, admin API, timezone, course registration and inventory-listed readers/writers use the service; the versioned inventory has zero unknown/direct writers; a complete production-like rehearsal and one deployed-development observation have zero unresolved mappings and zero dual-read mismatches; forward/reverse/backup-restore and application rollback pass; and every dependent adapter issue is accepted/integrated. Record this issue’s exact enabled/disabled rehearsal and aggregate mismatch evidence for that later gate.

Acceptance criteria

  • #247 and its dependencies are accepted/integrated and the exact merge/migration identities are recorded before engineering starts.
  • Exact HTML and API routes, authentication, private/no-store/noindex/zero-TTL response policy and generic unavailable/missing states match this issue.
  • /accounts/profile/ renders all 13 fields with canonical code representations, shared validation, accessible no-JS errors, preserved values and optimistic revision handling.
  • GET/PATCH self API returns exactly the pinned representation; CSRF, strong ETag/If-Match, partial-update, stale/replay, mass-assignment and safe error contracts pass.
  • Active/legacy/absorbed-survivor ownership and verified-email completion use #234/#247; collision, quarantine, inactivity, alias failure, mismatch and unverified/expired evidence fail closed without enumeration or mutation.
  • Session-only path intent is exact, bounded, one-use, path-only and allowlisted; unsafe/external/unknown/self-loop targets store nothing; refresh/back/duplicate/second-browser behavior matches the pinned fallback.
  • Existing incomplete accounts retain all current access and receive only the exact non-blocking server-rendered settings prompt.
  • Settings and timezone profile-covered writes use the service, preserve the legacy request representations, increment/reconcile revisions, project exact legacy fields atomically and leave unrelated toggles/preferences/courses unchanged.
  • Dual-read/post-write evidence contains bounded categories only; projection mismatch rolls back; source-policy tests detect direct owned-adapter writes.
  • Enabled/disabled adapter rollback is rehearsed with no covered-value loss, duplicate profile or revision corruption; global canonical activation and column removal remain off.
  • Signup/social return collects no profile value before ownership and no flow creates/links/queries/writes content.Person.
  • Generated OpenAPI, focused Django/security/accessibility tests, migration drift, compatibility/adoption checks, container checks and the graph-selected versioned verification plan all pass.
  • Independent tester Playwright passes at desktop and mobile and every required screenshot is inspected and contains only synthetic/redacted content.

Required Django/API/security scenarios

  1. GET/POST/PATCH for an incomplete and complete active owner, eligible legacy owner and valid absorbed-source-to-survivor session; verify one row and the exact revision/confirmation/completion transitions from #247.
  2. Unverified/expired verification, inactive, quarantine, normalized collision, multiple claims, missing/broken/cyclic alias, profile/user mismatch and missing foundation row; assert stable generic status/error and zero mutation.
  3. Every field: blank/required, trim, max/max+1, Unicode, every choice, unknown choice, safe/unsafe URL and timezone. Assert HTML/API parity and no submitted value in errors/logs/audits/metrics.
  4. GET side-effect freedom; CSRF absent/invalid; wrong content type; malformed JSON; null/wrong scalar type; unknown/hidden/user/email/completion/revision field injection; bearer-only principal; cross-account ETag.
  5. Missing/malformed/weak/wildcard/list/current/stale If-Match; two concurrent writers; transaction/projection failure; duplicate HTML submit and API replay. Assert one increment or conflict and no lost update.
  6. Exact settings legacy-name/code translation, omitted-field preservation, timezone adapter revision update, completed-required clear denial, optional clear, dark-mode/email preference preservation, course rows and empty state.
  7. Path intent external/protocol-relative/query/fragment/traversal/double-encoding/control/backslash/oversize/account-transition/self-loop/unknown route; valid settings intent; refresh/back/logout/consume once and second-browser safe fallback.
  8. Cutover enabled/disabled/re-enabled with exact covered-value parity and no duplicate profile; aggregate-only dual-read categories; direct-writer policy inventory.
  9. Assert private headers on every status/redirect, no sitemap/search/public serializer/OpenAPI-example PII, no Person query/write/link/permission, and zero network/job/email side effect.

Browser, no-JavaScript and screenshot scenarios

The independent tester uses synthetic non-email display data and captures under .tmp/screenshots/ at about 1440×900 and 390×844, reading every image:

  1. Incomplete eligible member opens settings, sees the exact non-blocking prompt, retains course/settings access, follows Continue profile, and sees the first missing required field.
  2. Full profile page with grouped country/choice controls, required/optional descriptions and private-profile explanation; keyboard order, labels, targets, zoom/reflow and contrast are usable.
  3. Invalid required/URL submission focuses the summary, links to fields, preserves safe synthetic values and never causes horizontal overflow; repeat with JavaScript disabled.
  4. Stale profile and stale settings revisions return the accessible conflict state without overwriting; refresh displays current state and permits retry.
  5. Safe settings intent resumes once after completion; refresh/back/duplicate submit does not duplicate; a second browser resumes missing fields and safely returns to settings without transferring the first session destination.
  6. Account settings edits legacy-represented country/role and the mapped fields, then profile shows canonical values; profile edits project back to settings. Timezone works with and without JavaScript; dark mode/email preferences/course rows remain usable.
  7. Complete profile has no incomplete banner and cannot clear a required value; optional clear succeeds.
  8. Signed-out HTML follows the safe login route; signed-out/self-API and unavailable-identity states are generic. Screenshots contain no email, profile UUID, entered free text/URL, raw country signal, token, CSRF/session value or real member data.
  9. Browser history/back and cache behavior do not reveal a prior member’s profile after logout or account switch.

Explicit non-goals

No MemberProfile schema/catalog/reconciliation change owned by #247; no Slack grant/reveal/secret/Relay/EmailDelivery; no course registration, enrollment conversion, shared-profile snapshot or course writer cutover; no Studio/admin/support correction/resend; no privacy rights/export/deletion engine; no CloudFront country trust/suggestion; no email change/verification redesign; no public member page/directory; no Person relation/inference/synchronization; no production import or protected-data inspection; no external network action; no global canonical activation, compatibility-column removal or rollback-window closure; no new profile field, avatar, arbitrary link, organization directory, ranking, recommendation, marketing consent or analytics containing profile data.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by confirming the accepted merge SHA and migration leaf for #247, then read _docs/PROCESS.md, _docs/architecture/app-boundaries.md, and the referenced member-profile and rollout specs. Inspect the existing accounts settings route, timezone adapter, session authentication, CSRF, and generated OpenAPI entry points. Done means the specified HTML/API flows, projections, privacy controls, concurrency behavior, and independent parity/security evidence are covered without bypassing the listed dependencies.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, authentication, backend, full-stack, security, testing-qa
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.