Home Paket Belajar Bootcamp Instruktur

Tutorial Laravel Authorization #9 - API Lengkap dengan Sanctum + Spatie Permission

Pelajari sistem Authorization di Laravel dari nol hingga implementasi dalam studi kasus nyata melalui 5 episode terstruktur. Ebook ini membahas konsep Authentication vs Authorization, Gates, Policies, fitur lanjutan, hingga menggabungkan seluruh konsep dalam sebuah aplikasi blog. Setiap materi dilengkapi penjelasan konsep, contoh kode siap pakai, tabel perbandingan, dan praktik terbaik agar mudah dipahami oleh developer Laravel pemula maupun menengah.

✅ Telah dilihat 37 kali

Rating: 5.00 ⭐

... 11 June 2026, 14:31

Di episode penutup ini, kita rangkum semua materi series dengan membangun API lengkap:

  • Autentikasi via Sanctum
  • Role & permission via Spatie
  • CRUD endpoint Posts dan Users
  • Postman collection siap pakai untuk testing

Skenario Project

API untuk aplikasi blog dengan tiga jenis user:

Role Hak Akses API
admin Semua endpoint, termasuk manajemen user
editor CRUD post milik sendiri, tidak bisa kelola user
viewer Hanya GET post yang published
Guest Hanya POST /login

Struktur File

app/Http/Controllers/Api/
├── AuthController.php
├── PostController.php
└── Admin/
    └── UserController.php

routes/
└── api.php

database/seeders/
├── RolePermissionSeeder.php
└── DatabaseSeeder.php

Step 1 — Permissions & Seeder Final

database/seeders/RolePermissionSeeder.php:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class RolePermissionSeeder extends Seeder
{
    public function run(): void
    {
        app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

        $permissions = [
            'post.viewAny', 'post.view', 'post.create', 'post.update', 'post.delete',
            'user.viewAny', 'user.view', 'user.create', 'user.update', 'user.delete',
        ];

        foreach ($permissions as $perm) {
            Permission::firstOrCreate(['name' => $perm]);
        }

        Role::firstOrCreate(['name' => 'admin'])->givePermissionTo(Permission::all());

        Role::firstOrCreate(['name' => 'editor'])->givePermissionTo([
            'post.viewAny', 'post.view', 'post.create', 'post.update', 'post.delete',
        ]);

        Role::firstOrCreate(['name' => 'viewer'])->givePermissionTo([
            'post.viewAny', 'post.view',
        ]);
    }
}

database/seeders/DatabaseSeeder.php:

<?php

namespace Database\Seeders;

use App\Models\Post;
use App\Models\User;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call(RolePermissionSeeder::class);

        // Buat 3 user untuk testing
        $admin = User::firstOrCreate(
            ['email' => '[email protected]'],
            ['name' => 'Admin', 'password' => bcrypt('password')]
        );
        $admin->assignRole('admin');

        $editor = User::firstOrCreate(
            ['email' => '[email protected]'],
            ['name' => 'Editor', 'password' => bcrypt('password')]
        );
        $editor->assignRole('editor');

        $viewer = User::firstOrCreate(
            ['email' => '[email protected]'],
            ['name' => 'Viewer', 'password' => bcrypt('password')]
        );
        $viewer->assignRole('viewer');

        // Buat dummy posts
        Post::factory()->count(10)->create(['user_id' => $editor->id, 'status' => 'published']);
        Post::factory()->count(3)->create(['user_id' => $editor->id, 'status' => 'draft']);
    }
}

Step 2 — Routes API Final

routes/api.php:

<?php

use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PostController;
use App\Http\Controllers\Api\Admin\UserController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Public Routes
|--------------------------------------------------------------------------
*/
Route::post('/login', [AuthController::class, 'login']);

