Files
banu-front/docs/backend-api-todo.md
T
sajjadtalkhabi 8990ceaddd fix: design
2026-05-19 22:53:05 +03:30

49 KiB

Backend API — response model gaps

This document tracks fields the frontend already consumes that are not yet present in the current backend response models. Each section lists:

  • Postman / current spec: what the backend returns today.
  • UI also needs: fields the frontend mock fixture exposes and the UI reads from. These must be added to the real response to remove the mock.
  • Used by: where on the frontend the field is consumed.

Naming convention: the backend uses snake_case; the HTTP client camelizes responses, so this doc lists backend keys in snake_case and notes the camelCase form the UI sees.


⚠️ Cross-cutting: name vs first_name / last_name

The current Postman models expose only a single name field on the user object. The UI cannot work with name alone.

  • The signup form (/register) collects two separate inputs: نام (first name) and نام خانوادگی (last name). If the backend only stores name, these two inputs collapse on the server and cannot be returned to the UI later.
  • After signup, GET /auth/me is the source of truth for the authenticated user — the layout header, the dashboard, and EditProfilePage.vue all read firstName / lastName (not name). A /auth/me response without first_name / last_name leaves these views blank.
  • Every other user-returning endpoint has the same need (/admin/users list rows, /admin/users/:id detail modal, /admin/users create, /admin/users/:id update, /admin/users/:id/role assign-roles response, login response data.user, register response, etc.).

Required action on the backend side:

  1. Persist first_name and last_name as independent columns on the user table — accept them as separate keys in every write endpoint that touches the user (/register, POST /admin/users, PATCH /admin/users/:id).
  2. Return first_name, last_name, and the convenience full_name (or keep name) in every user-returning response. The UI will continue reading firstName / lastName; fullName / name are used only where a single display string is needed (tables, badges).
  3. If the existing data already only has name, do a one-time migration (split on the first whitespace, fall back to leaving last_name empty for single-token names). New writes must keep the two columns in sync with name / full_name.

The per-endpoint tables below repeat first_name / last_name under each "Missing — UI also needs" section so the backend dev cannot miss it on individual routes; this top section explains why it is the same answer everywhere.


POST /media/upload — single-step media upload (canonical)

This is the only media-upload endpoint in the project. Earlier revisions of this doc referenced per-resource cover endpoints (POST /terms/:id/image, POST /courses/:id/image, POST /sessions/:id/media) — those were a mistake and have been removed from the FE, the API client, and the mock.

Flow

  1. User picks a file (avatar / cover / attachment) in the form.
  2. FE posts the file as multipart/form-data to POST /media/upload (was POST /upload-temp).
  3. Backend stores the file and returns its media id.
  4. FE puts that id on the resource create/update body under whichever key the resource expects: avatar_id, image_id, cover_id, material_id, etc.
  5. The resource endpoint (POST /courses, PATCH /terms/:id, POST /admin/users, …) resolves the id and attaches the media.

The previous mock endpoint path was /upload-temp. It now lives at /media/upload. The FE function useUploadTemporaryMutation was renamed to useUploadMediaMutation (consumed by the profile form, admin-user form, admin-term form, admin-course forms, admin-session form, and the student-register media/personal-info components).

Mock request

multipart/form-data with a single file field.

Mock response

{
  "success": true,
  "message": "Media uploaded.",
  "data": {
    "id": 1234,
    "upload_id": 1234,
    "url": "https://…/240/240"
  }
}

upload_id is an alias for id and is what older FE code reads (form.value.imageId = payload?.uploadId || payload?.id). Backend should return both keys, or eventually the FE migrates to reading only id.

How the resource body references the upload

Resource form Field on the body Backend should treat it as a media id
/auth/me profile avatar_id yes
POST /admin/users avatar_id yes
POST /terms image_id yes — sets cover_url
POST /courses image_id yes — sets cover_url
POST /sessions image_id / materials[] yes — image_id sets cover; materials array carries session attachments by their media ids

Decision pending: rename FE field image_idmedia_id across the board, or have the backend accept the resource-specific field names. The mock currently honors the resource-specific names.


GET /auth/me — current user

Mock fixture: src/services/mock/fixtures/me.js (currentMe). Frontend query: useGetMeQuery in src/services/query/auth.js.

