M
Mr Sugiarto
Developer
31 Aug 2026 5 min read

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"`
}
  • UserID menyimpan ID pemilik post (foreign key ke tabel users).
  • Author *User adalah relasi GORM "belongs to" — dipakai untuk memuat data user terkait lewat Preload, bukan kolom sungguhan di tabel posts.
  • Field ini bertipe pointer (*User) dan bertag json:"author,omitempty" supaya kalau tidak di-preload, field ini tidak ikut muncul di response sebagai null.

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_id bernilai 0 karena 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.

M
Mr Sugiarto

Developer

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