/*
|--------------------------------------------------------------------------
| Authenticated Routes
|--------------------------------------------------------------------------
*/
Route::middleware('auth:sanctum')->group(function () {

    // Auth
    Route::post('/logout', [AuthController::class, 'logout']);
    Route::get('/me', [AuthController::class, 'me']);

    // Posts
    Route::get('/posts', [PostController::class, 'index']);
    Route::get('/posts/{post}', [PostController::class, 'show']);

    Route::post('/posts', [PostController::class, 'store'])
        ->middleware('permission:post.create');

    Route::put('/posts/{post}', [PostController::class, 'update'])
        ->middleware('permission:post.update');

    Route::delete('/posts/{post}', [PostController::class, 'destroy'])
        ->middleware('permission:post.delete');

    // Admin only
    Route::middleware('role:admin')
        ->prefix('admin')
        ->name('admin.')
        ->group(function () {
            Route::apiResource('users', UserController::class);
        });
});

Step 3 — AuthController Final

app/Http/Controllers/Api/AuthController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email'    => 'required|email',
            'password' => 'required|string',
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Email atau password salah.'],
            ]);
        }

        $user->tokens()->delete();

        $token = $user->createToken(
            name: 'api-token',
            abilities: $user->getAllPermissions()->pluck('name')->toArray()
        )->plainTextToken;

        return response()->json([
            'message' => 'Login berhasil.',
            'token'   => $token,
            'user'    => $this->userData($user),
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();

        return response()->json(['message' => 'Logout berhasil.']);
    }

    public function me(Request $request): JsonResponse
    {
        return response()->json([
            'user' => $this->userData($request->user()),
        ]);
    }

    // Helper — format data user yang konsisten
    private function userData(User $user): array
    {
        return [
            'id'          => $user->id,
            'name'        => $user->name,
            'email'       => $user->email,
            'roles'       => $user->getRoleNames(),
            'permissions' => $user->getAllPermissions()->pluck('name'),
        ];
    }
}

Step 4 — PostController Final

app/Http/Controllers/Api/PostController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $posts = Post::with('user:id,name')
            ->when(! $request->user()->hasRole('admin'), function ($q) use ($request) {
                // Admin lihat semua; selainnya hanya published + milik sendiri
                $q->where('status', 'published')
                  ->orWhere('user_id', $request->user()->id);
            })
            ->latest()
            ->paginate(10);

        return response()->json($posts);
    }

    public function show(Request $request, Post $post): JsonResponse
    {
        // Draft hanya boleh dilihat pemilik atau admin
        if ($post->status === 'draft'
            && $post->user_id !== $request->user()->id
            && ! $request->user()->hasRole('admin')
        ) {
            return response()->json(['message' => 'Post tidak ditemukan.'], 404);
        }

        return response()->json([
            'data' => $post->load('user:id,name'),
        ]);
    }

    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'title'   => 'required|string|max:255',
            'content' => 'required|string',
            'status'  => 'required|in:draft,published',
        ]);

        $post = $request->user()->posts()->create($validated);

        return response()->json([
            'message' => 'Postingan berhasil dibuat.',
            'data'    => $post,
        ], 201);
    }

    public function update(Request $request, Post $post): JsonResponse
    {
        // Hanya pemilik atau admin yang bisa update
        if ($post->user_id !== $request->user()->id && ! $request->user()->hasRole('admin')) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $validated = $request->validate([
            'title'   => 'sometimes|string|max:255',
            'content' => 'sometimes|string',
            'status'  => 'sometimes|in:draft,published',
        ]);

        $post->update($validated);

        return response()->json([
            'message' => 'Postingan berhasil diperbarui.',
            'data'    => $post->fresh(),
        ]);
    }

    public function destroy(Request $request, Post $post): JsonResponse
    {
        // Hanya pemilik atau admin yang bisa hapus
        if ($post->user_id !== $request->user()->id && ! $request->user()->hasRole('admin')) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $post->delete();

        return response()->json(['message' => 'Postingan berhasil dihapus.']);
    }
}

Step 5 — Admin UserController

app/Http/Controllers/Api/Admin/UserController.php:

<?php

namespace App\Http\Controllers\Api\Admin;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;