This endpoint is the source of truth for the logged-in user immediately after /register (signup). Anything the signup form collects but /auth/me doesn't return is effectively lost from the UI's perspective. See the cross-cutting name vs first_name/last_name note above before reviewing this section.

Spec (already implemented)

Backend key UI key (camelCase) Type Notes
id id number
name name string Full display name
email email string
phone phone string E.164, e.g. +989121234567
roles roles string[] e.g. ["student"]
avatar_url avatarUrl string Public avatar URL
avatar_download_url avatarDownloadUrl string Signed/download endpoint
created_at createdAt string ISO 8601

Missing — UI also needs

Backend key (proposed) UI key (camelCase) Type Used by
first_name firstName string EditProfilePage.vue form, header/greeting
last_name lastName string EditProfilePage.vue form
full_name fullName string Lists/tables that render a single name string
phone_number phoneNumber string EditProfilePage.vue form (local format, e.g. 09121234567)
national_code nationalCode string EditProfilePage.vue form, identity validation
status status string enum: pending | approved | rejected — gates access in the UI
address.address address.address string EditProfilePage.vue address textarea
address.province.id address.province.id number EditProfilePage.vue province select
address.province.name address.province.name string
address.city.id address.city.id number EditProfilePage.vue city select
address.city.name address.city.name string
profile.bio profile.bio string EditProfilePage.vue bio textarea
profile.birth_date profile.birthDate string ISO 8601, EditProfilePage.vue date picker
profile.marital_status profile.maritalStatus string enum: single | married
profile.gender profile.gender string enum: male | female
profile.education_status profile.educationStatus string enum
profile.seminary_level profile.seminaryLevel string enum
profile.university_level profile.universityLevel string enum
profile.university_name profile.universityName string
profile.field_of_study profile.fieldOfStudy string
profile.avatar_id profile.avatarId number | null Media id returned by POST /media/upload

Suggested response shape

{
  "success": true,
  "message": "OK",
  "data": {
    "id": 5,
    "name": "Jane Student",
    "first_name": "Jane",
    "last_name": "Student",
    "full_name": "Jane Student",
    "email": "jane.student@example.com",
    "phone": "+989121234567",
    "phone_number": "09121234567",
    "national_code": "0079827498",
    "status": "approved",
    "roles": ["student"],
    "avatar_url": "http://localhost:8080/storage/users/5/avatar.jpg",
    "avatar_download_url": "http://localhost:8080/api/media/9/download",
    "created_at": "2026-02-01T10:00:00+00:00",
    "address": {
      "address": "...",
      "province": { "id": 1, "name": "تهران" },
      "city":     { "id": 11, "name": "تهران" }
    },
    "profile": {
      "bio": "...",
      "birth_date": "1989-06-12T00:00:00.000Z",
      "marital_status": "married",
      "gender": "male",
      "education_status": "graduate",
      "seminary_level": "level_3",
      "university_level": "master",
      "university_name": "تهران",
      "field_of_study": "علوم قرآنی",
      "avatar_id": null
    }
  }
}

GET /admin/users — list users (paginated)

Mock fixture: src/services/mock/fixtures/admin-users.js (adminUsers, makeUser). Mock route: src/services/mock/routes/admin-users.js. Frontend query: useAdminUsersListQuery in src/services/query/admin-users.js. Consumed by: src/features/admin/users/pages/UsersListPage.vue, src/features/admin/users/components/UsersTable.vue.

Spec envelope

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [ /* user objects */ ],
    "meta": {
      "current_page": 1,
      "per_page": 20,
      "total": 3,
      "last_page": 1
    }
  }
}

Query params the UI already sends: search, per_page, page, name, national_code, phone_number, role_id, from_date, to_date. The spec only documents search + per_page — backend should accept the rest or the UI filters need to be reworked.

Per-item spec (already implemented)

Backend key UI key (camelCase) Type
id id number
name name string
email email string
phone phone string | null
roles roles string[]
avatar_url avatarUrl string | null
avatar_download_url avatarDownloadUrl string | null
created_at createdAt string

Per-item missing — UI also needs

