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 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.