Profile — API
GET /api/profile and GET /api/mobile/portal/me — request, response shape, per-type block nullability, and error codes.
Profile — API
Endpoint
| Method | Path | Surface | Auth | Permission |
|---|---|---|---|---|
| GET | /api/profile | admin panel | Bearer (JwtAuthGuard + RoleGuard) | none declared — self-scoped by construction |
| GET | /api/mobile/portal/me | consumer portal | Bearer (JwtAuthGuard + RoleGuard) | none declared — self-scoped by construction |
No parameters. No body. No write counterpart.
The caller is taken from the session; there is no id to supply and none is accepted.
Two paths, one service
Both routes call ProfileService.findForActor and return the identical ProfileDto. There is one
service and one DTO; the second path exists because the consumer surface is mounted under
/api/mobile by RouterModule, and registering ProfileModule there instead would have MOVED
/api/profile to /api/mobile/profile and broken the admin panel.
The response is the same for every audience, and it returns every profile the caller holds
regardless of which role the session is acting as — a teacher who is also a parent receives both
the staff and the guardian block. That is deliberate: this route describes the person, not the
session's current context.
Response
200 with the project envelope.
{
"message": "Profile fetched.",
"data": {
"person": {
"id": "01a078b4-010d-7309-aaee-52d88c912d3d",
"firstName": "Deepa",
"middleName": null,
"lastName": "Rai",
"fullName": "Deepa Rai",
"email": "student@example.test",
"emailVerified": true,
"phone": null,
"phoneVerified": false,
"image": null,
"dateOfBirth": "2012-05-04",
"gender": "female",
"bloodGroup": "O+",
"maritalStatus": null,
"disabilityType": null,
"ethnicityId": 12,
"ethnicityName": "Chhetri",
"motherTongueId": 4,
"motherTongueName": "Nepali",
"permanentAddress": {
"provinceName": "Bagmati",
"districtName": "Kathmandu",
"municipalityName": "Kathmandu",
"wardNo": 5,
"tole": "Baluwatar",
"houseNo": "12"
},
"currentAddress": { "provinceName": null },
"bio": null
},
"student": {
"admissionNumber": "STU-2099-0001",
"studentId": "SID-2099-0001",
"admissionDate": "2026-04-15",
"recordStatus": "active",
"transportMode": "school_bus",
"interestsHobbies": null,
"guardians": [
{ "fullName": "Chandra Rai", "relationship": "mother", "isPrimary": true }
]
},
"staff": null,
"guardian": null
}
}Every identifier above is synthetic.
Block nullability
person is always present. The other three are null unless the caller holds a live
row in that table, and they are independent rather than exclusive — a teacher whose child
attends the school receives both staff and guardian.
| Block | Present when |
|---|---|
person | always |
student | a live students row exists for actor.id |
staff | a live staff row exists |
guardian | a live guardians row exists |
staff
{
"employeeCode": "EMP-2099-0001",
"joiningDate": "2026-04-15",
"employmentStatus": "active",
"qualification": "M.Ed",
"experienceYears": 6,
"department": { "publicId": "0198…", "name": "Science" },
"designation": { "publicId": "0198…", "name": "Senior Teacher", "isTeaching": true }
}department and designation are keyed by publicId, never the integer primary key,
and each is independently nullable — a department without a designation is legal; the
reverse is refused by staff_designation_needs_department.
guardian
{
"kind": "person",
"organizationName": null,
"occupation": "Engineer",
"children": [
{ "fullName": "Deepa Rai", "relationship": "mother", "isPrimary": true }
]
}kind is person | organization. The father | mother | local_guardian values belong
to relationship, which is a different enum on a different table
(student_guardian.relationship). Conflating the two is easy and was caught in review.
Fields this endpoint never returns
| Absent | Read it at | Permission |
|---|---|---|
| salary, allowances, total, bank, branch, PAN, citizenship, SSF, CIT | GET /staff/:id/salary | StaffSalary_READ |
| medical conditions, allergies, special needs | GET /students/:id/medical | StudentMedical_READ |
These are omitted from the DTO entirely rather than nulled, so a consumer cannot render them by mistake. See the feature page for why this is not a self-only carve-out.
Errors
| Status | errorCode | Meaning |
|---|---|---|
| 401 | — | No session |
| 401 | AUTH_ACTIVE_ROLE_REQUIRED | The caller holds roles but the session has not been pointed at one. Thrown by @CurrentAdmin(); the panel redirects to /dashboard/select-role |
There is no 404. The caller's own users row is not filtered on deleted_at —
jwt.strategy.ts already refuses a session whose user is soft-deleted, so filtering again
would be the only way to produce a 404 for a caller who demonstrably exists. This also
matters on the consumer side: adminApiGet calls Next's notFound() on any 404 GET, which
would render the app's not-found page rather than an in-page error.