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.

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