46 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 storesname, these two inputs collapse on the server and cannot be returned to the UI later. - After signup,
GET /auth/meis the source of truth for the authenticated user — the layout header, the dashboard, andEditProfilePage.vueall readfirstName/lastName(notname). A/auth/meresponse withoutfirst_name/last_nameleaves these views blank. - Every other user-returning endpoint has the same need (
/admin/userslist rows,/admin/users/:iddetail modal,/admin/userscreate,/admin/users/:idupdate,/admin/users/:id/roleassign-roles response, login responsedata.user, register response, etc.).
Required action on the backend side:
- Persist
first_nameandlast_nameas 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). - Return
first_name,last_name, and the conveniencefull_name(or keepname) in every user-returning response. The UI will continue readingfirstName/lastName;fullName/nameare used only where a single display string is needed (tables, badges). - If the existing data already only has
name, do a one-time migration (split on the first whitespace, fall back to leavinglast_nameempty for single-token names). New writes must keep the two columns in sync withname/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
- User picks a file (avatar / cover / attachment) in the form.
- FE posts the file as
multipart/form-datatoPOST /media/upload(wasPOST /upload-temp). - Backend stores the file and returns its media
id. - 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. - 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_id→media_idacross 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/medoesn't return is effectively lost from the UI's perspective. See the cross-cuttingnamevsfirst_name/last_namenote 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 | blocked — UserDetailsModal.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 fromuser.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
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: apiUpdateAdminUser — changed 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_confirmationin edit mode. national_codeis 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 viaroles. Backend must either acceptrole_idor the FE must switch to sendingroles: [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 ConfirmModal → onConfirm).
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 adata.messagekey. That has been replaced by the spec envelope above. If a localized toast is needed, the FE composes it client-side rather than readingdata.message.
PATCH /admin/users/:id/role — assign roles
Mock route: src/services/mock/routes/admin-users.js. API client: apiUpdateAdminUserRole — changed 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: apiUpdateAdminTerm — changed 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: apiUpdateAdminCourse — changed 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) andcourseTemplate(UI nested object). When the FE creates a session viacourse_template_id, the mock auto-derivescourse_idfrom the first offered course with that template (best-effort). - Backend should decide: either expose
course_template_idon 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:
- Adopt the 7-value enum and map to its internal 3-value domain server-side, or
- Add a sub-type column (
session_subtypeetc.) so the FE can still pickvideo/audio/pdfwhiletypestays 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 arrays — media 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: apiUpdateAdminSession — changed 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.