Skip to content

[API] User #39058

Open
Open

Description

@FuaadZam

Functional Area: User

Endpoints under this functional area:

Module API Version Resource / Endpoint HTTP Method Description
Account Management v1 /user/v1/account/password_reset/ GET Retrieves the password reset form description
Account Management v1 /user/v1/account/password_reset/token/validate/ POST Validates a password reset token
Account Management v1 /user/v1/account/registration/ GET Retrieves user registration form description
Account Management v1 /user/v1/account/registration/ POST Creates a new user account
Account Management v1 /user/v1/accounts GET Returns a list of user detail objects; supports filtering by usernames, emails, or LMS IDs
Account Management v1 /user/v1/accounts/cancel_retirement/ POST Cancels the retirement of a user's account
Account Management v1 /user/v1/accounts/deactivate_logout/ POST Marks user as having no password and logs them out
Account Management v1 /user/v1/accounts/name_change/ POST Creates a PendingNameChange request for profile name update
Account Management v1 /user/v1/accounts/name_change/{username}/confirm/ POST Confirms a pending name change request and updates user's profile name
Account Management v1 /user/v1/accounts/replace_usernames/ POST Replaces current usernames with desired usernames; returns mapping of successful and failed replacements
Account Management v1 /user/v1/accounts/retire/ POST Retires a user account including username, email, and other PII
Account Management v1 /user/v1/accounts/retire_misc/ POST Retires a user in the LMS
Account Management v1 /user/v1/accounts/retirement_cleanup/ POST Deletes a batch of retirement requests by username
Account Management v1 /user/v1/accounts/retirement_partner_report/ POST Returns list of UserRetirementPartnerReportingStatus users and updates their processing status
Account Management v1 /user/v1/accounts/retirement_partner_report/ PUT Creates a UserRetirementPartnerReportingStatus object for a user in the retirement pipeline
Account Management v1 /user/v1/accounts/retirement_partner_report_cleanup/ POST Deletes UserRetirementPartnerReportingStatus objects for reported users
Account Management v1 /user/v1/accounts/retirement_queue/ GET Returns RetirementStatus users in specified states and cool-off period
Account Management v1 /user/v1/accounts/retirements_by_status_and_date/ GET Lists UserRetirementStatusSerializer rows filtered by status and date range
Account Management v1 /user/v1/accounts/search_emails POST Returns user information for a list of emails; staff-only access
Account Management v1 /user/v1/accounts/update_retirement_status/ PATCH Updates the RetirementStatus for a user and appends messages to the log
Account Management v1 /user/v1/accounts/{username} GET Retrieves a single detailed user object
Account Management v1 /user/v1/accounts/{username} PATCH Updates user account/profile info (merge-patch supported)
Account Management v1 /user/v1/accounts/{username}/deactivate/ POST Marks user as having no password for deactivation purposes
Account Management v1 /user/v1/accounts/{username}/image POST Upload/update user profile image
Account Management v1 /user/v1/accounts/{username}/image DELETE Deletes a user's profile image
Account Management v1 /user/v1/accounts/{username}/retirement_status/ GET Returns the RetirementStatus of a user
Account Management v1 /user/v1/accounts/{username}/verification_status/ GET Returns ID verification status for a user
Account Management v1 /user/v1/accounts/{username}/verifications/ GET Retrieves detailed ID verification information for a user
Account Management v1 /user/v1/me GET Returns the authenticated user's username
Forum Management v1 /user/v1/forum_roles/{name}/users/ GET Lists users for a given forum role
Preferences Management v1 /user/v1/preferences/email_opt_in/ POST Update or create user's email opt-in preference
Preferences Management v1 /user/v1/preferences/time_zones/ GET List all time zones or by country
Preferences Management v1 /user/v1/preferences/{pref_key}/users/ GET List a user's preferences
Preferences Management v1 /user/v1/preferences/{username} GET Retrieve all preferences for a user
Preferences Management v1 /user/v1/preferences/{username} PATCH Update user preferences
Preferences Management v1 /user/v1/preferences/{username}/{preference_key} GET Retrieve a specific preference for a user
Preferences Management v1 /user/v1/preferences/{username}/{preference_key} PUT Update a specific preference for a user
Preferences Management v1 /user/v1/preferences/{username}/{preference_key} DELETE Delete a specific user preference
Preferences Management v1 /user/v1/user_prefs/ GET List all user preferences via ORM
Preferences Management v1 /user/v1/user_prefs/{id}/ GET Retrieve a single user preference via ORM
User Management v1 /user/v1/users/ GET List all users via ORM
User Management v1 /user/v1/users/{id}/ GET Retrieve a single user object via ORM
Validation & Registration v1 /user/v1/validation/registration POST Validate registration form inputs before creating a new user
Account Registration v2 /user/v2/account/registration/ GET Retrieve registration endpoint info
Account Registration v2 /user/v2/account/registration/ POST Create a new user account
Login Session v1 /user/{api_version}/account/login_session/ GET Get login session info
Login Session v1 /user/{api_version}/account/login_session/ POST Authenticate user and create login session

