Sejak Part 9 kita sudah mencatat sebuah keterbatasan yang disengaja: endpoint Update dan Delete Post cuma mensyaratkan "sudah login", bukan "pemilik post ini". Siapapun yang berhasil login bisa mengubah atau menghapus post siapa saja. Ini karena Post belum tahu siapa penulisnya. Di part ini kita perbaiki itu dengan menambahkan relasi ke User.
Menambahkan Kolom Author ke Model Post
Update models/post.go:
package models
import "gorm.io/gorm"
type Post struct {
gorm.Model
Title string `gorm:"size:255;not null" json:"title"`
Slug string `gorm:"size:255;uniqueIndex;not null" json:"slug"`
Body string `gorm:"type:text" json:"body"`
UserID uint `gorm:"index" json:"user_id"`
Author *User `gorm:"foreignKey:UserID" json:"author,omitempty"`
}
UserIDmenyimpan ID pemilik post (foreign key ke tabelusers).Author *Useradalah relasi GORM "belongs to" — dipakai untuk memuat data user terkait lewatPreload, bukan kolom sungguhan di tabelposts.- Field ini bertipe pointer (
*User) dan bertagjson:"author,omitempty"supaya kalau tidak di-preload, field ini tidak ikut muncul di response sebagainull.
Pastikan urutan AutoMigrate menempatkan User sebelum Post, supaya tabel yang menjadi rujukan foreign key sudah ada duluan:
func Migrate(db *gorm.DB) {
db.AutoMigrate(&models.User{}, &models.RefreshToken{}, &models.Post{})
}
Catatan: post-post yang sudah dibuat sebelum part ini (dari data seeder atau testing part-part awal) akan punya
user_idbernilai0karena memang belum ada pemiliknya saat dibuat — itu normal, bukan bug. Cukup buat post baru untuk melihat relasi Author bekerja.
Mengisi Author Saat Create
Ambil user_id yang sudah disimpan middleware AuthRequired di c.Locals, lalu isi ke Post saat dibuat. Update PostHandler.Create:
post := models.Post{
Title: req.Title,
Slug: req.Slug,
Body: req.Body,
UserID: c.Locals("user_id").(uint),
}
Memvalidasi Kepemilikan di Update & Delete
Ini bagian intinya. Setelah mengambil post dari database, bandingkan post.UserID dengan user yang sedang login:
func (h *PostHandler) Update(c *fiber.Ctx) error {
var post models.Post
id := c.Params("id")
if err := h.DB.First(&post, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return response.Error(c, fiber.StatusNotFound, "post tidak ditemukan")
}
return response.Error(c, fiber.StatusInternalServerError, "gagal mengambil data post")
}
if post.UserID != c.Locals("user_id").(uint) {
return response.Error(c, fiber.StatusForbidden, "anda tidak berhak mengubah post ini")
}
// ...sisanya sama seperti sebelumnya
}
Logic yang sama ditambahkan di Delete, dengan pesan "anda tidak berhak menghapus post ini". Urutan pengecekan penting: kita cek RecordNotFound dulu (404) baru kepemilikan (403) — supaya user yang mencoba akses post yang memang tidak ada tetap dapat pesan "tidak ditemukan", bukan "tidak berhak" (yang secara tidak sengaja mengonfirmasi post itu ada tapi bukan miliknya).
Menampilkan Data Author
Tambahkan Preload("Author") di Index dan Show supaya data penulis ikut termuat:
// Index
query.Preload("Author").Order("id desc").Offset(offset).Limit(perPage).Find(&posts)
// Show
h.DB.Preload("Author").First(&post, id)
Preload di GORM menjalankan query tambahan untuk mengambil data relasi (di sini: User terkait) dan otomatis menempelkannya ke field Author. Karena User.Password sudah bertag json:"-" sejak Part 8, data password tidak akan pernah ikut bocor lewat response ini walaupun seluruh struct User di-embed.
Testing
# Register user kedua untuk testing kepemilikan
curl -X POST http://127.0.0.1:3000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Siti Aminah","email":"siti@example.com","password":"rahasia123"}'
# Login sebagai kedua user, simpan masing-masing access_token
BUDI_TOKEN=...
SITI_TOKEN=...
# Budi membuat post
curl -X POST http://127.0.0.1:3000/api/v1/posts \
-H "Content-Type: application/json" -H "Authorization: Bearer $BUDI_TOKEN" \
-d '{"title":"Post Milik Budi","slug":"post-milik-budi","body":"isi"}'
# Siti mencoba update post milik Budi -> harus 403
curl -i -X PUT http://127.0.0.1:3000/api/v1/posts/17 \
-H "Content-Type: application/json" -H "Authorization: Bearer $SITI_TOKEN" \
-d '{"title":"Hacked","slug":"post-milik-budi","body":"hacked"}'
# Budi update post miliknya sendiri -> harus 200
curl -i -X PUT http://127.0.0.1:3000/api/v1/posts/17 \
-H "Content-Type: application/json" -H "Authorization: Bearer $BUDI_TOKEN" \
-d '{"title":"Post Milik Budi Updated","slug":"post-milik-budi","body":"isi updated"}'
Percobaan Siti akan dibalas 403 Forbidden dengan pesan "anda tidak berhak mengubah post ini", sementara Budi tetap bisa mengubah post miliknya sendiri seperti biasa. Cek juga GET /api/v1/posts/17 — response sekarang menyertakan objek author berisi nama dan email penulis (tanpa password).
Celah otorisasi yang sudah kita catat sejak Part 9 akhirnya tertutup. Di part berikutnya kita tambahkan kategori/tag untuk mengelompokkan post.
Bagian dari Series: GoFiber Dasar - Belajar Fundamental Lewat REST API Blog
Belajar konsep-konsep dasar GoFiber (routing, handler, GORM, MySQL, JWT auth) dengan membangun REST API blog sederhana dari nol - lengkap dengan auten...
Lihat Series Lengkap