Sejak awal seri ini, tabel posts kita belum punya hubungan sama sekali dengan tabel users — padahal secara logika, setiap post harusnya punya penulis (author). Di part ini kita akan menghubungkan Post dengan User yang membuatnya, sekaligus menambahkan proteksi supaya user hanya bisa mengubah atau menghapus post miliknya sendiri.
Menambahkan Kolom user_id
Buat migration baru untuk menambahkan kolom user_id ke tabel posts yang sudah ada:
go run . artisan make:migration add_user_id_to_posts_table
Isi migration-nya:
package migrations
import (
"github.com/goravel/framework/contracts/database/schema"
"github.com/goravel/framework/facades"
)
type M20260818000001AddUserIdToPostsTable struct{}
func (r *M20260818000001AddUserIdToPostsTable) Signature() string {
return "20260818000001_add_user_id_to_posts_table"
}
func (r *M20260818000001AddUserIdToPostsTable) Up() error {
if facades.Schema().HasTable("posts") && !facades.Schema().HasColumn("posts", "user_id") {
return facades.Schema().Table("posts", func(table schema.Blueprint) {
table.UnsignedBigInteger("user_id").Nullable()
})
}
return nil
}
func (r *M20260818000001AddUserIdToPostsTable) Down() error {
return facades.Schema().Table("posts", func(table schema.Blueprint) {
table.DropColumn("user_id")
})
}
Bedanya dengan migration yang pernah kita buat sebelumnya: kali ini kita menggunakan facades.Schema().Table(...) (bukan Create) karena tabel posts sudah ada, kita hanya menambahkan satu kolom baru ke tabel yang sudah ada.
Satu hal penting yang sering terlewat: migration baru harus didaftarkan manual di database/kernel.go, karena Goravel tidak otomatis scan folder migrations seperti Laravel:
func (kernel Kernel) Migrations() []schema.Migration {
return []schema.Migration{
&migrations.M20210101000001CreateUsersTable{},
&migrations.M20210101000002CreateJobsTable{},
&migrations.M20250930173224CreatePostsTable{},
&migrations.M20260818000001AddUserIdToPostsTable{}, // baru ditambahkan
}
}
Kalau lupa langkah ini, go run . artisan migrate akan selalu bilang "Nothing to migrate" walaupun file migration-nya sudah ada.
Jalankan migration:
go run . artisan migrate
Update Model
Tambahkan field UserID dan User di app/models/post.go:
type Post struct {
orm.Model
Title string `json:"title"`
Body string `json:"body"`
Status string `json:"status"`
UserID uint `json:"user_id"`
User User `json:"user,omitempty"`
}
Goravel (lewat GORM di baliknya) otomatis mengenali relasi belongs to dari pasangan field UserID + User tanpa perlu tag tambahan, selama penamaannya mengikuti konvensi ini.
Di app/models/user.go, tambahkan juga relasi kebalikannya (opsional) dan yang lebih penting: sembunyikan password dari response JSON:
type User struct {
orm.Model
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"`
Posts []Post `json:"posts,omitempty"`
}
Tag json:"-" di sini krusial. Sebelumnya field Password tidak pernah bocor karena Register/Login membangun response secara manual (hanya field id/name/email). Tapi begitu kita mulai eager-load relasi User di endpoint post, tanpa tag ini hash password akan ikut terekspos di response API.
Set Author Saat Membuat Post
Di app/http/controllers/post_controller.go, gunakan helper baru untuk mengambil user_id dari token JWT yang sudah divalidasi middleware, lalu isi field UserID saat membuat post:
post := models.Post{
Title: ctx.Request().Input("title"),
Body: ctx.Request().Input("body"),
Status: "DRAFT",
UserID: helpers.AuthUserID(ctx),
}
helpers.AuthUserID adalah fungsi baru di app/helpers/auth_helper.go:
package helpers
import (
"strconv"
"github.com/goravel/framework/contracts/http"
)
func AuthUserID(ctx http.Context) uint {
userID, ok := ctx.Value("user_id").(string)
if !ok {
return 0
}
id, err := strconv.Atoi(userID)
if err != nil {
return 0
}
return uint(id)
}
Ingat kembali jwt_middleware.go di Part 8 — di situ kita menyimpan user_id ke context lewat ctx.WithValue("user_id", claims.UserID). Fungsi AuthUserID inilah yang membaca kembali nilai tersebut dan mengonversinya ke uint supaya cocok dengan tipe UserID di model.
Eager Load Author di Index dan Show
Supaya response endpoint list dan detail post ikut menyertakan data author, tambahkan .With("User") pada query:
query := facades.Orm().Query().With("User")
err := facades.Orm().Query().With("User").Where("id", id).First(&post)
Proteksi Kepemilikan Post
Sekarang kita tahu siapa pemilik tiap post, kita bisa pastikan hanya pemiliknya yang boleh mengubah atau menghapusnya. Tambahkan pengecekan ini di method Update dan Destroy, tepat setelah post ditemukan:
if post.UserID != helpers.AuthUserID(ctx) {
return helpers.Error(ctx, http.StatusForbidden, "You are not allowed to update this post", nil)
}
(Untuk Destroy, pesannya disesuaikan menjadi "...delete this post".)
Testing
- Register dua user berbeda, simpan kedua token-nya.
- Buat post pakai token user pertama — cek field
user_iddi response. - Coba
GET /posts— pastikan fielduserdi tiap post sudah terisi nama & email author (tanpa password). - Coba
PUT /posts/{id}pakai token user kedua ke post milik user pertama — harus dapat response403 Forbidden.
Penutup
Relasi Post-User ini jadi fondasi buat fitur-fitur berikutnya yang butuh konsep "kepemilikan data", seperti Comment (Part 12) dan Role & Permission (Part 16).
Bagian dari Series: Seri Tutorial Belajar Framework Goravel Rest API untuk Pemula
Overview Seri ini membahas cara membangun REST API menggunakan Goravel, framework web berbasis Golang yang terinspirasi dari Laravel. Materi mencakup...
Lihat Series Lengkap