Activity

  1. added theissue type on Sep 2, 2026
  2. Abdul-Muqadim-Arbisoft commented on Oct 15, 2026

    @Abdul-Muqadim-Arbisoft
    Contributor

    Standardization PR open: #39237

    PR: #39237 — feat: standardize the user directory, preference and time-zone APIs as /api/user/v2/
    State: open

    What this does

    This is phase A of five for the [API] User functional area
    (#39058), under the API standardization
    umbrella #38137. The area is split A → E
    on that issue's plan; this PR carries A only — seven read-only operations — and B, C, D and E
    are separate PRs
    . B (accounts and profile images), C (preferences and email opt-in) and D (the
    retirement pipeline and username replacement) follow this one; E (registration, login session,
    validation and password reset) is not approved to build
    and is listed here only so the phase count
    is not mistaken for four.

    ADRs applied

    14 of 17 decisions applied on this pass; every one left open carries its reason.

    • 0025 serializers
    • 0026 permissions
    • 0027 schema / docs
    • 0028 viewsets
    • 0029 errors
    • 0030 GET idempotence
    • 0031 merged endpoints
    • 0032 pagination
    • 0033 filtering
    • 0034 authentication
    • 0036 nested JSON
    • 0038 URL structure
    • OEP-66 queryset scoping
    • OEP-69 conventions
    • 0035 MFE config — not applicable: no endpoint in this phase serves site or front-end configuration. The nearest candidate, the time-zone list, is static reference data with no site settings and no user context attached
    • 0037 versioning — not ticked because one clause is deliberately unmet: all new code is in a new v2/ package, no superseded file is in the diff, and the superseded addresses are flagged in the schema — but they carry no docstring marker, which decision 4 also asks for. The reason (FC-0118 review, OEP-21 timing) and the follow-up form are in the matrix row and in Deprecations
    • SDK tag — deliberately not applied in this phase: no operation here carries tags=["openedx-platform-sdk"], and SDK tagging starts at phase B. The learner-facing account and preference endpoints are the SDK candidates; an admin-only directory is not, and tagging it would put it in a generated client for no caller

    The evidence for each tick is the compliance matrix in the PR body; nothing is ticked without a named test or gate line.

    Gates

    run_gates.sh — feat/api-adr-user-v2-phase-a vs origin/master @ ce074d1 (2026-10-15)
    Six gates ran; none skipped; two are red.

    ==================== summary ====================
    versions PASS
    hygiene PASS
    schema PASS
    urls FAIL
    old-tests FAIL
    tests PASS
    artifacts: /tmp/api-adr-gates.pByf8M
    GATES FAILED: 2

    RESULT: PASS
    RESULT: PASS (0 FAIL, 1 WARN)
    RESULT: PASS
    RESULT: PASS (0 BREAKING, 18 WARN, 12 INFO)
    URL gate — head routes: 3042, base routes: 3036, new api routes checked: 6
    RESULT: FAIL (2 FAIL, 1 WARN)

    ==================== orchestrator reconciliation: final, post checkpoint-2 ====================
    Run 2026-10-15 after the two checkpoint-2 decisions (active-user requirement; alias removed
    and unknown query parameters refused). Committed as 1b30148 + a8eecf2.

    versions PASS 14 --protect paths unchanged. The --allow arguments are mechanically
    inert ("allowed: 0"); the evidence for the 7 in-place files is the
    per-file diff review, not this gate.
    hygiene PASS 0 FAIL, 1 WARN -- H3 on v2/paginators.py only. The two H11 warnings on
    the former ?user= alias text are gone with the alias.
    schema PASS 0 BREAKING, 18 WARN, 12 INFO. No path, operationId or component on
    master removed or renamed, in the LMS document or in /api-docs.
    tests PASS 302 passed (was 284 before the two decisions).
    urls FAIL DELIBERATE. All three findings are the JSON-404 catch-all
    api/user/v2/{_}. (R6, R8, R11) -- not an addressable resource. Six
    candidate shapes were measured; the only conforming one cannot answer
    the "unknown collection" case the fallback exists for. run_gates.sh
    passes no allowlist, so the composite gate cannot go green regardless.
    BLIND ON: R6/R8/R11 for a fallback route. The five real addresses pass
    every machine-checkable 0038 rule.
    old-tests FAIL RED AND BLIND, environment, identical on both refs:
    branch 7 failed / 100 passed / 16 errors (378.02s)
    base 7 failed / 100 passed / 16 errors (381.02s)
    23 FAILED/ERROR lines over 19 distinct tests, both sets diff EMPTY.
    Every one a pymongo ServerSelectionTimeoutError; the image has no
    modulestore, so these cannot run here on master either.
    PROVES: nothing that passes on master fails here.
    DOES NOT PROVE: that those 19 pass at all. Only CI has Mongo.
    Also: only 2 of the 11 legacy suites in this app were exercised.

    NOT a gate, but the same duty: the legacy comparison above was run by the orchestrator in a
    clean origin/master worktree, same image and interpreter, not inferred from the branch run.

    Deprecations

    Exactly seven legacy paths are marked by this PR — the seven Phase-A addresses below, and no
    others:

    Superseded Replacement
    GET /api/user/v1/users/ GET /api/user/v2/users/
    GET /api/user/v1/users/{id}/ GET /api/user/v2/users/{username}/
    GET /api/user/v1/user_prefs/ GET /api/user/v2/user_preferences/
    GET /api/user/v1/user_prefs/{id}/ GET /api/user/v2/user_preferences/{username},{key}/
    GET /api/user/v1/preferences/{pref_key}/users/ GET /api/user/v2/users/?preference_key={pref_key}
    GET /api/user/v1/forum_roles/{name}/users/ GET /api/user/v2/users/?forum_role={name}&course_key={course_key}
    GET /api/user/v1/preferences/time_zones/ GET /api/user/v2/time_zones/

    They are marked only by deprecated: true in the published schema, set by the LMS
    post-processing hook (lms/lib/spectacular.py) from a tuple of those seven exact paths. There is
    no response header and no deprecation note in any superseded code, docstring or comment — the
    superseded modules are not in the diff at all. That is a deliberate departure from ADR 0037's
    decision 4, which marks the superseded version in the schema and in the endpoint's docstring: the
    FC-0118 reviewer asked for the depr notes to be dropped from the replacement PRs (#39100, #39102),
    and OEP-21's marker for a REST API is a single Deprecated <issue link> line added after the DEPR
    ticket reaches Plan Accepted, not a notice. If a reviewer wants the marker, that one line in a
    follow-up is the form it should take.

    The /user_api/v1/… twins of these addresses are not deprecated here: they are called by
    frontend-app-account, frontend-app-profile and the Android app, and this PR mounts no successor
    at those addresses.

    No query parameter is deprecated alongside them. GET /api/user/v1/user_prefs/ accepts
    ?user=<id>; its successor names owners by username and publishes no identifier filter, so
    GET /api/user/v2/user_preferences/?user=<id> is a 400 validation envelope naming the parameters
    that list does read. Nothing in this version is published with deprecated: true and no response
    carries a Deprecation header. The removal note for the seven addresses carries the replacement
    filter name, so a caller reading the ticket has the whole move in one place.

    Tracked on the FC-0118 umbrella DEPR ticket,
    #39128, which removes them one named
    release after the release this ships in. This area's entry is posted on #39128 once the PR is
    open.

    Five behaviour notes the removal must carry:

    1. GET /forum_roles/{name}/users/ runs Role.objects.get_or_create on every call today. The v2
      filter does not create the role; an unknown role returns an empty page. No receiver is attached
      to Role, so nothing else fans out.
      Where that write still lives, for an operator who relied on reading the list to bring a role
      into existence: seed_permissions_roles(course_key)
      (openedx/core/djangoapps/django_comment_common/utils.py:49) creates the five standard roles
      with their permissions, and already runs when a course is created or imported in Studio
      (cms/djangoapps/contentstore/utils.py:126, contentstore/management/commands/import.py:82) and
      when a CCX course is set up (lms/djangoapps/ccx/utils.py:517); the LMS management command
      seed_permissions_roles <course_id> runs it for a course that predates that; and
      assign_role(course_id, user, rolename) (django_comment_common/models.py:62) creates a role
      when granting it to a user. A role created by the legacy GET carried no permissions and no
      members
      , so none of these is a like-for-like replacement for it — they are what should have
      created the role in the first place.
      What has to exist before the v1 address can be retired: every course whose roles were
      brought into existence by calling it must have them seeded, because the instructor endpoint that
      grants a role (POST …/instructor/api/update_forum_role_membership) reads the role with
      Role.objects.get and raises Role.DoesNotExist when it is missing.
      are_permissions_roles_seeded(course_id) reports whether a course is in that state and the
      management command puts it right; the check belongs in the removal ticket, not in this PR, since
      the legacy address keeps its behaviour until then.
    2. GET /api/user/v2/time_zones/ validates country_code as two letters. A well-formed but unknown
      code still falls back to every time zone, exactly as before; a malformed one (FRA) is now a 400
      envelope where v1 returned the full list. An empty value (?country_code=) narrows nothing and
      returns every zone, as it does at v1; that branch has a test on both counts.
    3. /api/user/v2/ does not accept X-Edx-Api-Key. Server-to-server callers of the v1 directory
      endpoints must issue a staff JWT before moving.
    4. A user with no profile row makes GET /api/user/v1/users/{id}/ raise; the v2 entry returns
      name: null. A caller moving to v2 gets a row where it used to get a server fault.
    5. The superseded member address is keyed by a database identifier and its replacement is keyed by
      username, so an integration holding identifiers has a step to take before it can address an
      entry: GET /api/user/v1/accounts?lms_user_id={id} — staff-gated, untouched by this PR and
      itself standardized in phase B — answers with the account whose username keys the v2 entry.
      The v2 directory publishes no identifier filter, and this version refuses a parameter it does
      not read, so GET /api/user/v2/users/?id={id} is a 400 validation envelope naming id rather
      than a page that looks narrowed, and the resolution step is not optional.
      v2/tests/test_parity.py::test_a_database_identifier_is_refused_instead_of_narrowing_nothing
      records the refusal and
      ::test_an_identifier_held_by_a_caller_resolves_to_the_username_the_entry_is_keyed_by walks the
      two calls end to end. This step belongs in this area's entry on the DEPR ticket. Whether a v2
      id filter should exist instead is Open decisions, item 3.

    Review on the PR, please — this issue stays open until it merges.

  3. Abdul-Muqadim-Arbisoft commented on Oct 23, 2026

    @Abdul-Muqadim-Arbisoft
    Contributor

    Standardization PR open: #39297

    PR: #39297 — feat: standardize the user accounts, profile image, verification and name-change API
    State: open

    ADRs applied

    15 of 16 applied on this pass; every box left open carries its reason.

    • 0025 serializers — every request body is validated by the serializer the operation
    • 0026 permissions — permission_classes on every view; every legacy check survives and two
    • 0027 schema / docs — eleven operations and seventeen superseded paths published, with the
    • 0028 viewsets — router-registered viewsets; the two non-resource operations are APIViews
    • 0029 errors — RFC 9457 envelope on every refusal, with this version's own
    • 0030 GET idempotence — every read captured under captureOnCommitCallbacks(execute=True)
    • 0031 merged endpoints — GET /api/user/v1/me is merged into the account member address,
    • 0032 pagination — all three lists: the account list, the verification list and the email
    • 0033 filtering — ordering and every selector validated by a serializer and published
    • 0034 authentication — the platform default on six of the eleven operations; the two views
    • 0035 MFE config — n/a: none of these eleven operations serves configuration, settings or
    • 0036 nested JSON — the extra profile fields a site collects and the social links are read
    • 0037 versioning — a new v2 package and a new profile_images/v2 package; no
    • 0038 URL structure — snake_case, plural collections, trailing slash, no version token in a
    • OEP-66 queryset scoping — the service-backed exemption, with the three layers named in
    • OEP-69 conventions — pagination envelope, RFC 9457 refusals, strict parameter handling,

    Gates

    run_gates.sh — feat/api-adr-user-v2-phase-b vs origin/master @ 7f01c42 (2026-10-23)
    Six gates ran; none skipped; two are red.

    ==================== summary ====================
    versions PASS
    hygiene PASS
    schema PASS
    urls FAIL
    old-tests FAIL
    tests PASS
    artifacts: /tmp/api-adr-gates.LrMgX6
    GATES FAILED: 2

    RESULT: PASS
    RESULT: PASS (0 FAIL, 1 WARN)
    RESULT: PASS
    RESULT: PASS (0 BREAKING, 54 WARN, 35 INFO)
    URL gate — head routes: 3051, base routes: 3036, new api routes checked: 15
    RESULT: FAIL (2 FAIL, 1 WARN)

    ==================== orchestrator reconciliation — Phase B FINAL ====================
    2026-10-21, after all 14 repair passes of the second review round. 900 passed (638 at round start).

    versions PASS 14 --protect paths unchanged, verified per path by pass 14's tidy sweep as well
    as by the gate. NOTE: the gate's --allow arguments are mechanically inert
    ("allowed: 0") and the gate does not echo its resolved --protect list -- a PACK
    defect for step 9 harvest, not a repo defect. Evidence is per-file diff review.
    hygiene PASS 0 FAIL, 1 WARN -- the known H3 on v2/paginators.py (justified subclass).
    schema PASS 0 BREAKING, 54 WARN, 35 INFO. Nothing on master removed or renamed in either
    the LMS document or service-wide /api-docs.
    tests PASS 900 passed.
    urls FAIL (2 FAIL, 1 WARN) -- IMPROVED from 2 FAIL / 2 WARN.
    The accounts/email_searches/ R2 warning is GONE: the address moved to the
    top-level POST /api/user/v2/email_searches/ in pass 5. That was the
    must-land-before-merge item, since ADR 0037 freezes an address once published.
    What remains is ONLY Phase A's deliberate JSON-404 catch-all api/user/v2/{_}.
    (R6, R8, R11) -- not an addressable resource; six candidate shapes were measured
    and the only conforming one cannot answer the "unknown collection" case.
    All 15 real addresses pass every machine-checkable 0038 rule.
    BLIND ON: R6/R8/R11 for a fallback route. Reported, not suppressed.
    old-tests FAIL RED AND BLIND, environment: 7 failed / 100 passed / 16 errors, all pymongo
    timeouts. SCOPE: this gate collects only user_api/tests/, which is NONE of the
    four surfaces Phase B supersedes. Those were measured separately, base vs branch,
    same image:
    accounts 18F/285P/18E both refs, 36 unique nodes each, diff EMPTY
    profile_images 73 passed
    verification_api 16 passed
    preferences only the known ModuleStoreTestCase mongo teardown pattern
    PROVES: no legacy test that passes on master fails on this branch.
    DOES NOT PROVE: that the mongo-dependent tests pass at all. Only CI can say.

    Review on the PR, please — this issue stays open until it merges.

  4. Abdul-Muqadim-Arbisoft commented on Nov 1, 2026

    @Abdul-Muqadim-Arbisoft
    Contributor

    Standardization PR open: #39314

    PR: #39314 — feat: standardize the user preferences and email opt-in API
    State: open (draft, stacked on #39297)

    ADRs applied

    12 of 16 ticked. The four open boxes are not gaps: 0032 pagination, 0033 filtering and
    0036 nested JSON are applied by exclusion — no response in this slice is a flat list of
    records, no operation reads a query parameter, and the document is a flat map — and 0035 MFE
    config is n/a. Each states its reason in the PR body.

    • 0025 serializers — every body is validated by a serializer: the merge patch key by key, the
    • 0026 permissions — permission_classes on both views; IsAddressedUser on all six
    • 0027 schema / docs — six operations and three superseded paths published, with the media
    • 0028 viewsets, one declared deviation — one plain ViewSet holds all five preference
    • 0029 errors — RFC 9457 envelope on every refusal, reusing this version's existing error
    • 0030 GET idempotence, one platform write and one platform announcement declared — both
    • 0031 merged endpoints, one recorded deviation — the one action-named legacy URL
    • 0032 pagination — applied by exclusion: no response here is a flat list of records. One
    • 0033 filtering / sorting — applied by exclusion: no operation here reads a query parameter,
    • 0034 authentication — one declared deviation on both views: the session half is the wider
    • 0035 MFE config — n/a: none of these six operations serves configuration, settings or
    • 0036 nested JSON — applied by exclusion: the document is a flat map of scalar text and one
    • 0037 versioning, two recorded deviations — every new line is in a current, unpublished
    • 0038 URL structure — plural collections, one level of nesting where the child cannot be
    • OEP-66 queryset scoping — the service-backed exemption: there is no view-level
    • OEP-69 conventions — RFC 9457 refusals, strict parameter handling, snake_case bodies and

    Gates

    run_gates.sh — feat/api-adr-user-v2-phase-c vs origin/master, with a MongoDB service attached and
    --old-tests widened to both superseded packages.

      versions             PASS
      hygiene              PASS
      schema               PASS
      urls                 FAIL
      old-tests            PASS
      tests                PASS
    GATES FAILED: 1
    
    • schema PASS, 0 BREAKING — 47 operationIds and 43 component schemas added, none removed or
      renamed
      . That is the check that matters: a generated SDK derives a module per operationId and a
      model class per component schema name.
    • old-tests PASS — 193 passed (user_api/tests/ 119 + user_api/preferences/tests/ 74).
    • tests PASS — 1118 passed; user_api/v2/ alone 920, profile_images/ 124.
    • hygiene PASS (0 FAIL, 1 WARN — the known H3 on Phase A's v2/paginators.py:6).
    • urls FAIL (2 FAIL, 1 WARN), deliberate — all three findings are on one route, Phase A's
      version-level JSON-404 fallback (v2/urls.py:126): no trailing slash, an identifier directly
      under the version, and no URL name. All three are correct for a regex fallback that is not an
      address and that nothing may reverse() to. Not to be read as passing.
    • Nothing was reported SKIPPED.

    A note on the earlier phases: #39237 and #39297 originally described this old-tests gate as "red
    and blind". That was a statement about the harness, not the code — the gate container had no
    MongoDB, so every failure was a pymongo connection timeout. Both descriptions have been
    corrected; two places carrying the retracted wording have not.
    #39237's first comment still
    reads "the gate that runs them is red and blind", and the tests row of #39237's own gate table
    still gives the no-database figures (7 failed, 100 passed, 16 errors) for the same directory its
    old-tests row two lines above now reports as 119 passed. Both are outstanding edits on that PR.

    Published-contract changes outside the preference surface

    Three changes this branch carries are not in the preference resource and were previously recorded
    in commit messages only. All three are now set out in the PR description: one operationId is
    renamed in the service-wide document — v2_user_preferences_retrieve becomes
    api_user_v2_user_preferences_retrieve, a name the descriptions published for phase A and for this
    phase both still show, so an SDK generated from /api-docs during this series must be regenerated;
    the username path parameter of /api/user/v2/accounts/{username}/profile_image/ gains the
    ^[\w .@_+-]+$ pattern the other ten addresses already publish; and ?username= on phase A's two
    directory collections is either kept as a comma-separated list and declared, or reverted — that one
    is still open and the PR description carries both wordings.

    Backward compatibility

    The three superseded /api/user/v1/ addresses and the unversioned twin
    /user_api/v1/preferences/email_opt_in/ keep answering exactly as they did; the twin keeps
    resolving and carries no schema mark, as the other unversioned twins do. The superseded packages are
    measured green with the database service their modulestore cases need (119 + 74 above).

    Deprecations

    Entry for the umbrella DEPR ticket #39128 is drafted at depr-entry-phase-c.md and still needs
    posting
    — four rows, including the unversioned twin, which is marked in no document and is listed
    on the ticket for exactly that reason. §9 of the PR description said the entry "is posted there
    once the PR is open"; the PR has been open since 2026-10-26 and the entry is not posted, so that
    sentence has been corrected to say so. This tracking comment is itself the outstanding item here.

  5. Abdul-Muqadim-Arbisoft commented on Nov 1, 2026

    @Abdul-Muqadim-Arbisoft
    Contributor

    Standardization PR open: #PR-NUMBER

    PR: https://github.com/openedx/openedx-platform/pull/PR-NUMBER — feat: standardize the account retirement pipeline as /api/user/v2/
    State: open (draft, stacked on #39314)

    This is Phase D1 of #39058: the
    eleven driver-facing operations of the account retirement pipeline, onto seven conforming
    addresses. Phase D2 — replace_usernames and the learner's own deactivate_logout — follows
    in a second PR; they have different callers and different credentials, and one of them has an open
    authentication question.

    ADRs applied

    13 of 16 ticked. The three open boxes are not gaps, and each states its reason in the PR body:
    0032 pagination carries one declared deviation (the partner-report claim answers an array, and
    carries no maximum, because the entries it answers with are the entries it marks and nothing can
    release one); 0035 MFE config is n/a; and 0037 versioning is deliberately left unticked
    because two of its clauses are unmet on purpose — no docstring marker on the superseded addresses,
    and two lines inside this unpublished version that earlier phases added are edited here.

    • 0025 serializers — serializer_class on all four views, help_text on every field
    • 0026 permissions — the two legacy capability classes imported, the capability per action
    • 0027 schema / docs — every action declares every status it can answer with
    • 0028 viewsets — a generic viewset for the collection, plain viewsets elsewhere
    • 0029 errors — RFC 9457 throughout, one new domain error type
    • 0030 GET idempotence, one declared exception — the platform's language sync
    • 0031 merged endpoints — two merges, and one pair deliberately not merged
    • 0032 pagination, one declared deviation — the claim answers an array
    • 0033 filtering — FilterSet + strict ordering, deterministic default order
    • 0034 authentication — no view declares authentication_classes
    • 0035 MFE config — n/a
    • 0036 nested JSON — ?view=minimal and ?fields=, both refusing an unknown value
    • 0037 versioning, two recorded deviations — see above
    • 0038 URL structure — one address table, nothing renamed or dropped
    • OEP-66 queryset scoping — every action resolves its records through the layer
    • OEP-69 conventions — one-line module docstrings, client-facing schema text

    Gates

    run_gates.sh against the merge-base 87cd3811a4, --settings test, with a MongoDB service
    attached and --old-tests widened to all three superseded packages.

      versions             PASS
      hygiene              PASS   (0 FAIL, 5 WARN)
      schema               PASS
      schema compare       PASS   (0 BREAKING, 85 WARN, 69 INFO)
      urls                 FAIL   (2 FAIL, 1 WARN)
      old-tests            PASS   (496 passed)
      tests                PASS   (1244 passed)
    GATES FAILED: 1
    
    • schema compare ran, and it is not this PR's compatibility evidence. The gate's
      0 BREAKING compares the narrow document against the merge-base, whose base document holds
      19 paths, all /api/enrollment/…, and no /api/user/ path at all — so every operation this PR
      alters is an addition there and cannot be reported. Re-run against this PR's branch point it is
      FAIL on both documents: 1 BREAKING narrow and 20 BREAKING, 35 WARN, 22 INFO
      service-wide. The twenty are the eight requestBody.required gains, the one loss and the nine
      changed version 1 success responses, all of them declared in the PR body.
    • What is measured, and holds: nothing published is removed. Both whole documents were
      generated at the merge-base and at this head and compared by hand, because the gate reads only
      one of the two: 0 operationIds, 0 component names and 0 paths removed in either, and
      0 operationIds moved onto an existing address.
    • hygiene PASS (0 FAIL, 5 WARN), and the five are expected. One is pre-existing (H3 on Phase
      A's v2/paginators.py:6). Four are new H7s and are the point of the last commit: the four
      retirement views inherit their permission classes from the layer they share instead of restating
      them, which ADR 0026 allows in terms; H7 can only see a class body, and a test asserts the exact
      tuple each view resolves. It is 5 WARN, not 1.
    • urls FAIL (2 FAIL, 1 WARN), declared. All three findings are on one route Phase A added —
      the version-level JSON-404 fallback, which has no trailing slash, puts an identifier directly
      under the version and carries no URL name, all three correct for a regex fallback that is not an
      address. head routes: 3061, base routes: 3036, nothing removed. Against this PR's own branch
      point the same gate is PASS (0 FAIL, 0 WARN) over the 7 routes this PR adds. Not to be read
      as passing against master.
    • old-tests PASS — 496 passed, equal to the count before this PR's first line.
    • tests PASS — 1244 passed over user_api/v2/; 1443 passed over all four packages this
      stack touches under lms.envs.test.
    • The Studio URL dump is BLOCKED — neither a pass nor a fail. Dumping Studio's URL
      configuration in this image raises ModuleNotFoundError: openedx_learning.urls, on the
      merge-base and on this head alike, so it says nothing about this change. No-Studio-impact is
      argued by inspection instead: the conforming version is mounted at one place, lms/urls.py:145,
      and this branch's cms/* diff is empty. Studio collection did run, with 0 collection
      errors
      , which is the whole of the claim: 2237 tests on this PR's own test tier's path set —
      the one figure comparable with that tier's baseline — and, for completeness, 1443 over the four
      packages and 2010 widened to the superseded ones, neither of which matches a baseline measured
      anywhere in this work.
    • Nothing was reported SKIPPED.

    Backward compatibility

    All eleven superseded addresses keep answering exactly as they did, to the same views and the same
    bound actions; the superseded view modules are absent from the diff and their tests are untouched
    and green. Three answers change at the conforming addresses — an ambiguous account name refused
    400, the platform's language-sync write on the two reads, and 405 with Allow instead of 403
    for an unwired method sent by a caller without the capability — each declared in the PR body and
    repeated on the DEPR entry.

    Across the whole stack, eight operations gain requestBody.required: true in the service-wide
    document and one — the partner-report claim — loses its published body entirely, because its
    handler reads none. That is the only requestBody.required lost in either document. Five of the
    eight gains belong to this PR, including POST /api/user/v1/preferences/email_opt_in/, which is
    not this area's address; the other three belong to earlier phases.

    Nine already-published version 1 operations change the success response the document publishes
    — six replace 200 with 204 and three replace a single UserRetirementStatus with an array of
    them. No handler changes and none of the nine carries the SDK tag, so no generated client is
    rebuilt; docs/lms-openapi.yaml and /api-docs do change. Set out operation by operation in the
    PR body.

    Deprecations

    The entry for the umbrella DEPR ticket #39128
    is drafted at depr-entry-phase-d1.md and still needs posting — eleven rows, plus the
    migration steps an operator has to act on: the superseded addresses are mounted by Studio as well
    as the LMS
    while every successor is LMS-only; the three changed answers above; the
    partner-report drain, which takes the whole queue in one transaction unless a ?limit= is sent —
    measured at 91.2 s, an 8.97 MB response and 369.9 MB of worker heap for 50,000 queued entries
    ;
    and the two schema settings keys (APPEND_COMPONENTS, ENUM_NAME_OVERRIDES) that a deployment
    assigning them replaces rather than extends, silently dropping the component and enum pins.

  6. Abdul-Muqadim-Arbisoft commented on Nov 1, 2026

    @Abdul-Muqadim-Arbisoft
    Contributor

    Standardization PR open: #39331

    PR: #39331 — feat: standardize the registration, sign-in and password reset APIs
    State: open

    What this does

    Part of [API] User, #39058, under the API standardization umbrella #38137 (FC-0118). This PR applies the platform's REST API decisions and OEP-69 to the last group of the user API: registration, sign-in, password reset and registration validation. The decisions are
    0025 ·
    0026 ·
    0027 ·
    0028 ·
    0029 ·
    0030 ·
    0031 ·
    0032 ·
    0033 ·
    0034 ·
    0035 ·
    0036 ·
    0037 ·
    0038,
    plus OEP-66 for scoping.

    ADRs applied

    8 of 16 decisions applied on this pass; every one left open carries its reason.

    • 0026 permissions
    • 0027 schema / docs
    • 0028 viewsets
    • 0030 GET idempotence
    • 0034 authentication
    • 0037 versioning
    • 0038 URL structure
    • OEP-69 conventions
    • 0025 serializers — not applied; deferred to a later version: the five classes add no handler of their own (a method written on a subclass would start the rate limits counting afresh), so every body comes from the frozen authentication views, and serializers there mean new handler code; every exchange is documented in the schema instead.
    • 0029 errors — not applied; deferred to a later version of the package: clients read the legacy error members (error_code, context.failure_count, field-keyed user_message lists, value); /api/user/v2/ therefore answers in two error shapes until then (Error-format decision).
    • 0031 merged endpoints — considered, not applied: registration and its validation, the reset form and the token check, sign-in and registration are different resources, and a merge would put differently rate-limited handlers behind one route.
    • 0032 pagination — n/a: no operation returns a list.
    • 0033 filtering — n/a: no list endpoint, and no filter or sort parameter on any of the nine operations.
    • 0035 MFE config — n/a: no endpoint here serves site or front-end configuration; each form description describes one form and is not shared-cached.
    • 0036 nested JSON — n/a: every body is flat or one level deep (a form description is a list of field objects).
    • OEP-66 queryset scoping — n/a as a mechanism: no list endpoint and no queryset; the three layers are stated in the matrix.

    The evidence for each tick is the compliance matrix in the PR body; nothing is ticked without a named test or gate line.

    Backward compatibility

    Trees: base = feat/api-adr-user-v2-phase-d1 @ ed83beab3d (#39330), head = 47925143f3, master =
    d178206575. Each number names the pair it compares. Full outputs are in the first comment. The URL dumps, the
    documents compared against base and master, and the wide test runs were made at 7325fef195, the head before the
    last two commits, which touch only authn_schemas.py and test_authn_addresses.py; a number from that tree says so.

    Gates

    Gates for feat/api-adr-user-v2-phase-e @ 4792514 (base ed83bea), all run at head, MongoDB attached:

    LMS against base: versions PASS | hygiene PASS | schema PASS (0 BREAKING) | urls PASS | old-tests PASS (820 passed, 4 skipped) | tests PASS (1528 passed)
    LMS against master: versions PASS | hygiene FAIL (H3, test_views.py:1500 - earlier phase of the stack) | schema PASS (0 BREAKING) | urls FAIL (R6/R8 on the stack's /api/user/v2/ fallback route) | old-tests PASS | tests PASS (1528 passed)
    Studio against base: versions PASS | hygiene PASS | schema FAIL (check-present: Studio serves no /api/user/v2/ path, by design) | urls PASS | old-tests PASS (177 passed) | tests PASS (40 passed)

    Full gate output: first comment of the PR.

    Deprecations

    The nine operations below are superseded by the addresses above. They are marked deprecated: true in the
    published schemas: the LMS's own document, and the service-wide documents of the LMS and of Studio, which both
    serve them. The superseded views are unchanged: there is no response header and no deprecation note in code.

    The replacements are served by the LMS only. Studio mounts no version 2 URLconf. Its document marks the nine
    operations because Studio serves them, but a client following a deprecation to a replacement address on the
    Studio host gets a 404 (SupersededUserOperationTest::test_no_address_replacing_a_superseded_one_is_published_here).

    superseded replacement what a client changes
    GET /api/user/v1/account/registration/ GET /api/user/v2/registrations/ the address; a configured confirm_email field is now listed (version 2)
    POST /api/user/v1/account/registration/ POST /api/user/v2/registrations/ the address; a configured required confirm_email is enforced (version 2)
    GET /api/user/v2/account/registration/ GET /api/user/v2/registrations/ the address only
    POST /api/user/v2/account/registration/ POST /api/user/v2/registrations/ the address only
    GET /api/user/v1/account/password_reset/ GET /api/user/v2/password_resets/ the address only
    POST /api/user/v1/account/password_reset/token/validate/ POST /api/user/v2/password_reset_token_validations/ the address only
    POST /api/user/v1/validation/registration POST /api/user/v2/registration_validations/ the address, which now has a closing slash
    GET /api/user/{api_version}/account/login_session/ (v1, v2) GET /api/user/v2/login_sessions/ the address only (same bytes)
    POST /api/user/{api_version}/account/login_session/ (v1, v2) POST /api/user/v2/login_sessions/ for a v2 client, the address only. A v1 client also sends email_or_username in place of email, which is the existing difference between the two versions

    The unversioned twins are not marked: /user_api/v1/account/registration/,
    /user_api/v2/account/registration/, /user_api/{api_version}/account/login_session/,
    /user_api/v1/account/password_reset/ and /user_api/v1/account/password_reset/token/validate/. Neither is
    /create_account. The /user_api/v1/ twins of every address superseded earlier in this series are not marked
    either, and deprecating the twins is one decision for all of them at once.

    Removal prerequisites. The DEPR entry carries these too.

    1. The stock LMS still renders its own sign-in page while ENABLE_AUTHN_MICROFRONTEND is off, which is its
      default. That page posts to the form description's submit_url, which names the v1 sign-in address with
      the v1 field email. Before the v1 arm can be removed, the form description has to name
      POST /api/user/v2/login_sessions/ and email_or_username.
    2. The registration form's submit_url names /api/user/v2/account/registration/ for every non-v1 request,
      including those at the new address. Before that address can be removed, it has to name
      POST /api/user/v2/registrations/.
    3. Callers to move first: the Android app posts POST /api/user/v1/account/registration/. The authentication
      front end posts the v2 registration and sign-in addresses and the v1 validation address.

    This is tracked on the FC-0118 umbrella DEPR ticket, #39128, which removes them one named release after the
    release this ships in. This area's entry is posted there once the PR is open. Of the umbrella's markers, only
    the schema flag is applied: there is no Warning header and no separate docs page.

    Follow-ups

    • Next version of these resources (/api/user/v3/): serializers and the standardized error envelope (0025,
      0029), JSON in place of Django's HTML CSRF page, the 200-with-error-body case above, and a sign-in form
      description naming the version 2 fields.
    • Before removal: the three prerequisites above.
    • No SDK change. None of the new operations is tagged for the generated client (the earlier phases tag only the
      account family), and adding the tag later is an addition, never a removal.
    • docs/lms-openapi.yaml is not regenerated here. It carries none of the version 2 work of this stack.

    Review on the PR, please — this issue stays open until it merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions