commit d09c21a41fe7b2dc170b6f809d40c0c3613a1941 Author: [Valentino Heman Budiarto] <[hemanvalentino@gmail.com]> Date: Thu Feb 19 18:09:15 2026 +0700 Menyelesaikan fitur Login, Dashboard Mahasiswa, dan Admin Panel diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba7c2d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Mengabaikan file berat di Frontend +frontend/node_modules/ +frontend/.next/ + +# Mengabaikan file exe/build di Backend +backend/*.exe +backend/s-class-backend + +# Mengabaikan file environment (kalau nanti ada) +.env \ No newline at end of file diff --git a/backend/cmd/main.go b/backend/cmd/main.go new file mode 100644 index 0000000..2799723 --- /dev/null +++ b/backend/cmd/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "net/http" + "s-class-backend/config" + "s-class-backend/controllers" + "s-class-backend/middleware" + "s-class-backend/models" // <--- Pastikan ini ada! + + "github.com/gin-gonic/gin" +) + +func main() { + // 1. Konek Database + config.ConnectDatabase() + + // 2. AutoMigrate (Membuat tabel jika belum ada) + // Kita masukkan semua model di sini agar relasi terbentuk rapi + config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}) + + r := gin.Default() + + // 3. CORS Middleware (Agar Frontend bisa masuk) + r.Use(CORSMiddleware()) + + r.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Server S-CLASS Backend Berjalan!"}) + }) + + // 4. Routes + auth := r.Group("/api/auth") + { + auth.POST("/register", controllers.Register) + auth.POST("/login", controllers.Login) + } + + protected := r.Group("/api") + protected.Use(middleware.AuthMiddleware()) + { + protected.GET("/profile", func(c *gin.Context) { + userID, _ := c.Get("user_id") + role, _ := c.Get("role") + c.JSON(http.StatusOK, gin.H{"message": "Masuk!", "user_id": userID, "role": role}) + }) + + // Rooms + protected.GET("/rooms", controllers.GetRooms) + protected.POST("/rooms", controllers.CreateRoom) + + // Bookings + protected.POST("/bookings", controllers.CreateBooking) + protected.GET("/bookings", controllers.GetUserBookings) + protected.PATCH("/bookings/:id", controllers.UpdateBookingStatus) + } + + r.Run(":8080") +} + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + } +} \ No newline at end of file diff --git a/backend/config/database.go b/backend/config/database.go new file mode 100644 index 0000000..f925242 --- /dev/null +++ b/backend/config/database.go @@ -0,0 +1,48 @@ +package config + +import ( + "fmt" + "log" + "os" + "s-class-backend/models" // Import package models + + "github.com/joho/godotenv" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func ConnectDatabase() { + // Load .env + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + dbHost := os.Getenv("DB_HOST") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + dbPort := os.Getenv("DB_PORT") + + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta", dbHost, dbUser, dbPassword, dbName, dbPort) + + database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }) + if err != nil { + panic("Gagal koneksi ke database!") + } + + // Auto Migrate (Membuat Tabel Otomatis) + err = database.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}) + if err != nil { + fmt.Println("Gagal migrasi:", err) + } else { + fmt.Println("✅ Database Migrated!") + } + + fmt.Println("🚀 Sukses terkoneksi ke Database PostgreSQL!") + DB = database +} diff --git a/backend/controllers/authcontroller.go b/backend/controllers/authcontroller.go new file mode 100644 index 0000000..147dfa2 --- /dev/null +++ b/backend/controllers/authcontroller.go @@ -0,0 +1,107 @@ +package controllers + +import ( + "net/http" + "os" + "s-class-backend/config" + "s-class-backend/models" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +// Input Register +type RegisterInput struct { + NrpNip string `json:"nrp_nip" binding:"required"` + FullName string `json:"full_name" binding:"required"` + Email string `json:"email" binding:"required,email"` + Phone string `json:"phone" binding:"required"` + Password string `json:"password" binding:"required,min=6"` +} + +// Input Login +type LoginInput struct { + NrpNip string `json:"nrp_nip" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// --- FUNGSI REGISTER --- +func Register(c *gin.Context) { + var input RegisterInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) + + user := models.User{ + NrpNip: input.NrpNip, + FullName: input.FullName, + Email: input.Email, + Phone: input.Phone, + PasswordHash: string(hashedPassword), + Role: "student", + } + + if err := config.DB.Create(&user).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "NRP/NIP atau Email sudah terdaftar!"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Registrasi Berhasil!", "data": user}) +} + +// --- FUNGSI LOGIN (BARU) --- +func Login(c *gin.Context) { + var input LoginInput + + // 1. Cek Input + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 2. Cari User berdasarkan NRP + var user models.User + if err := config.DB.Where("nrp_nip = ?", input.NrpNip).First(&user).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "NRP atau Password salah!"}) + return + } + + // 3. Cek Password (Bandingkan Hash) + err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "NRP atau Password salah!"}) + return + } + + // 4. Bikin Token JWT (Tiket Masuk) + // Token berlaku selama 24 jam + expirationTime := time.Now().Add(24 * time.Hour) + claims := &jwt.MapClaims{ + "user_id": user.UserID, + "role": user.Role, + "exp": expirationTime.Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal membuat token"}) + return + } + + // 5. Kirim Token ke Pengguna + c.JSON(http.StatusOK, gin.H{ + "message": "Login Berhasil!", + "token": tokenString, + "user": gin.H{ + "full_name": user.FullName, + "role": user.Role, + }, + }) +} \ No newline at end of file diff --git a/backend/controllers/bookingcontroller.go b/backend/controllers/bookingcontroller.go new file mode 100644 index 0000000..6b215fc --- /dev/null +++ b/backend/controllers/bookingcontroller.go @@ -0,0 +1,145 @@ +package controllers + +import ( + "fmt" + "net/http" + "s-class-backend/config" + "s-class-backend/models" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type BookingInput struct { + RoomID uint `json:"room_id" binding:"required"` + StartTime time.Time `json:"start_time" binding:"required"` + EndTime time.Time `json:"end_time" binding:"required"` + Purpose string `json:"purpose" binding:"required"` +} + +func CreateBooking(c *gin.Context) { + var input BookingInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // --- PERBAIKAN DI SINI (Handle UUID) --- + // Ambil user_id dari context (hasil login) + userIDInterface, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID tidak ditemukan"}) + return + } + + // Konversi interface{} ke string dulu, baru ke UUID + userIDStr, ok := userIDInterface.(string) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Format User ID token salah"}) + return + } + + userID, err := uuid.Parse(userIDStr) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal memproses User ID"}) + return + } + // --------------------------------------- + + // Validasi Waktu + if input.EndTime.Before(input.StartTime) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Waktu selesai tidak boleh lebih awal dari waktu mulai!"}) + return + } + + // CEK BENTROK (Overlap Check) + var count int64 + config.DB.Model(&models.Booking{}).Where("room_id = ? AND status != 'Cancelled' AND ((start_time < ? AND end_time > ?) OR (start_time < ? AND end_time > ?) OR (start_time >= ? AND end_time <= ?))", + input.RoomID, input.EndTime, input.StartTime, input.EndTime, input.StartTime, input.StartTime, input.EndTime).Count(&count) + + if count > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "Ruangan sudah dibooking pada jam tersebut!"}) + return + } + + // Simpan Booking + booking := models.Booking{ + UserID: userID, + RoomID: input.RoomID, + StartTime: input.StartTime, + EndTime: input.EndTime, + Purpose: input.Purpose, + Status: "Pending", + } + + // Tampilkan error asli database jika gagal + if err := config.DB.Create(&booking).Error; err != nil { + fmt.Println("Error DB:", err) // Print error ke terminal + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Booking Berhasil diajukan!", + "data": booking, + }) +} +func GetUserBookings(c *gin.Context) { + var bookings []models.Booking + + // 1. Ambil User ID dan Role dari token yang sedang login + userID, _ := c.Get("user_id") + role, _ := c.Get("role") + + // 2. Cek apakah dia Admin + if role == "admin" { + // Jika ADMIN: Ambil SEMUA data peminjaman dari semua user, urutkan dari yang terbaru + // Kita juga ambil data Room dan User agar detailnya lengkap + if err := config.DB.Preload("Room").Preload("User").Order("created_at desc").Find(&bookings).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } else { + // Jika MAHASISWA: Ambil HANYA data peminjaman miliknya sendiri + if err := config.DB.Preload("Room").Where("user_id = ?", userID).Order("created_at desc").Find(&bookings).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + + c.JSON(http.StatusOK, gin.H{"data": bookings}) +} + +type UpdateStatusInput struct { + Status string `json:"status" binding:"required"` // Isinya: 'Approved', 'Rejected', 'Completed', 'Cancelled' +} + +// --- FUNGSI 3: UPDATE STATUS (ADMIN) --- +func UpdateBookingStatus(c *gin.Context) { + // 1. Ambil ID Booking dari URL (misal: /bookings/123) + bookingID := c.Param("id") + + // 2. Cek apakah booking ada? + var booking models.Booking + if err := config.DB.First(&booking, "booking_id = ?", bookingID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Booking tidak ditemukan"}) + return + } + + // 3. Validasi Input JSON + var input UpdateStatusInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 4. Update Status ke Database + booking.Status = input.Status + config.DB.Save(&booking) + + c.JSON(http.StatusOK, gin.H{ + "message": "Status booking berhasil diperbarui!", + "data": booking, + }) +} diff --git a/backend/controllers/roomcontroller.go b/backend/controllers/roomcontroller.go new file mode 100644 index 0000000..fd4f261 --- /dev/null +++ b/backend/controllers/roomcontroller.go @@ -0,0 +1,33 @@ +package controllers + +import ( + "net/http" + "s-class-backend/config" + "s-class-backend/models" + + "github.com/gin-gonic/gin" +) + +// --- GET ALL ROOMS --- +func GetRooms(c *gin.Context) { + var rooms []models.Room + if err := config.DB.Find(&rooms).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal mengambil data ruangan"}) + return + } + c.JSON(http.StatusOK, gin.H{"data": rooms}) +} + +// --- CREATE ROOM --- +func CreateRoom(c *gin.Context) { + var input models.Room + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := config.DB.Create(&input).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal membuat ruangan"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Ruangan berhasil ditambahkan!", "data": input}) +} \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..aed8b2a --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,50 @@ +module s-class-backend + +go 1.25.6 + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..3c032c3 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,107 @@ +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/backend/middleware/auth_middleware.go b/backend/middleware/auth_middleware.go new file mode 100644 index 0000000..04d24f7 --- /dev/null +++ b/backend/middleware/auth_middleware.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "fmt" + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 1. Ambil Header "Authorization" + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Butuh Token untuk akses ini!"}) + c.Abort() + return + } + + // 2. Format token biasanya "Bearer " + // Kita pisahkan kata "Bearer" dan ambil tokennya saja + tokenString := strings.Split(authHeader, " ") + if len(tokenString) != 2 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Format Token salah!"}) + c.Abort() + return + } + + // 3. Validasi Token dengan Secret Key kita + token, err := jwt.Parse(tokenString[1], func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("metode sign tidak valid") + } + return []byte(os.Getenv("JWT_SECRET")), nil + }) + + // 4. Jika token valid, ambil data user di dalamnya + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + // Simpan user_id ke dalam konteks agar bisa dipakai di controller nanti + c.Set("user_id", claims["user_id"]) + c.Set("role", claims["role"]) + c.Next() // Lanjut ke proses berikutnya + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Token tidak valid: " + err.Error()}) + c.Abort() + } + } +} \ No newline at end of file diff --git a/backend/models/entity.go b/backend/models/entity.go new file mode 100644 index 0000000..667d688 --- /dev/null +++ b/backend/models/entity.go @@ -0,0 +1,46 @@ +package models + +import ( + "time" + + "github.com/google/uuid" // Pastikan import ini ada +) + +type User struct { + // WAJIB UUID + UserID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"user_id"` + NrpNip string `gorm:"unique;not null" json:"nrp_nip"` + FullName string `gorm:"not null" json:"full_name"` + Email string `gorm:"unique;not null" json:"email"` + Phone string `gorm:"not null" json:"phone"` + PasswordHash string `gorm:"not null" json:"-"` + Role string `gorm:"type:user_role;default:'student'" json:"role"` + CreatedAt time.Time `json:"created_at"` +} + +type Room struct { + RoomID uint `gorm:"primaryKey" json:"room_id"` + Name string `gorm:"not null" json:"name"` + Category string `gorm:"not null" json:"category"` + Capacity int `gorm:"not null" json:"capacity"` + Floor string `gorm:"not null" json:"floor"` + Status string `gorm:"type:room_status;default:'Available'" json:"status"` +} + +type Booking struct { + BookingID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"booking_id"` + + // Tambahkan references:UserID + UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` + User User `gorm:"foreignKey:UserID;references:UserID" json:"user,omitempty"` + + // Tambahkan references:RoomID + RoomID uint `gorm:"not null" json:"room_id"` + Room Room `gorm:"foreignKey:RoomID;references:RoomID" json:"room"` + + StartTime time.Time `gorm:"not null" json:"start_time"` + EndTime time.Time `gorm:"not null" json:"end_time"` + Purpose string `gorm:"not null" json:"purpose"` + Status string `gorm:"type:booking_status;default:'Pending'" json:"status"` + CreatedAt time.Time `json:"created_at"` +} \ No newline at end of file diff --git a/frontend b/frontend new file mode 160000 index 0000000..557ce3b --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 557ce3b43798549993dbd59b8afa3d053356e57d