Backend key (proposed) UI key (camelCase) Used by
first_name firstName UsersTable.vue (display name), filters
last_name lastName UsersTable.vue, delete-confirm dialog
phone_number phoneNumber UsersTable.vue (local format display), UserDetailsModal.vue
national_code nationalCode UsersTable.vue, UserDetailsModal.vue
status status enum pending | approved | blockedUserDetailsModal.vue badge
role_id roleId UsersTable.vue role-change dropdown

GET /admin/users/:id — show user

Mock route: same file as above. Frontend query: useAdminUserQuery in src/services/query/admin-users.js. Consumed by: src/features/admin/users/components/modals/UserDetailsModal.vue (opened from the users list — not a route page).

Spec envelope

{
  "success": true,
  "message": "OK",
  "data": { /* user object */ }
}

Spec fields (already implemented)

Same as list-item spec above.

Missing — UI also needs

Detail modal renders the full profile, so it needs the list-item missing fields plus the nested address and profile blocks:

Backend key (proposed) UI key (camelCase) Type
address.address address.address string
address.province.id address.province.id number
address.province.name address.province.name string
address.city.id address.city.id number
address.city.name address.city.name string
bio bio string
birth_date birthDate string (ISO)
fa_birth_date faBirthDate string (Jalaali)
marital_status maritalStatus enum
gender gender enum
education_status educationStatus enum
seminary_level seminaryLevel enum
university_level universityLevel enum
university_name universityName string
field_of_study fieldOfStudy string
profile_completed profileCompleted boolean

The modal flattens these (user.bio, user.birthDate, …) rather than reading from user.profile.*, so backend can either return them flat at the top level or the FE select can flatten them — whichever is easier. Document this once decided.

404 — Not found

{
  "success": false,
  "message": "Resource not found."
}

The FE findOrThrow mock throws a 404; the toast/error layer surfaces the message field — keep that key name.


POST /admin/users — create user

Mock route: src/services/mock/routes/admin-users.js. API client: apiAddAdminUser in src/services/api/admin-users.js. Query: useAddAdminUserMutation in src/services/query/admin-users.js. Consumed by: src/features/admin/users/pages/UserFormPage.vue (add mode).

Spec request body

{
  "name": "New Teacher",
  "email": "teacher.new@example.com",
  "phone": "+989120000000",
  "password": "StrongPass123!",
  "roles": ["teacher"]
}

What the UI form actually sends today

UserFormPage.vue posts a much richer payload (snake-cased by the HTTP interceptor before going out). The keys that map to the spec are marked with → spec; the rest have no spec equivalent yet:

FE form key (camelCase) Wire key (snake_case) Maps to spec?
firstName first_name → derives name together with lastName
lastName last_name → derives name
phoneNumber phone_number → maps to phone (needs 09…+98…)
roleId role_id → maps to roles (single id → name array)
password password password
passwordConfirmation password_confirmation (spec doesn't document, FE still sends)
nationalCode national_code missing
birthDate birth_date missing
maritalStatus marital_status missing
gender gender missing
bio bio missing
provinceId province_id missing
cityId city_id missing
address address missing
avatarId avatar_id missing (returned by POST /media/upload)

The form has no email input. The spec requires email. Either the backend should treat email as optional (auto-generate or accept null), or the FE form needs a new field. Decision pending.

Spec response — 201 Created

{
  "success": true,
  "message": "User created.",
  "data": { /* user object, same shape as GET /admin/users/:id */ }
}

The response should carry every field listed for GET /admin/users/:id (spec + "ui also needs" — list-item missing fields and the nested address/profile blocks), so the UI can refresh the cache without an extra round-trip.

Spec response — 422 Unprocessable Entity

{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The email field is required."]
  }
}

The FE doesn't currently render per-field server errors — it would need a small hook into the form's errors ref. Out of scope for the backend, but worth flagging.


PATCH /admin/users/:id — update user

Mock route: same file. API client: apiUpdateAdminUserchanged from PUT to PATCH to match the spec. Query: useUpdateAdminUserMutation. Consumed by: src/features/admin/users/pages/UserFormPage.vue (edit mode).

Spec request body

{
  "name": "Renamed User",
  "roles": ["teacher", "counselor"]
}

Spec implies partial update — only the keys present are changed.

What the UI form actually sends today

Same rich payload as POST above (the same form is used in add + edit mode). Notable differences:

  • No password / password_confirmation in edit mode.
  • national_code is sent but is disabled in the UI when the user already has one.
  • The FE still sends only a single role via role_id, not an array via roles. Backend must either accept role_id or the FE must switch to sending roles: [name]. Decision pending.

Spec response — 200 OK

{
  "success": true,
  "message": "User updated.",
  "data": { /* user object, same shape as GET /admin/users/:id */ }
}

Same superset of fields as POST response — see GET /admin/users/:id table for the full list the UI needs back.


DELETE /admin/users/:id — delete user

Mock route: src/services/mock/routes/admin-users.js. API client: apiDeleteAdminUser in src/services/api/admin-users.js. Query: useDeleteAdminUserMutation in src/services/query/admin-users.js. Consumed by: src/features/admin/users/pages/UsersListPage.vue (via ConfirmModalonConfirm).

Spec response — 200 OK

{
  "success": true,
  "message": "User deleted.",
  "data": null
}

The FE only checks for a non-error completion (the mutation's onSuccess invalidates the list cache); the response body isn't read. No additional fields needed.

The previous mock returned a Persian success message (حذف موفق) under a data.message key. That has been replaced by the spec envelope above. If a localized toast is needed, the FE composes it client-side rather than reading data.message.


PATCH /admin/users/:id/role — assign roles

Mock route: src/services/mock/routes/admin-users.js. API client: apiUpdateAdminUserRolechanged from POST to PATCH to match the spec. Query: useUpdateAdminUserRoleMutation. Consumed by: src/features/admin/users/components/UsersTable.vue (role-badge dropdown → onChangeRole).

Spec request body

{
  "roles": ["missionary"]
}

The FE now sends { roles: [targetRoleName] } exactly per spec.

Spec response — 200 OK

{
  "success": true,
  "message": "Roles updated.",
  "data": { /* user object, same shape as GET /admin/users/:id */ }
}

The mock additionally writes roleId (legacy UI key) onto the user so the same record stays consistent in the in-memory store. The FE mutation only triggers invalidateUsers on success, so the envelope itself is not read.

The mock also accepts legacy bodies ({ roleId } and { role }) as a fallback in case other callers exist; new code should send { roles: [...] }.


Terms (/terms) — global notes

The previous FE wiring used /admin/terms paths; the spec uses /terms. All five term endpoints below moved to the /terms namespace. The admin-only auxiliary endpoints (/admin/terms/:id/clone, /admin/terms/:id/status, the term-students sub-tree, the term-courses sub-tree) stayed on /admin/terms until the backend specs them.

⚠️ Field name mismatches (apply to every terms endpoint below)

FE / mock key (camelCase) Spec key (snake_case) Notes
image cover_url FE reads term.image
startDate starts_at FE reads term.startDate
endDate ends_at FE reads term.endDate
studentsCount (missing) Used in list view
coursesCount (missing) Used in list view
faStartDate / faEndDate (missing) Jalaali-formatted display strings; FE can compute client-side, but if backend provides them the FE saves a formatJalaaliDate call

The mock currently writes both the spec key and the legacy UI key (coverUrl + image, startsAt + startDate, endsAt + endDate) onto every fixture and response. Required action: backend should either rename to the legacy keys, ship both, or the FE consumers (TermItem.vue, TermFormPage.vue, TermsListPage.vue) need to be migrated to read the spec keys. Decision pending.


GET /terms — list terms (paginated)

Mock fixture: src/services/mock/fixtures/admin-terms.js (adminTerms). Mock route: src/services/mock/routes/admin-terms.js. Frontend query: useAdminTermsListQuery. Consumed by: src/features/admin/terms/pages/TermsListPage.vue, src/features/admin/terms/components/TermItem.vue.

Spec envelope

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [ /* term objects */ ],
    "meta": { "current_page": 1, "per_page": 20, "total": 2, "last_page": 1 }
  }
}

Per-item spec

Backend key UI key Type
id id number
title title string
description description string
is_active isActive boolean
starts_at startsAt string (ISO)
ends_at endsAt string (ISO)
cover_url coverUrl string | null
created_at createdAt string (ISO)

Per-item missing — UI also needs

See "Field name mismatches" above + students_count and courses_count for the term list cards.

Query params the FE sends: per_page, page, title (free-text), status (0/1), from_date/to_date. The spec only documents per_page and active_only=1 — backend should accept the rest or drop them from the FE.

