{"openapi":"3.1.0","info":{"title":"Mekteb API","version":"1.0.0","description":"The Mekteb mosque education & community platform API. This is the contract served by the web app at `/api/v1/**` and consumed by the native mobile apps and home-screen widgets.\n\n## Authentication\nAlmost every `/api/v1` endpoint requires `Authorization: Bearer <accessToken>`. Tokens are issued by `POST /api/v1/auth/sign-in` (and refreshed via `POST /api/v1/auth/refresh`). The token resolves the caller to a role — `mosque_admin`, `examiner`, `teacher`, `parent`, `student` — and every guard derives the tenant (`mosque_id`) from that role's active membership. Users flagged `must_rotate_password` are rejected by all strict guards (401) until they rotate via `POST /api/v1/auth/change-password`.\n\nCron endpoints (`/api/notifications/send-emails`, `/api/notifications/send-push`, `/api/reports/send-cards`) authenticate with `Authorization: Bearer <CRON_SECRET>`; `/api/notifications/send-push` additionally accepts `PUSH_WEBHOOK_SECRET` so the database trigger can call it. The Stripe webhook authenticates via the `stripe-signature` header.\n\n## Response envelope\nSuccess: `{ \"ok\": true, \"data\": ... }` · Failure: `{ \"ok\": false, \"error\": \"...\", \"code\": \"...\" }`. `code` is a stable machine-readable key; `error` is already sanitised and safe to display. Note: the internal endpoints under `/api/` (webhooks, cron, checkout, health, dev) deliberately use their own bare response shapes.\n\n## Rate limiting\nOnly the auth and account endpoints are rate-limited (`429 rate_limited` with a `Retry-After` header): sign-in (10 / 15 min per account), refresh (60 / 60 s), change-password (5 / 15 min), forgot-password (5 / 60 min per address + 20 / IP), reset-password (10 / 60 min), account delete-request and export (3 / 24 h each)."},"servers":[{"url":"https://{slug}.mekteb.de","description":"Production (mosque subdomain)","variables":{"slug":{"default":"app","description":"Mosque slug (subdomain)"}}},{"url":"https://mekteb.de","description":"Production (root domain)"},{"url":"http://localhost:3000","description":"Local development"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Authentication","description":"Sign in, refresh, sign out, password rotation and recovery, TOTP MFA."},{"name":"Account","description":"The caller's own profile and GDPR self-service (export, deletion request)."},{"name":"Devices","description":"Push-notification device token registration for the native apps."},{"name":"Admin · Groups","description":"Classes: creation, enrolment and teacher assignment."},{"name":"Admin · Students","description":"Student records, logins, parent links and per-student lesson toggles."},{"name":"Admin · Teachers","description":"Teacher records and their accounts."},{"name":"Admin · Parents","description":"Parent records and their accounts."},{"name":"Admin · People","description":"Cross-role people lookup and bulk account creation."},{"name":"Admin · Lessons","description":"Lesson content and attached resources."},{"name":"Admin · Topics","description":"Curriculum topics and their ordering."},{"name":"Admin · Announcements","description":"Mosque-wide announcements: drafting and publishing."},{"name":"Admin · Enrollment","description":"Public enrolment requests awaiting a decision."},{"name":"Admin · Exams","description":"Mosque-wide view of scheduled and completed exams."},{"name":"Admin · Settings","description":"Mosque profile, branding, logo, prayer times and plugin toggles."},{"name":"Admin · Security","description":"The tenant audit log."},{"name":"Admin · GDPR","description":"Handling data-subject requests, including executing deletions."},{"name":"Teacher","description":"A teacher's own groups, students, notes and announcements."},{"name":"Parent","description":"A parent's children and homework acknowledgements."},{"name":"Student","description":"The signed-in student's own profile, groups, attendance, homework and exams."},{"name":"Student · Lessons","description":"Lesson listings and content for the signed-in student."},{"name":"Exams · Requests","description":"Students requesting an exam; staff triaging the queue."},{"name":"Exams · Scheduling","description":"Confirming, scheduling and rescheduling exam sittings."},{"name":"Exams · Sessions","description":"Running an exam sitting and recording lesson checks."},{"name":"Exams · Written tests","description":"Token-addressed written tests and their submissions."},{"name":"Exams · Question bank","description":"The examiner's pool of questions."},{"name":"Messaging","description":"Threads between staff, parents and students."},{"name":"Notifications","description":"The caller's notification feed and read state."},{"name":"Calendar","description":"Lesson sessions and other dated events."},{"name":"Attendance","description":"Per-group attendance registers."},{"name":"Check-in","description":"Token-addressed self check-in, used by the kiosk flow."},{"name":"Homework","description":"Assigning and editing homework for a group."},{"name":"Announcements","description":"Announcements as read by members."},{"name":"Prayer times","description":"The mosque's prayer schedule."},{"name":"Quran","description":"Saved ayahs for the signed-in user."},{"name":"Report","description":"Report-card generation."},{"name":"Widget","description":"The compact payload behind the home-screen widgets."},{"name":"Internal","description":"Not part of the public contract: health, billing, cron jobs, webhooks and this document. These use their own bare response shapes, not the `{ ok, data }` envelope."}],"paths":{"/api/v1/auth/sign-in":{"post":{"tags":["Authentication"],"summary":"Sign in with an email or mosque-qualified username","description":"Authenticates an admin, teacher, examiner or parent by email, or a student by mosque-qualified username (`al-nour.amina`). Returns a fresh session plus the caller's role, the full set of roles, mosque, plugins and profile — everything `/auth/me` would return — so a mobile client can skip the follow-up call. Anything containing `@` is treated as an email; `slug.username` resolves to the student's derived address; a bare username needs `mosqueSlug` (from the mosque subdomain). Unknown slugs deliberately produce normal invalid-credentials rather than a mosque lookup, to avoid enumeration.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"login":{"type":"string","description":"Email or username. Either `login` or `email` is required."},"email":{"type":"string","description":"Deprecated alias for `login`."},"password":{"type":"string","minLength":1},"mosqueSlug":{"type":"string","maxLength":64,"description":"Lets a subdomain caller send a bare username."}},"required":["password"]}}}},"responses":{"200":{"description":"Signed in.","content":{"application/json":{"schema":{"type":"object","required":["ok","data"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"type":"integer"},"expiresAt":{"type":"integer"},"tokenType":{"type":"string","example":"bearer"},"mustRotatePassword":{"type":"boolean"},"mfaRequired":{"type":"boolean","description":"True when the user has a verified TOTP factor; follow up with /auth/mfa/challenge + /auth/mfa/verify."},"role":{"$ref":"#/components/schemas/UserRole"},"userId":{"type":"string","format":"uuid"},"email":{"type":"string"},"roles":{"type":"array","items":{"type":"string"},"description":"Every active role, e.g. [\"teacher\",\"examiner\"]."},"mosqueId":{"type":"string","format":"uuid","nullable":true},"mosqueName":{"type":"string","nullable":true},"plugins":{"type":"array","items":{"type":"string"},"description":"Active plugin ids for the mosque."},"profile":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true}}}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"description":"Invalid credentials. `code: invalid_credentials`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authenticated but has no active membership (`role: none`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Exchange a refresh token for a new session","description":"The refresh token is the credential and travels in the body. Rate-limited to 60 requests / 60 s (global bucket).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"refreshToken":{"type":"string","minLength":10}},"required":["refreshToken"]}}}},"responses":{"200":{"description":"New session.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"type":"integer"},"expiresAt":{"type":"integer"},"tokenType":{"type":"string"},"userId":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"description":"Invalid refresh token. `code: invalid_refresh_token`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/auth/sign-out":{"post":{"tags":["Authentication"],"summary":"Revoke all sessions","description":"Invalidates every refresh token for the user (global scope) and records a logout audit entry.","responses":{"200":{"description":"Signed out.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"signedOut":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/auth/me":{"get":{"tags":["Authentication"],"summary":"Current user, roles, mosque and plugins","description":"Returns the resolved landing role, the full set of roles (a user can be teacher and examiner at once), the active mosque, and the mosque's enabled plugins. Works even when `mustRotatePassword` is true so the client can show the forced-rotation screen.","responses":{"200":{"description":"Profile.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"email":{"type":"string"},"mustRotatePassword":{"type":"boolean"},"role":{"$ref":"#/components/schemas/UserRole"},"roles":{"type":"array","items":{"type":"string"},"description":"Every active role, e.g. [\"teacher\",\"examiner\"]."},"mosqueId":{"type":"string","format":"uuid","nullable":true},"mosqueName":{"type":"string","nullable":true},"plugins":{"type":"array","items":{"type":"string"},"description":"Active plugin ids for the mosque."},"profile":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/auth/change-password":{"post":{"tags":["Authentication"],"summary":"Rotate the current password","description":"Re-authenticates with `current_password`, updates the password via the service role, clears `must_rotate_password`, and activates the newest pending OTP. This is the endpoint a forced-rotation user is allowed to call. Rate-limited to 5 / 15 min per user.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"current_password":{"type":"string","minLength":1},"new_password":{"type":"string","minLength":8},"confirm_password":{"type":"string","minLength":1}},"required":["current_password","new_password","confirm_password"]}}}},"responses":{"200":{"description":"Password rotated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"mustRotatePassword":{"type":"boolean","enum":[false]},"role":{"$ref":"#/components/schemas/UserRole"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"description":"Not authenticated or current password incorrect (`code: invalid_credentials`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/auth/forgot-password":{"post":{"tags":["Authentication"],"summary":"Request a password-recovery email","description":"Always returns `{ sent: true }` (even for unknown addresses) to prevent email enumeration. Recovery links are generated by Supabase; `redirectTo` is honoured only when it matches the configured allowlist or the site URL. Rate-limited to 5 / 60 min per address plus 20 / IP.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"redirectTo":{"type":"string","format":"uri"},"client":{"type":"string","enum":["web","ios","android"]},"locale":{"$ref":"#/components/schemas/Locale"}},"required":["email"]}}}},"responses":{"200":{"description":"Recovery email sent (or deliberately faked).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"sent":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/auth/reset-password":{"post":{"tags":["Authentication"],"summary":"Complete a password reset with a recovery token","description":"Verifies the `token_hash` from the recovery deep link, sets the new password, clears `must_rotate_password`, activates the pending OTP, and returns a fresh session. Rate-limited to 10 / 60 min.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tokenHash":{"type":"string","minLength":10},"type":{"type":"string","enum":["recovery","email","invite"],"default":"recovery"},"newPassword":{"type":"string","minLength":8}},"required":["tokenHash","newPassword"]}}}},"responses":{"200":{"description":"Password reset and signed in.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"type":"integer"},"expiresAt":{"type":"integer"},"tokenType":{"type":"string"},"mustRotatePassword":{"type":"boolean","enum":[false]},"role":{"$ref":"#/components/schemas/UserRole"},"userId":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"description":"Invalid or expired token. `code: invalid_token`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/auth/mfa/challenge":{"post":{"tags":["Authentication"],"summary":"Create a TOTP MFA challenge","description":"Creates a challenge for the user's first verified TOTP factor. Call after `sign-in` reported `mfaRequired: true`. Responds with the factor and challenge ids used by `/auth/mfa/verify`.","responses":{"200":{"description":"Challenge created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"factorId":{"type":"string"},"challengeId":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/auth/mfa/verify":{"post":{"tags":["Authentication"],"summary":"Verify an MFA code and complete the session","description":"Verifies the 6-digit code against the challenge created by `/auth/mfa/challenge` and returns the completed session plus the user's role. Uses the pending sign-in token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"factorId":{"type":"string"},"challengeId":{"type":"string"},"code":{"type":"string","minLength":6,"maxLength":6}},"required":["factorId","challengeId","code"]}}}},"responses":{"200":{"description":"MFA verified.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"type":"integer"},"expiresAt":{"type":"integer"},"role":{"$ref":"#/components/schemas/UserRole"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/account/profile":{"patch":{"tags":["Account"],"summary":"Update the caller's own profile","description":"Partial update of `fullName`, `displayName` and `phone`. Only explicitly-provided fields are changed; an empty `phone` is coerced to `null`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"fullName":{"type":"string","minLength":1,"maxLength":120},"displayName":{"type":"string","minLength":1,"maxLength":120},"phone":{"type":"string","maxLength":40,"nullable":true}}}}}},"responses":{"200":{"description":"Updated profile row.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"full_name":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/account/gdpr-requests":{"get":{"tags":["Account"],"summary":"List the caller's own GDPR requests","description":"Returns the caller's export and deletion requests, newest first (max 50). RLS scopes to `auth.uid()`.","responses":{"200":{"description":"List of requests.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"requests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["deletion","export"]},"status":{"type":"string","enum":["pending","processing","sent","completed","rejected","failed"]},"reason":{"type":"string","nullable":true},"requested_at":{"type":"string","format":"date-time"},"processed_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/account/delete-request":{"post":{"tags":["Account"],"summary":"Submit an account-deletion request","description":"GDPR deletion request. Nothing is deleted here — the mosque admin processes it (see `/api/v1/admin/gdpr-requests/{id}/execute-deletion`). Mosque admins cannot request deletion for themselves. Rate-limited to 3 / 24 h.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Request created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"requested":{"type":"boolean","enum":[true]},"requestId":{"type":"string","format":"uuid"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Mosque admins cannot request deletion. `code: admin_forbidden`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A deletion request is already pending. `code: already_pending`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/account/export":{"post":{"tags":["Account"],"summary":"Request a GDPR data export","description":"Builds the caller's full data export, renders it to HTML and emails it as `mekteb-export.html`. The request row is visible immediately with `status: processing`. Rate-limited to 3 / 24 h.","responses":{"200":{"description":"Export requested.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"requested":{"type":"boolean","enum":[true]},"requestId":{"type":"string","format":"uuid"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/devices":{"post":{"tags":["Devices"],"summary":"Register a push-token device","description":"Registers or refreshes an APNs / FCM / Web push token for the caller. Idempotent on `(user_id, token)`; call on every launch.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"string","enum":["ios","android","web"]},"token":{"type":"string","minLength":10,"maxLength":512},"bundleId":{"type":"string","maxLength":256},"appVersion":{"type":"string","maxLength":64},"deviceModel":{"type":"string","maxLength":128},"locale":{"type":"string","maxLength":16}},"required":["platform","token"]}}}},"responses":{"200":{"description":"Registered.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"registered":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"delete":{"tags":["Devices"],"summary":"Remove a push-token device","description":"Deletes the device token, typically on logout.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":10,"maxLength":512}},"required":["token"]}}}},"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/config":{"get":{"tags":["Devices"],"summary":"Realtime configuration","description":"Returns the Supabase project URL and anon key so the client can open a Realtime websocket directly. The one sanctioned direct-Supabase call, because a websocket cannot be proxied. The URL is rewritten for LAN devices in local dev.","responses":{"200":{"description":"Realtime config.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"supabaseUrl":{"type":"string","format":"uri"},"supabaseAnonKey":{"type":"string","description":"Public by design; RLS enforces everything."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"Realtime not configured. `code: no_realtime`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/groups":{"get":{"tags":["Admin · Groups"],"summary":"List groups","description":"All groups in the caller's mosque with active-student and teacher counts.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Groups.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"student_count":{"type":"integer","description":"Active enrollments."},"teacher_count":{"type":"integer"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Groups"],"summary":"Create a group","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}},"required":["name"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"student_count":{"type":"integer","description":"Active enrollments."},"teacher_count":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/groups/{id}":{"get":{"tags":["Admin · Groups"],"summary":"Get a group","description":"One group with its active enrollments and teacher links, names resolved.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Group detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"enrollments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"studentProfileId":{"type":"string","format":"uuid"},"studentName":{"type":"string"},"enrolledAt":{"type":"string"}}}},"teacherLinks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"teacherProfileId":{"type":"string","format":"uuid"},"teacherName":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Admin · Groups"],"summary":"Delete a group","description":"Permanently deletes the group row (scoped to the mosque).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/groups/{id}/candidates":{"get":{"tags":["Admin · Groups"],"summary":"Enrollable students and assignable teachers","description":"Active students not yet enrolled in the group and active teachers not yet linked, for pickers.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Candidates.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"students":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"}}}},"teachers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/admin/groups/{id}/enroll":{"post":{"tags":["Admin · Groups"],"summary":"Bulk-enroll students","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"student_profile_ids":{"type":"array","items":{"type":"string"}}},"required":["student_profile_ids"]}}}},"responses":{"200":{"description":"Enrolled.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"enrolled":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/groups/{id}/enroll/{eid}":{"delete":{"tags":["Admin · Groups"],"summary":"Soft-unenroll a student","description":"Deactivates one enrollment row.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"eid","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Unenrolled.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"unenrolled":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/groups/{id}/teachers":{"post":{"tags":["Admin · Groups"],"summary":"Bulk-assign teachers","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"teacher_profile_ids":{"type":"array","items":{"type":"string"}}},"required":["teacher_profile_ids"]}}}},"responses":{"200":{"description":"Assigned.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"assigned":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/groups/{id}/teachers/{lid}":{"delete":{"tags":["Admin · Groups"],"summary":"Soft-unassign a teacher","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"lid","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Unassigned.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"unassigned":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/students":{"get":{"tags":["Admin · Students"],"summary":"List students","description":"Paginated, searchable student list with a map of each student's linked parent display names.","security":[{"bearerAuth":[]}],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":200},"description":"Case-insensitive substring filter on full name."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":100}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Students.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"students":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"date_of_birth":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}},"parentsByStudent":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"student id -> parent display names."},"pagination":{"$ref":"#/components/schemas/PaginationOffset"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Students"],"summary":"Create a student (no login)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string","minLength":1},"date_of_birth":{"type":"string","nullable":true}},"required":["full_name"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"date_of_birth":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/students/with-login":{"post":{"tags":["Admin · Students"],"summary":"Create a student with a login account","description":"Provisions the auth user, membership, student profile, and a pending OTP. Returns the one-time temporary password.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"full_name":{"type":"string","minLength":1},"date_of_birth":{"type":"string","nullable":true}},"required":["email","full_name"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/TempPassword"}}}}}},"400":{"description":"Validation or duplicate email (`code: account_email_exists`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/students/{id}":{"get":{"tags":["Admin · Students"],"summary":"Get a student","description":"Profile, active enrollments, parent links, and completed lesson ids.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Student detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"student":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"date_of_birth":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"notes":{"type":"string","nullable":true},"profile_id":{"type":"string","format":"uuid","nullable":true}}},"enrollments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"groups":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}},"parentLinks":{"type":"array","items":{"type":"object"}},"completions":{"type":"array","items":{"type":"string"},"description":"Completed lesson ids."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Admin · Students"],"summary":"Update a student","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string","minLength":1},"date_of_birth":{"type":"string","nullable":true},"is_active":{"type":"boolean"}},"required":["full_name"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"is_active":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Admin · Students"],"summary":"Delete a student","description":"`soft` deactivates; `hard` permanently erases the auth user and student row.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","enum":["soft","hard"],"default":"soft"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"mode":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/admin/students/{id}/link-parent":{"post":{"tags":["Admin · Students"],"summary":"Link a parent to a student","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"parent_profile_id":{"type":"string","format":"uuid"}},"required":["parent_profile_id"]}}}},"responses":{"200":{"description":"Linked.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"linked":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/students/{id}/unlink-parent":{"post":{"tags":["Admin · Students"],"summary":"Unlink a parent by link id","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"link_id":{"type":"string","format":"uuid"}},"required":["link_id"]}}}},"responses":{"200":{"description":"Unlinked.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"unlinked":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/students/{id}/reset-password":{"post":{"tags":["Admin · Students"],"summary":"Issue a temporary password","description":"Rotates a student's password to a fresh OTP and revokes any pending OTPs. Returns the one-time password.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Temporary password issued.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"tempPassword":{"type":"string","description":"XXXX-XXXX-XXXX form."},"expires_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/students/{id}/toggle-lesson":{"post":{"tags":["Admin · Students"],"summary":"Mark a lesson complete or not","description":"`completed: true` upserts a lesson completion; any other value removes it.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lesson_id":{"type":"string","format":"uuid"},"completed":{"type":"boolean"}},"required":["lesson_id"]}}}},"responses":{"200":{"description":"Toggled.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"lesson_id":{"type":"string"},"completed":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/teachers":{"get":{"tags":["Admin · Teachers"],"summary":"List teachers","description":"All teachers in the mosque, optionally filtered by name (client-side filter).","security":[{"bearerAuth":[]}],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Teachers.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bio":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Teachers"],"summary":"Create a teacher with a login account","description":"Provisions auth user, membership, teacher profile and OTP. Returns the one-time password.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"full_name":{"type":"string","minLength":1},"phone":{"type":"string","nullable":true},"bio":{"type":"string","nullable":true}},"required":["email","full_name"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/TempPassword"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/teachers/{id}":{"get":{"tags":["Admin · Teachers"],"summary":"Get a teacher","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Teacher detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bio":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"profiles":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"email":{"type":"string"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Admin · Teachers"],"summary":"Update a teacher","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string","minLength":1},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"bio":{"type":"string","nullable":true}},"required":["full_name"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Admin · Teachers"],"summary":"Delete a teacher","description":"Permanently erases the linked auth user (cascade removes the teacher profile).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/admin/parents":{"get":{"tags":["Admin · Parents"],"summary":"List parents","description":"All parents in the mosque with their linked children's names.","security":[{"bearerAuth":[]}],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Parents.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"relation":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true}}},"children":{"type":"array","items":{"type":"string"},"description":"Linked students' full names."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Parents"],"summary":"Create a parent with a login account","description":"Provisions auth user, membership, parent profile and OTP. Returns the one-time password.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"full_name":{"type":"string","minLength":1},"phone":{"type":"string","nullable":true},"relation":{"type":"string","nullable":true,"description":"e.g. mother / father / guardian"}},"required":["email","full_name"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/TempPassword"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/parents/{id}":{"get":{"tags":["Admin · Parents"],"summary":"Get a parent","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Parent detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"relation":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"profiles":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"email":{"type":"string"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Admin · Parents"],"summary":"Update a parent","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string","minLength":1},"display_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"relation":{"type":"string","nullable":true}},"required":["full_name"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Admin · Parents"],"summary":"Delete a parent","description":"Permanently erases the linked auth user (cascade removes the parent profile).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/admin/lessons":{"get":{"tags":["Admin · Lessons"],"summary":"List lessons","description":"All lessons for the mosque. Open to admins and (fallback) teachers.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Lessons.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"topic_id":{"type":"string","format":"uuid","nullable":true},"sort_order":{"type":"integer"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Lessons"],"summary":"Create a lesson","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"$ref":"#/components/schemas/BlockNoteJson"},"topic_id":{"type":"string","format":"uuid","nullable":true}},"required":["title"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/lessons/{id}":{"get":{"tags":["Admin · Lessons"],"summary":"Get a lesson with resources","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Lesson detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"$ref":"#/components/schemas/BlockNoteJson"},"topic_id":{"type":"string","format":"uuid","nullable":true},"sort_order":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"resources":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"storage_path":{"type":"string"},"mime_type":{"type":"string","nullable":true},"size_bytes":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Admin · Lessons"],"summary":"Update a lesson","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"$ref":"#/components/schemas/BlockNoteJson"},"topic_id":{"type":"string","format":"uuid","nullable":true}},"required":["title"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"delete":{"tags":["Admin · Lessons"],"summary":"Delete a lesson","description":"Removes the lesson, its resource files from storage, and related rows.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/lessons/{id}/resources":{"post":{"tags":["Admin · Lessons"],"summary":"Upload a lesson resource","description":"Multipart upload to the `lesson-resources` bucket. Allowed MIME types: PDF, Office documents, images (png/jpeg/webp/gif), plain text/markdown/csv, audio (mpeg/mp4/m4a/wav/ogg), video (mp4/webm/quicktime), ZIP. SVG and HTML are rejected.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"title":{"type":"string"},"file":{"type":"string","format":"binary"}},"required":["title","file"]}}}},"responses":{"201":{"description":"Uploaded.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/lessons/{id}/resources/{rid}":{"delete":{"tags":["Admin · Lessons"],"summary":"Delete a lesson resource","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"rid","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/topics":{"get":{"tags":["Admin · Topics"],"summary":"List topics","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Topics.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"sort_order":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Topics"],"summary":"Create a topic","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"description":{"type":"string"}},"required":["title"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/topics/{id}":{"put":{"tags":["Admin · Topics"],"summary":"Update a topic and its translations","description":"`translations` keys are locales (de/en/bs/tr); a `null` value deletes that locale's translation.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"description":{"type":"string","nullable":true},"translations":{"type":"object","additionalProperties":{"anyOf":[{"type":"null"},{"type":"object","properties":{"title":{"type":"string","minLength":1},"description":{"type":"string","nullable":true}},"required":["title"]}]}}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Admin · Topics"],"summary":"Delete a topic","description":"Deletes the topic row. Child lessons are not cascaded or blocked.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/topics/reorder":{"put":{"tags":["Admin · Topics"],"summary":"Reorder topics","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"itemIds":{"type":"array","items":{"type":"string"}}},"required":["itemIds"]}}}},"responses":{"200":{"description":"Reordered.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/settings/branding":{"put":{"tags":["Admin · Settings"],"summary":"Save brand colors","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"primary_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","nullable":true},"secondary_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","nullable":true}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/settings/info":{"put":{"tags":["Admin · Settings"],"summary":"Update mosque info","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"timezone":{"type":"string"},"locale":{"$ref":"#/components/schemas/Locale"}},"required":["name"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/settings/logo":{"post":{"tags":["Admin · Settings"],"summary":"Upload the mosque logo","description":"Multipart upload to the `mosque-logos` bucket (replaces existing). PNG/JPEG/WebP only, max 2 MB. SVG rejected (stored-XSS).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"logo":{"type":"string","format":"binary"}},"required":["logo"]}}}},"responses":{"200":{"description":"Uploaded.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"logo_url":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/settings/plugins":{"get":{"tags":["Admin · Settings"],"summary":"List plugins with mosque state","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Plugins.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"plugins":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"category":{"type":"string","nullable":true},"is_active":{"type":"boolean"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Admin · Settings"],"summary":"Toggle a plugin","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plugin_id":{"type":"string","minLength":1},"is_active":{"type":"boolean"}},"required":["plugin_id","is_active"]}}}},"responses":{"200":{"description":"Toggled.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"plugin_id":{"type":"string"},"is_active":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/settings/prayer":{"get":{"tags":["Admin · Settings"],"summary":"Get prayer settings","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Prayer settings.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"prayer_location":{"type":"string","nullable":true},"prayer_method":{"type":"string","nullable":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Admin · Settings"],"summary":"Update prayer settings","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prayer_location":{"type":"string","minLength":1},"prayer_method":{"type":"string","enum":["MWL","ISNA","Egypt","Makkah","Karachi","Tehran","Jafari"],"default":"MWL"}},"required":["prayer_location"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"prayer_location":{"type":"string"},"prayer_method":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/announcements":{"get":{"tags":["Admin · Announcements"],"summary":"List announcements","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Announcements.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"audience":{"type":"string","enum":["mosque","group"]},"group_id":{"type":"string","format":"uuid","nullable":true},"is_published":{"type":"boolean"},"published_at":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Admin · Announcements"],"summary":"Create an announcement","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string","minLength":1},"audience":{"type":"string","enum":["mosque","group"],"default":"mosque"},"group_id":{"type":"string","format":"uuid","description":"Required when audience is `group`."},"publish":{"type":"boolean","default":false}},"required":["title","body"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/announcements/{id}":{"delete":{"tags":["Admin · Announcements"],"summary":"Delete an announcement","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/announcements/{id}/publish":{"put":{"tags":["Admin · Announcements"],"summary":"Publish an announcement","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Published.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/audit":{"get":{"tags":["Admin · Security"],"summary":"List audit logs","description":"Paginated (50/page) audit log for the mosque with actor-name resolution.","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1}},{"name":"action","in":"query","required":false,"schema":{"type":"string"},"description":"Case-insensitive substring filter."}],"responses":{"200":{"description":"Audit log.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"logs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"action":{"type":"string"},"actor_user_id":{"type":"string","nullable":true},"target_table":{"type":"string","nullable":true},"target_id":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"page":{"type":"integer"},"totalPages":{"type":"integer"},"total":{"type":"integer"},"actors":{"type":"object","additionalProperties":{"type":"string"},"description":"actor_user_id -> display name."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/enrollment-requests":{"get":{"tags":["Admin · Enrollment"],"summary":"List enrollment-request queue","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","approved","rejected"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":100}}],"responses":{"200":{"description":"Requests.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"requests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parent_name":{"type":"string"},"parent_email":{"type":"string"},"parent_phone":{"type":"string","nullable":true},"child_name":{"type":"string"},"child_birth_year":{"type":"string","nullable":true},"message":{"type":"string","nullable":true},"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}},"pendingCount":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/enrollment-requests/{id}":{"post":{"tags":["Admin · Enrollment"],"summary":"Approve, reject or delete an enrollment request","description":"Approving sets the status only — no account is provisioned from an approved request.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["approve","reject","delete"]}},"required":["action"]}}}},"responses":{"200":{"description":"Processed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"status":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/admin/exams":{"get":{"tags":["Admin · Exams"],"summary":"List exam sessions and pending requests","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Exact match on session status."},{"name":"group","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Exact match on from_group_id."}],"responses":{"200":{"description":"Sessions and requests.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"diploma_generated_at":{"type":"string","nullable":true},"exam_request_id":{"type":"string","nullable":true},"examiner_profile_id":{"type":"string","nullable":true},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"teacher_profiles":{"type":"object","nullable":true,"properties":{"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}}}}}}},"pendingRequests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"notes":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}},"teacher_profiles":{"type":"object","nullable":true,"properties":{"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}}}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/gdpr-requests":{"get":{"tags":["Admin · GDPR"],"summary":"List mosque GDPR requests","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","processing","sent","completed","rejected","failed"]}},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["export","deletion"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":100}}],"responses":{"200":{"description":"Requests.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"requests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"type":{"type":"string"},"status":{"type":"string"},"reason":{"type":"string","nullable":true},"requested_at":{"type":"string"},"processed_at":{"type":"string","nullable":true},"processed_by":{"type":"string","nullable":true}}}}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/gdpr-requests/{id}":{"patch":{"tags":["Admin · GDPR"],"summary":"Mark a GDPR request completed or rejected","description":"Status-only update; does not itself delete data (see execute-deletion).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["completed","rejected"]},"note":{"type":"string","maxLength":2000}},"required":["status"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/admin/gdpr-requests/{id}/execute-deletion":{"post":{"tags":["Admin · GDPR"],"summary":"Execute a deletion request","description":"Scrubs residual PII, hard-deletes the auth user (cascading), and writes a PII-free tombstone + audit row. Cannot delete your own account.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Cannot delete your own account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Not a deletion request or already completed. Codes: `wrong_type`, `already_done`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/people":{"post":{"tags":["Admin · People"],"summary":"Create a teacher, parent or student account","description":"One-shot account provisioning for any role. Teachers/parents need `email`; students need `username` (used to derive their login email). Enforces the plan's student limit. Returns the one-time temporary password.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["teacher","parent","student"]},"full_name":{"type":"string","minLength":1},"email":{"type":"string","format":"email","description":"Required for teacher/parent."},"username":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$","description":"Required for student; mosque-qualified as slug.username."},"phone":{"type":"string"},"bio":{"type":"string","description":"Teacher only."},"relation":{"type":"string","description":"Parent only."},"date_of_birth":{"type":"string","description":"Student only."}},"required":["role","full_name"]}}}},"responses":{"200":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"email":{"type":"string"},"full_name":{"type":"string"},"tempPassword":{"type":"string"},"expires_at":{"type":"string","format":"date-time"},"username":{"type":"string","description":"Student only: the mosque-qualified login id."}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Mosque not found (student path). `code: mosque_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/teacher/announcements":{"get":{"tags":["Teacher"],"summary":"List own announcements","description":"The calling teacher's announcements, newest first (max 100).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Announcements.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"audience":{"type":"string","enum":["mosque","group"]},"group_id":{"type":"string","nullable":true},"is_published":{"type":"boolean"},"published_at":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Teacher"],"summary":"Create and publish an announcement","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string","minLength":1},"audience":{"type":"string","enum":["mosque","group"],"default":"mosque"},"group_id":{"type":"string","format":"uuid","description":"Required when audience is `group`."}},"required":["title","body"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"created":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/teacher/announcements/{id}":{"put":{"tags":["Teacher"],"summary":"Update an own announcement","description":"Scoped to the caller's own author id.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string","minLength":1},"audience":{"type":"string","enum":["mosque","group"],"default":"mosque"},"group_id":{"type":"string","format":"uuid"},"is_published":{"type":"boolean","default":true}},"required":["title","body"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"delete":{"tags":["Teacher"],"summary":"Delete an own announcement","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/teacher/groups":{"get":{"tags":["Teacher"],"summary":"List own groups","description":"Groups the calling teacher is linked to, with student/teacher counts.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Groups.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"student_count":{"type":"integer"},"teacher_count":{"type":"integer"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/teacher/groups/{id}":{"get":{"tags":["Teacher"],"summary":"Get a group the teacher teaches","description":"Requires an active teacher-group link (403 otherwise).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Group detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"enrollments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"studentProfileId":{"type":"string","format":"uuid"},"studentName":{"type":"string"},"enrolledAt":{"type":"string"}}}},"teacherLinks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"teacherProfileId":{"type":"string","format":"uuid"},"teacherName":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/teacher/groups/{id}/parents":{"post":{"tags":["Teacher"],"summary":"Create a parent and link to an enrolled student","description":"Provisions a parent account, links them to the student, and returns the one-time password. Requires an active teacher-group link for the group and the student must be enrolled in it.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string","minLength":1},"email":{"type":"string","format":"email"},"phone":{"type":"string"},"relation":{"type":"string"},"student_profile_id":{"type":"string","format":"uuid"}},"required":["full_name","email","student_profile_id"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"email":{"type":"string"},"full_name":{"type":"string"},"tempPassword":{"type":"string"},"expires_at":{"type":"string","format":"date-time"},"parent_profile_id":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/teacher/groups/{id}/students":{"post":{"tags":["Teacher"],"summary":"Create a student and enroll them in the group","description":"Provisions a student account, enrolls them, and returns the one-time password.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string","minLength":1},"email":{"type":"string","format":"email"},"date_of_birth":{"type":"string"}},"required":["full_name","email"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"email":{"type":"string"},"full_name":{"type":"string"},"tempPassword":{"type":"string"},"expires_at":{"type":"string","format":"date-time"},"student_profile_id":{"type":"string","format":"uuid"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/teacher/notes":{"get":{"tags":["Teacher"],"summary":"List own progress notes","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Notes.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"group_id":{"type":"string"},"student_profile_id":{"type":"string"},"body":{"type":"string"},"visible_to_parents":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Teacher"],"summary":"Create a progress note","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"group_id":{"type":"string","format":"uuid"},"student_profile_id":{"type":"string","format":"uuid"},"body":{"type":"string","minLength":1},"visible_to_parents":{"type":"boolean","default":false}},"required":["group_id","student_profile_id","body"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"created":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/teacher/notes/{id}":{"put":{"tags":["Teacher"],"summary":"Update an own progress note","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1},"visible_to_parents":{"type":"boolean","default":false}},"required":["body"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"delete":{"tags":["Teacher"],"summary":"Delete an own progress note","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/teacher/students/{id}":{"get":{"tags":["Teacher"],"summary":"Get a student in a taught group","description":"Full student view for a teacher: groups, attendance (last 20), progress notes (last 15), homework (last 10), parent contacts, and hifz progress. 404 unless the student is enrolled in a group the teacher actively teaches.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Student detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"student":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"full_name":{"type":"string"},"date_of_birth":{"type":"string","nullable":true},"is_active":{"type":"boolean"}}},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"isHifz":{"type":"boolean"}}}},"attendance":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"sessionDate":{"type":"string","nullable":true},"groupName":{"type":"string","nullable":true}}}},"progressNotes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"visibleToParents":{"type":"boolean"},"createdAt":{"type":"string"},"groupName":{"type":"string","nullable":true}}}},"homework":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"dueDate":{"type":"string","nullable":true},"groupName":{"type":"string","nullable":true}}}},"parents":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","nullable":true},"relation":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true}}}},"hifz":{"type":"array","items":{"type":"object","properties":{"pages":{"type":"number"},"groupName":{"type":"string","nullable":true}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/teacher/weekly-notes":{"get":{"tags":["Teacher"],"summary":"List own weekly notes","description":"Newest week first (max 52).","security":[{"bearerAuth":[]}],"parameters":[{"name":"group_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Weekly notes.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"group_id":{"type":"string"},"week_start":{"type":"string"},"body":{"type":"string"},"is_published":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Teacher"],"summary":"Upsert a weekly note","description":"Upsert keyed on `(group_id, week_start)` — posting the same week replaces the row.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"group_id":{"type":"string","format":"uuid"},"week_start":{"type":"string","description":"YYYY-MM-DD."},"body":{"type":"string","minLength":1},"is_published":{"type":"boolean","default":false}},"required":["group_id","week_start","body"]}}}},"responses":{"200":{"description":"Upserted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"upserted":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/parent/children":{"get":{"tags":["Parent"],"summary":"List linked children","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Children.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"fullName":{"type":"string"},"dateOfBirth":{"type":"string","nullable":true},"isActive":{"type":"boolean"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/parent/children/{id}":{"get":{"tags":["Parent"],"summary":"Get a child's full dashboard","description":"Groups, homework (with acknowledgement state, max 50), attendance (max 60), exams (max 50), parent-visible notes (max 50) and written-test results (max 20). Requires a parent-student link (403 otherwise).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Child dashboard.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"student":{"type":"object","properties":{"id":{"type":"string"},"fullName":{"type":"string"},"dateOfBirth":{"type":"string","nullable":true},"isActive":{"type":"boolean"}}},"groups":{"type":"array","items":{"type":"object","properties":{"enrollmentId":{"type":"string"},"groupId":{"type":"string"},"groupName":{"type":"string"}}}},"homework":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"body":{"type":"string","nullable":true},"dueDate":{"type":"string","nullable":true},"groupId":{"type":"string"},"acknowledgedAt":{"type":"string","nullable":true}}}},"attendance":{"type":"array","items":{"type":"object","properties":{"sessionId":{"type":"string"},"sessionDate":{"type":"string"},"status":{"type":"string"}}}},"exams":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"schedule_status":{"type":"string","nullable":true},"proposed_date":{"type":"string","nullable":true},"proposed_by":{"type":"string","nullable":true}}}},"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"createdAt":{"type":"string"},"groupId":{"type":"string"}}}},"writtenTests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"overallResult":{"type":"string","nullable":true},"examinerNote":{"type":"string","nullable":true},"exam":{"type":"object","nullable":true,"properties":{"status":{"type":"string"},"examDate":{"type":"string","nullable":true}}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/parent/homework/{id}/acknowledge":{"post":{"tags":["Parent"],"summary":"Acknowledge homework for a child","description":"Idempotent upsert of the child's acknowledgement.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"student_profile_id":{"type":"string","format":"uuid"}},"required":["student_profile_id"]}}}},"responses":{"200":{"description":"Acknowledged.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"acknowledged":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/student/me":{"get":{"tags":["Student"],"summary":"Own identity and groups","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Identity.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"studentProfileId":{"type":"string","format":"uuid"},"fullName":{"type":"string"},"groups":{"type":"array","items":{"type":"object","properties":{"enrollmentId":{"type":"string"},"groupId":{"type":"string"},"groupName":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/student/groups":{"get":{"tags":["Student"],"summary":"Enrolled groups","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Groups.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"room":{"type":"string","nullable":true}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/student/groups/{id}":{"get":{"tags":["Student"],"summary":"Get an enrolled group","description":"Group, upcoming sessions (max 5), published weekly notes (max 4), and parent-visible progress notes about this student (max 10).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Group detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"group":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"room":{"type":"string","nullable":true}}},"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string"},"start_time":{"type":"string","nullable":true},"end_time":{"type":"string","nullable":true},"is_cancelled":{"type":"boolean"},"notes":{"type":"string","nullable":true}}}},"weekly":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"week_start":{"type":"string"},"body":{"type":"string"}}}},"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"created_at":{"type":"string"},"visible_to_parents":{"type":"boolean"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/attendance":{"get":{"tags":["Student"],"summary":"Own attendance history","description":"Latest first (max 120 records).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Attendance records.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"sessionId":{"type":"string"},"sessionDate":{"type":"string"},"groupId":{"type":"string"},"groupName":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/student/homework":{"get":{"tags":["Student"],"summary":"Own homework","description":"Group-wide and individually-targeted assignments (max 100), each with the caller's acknowledgement timestamp.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Homework.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"body":{"type":"string","nullable":true},"dueDate":{"type":"string","nullable":true},"groupId":{"type":"string"},"groupName":{"type":"string"},"acknowledgedAt":{"type":"string","nullable":true}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/student/homework/{id}/acknowledge":{"post":{"tags":["Student"],"summary":"Acknowledge own homework","description":"Idempotent upsert; the first acknowledgement timestamp is kept (re-tapping does not move it).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Acknowledged.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"acknowledged":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Homework not found or unpublished.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/student/exams":{"get":{"tags":["Student"],"summary":"Own exam sessions","description":"Includes scheduling fields that decide whose turn it is to propose a date.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Exam sessions.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"schedule_status":{"type":"string"},"proposed_date":{"type":"string","nullable":true},"proposed_by":{"type":"string","nullable":true},"diploma_generated_at":{"type":"string","nullable":true}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/student/lessons":{"get":{"tags":["Student · Lessons"],"summary":"Lesson library","description":"Published topics with their published lessons (titles only). Requires the `lesson_library` plugin. Also open to parents/teachers via the member guard.","security":[{"bearerAuth":[]}],"parameters":[{"name":"locale","in":"query","required":false,"schema":{"$ref":"#/components/schemas/Locale"},"description":"Resolves lesson/topic titles (and bodies) through the translation catalogue; falls back to the default title."}],"responses":{"200":{"description":"Topics.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"sortOrder":{"type":"number"},"lessons":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"sortOrder":{"type":"number"},"updatedAt":{"type":"string"}}}}}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/lessons/{id}":{"get":{"tags":["Student · Lessons"],"summary":"Get a lesson with resources and audio","description":"Lesson body (BlockNote JSON) plus resources and audio behind 1-hour signed URLs (`signedUrlTtlSeconds: 3600`). Cache the resource list, never the URLs — refetch for fresh URLs.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"locale","in":"query","required":false,"schema":{"$ref":"#/components/schemas/Locale"},"description":"Resolves lesson/topic titles (and bodies) through the translation catalogue; falls back to the default title."}],"responses":{"200":{"description":"Lesson detail.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"body":{"$ref":"#/components/schemas/BlockNoteJson"},"updatedAt":{"type":"string","format":"date-time"},"topic":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"title":{"type":"string"}}},"resources":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"signedUrl":{"type":"string","nullable":true}}}},"audio":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"locale":{"type":"string","nullable":true},"title":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"durationSeconds":{"type":"number","nullable":true},"signedUrl":{"type":"string","nullable":true}}}},"signedUrlTtlSeconds":{"type":"integer","example":3600}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/lessons/content":{"get":{"tags":["Student · Lessons"],"summary":"All lesson content for offline cache","description":"Titles + bodies of every published lesson in one request. Deliberately no signed URLs — use the detail endpoint for fresh attachment/audio URLs.","security":[{"bearerAuth":[]}],"parameters":[{"name":"locale","in":"query","required":false,"schema":{"$ref":"#/components/schemas/Locale"},"description":"Resolves lesson/topic titles (and bodies) through the translation catalogue; falls back to the default title."}],"responses":{"200":{"description":"Lessons.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"body":{"$ref":"#/components/schemas/BlockNoteJson"},"updatedAt":{"type":"string","format":"date-time"},"topic":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"title":{"type":"string"}}}}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/hifz":{"get":{"tags":["Student"],"summary":"Own hifz progress","description":"Per-group pages plus combined totals (juz = 20 pages of the 604-page mushaf). Requires the `quran_hifz` plugin. Read-only — progress is written by the assessing teacher.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Hifz progress.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"pagesMemorized":{"type":"number"},"pagesTotal":{"type":"number","example":604},"juzMemorized":{"type":"number"},"percentComplete":{"type":"number"},"groups":{"type":"array","items":{"type":"object","properties":{"groupId":{"type":"string"},"groupName":{"type":"string","nullable":true},"pagesMemorized":{"type":"number","nullable":true},"notes":{"type":"string","nullable":true},"updatedAt":{"type":"string","format":"date-time"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/progress-notes":{"get":{"tags":["Student"],"summary":"Own progress notes","description":"Max 100. Requires the `lesson_library` plugin.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Notes.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"groupName":{"type":"string","nullable":true}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/weekly-notes":{"get":{"tags":["Student"],"summary":"Published weekly notes","description":"For the student's groups (max 100). Requires the `lesson_library` plugin.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Weekly notes.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"weekStart":{"type":"string"},"groupName":{"type":"string","nullable":true}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/student/written-tests":{"get":{"tags":["Student"],"summary":"Own written tests","description":"Newest first (max 20), including each test's ownership token (also printed on the paper sheet).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Written tests.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"submitted_at":{"type":"string","nullable":true},"graded_at":{"type":"string","nullable":true},"overall_result":{"type":"string","nullable":true},"examiner_note":{"type":"string","nullable":true},"exam_session_id":{"type":"string","nullable":true},"exam_sessions":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"status":{"type":"string"},"exam_date":{"type":"string","nullable":true},"schedule_status":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/examiner/questions":{"get":{"tags":["Exams · Question bank"],"summary":"Question bank grouped by topic","description":"Active questions and mosque topics for building a written test. Examiner or admin.","security":[{"bearerAuth":[]}],"parameters":[{"name":"locale","in":"query","required":false,"schema":{"$ref":"#/components/schemas/Locale"}}],"responses":{"200":{"description":"Questions and topics.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"questions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"question_text":{"type":"string"},"difficulty":{"type":"string"},"topic_id":{"type":"string","nullable":true}}}},"topics":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}}}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/exams":{"get":{"tags":["Exams · Sessions"],"summary":"Pending requests and recent sessions","description":"Examiner view: pending exam requests and the latest 50 sessions.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Requests and sessions.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"pendingRequests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"notes":{"type":"string","nullable":true},"status":{"type":"string"},"created_at":{"type":"string"},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}},"teacher_profiles":{"type":"object","nullable":true,"properties":{"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}}}}}}},"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"diploma_generated_at":{"type":"string","nullable":true},"schedule_status":{"type":"string"},"proposed_date":{"type":"string","nullable":true},"proposed_by":{"type":"string","nullable":true},"oral_required":{"type":"boolean"},"written_required":{"type":"boolean"},"oral_passed":{"type":"boolean"},"written_passed":{"type":"boolean"},"retake_of_session_id":{"type":"string","nullable":true},"from_group_id":{"type":"string","nullable":true},"to_group_id":{"type":"string","nullable":true},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Exams · Sessions"],"summary":"Turn a pending request into an exam session","description":"Without `proposed_date` the session starts `in_progress`; with one it becomes a proposal the student/parent must accept. Oral or written must be required when proposing.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"proposed_date":{"type":"string","description":"YYYY-MM-DD."},"oral_required":{"type":"boolean","default":false},"written_required":{"type":"boolean","default":false}},"required":["request_id"]}}}},"responses":{"201":{"description":"Session created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"exam_date":{"type":"string","nullable":true},"proposed_date":{"type":"string","nullable":true},"proposed_by":{"type":"string","nullable":true},"schedule_status":{"type":"string"},"oral_required":{"type":"boolean"},"written_required":{"type":"boolean"},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/exams/{id}":{"get":{"tags":["Exams · Sessions"],"summary":"Get an exam session","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Session.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"diploma_generated_at":{"type":"string","nullable":true},"from_group_id":{"type":"string","nullable":true},"to_group_id":{"type":"string","nullable":true},"exam_request_id":{"type":"string","nullable":true},"schedule_status":{"type":"string"},"proposed_date":{"type":"string","nullable":true},"proposed_by":{"type":"string","nullable":true},"oral_required":{"type":"boolean"},"written_required":{"type":"boolean"},"oral_passed":{"type":"boolean"},"written_passed":{"type":"boolean"},"retake_of_session_id":{"type":"string","nullable":true},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Exams · Sessions"],"summary":"Update session status / summary / date","description":"Status transitions: `scheduled`, `in_progress`, `passed`, `failed`. `passed`/`failed` are terminal. Passing with `to_group_id` promotes the student (deactivates the old enrollment, creates the new). Only the session's examiner may update.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["scheduled","in_progress","passed","failed"]},"summary":{"type":"string"},"to_group_id":{"type":"string","format":"uuid","description":"Only honored when status is `passed`."},"exam_date":{"type":"string"}},"required":["status"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/exams/{id}/confirm":{"post":{"tags":["Exams · Scheduling"],"summary":"Confirm the proposed date","description":"Examiner accepts the currently proposed date and schedules the exam.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/exams/{id}/schedule":{"post":{"tags":["Exams · Scheduling"],"summary":"Examiner proposes a date","description":"Puts a new proposed date on the table during schedule negotiation.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"proposed_date":{"type":"string","description":"YYYY-MM-DD."},"oral_required":{"type":"boolean"},"written_required":{"type":"boolean"}},"required":["proposed_date"]}}}},"responses":{"200":{"description":"Proposed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/exams/{id}/respond":{"post":{"tags":["Exams · Scheduling"],"summary":"Student/parent response to a proposal","description":"`accept`, `counter` (with `counterDate`), or `cancel`. Authorization and state transitions are delegated to Postgres functions that resolve the caller to the student or a linked parent.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["accept","counter","cancel"]},"counterDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Required when action is `counter`."}},"required":["action"]}}}},"responses":{"200":{"description":"Processed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"action":{"type":"string"},"sessionId":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Session no longer pending. `code: exam_not_pending`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/exams/{id}/retake":{"post":{"tags":["Exams · Sessions"],"summary":"Propose a retake after a failure","description":"Creates a new `proposed` session linked to the failed one via `retake_of_session_id`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"proposed_date":{"type":"string","description":"YYYY-MM-DD."},"oral_required":{"type":"boolean","default":false},"written_required":{"type":"boolean","default":false}},"required":["proposed_date"]}}}},"responses":{"201":{"description":"Retake proposed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"retake_session_id":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/exams/{id}/lesson-checks":{"get":{"tags":["Exams · Sessions"],"summary":"Curriculum checklist for a session","description":"Published lessons plus which are ticked off for the session.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Checklist.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"lessons":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"sort_order":{"type":"integer"}}}},"checked":{"type":"array","items":{"type":"string"},"description":"lesson ids already ticked."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Exams · Sessions"],"summary":"Tick / untick a lesson result","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lesson_id":{"type":"string","format":"uuid"},"passed":{"type":"boolean","default":true,"description":"false unticks."}},"required":["lesson_id"]}}}},"responses":{"200":{"description":"Toggled.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"lesson_id":{"type":"string"},"passed":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/exam-requests":{"get":{"tags":["Exams · Requests"],"summary":"List own exam requests","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Requests.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"group_id":{"type":"string"},"student_profile_id":{"type":"string"},"notes":{"type":"string","nullable":true},"status":{"type":"string"},"created_at":{"type":"string"},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Exams · Requests"],"summary":"Request an exam for a student","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"student_profile_id":{"type":"string","format":"uuid"},"group_id":{"type":"string","format":"uuid"},"notes":{"type":"string"}},"required":["student_profile_id","group_id"]}}}},"responses":{"201":{"description":"Requested.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string"},"group_id":{"type":"string"},"student_profile_id":{"type":"string"},"status":{"type":"string"},"notes":{"type":"string","nullable":true},"created_at":{"type":"string"},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/exam-requests/{id}":{"get":{"tags":["Exams · Requests"],"summary":"Get an exam request with its session","description":"Readable by teachers or examiners.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Request.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"notes":{"type":"string","nullable":true},"created_at":{"type":"string"},"student_profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}},"groups":{"type":"object","nullable":true,"properties":{"name":{"type":"string"}}},"teacher_profiles":{"type":"object","nullable":true,"properties":{"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string"}}}}},"session":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"status":{"type":"string"},"summary":{"type":"string","nullable":true},"exam_date":{"type":"string","nullable":true},"schedule_status":{"type":"string"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Exams · Requests"],"summary":"Cancel an own pending request","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"cancelled":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/written-tests":{"post":{"tags":["Exams · Written tests"],"summary":"Create a written test","description":"Creates an online written test for an exam session from the mosque's active question bank (1-100 questions) and notifies student + parents with the token.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"exam_session_id":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":200},"question_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":100}},"required":["exam_session_id","title","question_ids"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Session has no student or already has a written test.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/written-tests/{token}":{"get":{"tags":["Exams · Written tests"],"summary":"Fetch a written test by token","description":"Requires authentication (unlike the public web page) and authorization: the test's student, its examiner, a mosque admin, or a linked parent. Response shape depends on status — `pending` returns questions, `submitted` returns confirmation, `graded` returns result and answers.","security":[{"bearerAuth":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Test state.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"status":{"type":"string","enum":["pending","submitted","graded"]},"title":{"type":"string"},"mosqueName":{"type":"string"},"studentName":{"type":"string"},"questions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"question_text":{"type":"string"},"order":{"type":"integer"}}},"description":"Only when pending."},"overallResult":{"type":"string","nullable":true},"examinerNote":{"type":"string","nullable":true},"answers":{"type":"array","items":{"type":"object","properties":{"order":{"type":"integer"},"questionText":{"type":"string"},"answerText":{"type":"string"},"examinerComment":{"type":"string","nullable":true}}},"description":"Only when graded."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Test has no questions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/written-tests/{token}/submit":{"post":{"tags":["Exams · Written tests"],"summary":"Submit answers for a written test","description":"One-time submit. Allowed: the test's student, its examiner, or a mosque admin. Parents may read but not submit. Only questions actually on the test are accepted.","security":[{"bearerAuth":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"answers":{"type":"array","minItems":1,"items":{"type":"object","properties":{"question_id":{"type":"string"},"question_order":{"type":"integer"},"answer_text":{"type":"string"}},"required":["question_id","answer_text"]}}},"required":["answers"]}}}},"responses":{"200":{"description":"Submitted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"submitted":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/messages/contacts":{"get":{"tags":["Messaging"],"summary":"People the caller can message","description":"Full contact list for the recipient picker, derived from the same security-definer RPC the write path uses.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Contacts.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/messages/find-or-create":{"post":{"tags":["Messaging"],"summary":"Find or create a 1:1 thread","description":"Idempotent: returns the existing 1:1 thread with the recipient, or creates one. Cannot chat with yourself; the recipient must be in the caller's contact list.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recipient_id":{"type":"string"}},"required":["recipient_id"]}}}},"responses":{"200":{"description":"Existing thread.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"threadId":{"type":"string"}}}}}}}},"201":{"description":"New thread created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"threadId":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/messages/threads":{"get":{"tags":["Messaging"],"summary":"List own threads","description":"Newest first with participant names and the last message per thread. Keyset pagination on `updated_at`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"before","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Return threads updated strictly before this timestamp."}],"responses":{"200":{"description":"Threads.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"threads":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject":{"type":"string","nullable":true},"updated_at":{"type":"string","format":"date-time"},"participants":{"type":"array","items":{"type":"object","properties":{"profile_id":{"type":"string","nullable":true},"last_read_at":{"type":"string","nullable":true},"name":{"type":"string"}}}},"lastMessage":{"type":"object","nullable":true,"properties":{"body":{"type":"string"},"author_profile_id":{"type":"string","nullable":true}}}}}},"nextCursor":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Messaging"],"summary":"Create a thread with a first message","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subject":{"type":"string","nullable":true},"body":{"type":"string","minLength":1},"recipient_ids":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["body","recipient_ids"]}}}},"responses":{"201":{"description":"Thread created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"threadId":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/messages/threads/{id}":{"get":{"tags":["Messaging"],"summary":"Get a thread with all messages","description":"Participants with names and messages oldest-first. Caller must be a participant (403 otherwise).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Thread.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"thread":{"type":"object","properties":{"id":{"type":"string"},"subject":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"message_participants":{"type":"array","items":{"type":"object","properties":{"profile_id":{"type":"string","nullable":true},"last_read_at":{"type":"string","nullable":true},"profiles":{"type":"object","nullable":true,"properties":{"full_name":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true}}}}}}}},"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"author_profile_id":{"type":"string","nullable":true},"body":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Messaging"],"summary":"Send a message","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1}},"required":["body"]}}}},"responses":{"200":{"description":"Sent.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"sent":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/messages/threads/{id}/read":{"put":{"tags":["Messaging"],"summary":"Mark a thread as read","description":"Sets the caller's `last_read_at` to now for this thread.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Marked read.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"marked_read":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/notifications":{"get":{"tags":["Notifications"],"summary":"List own notifications","description":"Newest first with keyset paging on `created_at`. `thread_id` is flattened from the source message for tap-through.","security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"before","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"unreadOnly","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"Notifications.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"notifications":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject":{"type":"string","nullable":true},"body":{"type":"string","nullable":true},"channel":{"type":"string"},"status":{"type":"string"},"is_read":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"source_announcement_id":{"type":"string","nullable":true},"source_message_id":{"type":"string","nullable":true},"template_key":{"type":"string","nullable":true},"template_params":{"type":"object","nullable":true},"thread_id":{"type":"string","nullable":true}}}},"nextCursor":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Notifications"],"summary":"Mark all notifications read","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Marked read.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"marked_all_read":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/calendar":{"get":{"tags":["Calendar"],"summary":"Week calendar","description":"Sessions, events and school holidays for a date range. `scope=mine` shows the caller's own groups/children; `scope=mosque` (or anything else) shows everything. Requires the `calendar` plugin. Range limited to 62 days.","security":[{"bearerAuth":[]}],"parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Defaults to Monday of the current week."},{"name":"to","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Defaults to from + 6 days."},{"name":"scope","in":"query","required":false,"schema":{"type":"string","enum":["mine","mosque"]},"description":"`mosque` shows everything to every role."}],"responses":{"200":{"description":"Calendar week.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string"},"startTime":{"type":"string","nullable":true},"endTime":{"type":"string","nullable":true},"isCancelled":{"type":"boolean"},"notes":{"type":"string","nullable":true},"groupId":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"room":{"type":"string","nullable":true},"color":{"type":"string","nullable":true}}}},"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"date":{"type":"string"},"startTime":{"type":"string","nullable":true},"endTime":{"type":"string","nullable":true}}}},"holidays":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"startDate":{"type":"string"},"endDate":{"type":"string"}}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/calendar/sessions/{id}":{"patch":{"tags":["Calendar"],"summary":"Cancel or restore a lesson session","description":"Teacher-only: the teacher must be linked to the session's group. Cancellation notifications are fanned out by a DB trigger.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"is_cancelled":{"type":"boolean"},"notes":{"type":"string","maxLength":500,"nullable":true,"description":"Only applied when cancelling."}},"required":["is_cancelled"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/checkin/{token}":{"get":{"tags":["Check-in"],"summary":"Resolve a check-in code","description":"What a signed-in student/parent sees when opening a check-in QR: the session summary and eligible students' presence. Codes expire 12 hours after opening, require the `student_checkin` plugin, and the caller must be enrolled in the session's group.","security":[{"bearerAuth":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Session state.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"groupName":{"type":"string"},"sessionDate":{"type":"string"},"students":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"full_name":{"type":"string"},"present":{"type":"boolean"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Code invalid/expired/plugin-off, or caller not enrolled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Check-in"],"summary":"Check in a student","description":"Marks an eligible student present (upsert). Teacher-set statuses (non-absent) are never overwritten.","security":[{"bearerAuth":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"studentId":{"type":"string","format":"uuid"}},"required":["studentId"]}}}},"responses":{"200":{"description":"Checked in.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"alreadyPresent":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Not allowed to check in this student.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Code invalid/expired/plugin-off, or caller not enrolled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/groups/{id}/attendance":{"get":{"tags":["Attendance"],"summary":"Attendance sessions for a group","description":"Admin or the group's teacher.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sessions.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"session_date":{"type":"string"},"created_at":{"type":"string"},"attendance_records":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"student_profile_id":{"type":"string"},"status":{"type":"string","enum":["present","absent","late","excused"]}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Attendance"],"summary":"Save the attendance sheet","description":"Upserts the session (on `group_id, session_date`) and its records (on `session_id, student_profile_id`). Admin or the group's teacher.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"session_date":{"type":"string","minLength":4},"records":{"type":"array","minItems":1,"items":{"type":"object","properties":{"student_profile_id":{"type":"string"},"status":{"type":"string","enum":["present","absent","late","excused"]}},"required":["student_profile_id","status"]}}},"required":["session_date","records"]}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"session_id":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/groups/{id}/homework":{"get":{"tags":["Homework"],"summary":"Homework for a group","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Assignments.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"body":{"type":"string","nullable":true},"due_date":{"type":"string","nullable":true},"audience":{"type":"string","enum":["group","individual"]},"lesson_id":{"type":"string","nullable":true},"created_at":{"type":"string"},"updated_at":{"type":"string"},"homework_targets":{"type":"array","items":{"type":"object","properties":{"student_profile_id":{"type":"string"}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Homework"],"summary":"Create a homework assignment","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string","nullable":true},"lesson_id":{"type":"string","nullable":true},"due_date":{"type":"string","nullable":true},"audience":{"type":"string","enum":["group","individual"],"default":"group"},"student_ids":{"type":"array","items":{"type":"string"},"description":"Required non-empty when audience is `individual`."}},"required":["title"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/homework/{id}":{"put":{"tags":["Homework"],"summary":"Update a homework assignment","description":"Teacher-only (admins are not accepted here). The teacher must be linked to the assignment's group.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"group_id":{"type":"string"},"title":{"type":"string","minLength":1},"body":{"type":"string"},"due_date":{"type":"string"},"lesson_id":{"type":"string"}},"required":["group_id","title"]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"updated":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"delete":{"tags":["Homework"],"summary":"Delete a homework assignment","description":"Teacher-only.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"group_id":{"type":"string"}},"required":["group_id"]}}}},"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/announcements":{"get":{"tags":["Announcements"],"summary":"Published announcements feed","description":"Published announcements targeting the caller's mosque and (where relevant) their groups. Max 100.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Announcements.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"audience":{"type":"string","enum":["mosque","group"]},"group_id":{"type":"string","nullable":true},"published_at":{"type":"string"},"created_at":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/prayer-times":{"get":{"tags":["Prayer times"],"summary":"Today's prayer times","description":"Resolved through the AlAdhan API from the mosque's stored location/coordinates. Degrades to `prayerTimes: null` on any upstream failure (never errors). Requires the `prayer_times` plugin.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Prayer times.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"prayerTimes":{"type":"object","nullable":true,"properties":{"fajr":{"type":"string"},"sunrise":{"type":"string"},"dhuhr":{"type":"string"},"asr":{"type":"string"},"maghrib":{"type":"string"},"isha":{"type":"string"}}},"date":{"type":"string","nullable":true,"description":"YYYY-MM-DD."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/quran/saved-ayahs":{"get":{"tags":["Quran"],"summary":"Own saved ayahs","description":"Personal bookmark list, keyed on the user (not the mosque). Ordered by surah then ayah.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Saved ayahs.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"surahNumber":{"type":"integer"},"ayahNumber":{"type":"integer"},"surahName":{"type":"string"},"arabicText":{"type":"string"},"translationText":{"type":"string","nullable":true},"translationEdition":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Quran"],"summary":"Save an ayah bookmark","description":"Upsert on `(user_id, surah_number, ayah_number)`. The client sends the ayah text so the list renders offline.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"surahNumber":{"type":"integer","minimum":1,"maximum":114},"ayahNumber":{"type":"integer","minimum":1},"surahName":{"type":"string","minLength":1,"maxLength":128},"arabicText":{"type":"string","minLength":1},"translationText":{"type":"string"},"translationEdition":{"type":"string","maxLength":64},"note":{"type":"string","maxLength":2000}},"required":["surahNumber","ayahNumber","surahName","arabicText"]}}}},"responses":{"201":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","nullable":true},"saved":{"type":"boolean","enum":[true]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/quran/saved-ayahs/{id}":{"delete":{"tags":["Quran"],"summary":"Remove a saved ayah","description":"Owner-scoped; deleting someone else's row returns 404.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/report-card":{"get":{"tags":["Report"],"summary":"On-demand report card","description":"Attendance since the school-year start, hifz pages, passed/failed exams, and lesson completions — for the calling student or a linked parent's child (`studentId`). Requires the `annual_report` plugin.","security":[{"bearerAuth":[]}],"parameters":[{"name":"studentId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Target student (parent view); omit for the caller's own card."}],"responses":{"200":{"description":"Report card.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"studentName":{"type":"string"},"since":{"type":"string"},"isParentView":{"type":"boolean"},"groups":{"type":"array","items":{"type":"string"}},"attendancePresent":{"type":"integer"},"attendanceTotal":{"type":"integer"},"attendanceRate":{"type":"number","nullable":true},"hifz":{"type":"array","items":{"type":"object","properties":{"groupName":{"type":"string"},"pages":{"type":"number"}}}},"examsPassed":{"type":"integer"},"examsFailed":{"type":"integer"},"lessonsCompleted":{"type":"integer"},"totalLessons":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/widget":{"get":{"tags":["Widget"],"summary":"Home-screen widget payload","description":"Aggregated payload by role: due homework, next lesson, and hifz progress (gated on the `quran_hifz` plugin). Raw values — formatting is left to the app.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Widget data.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"role":{"type":"string"},"assignments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"dueDate":{"type":"string","nullable":true},"studentName":{"type":"string","description":"Parent view only."}}}},"nextLesson":{"type":"object","nullable":true,"properties":{"date":{"type":"string"},"startTime":{"type":"string"},"groupName":{"type":"string"}}},"hifz":{"type":"object","nullable":true,"properties":{"pagesMemorized":{"type":"number"},"pagesTotal":{"type":"number"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/health":{"get":{"tags":["Internal"],"summary":"Health check","description":"Proves the instance is up and the database answers. Deliberately unauthenticated (a secret-gated health check fails exactly when diagnosing secret rotation). Note: not the standard envelope.","security":[],"responses":{"200":{"description":"Healthy.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"checks":{"type":"object","properties":{"database":{"type":"object","properties":{"ok":{"type":"boolean"},"rows":{"type":"integer"}}}}},"uptime":{"type":"integer","description":"Milliseconds."}}}}}},"500":{"description":"Down.","content":{"application/json":{"schema":{"type":"object"}}}},"503":{"description":"Degraded (DB configured but failing, or not configured).","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/checkout":{"post":{"tags":["Internal"],"summary":"Create a Stripe SetupIntent for billing","description":"Get-or-create the mosque's Stripe customer and return a SetupIntent client secret so the client can collect the card (subscription is created server-side by `/api/checkout/activate` afterwards). Authenticated by cookie session; requires a mosque_admin membership. Note: not the standard envelope.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"planId":{"type":"string"}},"required":["planId"]}}}},"responses":{"200":{"description":"Setup intent ready.","content":{"application/json":{"schema":{"type":"object","properties":{"clientSecret":{"type":"string"},"setupIntentId":{"type":"string"}}}}}},"400":{"description":"Invalid plan.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"No mosque admin membership.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/checkout/activate":{"post":{"tags":["Internal"],"summary":"Activate billing after Stripe setup","description":"Retrieves the SetupIntent, sets the saved payment method as default, and creates or switches the mosque's Stripe subscription. Authenticated by cookie session; verifies the caller is a mosque_admin of the mosque in the SetupIntent metadata. Note: not the standard envelope.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"setupIntentId":{"type":"string"}},"required":["setupIntentId"]}}}},"responses":{"200":{"description":"Activated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"status":{"type":"string"}}}}}},"400":{"description":"Missing/incomplete setup intent.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Not an admin of the target mosque.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/notifications/send-emails":{"post":{"tags":["Internal"],"summary":"Cron: drain the notification email queue","description":"Processes up to 20 queued `channel=email` notifications and sends them via Resend. Runs daily at 07:00 and 19:00 (see `vercel.json`). Authenticates with `Authorization: Bearer <CRON_SECRET>`. Note: not the standard envelope.","security":[{"cronSecret":[]}],"responses":{"200":{"description":"Processed.","content":{"application/json":{"schema":{"type":"object","properties":{"processed":{"type":"integer"},"failed":{"type":"integer"},"total":{"type":"integer"},"remaining":{"type":"string","enum":["more","none"]}}}}}},"401":{"description":"Missing or invalid CRON_SECRET.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/notifications/send-push":{"post":{"tags":["Internal"],"summary":"Cron/webhook: deliver queued notifications as push","description":"Sends pending `notification_queue` rows to every registered device of each recipient via APNs (iOS) and FCM (Android), then stamps `pushed_at`.\n\nTwo callers: the `pg_net` trigger posts `{ \"notificationId\": \"<uuid>\" }` the moment a row lands, which delivers exactly that row; the cron (08:00 and 20:00, see `vercel.json`) posts an empty body and drains a batch of up to 20 rows created in the last 3 days.\n\nAuthenticates with `Authorization: Bearer <token>` where the token is either `CRON_SECRET` or `PUSH_WEBHOOK_SECRET`. Tokens that APNs/FCM report as unregistered or invalid are deleted; transient failures leave the row pending for the next tick. Note: not the standard envelope.","security":[{"cronSecret":[]}],"requestBody":{"required":false,"description":"Omit the body for the cron batch.","content":{"application/json":{"schema":{"type":"object","properties":{"notificationId":{"type":"string","format":"uuid","description":"Deliver only this queued notification."}}}}}},"responses":{"200":{"description":"Processed.","content":{"application/json":{"schema":{"type":"object","properties":{"processed":{"type":"integer","description":"Queue rows stamped as handled."},"pushed":{"type":"integer","description":"Rows that reached at least one device."},"devices":{"type":"integer","description":"Individual device deliveries that succeeded."},"failed":{"type":"integer"}}}}}},"401":{"description":"Missing or invalid CRON_SECRET / PUSH_WEBHOOK_SECRET.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Queue lookup failed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/reports/send-cards":{"post":{"tags":["Internal"],"summary":"Cron: email quarterly report cards","description":"Generates and emails report-card PDFs to parents of active students for every mosque with the `annual_report` plugin. Runs quarterly (Jan 1, Apr 1, Jul 1, Oct 1) at 08:00. Authenticates with `Authorization: Bearer <CRON_SECRET>`. Note: not the standard envelope.","security":[{"cronSecret":[]}],"responses":{"200":{"description":"Processed.","content":{"application/json":{"schema":{"type":"object","properties":{"mosques":{"type":"integer"},"processed":{"type":"integer"},"emailed":{"type":"integer"},"skipped":{"type":"integer"},"failed":{"type":"integer"}}}}}},"401":{"description":"Missing or invalid CRON_SECRET.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/webhooks/stripe":{"post":{"tags":["Internal"],"summary":"Stripe webhook receiver","description":"Keeps `mosque_subscriptions` in sync with checkout, subscription lifecycle and invoice events. Authenticated by the `stripe-signature` header verified against `STRIPE_WEBHOOK_SECRET`; duplicate events are de-duplicated via `stripe_webhook_events`. Note: not the standard envelope.","security":[{"stripeSignature":[]}],"responses":{"200":{"description":"Received.","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"boolean"},"duplicate":{"type":"boolean"}}}}}},"400":{"description":"Missing or invalid signature.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/docs/openapi.json":{"get":{"tags":["Internal"],"summary":"This OpenAPI document","description":"Serves the machine-readable spec that renders the reference at `/docs/api`. Unauthenticated and cacheable for an hour. Note: not the standard envelope — it is the raw OpenAPI document.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"cronSecret":{"type":"http","scheme":"bearer","description":"Bearer <CRON_SECRET> for cron-triggered jobs."},"stripeSignature":{"type":"apiKey","in":"header","name":"stripe-signature","description":"Stripe webhook signature."}},"schemas":{"Success":{"type":"object","required":["ok","data"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"nullable":true,"description":"Endpoint-specific payload."}}},"Error":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"string","description":"Human-readable, sanitised message."},"code":{"type":"string","nullable":true,"description":"Stable machine-readable error code when available."}}},"PaginationOffset":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer","nullable":true},"nextOffset":{"type":"integer","nullable":true}}},"PaginationKeyset":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true,"description":"Pass back as `before` on the next page."}}},"TempPassword":{"type":"object","properties":{"email":{"type":"string"},"full_name":{"type":"string"},"tempPassword":{"type":"string","description":"One-time password in XXXX-XXXX-XXXX form. Returned exactly once."},"expires_at":{"type":"string","format":"date-time","description":"OTP expiry, 7 days from issue."}}},"BlockNoteJson":{"type":"array","description":"BlockNote editor JSON. Real lessons use only paragraph, heading (2-3), bulletListItem and numberedListItem blocks with plain-text content. Unknown block types must be ignored by clients.","items":{"type":"object"}},"UserRole":{"type":"string","enum":["mosque_admin","examiner","teacher","parent","student","none"]},"Locale":{"type":"string","enum":["de","en","bs","tr"]}},"responses":{"Unauthorized":{"description":"Missing/invalid bearer token, or the user must rotate their password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Authenticated but not permitted for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found, or (for plugin gates) the feature is not enabled for this mosque.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Malformed request or invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationFailed":{"description":"Zod validation failed. `error` reads `<path>: <message>`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"DbError":{"description":"A database/Supabase failure, mapped to a safe message by the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded. Honor the `Retry-After` header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}