class UserController extends Controller
{
    public function index(): JsonResponse
    {
        $users = User::with('roles')->latest()->paginate(15);
        return response()->json($users);
    }

    public function show(User $user): JsonResponse
    {
        return response()->json([
            'data' => $user->load('roles', 'permissions'),
        ]);
    }

    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name'     => 'required|string|max:255',
            'email'    => 'required|email|unique:users,email',
            'password' => 'required|string|min:8',
            'role'     => 'required|exists:roles,name',
        ]);

        $user = User::create([
            'name'     => $validated['name'],
            'email'    => $validated['email'],
            'password' => bcrypt($validated['password']),
        ]);

        $user->assignRole($validated['role']);

        return response()->json([
            'message' => 'User berhasil dibuat.',
            'data'    => $user->load('roles'),
        ], 201);
    }

    public function update(Request $request, User $user): JsonResponse
    {
        $validated = $request->validate([
            'name'  => 'sometimes|string|max:255',
            'email' => "sometimes|email|unique:users,email,{$user->id}",
            'role'  => 'sometimes|exists:roles,name',
        ]);

        $user->update(collect($validated)->except('role')->toArray());

        if (isset($validated['role'])) {
            $user->syncRoles([$validated['role']]);
        }

        return response()->json([
            'message' => 'User berhasil diperbarui.',
            'data'    => $user->fresh()->load('roles'),
        ]);
    }

    public function destroy(User $user): JsonResponse
    {
        if ($user->id === auth()->id()) {
            return response()->json(['message' => 'Tidak bisa menghapus akun sendiri.'], 403);
        }

        $user->tokens()->delete();
        $user->delete();

        return response()->json(['message' => 'User berhasil dihapus.']);
    }
}

Step 6 — Error Handler Final

bootstrap/app.php:

->withExceptions(function (Exceptions $exceptions) {
    $isApiRequest = fn($request) => $request->is('api/*') || $request->expectsJson();

    $exceptions->render(function (
        \Illuminate\Auth\AuthenticationException $e, $request
    ) use ($isApiRequest) {
        if ($isApiRequest($request)) {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }
    });

    $exceptions->render(function (
        \Illuminate\Auth\Access\AuthorizationException $e, $request
    ) use ($isApiRequest) {
        if ($isApiRequest($request)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }
    });

    $exceptions->render(function (
        \Spatie\Permission\Exceptions\UnauthorizedException $e, $request
    ) use ($isApiRequest) {
        if ($isApiRequest($request)) {
            return response()->json([
                'message'  => 'Forbidden. Role atau permission tidak mencukupi.',
                'required' => $e->getRequiredRoles(),
            ], 403);
        }
    });

    $exceptions->render(function (
        \Illuminate\Validation\ValidationException $e, $request
    ) use ($isApiRequest) {
        if ($isApiRequest($request)) {
            return response()->json([
                'message' => 'Validasi gagal.',
                'errors'  => $e->errors(),
            ], 422);
        }
    });
})

Postman Collection

Simpan sebagai api.postman_collection.json lalu import ke Postman.

