Overview — 5W

📦
What

Token Revocation Endpoint

  • POST /api/v1/auth/logout — revokes the authenticated student's current Bearer token
  • Requires a valid Sanctum token (auth:sanctum middleware)
  • Returns HTTP 200 with a standard success envelope: data: null
  • Dispatches StudentLoggedOut event (async → Moodle plugin)
When

Request Lifecycle Trigger

Triggered by any authenticated student calling POST /api/v1/auth/logout with a valid Bearer token. The auth:sanctum middleware resolves the token before the controller executes. The event is dispatched asynchronously via the queue after the token is deleted — it does not block the HTTP response.

💡
Why

Security & Audit Compliance

  • Students on shared devices need a way to explicitly invalidate their session
  • Compromised accounts can be secured immediately by revoking the active token
  • Moodle's event system requires a user_loggedout event for audit trails and activity reports
  • Prevents token persistence until natural expiry (the only prior safeguard)
📍
Where

Codebase Location

  • Controller: app/Modules/Auth/Controllers/LogoutController.php
  • Service: app/Modules/Auth/Services/AuthenticationService.php
  • Event: app/Modules/Auth/Events/StudentLoggedOut.php
  • Shared: app/Shared/Http/Resources/ApiResource.php
  • Routes: app/Modules/Auth/routes.php
⚙️
How

End-to-End Mechanism

  • Sanctum resolves MoodleUser from the Authorization header and sets currentAccessToken()
  • AuthenticationService::logout() calls $user->currentAccessToken()->delete() — only the token from the current request is revoked; other device tokens remain intact
  • Event::dispatch(new StudentLoggedOut(...)) queues the Moodle event asynchronously via ForwardEventToMoodle listener
  • ApiResource::success() returns the standard envelope with data: null
👤
Who

Actors

  • Students: call the endpoint with their active Bearer token to end their session
  • Sanctum middleware: authenticates the token before the controller runs
  • Queue worker: forwards the StudentLoggedOut event to Moodle asynchronously
  • Moodle plugin: receives \core\event\user_loggedout and logs it in Moodle's event system

Sequence Diagram

sequenceDiagram participant B as Browser participant M as auth:sanctum Middleware participant C as LogoutController participant S as AuthenticationService participant DB as mdldf_personal_access_tokens participant Q as Queue participant MP as Moodle Plugin B->>+M: POST /api/v1/auth/logout
Authorization: Bearer {token} M->>DB: SELECT token WHERE token = hash(bearer) DB-->>M: PersonalAccessToken row M->>M: Set currentAccessToken on MoodleUser M->>+C: invoke(Request) C->>C: $user = $request->user() C->>+S: logout(MoodleUser $user) S->>+DB: currentAccessToken()->delete() DB-->>-S: token row deleted S->>Q: Event::dispatch(StudentLoggedOut) Note over Q: Async — does not block response S-->>-C: void C->>C: ApiResource::success('Logged out successfully.') C-->>-M: JsonResponse 200 M-->>-B: 200 { success: true, message: "Logged out successfully.", data: null } Q->>+MP: ForwardEventToMoodle listener
POST Moodle REST API Note over MP: user_loggedout recorded in Moodle event log MP-->>-Q: 200 ok

Flowchart

flowchart TD A([POST /api/v1/auth/logout]) --> B{Authorization\nheader present?} B -- No --> C[401 Unauthenticated]:::error B -- Yes --> D{Sanctum resolves\ntoken from DB?} D -- Token not found\nor expired --> E[401 Unauthenticated]:::error D -- Yes, token valid --> F[Set currentAccessToken\non MoodleUser] F --> G[LogoutController::__invoke] G --> H[AuthenticationService::logout] H --> I[currentAccessToken()->delete\nfrom personal_access_tokens] I --> J{Delete\nsucceeded?} J -- Error --> K[500 Unexpected Error]:::error J -- Yes --> L[Event::dispatch\nStudentLoggedOut] L --> M[Queued async:\nForwardEventToMoodle listener] M --> N[ApiResource::success\n'Logged out successfully.'] N --> O([200 OK\nsuccess:true, data:null]):::success M -.-> P[HTTP POST to Moodle REST API] P -.-> Q{Moodle API\nresponds?} Q -.-> R[user_loggedout logged\nin Moodle event system]:::moodle Q -. Failure .-> S[Queue retry\nLaravel retry mechanism]:::warn classDef success fill:#10b981,color:#fff,stroke:none classDef error fill:#ef4444,color:#fff,stroke:none classDef warn fill:#f59e0b,color:#fff,stroke:none classDef moodle fill:#3b82f6,color:#fff,stroke:none

Files Changed

