Feature Analysis — 6W Framework

🔍 What
  • Changed SESSION_DRIVER from database to array — no DB session-table interaction possible for a REST API.
  • Added ValidateMoodleUserStatus middleware that re-checks mdldf_user status on every authenticated request.
  • Added MoodleSession read-only model wrapping mdldf_sessions as shared infrastructure for future SSO session lookups.
  • Added boolean casts for deleted, suspended, confirmed to MoodleUser.
When
  • Middleware fires on every authenticated API request after auth:sanctum resolves the user.
  • Status is checked from Cache::remember with a 60-second TTL — real DB hit only on cache miss.
  • Revocation happens immediately when a suspended or deleted status is detected.
  • Admin actions propagate to the portal within ≤ 60 seconds.
💡 Why
  • Session-driver risk: SESSION_DRIVER=database + DB_TABLE_PREFIX=mdldf_ would target Moodle's mdldf_sessions table — schema mismatch would corrupt it.
  • SSO logout gap: No mechanism existed to revoke portal access when a Moodle admin suspends or deletes a student — tokens remained valid indefinitely.
  • This is the coexistence contract: our portal respects Moodle's authority over user accounts.
📂 Where
  • app/Shared/Middleware/ValidateMoodleUserStatus
  • app/Shared/Models/MoodleSession, MoodleUser (casts)
  • bootstrap/app.phpmoodle.active alias registration
  • .env / .env.exampleSESSION_DRIVER=array
  • tests/Unit/Shared/Middleware/ and tests/Unit/Shared/Models/
⚙️ How
  • Cache::remember("moodle_user_status:{id}", 60s, ...) fetches an int sentinel: 1=active, 0=inactive, 2=suspended.
  • Int sentinel avoids PHP strict-type conflicts in closures returning booleans.
  • On inactive/suspended: $user->tokens()->delete() + Cache::forget(key), then JSON 401/403 with AuthErrorCode.
  • Active users pass straight through — zero overhead beyond a cache lookup.
👥 Who
  • Students: Transparently affected — access revoked within ≤60 s of admin action in Moodle.
  • Moodle admins: Suspend/delete actions propagate to the portal automatically.
  • Our team: Apply ['auth:sanctum', 'moodle.active'] stack to all protected route groups.

Sequence Diagram — SSO Logout Flow

sequenceDiagram
    participant C as Client (Bearer token)
    participant S as auth:sanctum
    participant M as moodle.active middleware
    participant Ca as Cache (Redis/Array)
    participant DB as mdldf_user
    participant T as personal_access_tokens
    participant H as Route Handler

    C->>+S: GET /api/v1/... Authorization: Bearer {token}
    S->>S: Resolve token → MoodleUser
    S->>+M: next(request) with $user

    M->>+Ca: Cache::remember("moodle_user_status:{id}", 60s)
    alt Cache HIT
        Ca-->>M: int status (0 / 1 / 2)
    else Cache MISS
        Ca->>+DB: SELECT deleted, suspended, confirmed WHERE id=?
        DB-->>-Ca: user row
        Ca->>Ca: compute int status
        Ca-->>M: int status stored for 60s
    end
    deactivate Ca

    alt status = ACTIVE (1)
        M->>+H: next(request)
        H-->>-M: 200 OK {data}
        M-->>C: 200 OK
    else status = INACTIVE (0) — deleted or unconfirmed
        M->>T: DELETE WHERE tokenable_id=user.id
        M->>Ca: Cache::forget("moodle_user_status:{id}")
        M-->>-C: 401 {success:false, code:1000}
    else status = SUSPENDED (2)
        M->>T: DELETE WHERE tokenable_id=user.id
        M->>Ca: Cache::forget("moodle_user_status:{id}")
        M-->>C: 403 {success:false, code:1002}
    end
    deactivate S
      

Flowchart — Middleware Decision Tree

flowchart TD
    A([Incoming Request]) --> B{auth:sanctum resolves user?}
    B -- No --> B1([401 Unauthenticated])
    B -- Yes --> C{instanceof MoodleUser?}
    C -- No --> D([Pass through])
    C -- Yes --> E[Cache::remember moodle_user_status:userid TTL 60s]
    E --> F{Cache HIT?}
    F -- Yes --> G{status int}
    F -- No --> H[(SELECT deleted, suspended, confirmed FROM mdldf_user)]
    H --> I{deleted=1 or confirmed=0?}
    I -- Yes --> J[status = 0 INACTIVE]
    I -- No --> K{suspended=1?}
    K -- Yes --> L[status = 2 SUSPENDED]
    K -- No --> M[status = 1 ACTIVE]
    J --> G
    L --> G
    M --> G
    G -- 1 ACTIVE --> N([Pass to Route Handler 200 OK])
    G -- 0 INACTIVE --> O[Revoke all tokens + forget cache]
    G -- 2 SUSPENDED --> P[Revoke all tokens + forget cache]
    O --> Q([401 code 1001 Account not active])
    P --> R([403 code 1002 Account suspended])
    style B1 fill:#7f1d1d,color:#fca5a5,stroke:#ef4444
    style Q fill:#7f1d1d,color:#fca5a5,stroke:#ef4444
    style R fill:#78350f,color:#fde68a,stroke:#f59e0b
    style N fill:#064e3b,color:#6ee7b7,stroke:#10b981
    style D fill:#1e3a5f,color:#93c5fd,stroke:#3b82f6
      

Files Changed

File Path Layer Description
app/Shared/Middleware/ValidateMoodleUserStatus.php Middleware Core SSO logout middleware — checks mdldf_user status via cache, revokes tokens on inactive/suspended, returns unified JSON error envelope.
app/Shared/Models/MoodleSession.php Model New read-only Eloquent model for mdldf_sessions — extends MoodleModel, timestamps disabled, write-protected. Shared infrastructure for future SSO session lookups.
app/Shared/Models/MoodleUser.php Model Added $casts for deleted, suspended, confirmed'boolean' so PHP strict types work correctly in the middleware closure.
bootstrap/app.php Bootstrap Registered moodle.active middleware alias pointing to ValidateMoodleUserStatus::class.
.env / .env.example Config / Env Changed SESSION_DRIVER from database to array — prevents accidental writes to mdldf_sessions table via Laravel's session system.
tests/Unit/Shared/Middleware/ValidateMoodleUserStatusTest.php Test 8 unit tests covering: pass-through for unauthenticated/active users, 401 for deleted/unconfirmed, 403 for suspended, token revocation, cache behaviour, cache invalidation on revocation.
tests/Unit/Shared/Models/MoodleSessionTest.php Test 4 unit tests: correct table name, timestamps disabled, write protection throws LogicException, extends MoodleModel.

Rules Applied