401 — Unauthenticated

{ "message": "Unauthenticated." }

The global HTTP layer already redirects to /login on 401 (see addUnauthorizeInterceptor in src/services/api/http.js).


GET /terms/:id — show term

Mock route: same file. Frontend query: useAdminTermQuery. Consumed by: src/features/admin/terms/pages/TermFormPage.vue (edit mode), src/features/admin/terms/components/modals/TermDetailsModal.vue.

Spec envelope

{ "success": true, "message": "OK", "data": { /* term object */ } }

Same fields as a list item. The detail modal needs the same UI-only keys (image, startDate, endDate, studentsCount, coursesCount).

404 — Not found

{ "success": false, "message": "Resource not found." }

POST /terms — create term

Mock route: same file. API client: apiAddAdminTerm. Query: useAddAdminTermMutation. Consumed by: src/features/admin/terms/pages/TermFormPage.vue (add).

Spec request body

{
  "title": "Spring 2026",
  "description": "Spring term covering intro & intermediate units.",
  "is_active": true,
  "starts_at": "2026-03-20",
  "ends_at": "2026-06-20"
}

What the FE form sends today

FE key (camelCase) Wire key Maps to spec?
title title title
description description description
startDate start_date starts_at (key rename needed)
endDate end_date ends_at (key rename needed)
imageId image_id not in spec body — see "Image flow" below

Image flow (canonical): the FE uploads the file to POST /media/upload, gets a media id back, and submits it as image_id with the term create/update body. The backend should resolve image_id against the media table and populate cover_url. No separate per-resource upload endpoint exists — the previously documented POST /terms/:id/image was a mistake and has been removed.

is_active is required by the spec but the FE form doesn't expose a toggle on create (terms are forced to active). The mock defaults it to true; backend should treat is_active as optional with a default of true, or the FE needs a switch on the create form.

Spec response — 201 Created

{ "success": true, "message": "Term created.", "data": { /* term */ } }

The response should carry every "UI also needs" field from the list section so the FE can refresh the cache without an extra GET.

Spec errors

  • 403 { "success": false, "message": "This action is unauthorized." }
  • 422 { "message": "The given data was invalid.", "errors": { … } }

FE does not yet render per-field server errors.


PATCH /terms/:id — update term

Mock route: same file. API client: apiUpdateAdminTermchanged from PUT to PATCH. Query: useUpdateAdminTermMutation. Consumed by: src/features/admin/terms/pages/TermFormPage.vue (edit).

Spec request body — partial

{ "title": "Spring 2026 (Revised)", "is_active": true }

What the FE form sends today

Same payload as create (the same form handles add + edit). The mock applies a true partial update — keys that are undefined in the payload are left unchanged on the stored term.

Spec response — 200 OK

{ "success": true, "message": "Term updated.", "data": { /* term */ } }

Spec errors

  • 403 { "success": false, "message": "This action is unauthorized." }

DELETE /terms/:id — delete term

Mock route: same file. API client: apiDeleteAdminTerm. Query: useDeleteAdminTermMutation. Consumed by: src/features/admin/terms/pages/TermsListPage.vue (via ConfirmModal).

Spec response — 200 OK

{ "success": true, "message": "Term deleted.", "data": null }

Spec errors

  • 403 { "success": false, "message": "This action is unauthorized." }

The previous mock returned a Persian success message under data.message — replaced by the spec envelope. FE only invalidates the list cache on success and never reads the body.


Courses (/courses) — global notes

These spec endpoints are for offered courses (an instance of a course template tied to a term + teacher). The previous FE wiring used /admin/courses; the spec uses /courses. All six course endpoints below moved to /courses. The admin-only /admin/courses/:id/toggle-status stays where it is until the backend specs it (or until the FE migrates to PATCHing is_active).

The separate course templates API (/admin/course-templates, useAdminCourseTemplatesListQuery, etc.) is not in the spec yet and was not touched.

⚠️ Field name mismatches (apply to every courses endpoint below)

FE / mock key (camelCase) Spec key (snake_case) Notes
image cover_url FE reads course.image
teacher (nested object) (only teacher_id in list) FE list cards read course.teacher for name/avatar; need teacher nested on list too
term (nested object) (only term_id in list) FE list cards read course.term?.title; need term nested on list too
template (nested object) (missing entirely) FE-only concept (course-template link)
templateId (missing) Course-template id
prerequisitesCount (missing) Number badge on list cards
startDate / endDate (missing — read from term.starts_at/ends_at) List cards currently store dates flat on the course; the FE could derive from the nested term instead