{
  "info": {
    "name": "Blog API — Sanctum + Spatie",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "base_url", "value": "http://localhost:8000/api" },
    { "key": "token", "value": "" }
  ],
  "item": [
    {
      "name": "Auth",
      "item": [
        {
          "name": "Login sebagai Admin",
          "event": [{ "listen": "test", "script": { "exec": [
            "const res = pm.response.json();",
            "pm.collectionVariables.set('token', res.token);"
          ]}}],
          "request": {
            "method": "POST",
            "url": "{{base_url}}/login",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"email\":\"[email protected]\",\"password\":\"password\"}" }
          }
        },
        {
          "name": "Login sebagai Editor",
          "event": [{ "listen": "test", "script": { "exec": [
            "const res = pm.response.json();",
            "pm.collectionVariables.set('token', res.token);"
          ]}}],
          "request": {
            "method": "POST",
            "url": "{{base_url}}/login",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"email\":\"[email protected]\",\"password\":\"password\"}" }
          }
        },
        {
          "name": "Login sebagai Viewer",
          "event": [{ "listen": "test", "script": { "exec": [
            "const res = pm.response.json();",
            "pm.collectionVariables.set('token', res.token);"
          ]}}],
          "request": {
            "method": "POST",
            "url": "{{base_url}}/login",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"email\":\"[email protected]\",\"password\":\"password\"}" }
          }
        },
        {
          "name": "Me",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/me",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        },
        {
          "name": "Logout",
          "request": {
            "method": "POST",
            "url": "{{base_url}}/logout",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        }
      ]
    },
    {
      "name": "Posts",
      "item": [
        {
          "name": "GET semua post",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/posts",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        },
        {
          "name": "GET satu post",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/posts/1",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        },
        {
          "name": "POST buat post baru",
          "request": {
            "method": "POST",
            "url": "{{base_url}}/posts",
            "header": [
              { "key": "Authorization", "value": "Bearer {{token}}" },
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": { "mode": "raw", "raw": "{\"title\":\"Post Baru\",\"content\":\"Isi konten di sini.\",\"status\":\"published\"}" }
          }
        },
        {
          "name": "PUT update post",
          "request": {
            "method": "PUT",
            "url": "{{base_url}}/posts/1",
            "header": [
              { "key": "Authorization", "value": "Bearer {{token}}" },
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": { "mode": "raw", "raw": "{\"title\":\"Judul Diperbarui\",\"status\":\"draft\"}" }
          }
        },
        {
          "name": "DELETE hapus post",
          "request": {
            "method": "DELETE",
            "url": "{{base_url}}/posts/1",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        }
      ]
    },
    {
      "name": "Admin — Users",
      "item": [
        {
          "name": "GET semua user",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/admin/users",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        },
        {
          "name": "POST buat user baru",
          "request": {
            "method": "POST",
            "url": "{{base_url}}/admin/users",
            "header": [
              { "key": "Authorization", "value": "Bearer {{token}}" },
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": { "mode": "raw", "raw": "{\"name\":\"User Baru\",\"email\":\"[email protected]\",\"password\":\"password\",\"role\":\"viewer\"}" }
          }
        },
        {
          "name": "PUT update role user",
          "request": {
            "method": "PUT",
            "url": "{{base_url}}/admin/users/3",
            "header": [
              { "key": "Authorization", "value": "Bearer {{token}}" },
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": { "mode": "raw", "raw": "{\"role\":\"editor\"}" }
          }
        },
        {
          "name": "DELETE hapus user",
          "request": {
            "method": "DELETE",
            "url": "{{base_url}}/admin/users/3",
            "header": [{ "key": "Authorization", "value": "Bearer {{token}}" }]
          }
        }
      ]
    }
  ]
}

Ringkasan Keseluruhan Series

Episode Materi
1 Pengenalan Authorization — Authentication vs Authorization, Gates vs Policies
2 Gates — define, controller, Blade
3 Policies — Artisan, struktur, auto-discovery, controller & Blade
4 Fitur lanjutan — before(), guest, middleware route, Form Request
5 Mini project blog — Gates + Policies lengkap
6 Spatie Laravel Permission — instalasi, seeder, operasi dasar
7 UI manajemen role & permission dari panel admin
8 Spatie + Sanctum untuk API — token abilities, error handling JSON
9 Mini project API lengkap + Postman collection

Langkah Selanjutnya

Setelah menguasai authorization dari dasar hingga API, kamu bisa lanjut ke:

  • Multi-tenancy — memisahkan data dan hak akses antar organisasi/sekolah dalam satu instalasi
  • Laravel Passport — jika project butuh OAuth2 penuh (login via Google, third-party access)
  • Row-level security — menggunakan Policies + Scopes agar user hanya bisa query data miliknya
  • Audit trail — mencatat siapa mengubah apa, kapan, menggunakan package spatie/laravel-activitylog

Selamat! Kamu sudah menguasai authorization Laravel dari Gates/Policies hingga Spatie Permission + API.

Daftar eBook