File Path Layer Description
app/Modules/Auth/Controllers/LogoutController.php Controller Invokable controller for POST /api/v1/auth/logout. Resolves the authenticated MoodleUser from the request, delegates to AuthenticationService::logout(), and returns ApiResource::success().
app/Modules/Auth/Events/StudentLoggedOut.php Event Domain event extending BaseEvent. Carries Moodle-compatible metadata: event name \core\event\user_loggedout, component core, action loggedout, target user, CrudType::Read, EduLevel::Other, ContextLevel::User. Context instance ID equals the user's ID.
app/Modules/Auth/Services/AuthenticationService.php Service Added logout(MoodleUser $user): void method. Revokes the current access token via currentAccessToken()->delete() and dispatches StudentLoggedOut event.
app/Modules/Auth/routes.php Routes Split into two middleware groups under v1/auth prefix: throttle:5,1 for login, and auth:sanctum for logout. Added POST logoutLogoutController named api.v1.auth.logout.
app/Shared/Http/Resources/ApiResource.php Resource Added static success(string $message, int $status = 200): JsonResponse. Returns the standard envelope with data: null, errors: null, code: null. Mirrors the existing error() static method pattern.
tests/Feature/Auth/LogoutTest.php Test Feature test suite (6 tests): 200 response shape, 401 when unauthenticated, token revocation verified in DB, revoked token returns 401 on reuse, StudentLoggedOut event dispatched, other device tokens not affected.
tests/Unit/Modules/Auth/Events/StudentLoggedOutTest.php Test Unit test suite (10 tests): verifies every abstract method implementation on StudentLoggedOut — event name, component, action, target, object table, CRUD type, edu level, context level, and context instance ID.
tests/Unit/Shared/Http/Resources/ApiResourceTest.php Test Added 2 tests for the new ApiResource::success() method: verifies null data payload and correct HTTP status code.
docs/openapi.yaml Docs Added POST /v1/auth/logout path with Bearer security requirement, 200 success response schema, and 401 unauthenticated response.
docs/postman_collection.json Docs Added Logout request to the Auth folder with Bearer {{token}} header, example 200 and 401 responses, and test script validating the envelope shape.

Rules Applied

Architecture

Thin Controller (5–15 lines)

LogoutController::__invoke() is 6 lines: resolve user, delegate to service, return response. Zero business logic. All token deletion and event dispatch live in AuthenticationService::logout().

Architecture

Module Self-Containment

All Auth-specific files (LogoutController, StudentLoggedOut, route definition) reside under app/Modules/Auth/. Only the cross-cutting ApiResource::success() was added to app/Shared/ because it serves any module's no-data responses.

Architecture

Domain Event System — BaseEvent

StudentLoggedOut extends BaseEvent and implements all abstract methods with Moodle-compatible values. Dispatched via Event::dispatch() (Laravel façade). The ForwardEventToMoodle listener runs on the queue — event forwarding does not block the HTTP response.

Architecture

Standard API Response Envelope

Response follows the mandated shape: success, message, data, errors, code. No bespoke LogoutResource was created — ApiResource::success() fills this role for any operation that returns no resource data.

Security

auth:sanctum Middleware — Authentication Required

The logout route is grouped under auth:sanctum. Requests without a valid Bearer token receive 401 before reaching the controller. No endpoint is public by default.

Security

Single-Device Token Revocation

currentAccessToken()->delete() revokes only the token carried in the current request — other active sessions on other devices are unaffected. This is the narrowest possible revocation scope, protecting sessions the student did not intend to end.

Coding Style

final class + Constructor Injection

Both LogoutController and StudentLoggedOut are declared final. AuthenticationService is injected via constructor promotion into LogoutController. No app() or resolve() calls in business logic.

Coding Style

PHPDoc on Every Class and Public Method

All new classes and public methods carry brief PHPDoc blocks. The class docblock is one sentence describing responsibility. The logout() method docblock is one sentence with no redundant @param/@return (types are self-evident).

Coding Style

Enums for Fixed Value Sets

StudentLoggedOut returns CrudType::Read, EduLevel::Other, and ContextLevel::User — never raw strings or integer codes. Enums enforce type safety and prevent magic value drift across event implementations.

Testing

TDD Workflow — Red → Green → Refactor

Tests were written before implementation code for every component: ApiResourceTest (2 new tests), StudentLoggedOutTest (10 tests), and LogoutTest (6 feature tests). Implementation was the minimum code needed to make each test pass.

Testing

RefreshDatabase + Real DB (No Mocks for Eloquent)

Feature tests use the RefreshDatabase trait and hit a real SQLite test database. Token revocation and multi-token isolation are verified with assertDatabaseMissing/assertDatabaseHas. The Moodle REST call is mocked via Http::fake() per the rules.

Testing

100% Event Dispatch Coverage

test_it_dispatches_student_logged_out_event uses Event::fake() and Event::assertDispatched(StudentLoggedOut::class). All 10 abstract method implementations on the event are individually verified in StudentLoggedOutTest.