The mock currently writes both the spec key and the legacy UI key (coverUrl + image) and includes nested term/teacher objects on both list and detail responses so the existing list cards keep rendering. Required action: backend should either include the nested objects + UI-only counters on every course response, or the FE needs migration. Decision pending.

Nested teacher shape

The spec's detail response nests a full teacher user object using the top-level user spec (name, email, phone, roles, avatar_url, …). The UI cards already read course.teacher.firstName / course.teacher.lastName — so this is yet another instance of the cross-cutting name vs first_name/last_name problem. The mock writes both forms on the nested teacher.


GET /courses — list courses (paginated)

Mock fixture: src/services/mock/fixtures/admin-courses.js (adminOfferedCourses). Mock route: src/services/mock/routes/admin-courses.js. Frontend query: useAdminCoursesListQuery. Consumed by: src/features/admin/courses/pages/CoursesListPage.vue, src/features/admin/courses/components/CourseItem.vue.

Spec envelope

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [ /* course objects */ ],
    "meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
  }
}

Per-item spec

Backend key UI key Type
id id number
term_id termId number
teacher_id teacherId number
title title string
description description string
capacity capacity number
is_active isActive boolean
cover_url coverUrl string | null

Per-item missing — UI also needs

See "Field name mismatches" above. The list cards specifically need term (nested with at least id, title), teacher (nested with id + the user-shape so firstName/lastName/name are available), prerequisitesCount, and image (or the FE migrates to cover_url). template / templateId are FE-only — backend may ignore.

Query params

Spec documents term_id and per_page. FE additionally sends page, title (free-text), status (0/1), from_date, to_date. Backend should accept or the FE filter set needs trimming.


GET /courses/:id — show course

Mock route: same file. Frontend query: useAdminCourseQuery. Consumed by: src/features/admin/courses/components/modals/CourseDetailsModal.vue (opened from the list — not a route page), src/features/admin/courses/pages/CourseTemplateFormPage.vue (when editing offered courses via the unified form).

Spec envelope

{
  "success": true,
  "message": "OK",
  "data": {
    "id": 1,
    "term_id": 1, "teacher_id": 2,
    "title": "…", "description": "…", "capacity": 50, "is_active": true,
    "cover_url": "…",
    "term":    { "id": 1, "title": "…", "description": "…", "is_active": true, "starts_at": "…", "ends_at": "…", "cover_url": "…", "created_at": "…" },
    "teacher": { "id": 2, "name": "John Teacher", "email": "…", "phone": "…", "roles": ["teacher"], "avatar_url": null, "avatar_download_url": null, "created_at": "…" }
  }
}

Missing — UI also needs

Same superset as list-item missing (template/templateId, prerequisitesCount, image, nested teacher carrying firstName/lastName/fullName).

404 — Not found

{ "success": false, "message": "Resource not found." }

POST /courses — create course

Mock route: same file. API client: apiAddAdminCourse. Query: useAddAdminCourseMutation. Consumed by: course-creation form (offered-course branch of the existing course form page / modal).

Spec request body

{
  "term_id": 1,
  "teacher_id": 2,
  "title": "Intro to Theology",
  "description": "Foundational course.",
  "capacity": 50,
  "is_active": true
}

What the FE form sends today

FE key (camelCase) Wire key Maps to spec?
termId term_id term_id
teacherId teacher_id teacher_id
title title title
description description description
capacity capacity capacity
isActive is_active is_active
templateId template_id does not exist in spec — FE-only
imageId image_id not in spec body — see "Image flow" below

Image flow (canonical): the FE uploads the file to POST /media/upload, gets a media id back, and submits it as image_id with the course create/update body. Backend resolves image_id against the media table and populates cover_url. No separate per-resource upload endpoint exists — the previously documented POST /courses/:id/image was a mistake and has been removed.

Spec response — 201 Created

{ "success": true, "message": "Course created.", "data": { /* course */ } }

The response should carry every list-item "UI also needs" field (nested term, nested teacher, image, template, templateId, prerequisitesCount) so the FE cache refresh has all it needs.

Spec errors

  • 403 { "success": false, "message": "This action is unauthorized." }
  • 422 { "message": "The given data was invalid.", "errors": { … } }

PATCH /courses/:id — update course

Mock route: same file. API client: apiUpdateAdminCoursechanged from PUT to PATCH. Query: useUpdateAdminCourseMutation.

Spec request body — partial

{ "title": "Intro to Theology (v2)", "capacity": 60 }

What the FE form sends today

Same payload shape as create (the same form handles add + edit). The mock now applies a true partial update — only keys present in the payload are written.

Spec response — 200 OK

{ "success": true, "message": "Course updated.", "data": { /* course */ } }

DELETE /courses/:id — delete course

Mock route: same file. API client: apiDeleteAdminCourse. Query: useDeleteAdminCourseMutation. Consumed by: src/features/admin/courses/pages/CoursesListPage.vue (via ConfirmModal).

Spec response — 200 OK

{ "success": true, "message": "Course deleted.", "data": null }

Previous mock returned a Persian message under data.message — replaced by the envelope. The FE only invalidates the list cache on success.


Sessions (/sessions) — global notes

The previous FE wiring used /admin/sessions; the spec uses /sessions. All six session endpoints below moved to /sessions. The admin-only /admin/sessions/:id/toggle-status and /admin/sessions/:sessionId/attendances stay where they are until the backend specs them.

⚠️ Structural mismatch: course vs course-template

The spec says a session belongs to an offered course (course_id). The FE models sessions as belonging to a course template (course_template_id), where a single session can be reused across multiple terms/offered courses (session.used_in_terms).

  • The mock now stores both courseId (spec) and courseTemplate (UI nested object). When the FE creates a session via course_template_id, the mock auto-derives course_id from the first offered course with that template (best-effort).
  • Backend should decide: either expose course_template_id on the session endpoints, or the FE needs to migrate to picking an offered course directly when creating a session. Decision pending.

⚠️ Enum mismatch: type (spec) vs session_type (FE)

Spec values for type: online | offline | content (3 values). FE values for session_type (enum SESSION_TYPE): in_person | online | video | audio | text | slide | pdf (7 values).

The mock derives type from session_type like this (see SESSION_TYPE_TO_SPEC in the fixture):

FE session_type Spec type
in_person offline
online online
video / audio / text / slide / pdf content

The FE form picks from the 7-value enum and renders different config blocks per value (SessionFormPage.vue). Backend should either:

  1. Adopt the 7-value enum and map to its internal 3-value domain server-side, or
  2. Add a sub-type column (session_subtype etc.) so the FE can still pick video/audio/pdf while type stays in the 3-value spec set.

Decision pending.

⚠️ Flat fields vs session_config

The spec flattens what the FE keeps nested in session_config:

Spec key (top-level) FE key (nested)
starts_at session_config.start_time
location session_config.location (in_person)
link session_config.meeting_link (online)
(missing) session_config.platform (online)
(missing) session_config.min_watched_percent (video/audio)
(missing) session_config.min_read_percent (text/slide/pdf)
(missing) session_config.must_complete_before_next

The mock now writes the spec keys flat AND keeps the legacy session_config nested block so SessionDetailsModal.vue (which reads session.session_config.*) keeps rendering. Required action: backend should either accept the nested session_config and return it back, or the FE detail modal needs to be migrated to read the flat keys + the missing config knobs need a new home (probably a JSON column).

Other UI-only fields the spec doesn't cover

image, duration_minutes, order, materials, used_in_terms, course_template (nested). The FE list cards and detail modal all read these — backend should include them or the FE needs migration. The mock keeps them all.


GET /sessions — list sessions (paginated)

Mock fixture: src/services/mock/fixtures/admin-sessions.js (adminSessions). Mock route: src/services/mock/routes/admin-sessions.js. Frontend query: useAdminSessionsListQuery. Consumed by: src/features/admin/sessions/pages/SessionsListPage.vue, src/features/admin/sessions/components/SessionItem.vue.

Spec envelope

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [ /* session objects */ ],
    "meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
  }
}

Per-item spec

Backend key UI key Type
id id number
course_id courseId number
title title string
description description string
type type enum: online | offline | content
starts_at startsAt string (ISO with tz)
location location string | null
link link string | null

Per-item missing — UI also needs

See "Structural", "Enum", and "Flat vs session_config" callouts above. Concretely the list card reads: image, courseTemplate.title, durationMinutes, sessionType / sessionTypeFa, usedInTerms[]. None of those are in the spec yet.

Query params

Spec documents course_id + per_page. FE additionally sends title, course_template_id, session_type, from_date, to_date, page. Backend should accept or the FE filter set needs trimming.


GET /sessions/:id — show session

Mock route: same file. Frontend query: useAdminSessionQuery. Consumed by: src/features/admin/sessions/components/modals/SessionDetailsModal.vue, src/features/admin/sessions/pages/SessionFormPage.vue (edit mode).

Spec envelope

{
  "success": true, "message": "OK",
  "data": {
    "id": 1, "course_id": 1,
    "title": "…", "description": "…", "type": "online",
    "starts_at": "…", "location": null, "link": "…",
    "media": [ { "id": 15, "collection_name": "attachments", "file_name": "…", "mime_type": "…", "file_size": 204800, "url": "…", "download_url": "…" } ],
    "course": { /* full offered-course object, same shape as GET /courses/:id flat fields */ }
  }
}

Missing — UI also needs

Detail modal needs: session_config.* (with the config knobs not in spec — platform, min_watched_percent, etc.), course_template (nested), image, duration_minutes, order, materials[], used_in_terms[]. The mock writes all of them.

The spec's media[] and the FE's materials[] are different arraysmedia is the new attachments collection (PDFs, slides, recordings); materials is a legacy FE concept. Backend can either collapse them into one field or keep both — flagged for decision.

404 — Not found

{ "success": false, "message": "Resource not found." }

POST /sessions — create session

Mock route: same file. API client: apiAddAdminSession. Query: useAddAdminSessionMutation. Consumed by: src/features/admin/sessions/pages/SessionFormPage.vue (add).

Spec request body

{
  "course_id": 1,
  "title": "Week 1 — Overview",
  "description": "Opening session.",
  "type": "online",
  "starts_at": "2026-03-25T18:00:00+03:30",
  "location": null,
  "link": "https://meet.example.com/abc"
}

What the FE form sends today

FE key (camelCase) Wire key Maps to spec?
title title title
description description description
courseTemplateId course_template_id not in spec — see structural callout
sessionType session_type maps to spec type via the table above
durationMinutes duration_minutes missing in spec
order order missing in spec
sessionConfig.startTime session_config.start_time maps to spec starts_at
sessionConfig.location session_config.location maps to spec location
sessionConfig.meetingLink session_config.meeting_link maps to spec link
sessionConfig.platform session_config.platform missing in spec
sessionConfig.minWatchedPercent session_config.min_watched_percent missing in spec
sessionConfig.minReadPercent session_config.min_read_percent missing in spec
sessionConfig.mustCompleteBeforeNext session_config.must_complete_before_next missing in spec
imageId image_id media id from POST /media/upload; backend should set cover_url
materials materials[] array of media ids (each from POST /media/upload); backend attaches them as session media; overlaps with spec media[] — see callout

The mock accepts both naming conventions on input. The FE form needs a flat course_id selector to fully match the spec (or backend must accept course_template_id).

Spec response — 201 Created

{ "success": true, "message": "Session created.", "data": { /* session */ } }

Spec errors

  • 422 { "message": "The given data was invalid.", "errors": { "course_id": [...], "title": [...], "type": [...] } }

PATCH /sessions/:id — update session

Mock route: same file. API client: apiUpdateAdminSessionchanged from PUT to PATCH. Query: useUpdateAdminSessionMutation.

Spec request body — partial

{ "title": "…", "starts_at": "…" }

What the FE sends today

Same payload shape as create. The mock now applies a true partial update — only keys present in the payload are written.

Spec response — 200 OK

{ "success": true, "message": "Session updated.", "data": { /* session */ } }

DELETE /sessions/:id — delete session

Mock route: same file. API client: apiDeleteAdminSession. Query: useDeleteAdminSessionMutation.

Spec response — 200 OK

{ "success": true, "message": "Session deleted.", "data": null }

Previous mock returned a Persian message under data.message — replaced by the envelope. The FE only invalidates the list cache on success.