From 42e9422e6f9a0db2e8fe83694a2d042016ef7d90 Mon Sep 17 00:00:00 2001 From: Fatih Maulana <85884851+NoBody313@users.noreply.github.com> Date: Tue, 17 Dec 2024 00:33:25 +0700 Subject: [PATCH 1/4] Perbaikan File gw ga bisa clone, jadinya gw upload file ke sini biar ke commit, gw tau lu dah tidur, jadi gudnet yaaakkk. gw mau tidur juga hbs push ini. --- config/database.go | 68 ++++ controllers/achievementController.go | 339 ++++------------- controllers/activityController.go | 326 +++++------------ controllers/authAdminController.go | 58 +++ controllers/newsController.go | 482 +++---------------------- middlewares/auth.go | 45 +++ middlewares/middleware.go | 94 ++--- models/achievement.go | 11 + models/activities.go | 9 + models/news.go | 13 + models/user.go | 40 ++ repositories/achievement_repository.go | 107 ++++++ repositories/activity_repository.go | 62 ++++ repositories/news_repository.go | 64 ++++ repositories/user_repository.go | 40 ++ routes/achievement_routes.go | 19 + routes/activity_routes.go | 27 ++ routes/auth_routes.go | 13 + routes/news_routes.go | 15 + routes/routes.go | 110 ++---- services/achievement_service.go | 47 +++ services/activity_service.go | 42 +++ services/auth_service.go | 37 ++ services/news_service.go | 49 +++ utils/auth.go | 31 ++ utils/error.go | 12 + utils/file_validation.go | 34 ++ utils/helper.go | 106 +++--- utils/token.go | 38 ++ utils/token/token.go | 206 +++++------ 30 files changed, 1334 insertions(+), 1210 deletions(-) create mode 100644 config/database.go create mode 100644 controllers/authAdminController.go create mode 100644 middlewares/auth.go create mode 100644 models/achievement.go create mode 100644 models/activities.go create mode 100644 models/news.go create mode 100644 models/user.go create mode 100644 repositories/achievement_repository.go create mode 100644 repositories/activity_repository.go create mode 100644 repositories/news_repository.go create mode 100644 repositories/user_repository.go create mode 100644 routes/achievement_routes.go create mode 100644 routes/activity_routes.go create mode 100644 routes/auth_routes.go create mode 100644 routes/news_routes.go create mode 100644 services/achievement_service.go create mode 100644 services/activity_service.go create mode 100644 services/auth_service.go create mode 100644 services/news_service.go create mode 100644 utils/auth.go create mode 100644 utils/error.go create mode 100644 utils/file_validation.go create mode 100644 utils/token.go diff --git a/config/database.go b/config/database.go new file mode 100644 index 0000000..2403eb8 --- /dev/null +++ b/config/database.go @@ -0,0 +1,68 @@ +package config + +import ( + "backend/models" + "backend/utils" + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "log" +) + +// DatabaseConfig stores the database configuration +type DatabaseConfig struct { + Username string + Password string + Database string + Host string +} + +// GetDatabaseConfig retrieves the database configuration from environment variables +func GetDatabaseConfig() DatabaseConfig { + return DatabaseConfig{ + Username: utils.Getenv("DB_USERNAME", ""), + Password: utils.Getenv("DB_PASSWORD", ""), + Database: utils.Getenv("DB_DATABASE", ""), + Host: utils.Getenv("DB_HOST", ""), + } +} + +// ConnectDatabase establishes a connection to the database +func ConnectDatabase() *gorm.DB { + // Get the database configuration + config := GetDatabaseConfig() + + // Create the Data Source Name (DSN) string for the connection + dsn := fmt.Sprintf( + "%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.Username, + config.Password, + config.Host, + config.Database, + ) + + // Open a connection to the MySQL database + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect to database: ", err) + } + + // Automatically migrate the database schema + AutoMigrate(db) + + return db +} + +// AutoMigrate performs automatic migration of database schema +func AutoMigrate(db *gorm.DB) { + // Run migrations for the models + err := db.AutoMigrate( + &models.Achievement{}, + &models.Activities{}, + &models.News{}, + &models.User{}, + ) + if err != nil { + log.Fatal("Failed to auto migrate: ", err) + } +} diff --git a/controllers/achievementController.go b/controllers/achievementController.go index c90fcd8..4cfdc2a 100644 --- a/controllers/achievementController.go +++ b/controllers/achievementController.go @@ -2,317 +2,130 @@ package controllers import ( "backend/models" + "backend/services" "backend/utils" - "fmt" "net/http" - "os" - "path/filepath" "github.com/gin-gonic/gin" "github.com/google/uuid" - "gorm.io/gorm" ) -type AchievementsController struct { - DB *gorm.DB +// AchievementController menangani request terkait achievements +type AchievementController struct { + Service services.AchievementService } -// GetAllAchievements adalah fungsi untuk mendapatkan semua achievements dari database. -// @Summary Get All Achievement -// @Description Retrieves all achievements from the database. -// @Tags Achievement -// @Produce json -// @Success 200 {array} models.Achievement -// @Router /achievements [get] -func (ac *AchievementsController) GetAllAchievement(ctx *gin.Context) { - var achievements[]models.Achievement - - // Retrieve all achievementsfrom the database - if err := ac.DB.Find(&achievements).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get achievements from database"}) - return - } - - // Response with achievements - ctx.JSON(http.StatusOK, achievements) +// NewAchievementController membuat instance baru dari AchievementController +func NewAchievementController(service services.AchievementService) *AchievementController { + return &AchievementController{Service: service} } -// GetAchievementById adalah fungsi untuk mengambil achievements berdasarkan ID. -// @Summary Get achievement By ID -// @Description Retrieves achievement data by its ID. -// @Tags Achievement -// @Param id path string true "achievement ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /achievements/{id} [get] -func (ac *AchievementsController) GetAchievementById(ctx *gin.Context) { - // Get achievement ID from URL path parameter - achievementsId := ctx.Param("id") - - // Retrieve achievement from the database by its ID - var achievements models.Achievement - if err := ac.DB.Where("id = ?", achievementsId).First(&achievements).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "achievements not found"}) +// GetAllAchievements mengambil semua achievement +func (ac *AchievementController) GetAllAchievements(ctx *gin.Context) { + achievements, err := ac.Service.GetAllAchievements() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve achievements"}) return } - - // Response with achievements ctx.JSON(http.StatusOK, achievements) } -// GetFotoAchievement adalah fungsi untuk mengambil Foto Achievement berdasarkan ID. -// @Summary Get Foto Achievement -// @Description Retrieves the image of an Achievement by its ID. -// @Tags Achievement -// @Param id path string true "Achievement ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /achievements/foto/{id} [get] -func (ac *AchievementsController) GetFotoAchievement(ctx *gin.Context) { - // Get achievements image ID from URL path parameter, pakenya param klo mau diubah ke yg laen sok - achievementsID := ctx.Param("id") +// GetAchievementByID mengambil achievement berdasarkan ID +func (ac *AchievementController) GetAchievementByID(ctx *gin.Context) { + id := ctx.Param("id") - // Retrieve Thumbnail from the database by its ID - var achievements models.Achievement - if err := ac.DB.Where("id = ?", achievementsID).First(&achievements).Error; err != nil { + achievement, err := ac.Service.GetAchievementByID(id) + if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) return } + ctx.JSON(http.StatusOK, achievement) +} - // Define the file path - filePath := filepath.Join("uploads", achievements.Foto) +// CreateAchievement menambahkan achievement baru +func (ac *AchievementController) CreateAchievement(ctx *gin.Context) { + var achievement models.Achievement - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + // Bind JSON ke struct Achievement + if err := ctx.ShouldBindJSON(&achievement); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) return } - // Set the headers for the file transfer and return the file - ctx.Header("Content-Description", "File Transfer") - ctx.Header("Content-Transfer-Encoding", "binary") - ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", achievements.Foto)) - - // switch case buat content type - ext := filepath.Ext(achievements.Foto) - switch ext { - case ".png": - ctx.Header("Content-Type", "image/png") - case ".jpg", ".jpeg": - ctx.Header("Content-Type", "image/jpeg") - case ".gif": - ctx.Header("Content-Type", "image/gif") - case ".pdf": - ctx.Header("Content-Type", "application/pdf") - default: - ctx.Header("Content-Type", "application/octet-stream") + // Simpan achievement + if err := ac.Service.CreateAchievement(&achievement); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create achievement"}) + return } - ctx.File(filePath) + ctx.JSON(http.StatusCreated, gin.H{"message": "Achievement created successfully", "data": achievement}) } -// GetAchievementsByCategory adalah fungsi untuk mengambil Achievements berdasarkan Category. -// @Summary Get All Achievements By Category -// @Description Retrieves Achievements data by its Category. -// @Tags Achievement -// @Param category path string true "Achievements Category" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /achievements/category/{category} [get] -func (ac *AchievementsController) GetAchievementsByCategory(ctx *gin.Context) { - // Get Achievements category from URL path parameter - achievementsCategory := ctx.Param("category") - - // Retrieve achievements from the database by its ID - var achievements []models.Achievement - if err := ac.DB.Where("kategori = ?", achievementsCategory).Find(&achievements).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) +// UploadAchievementImage mengunggah foto untuk achievement +func (ac *AchievementController) UploadAchievementImage(ctx *gin.Context) { + // Ambil file dari form-data + file, err := ctx.FormFile("foto") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "File is required"}) return } - // Response with achievements - ctx.JSON(http.StatusOK, achievements) - -} - -// InsertAchievement adalah fungsi untuk membuat post Achievements terbaru. -// @Summary Insert a new Achievement -// @Description Insert a Achievements and saves them to the database. -// @Tags Achievement -// @Accept multipart/form-data -// @Param nama formData string true "Nama peraih achievement" -// @Param pencapaian formData string true "Pencapaian yang diraih" -// @Param link formData string true "link ke Achievementnya" -// @Param kategori formData string true "Kategori Achievement" -// @Param foto formData file true "Foto peraih Achievement" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {object} models.Achievement -// @Router /achievements [post] -func (ac *AchievementsController) InsertAchievement(ctx *gin.Context) { - // Get the file from the form data - file, err := ctx.FormFile("foto") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - host := utils.Getenv("ENV_HOST", "localhost") - - // Define the path where the file will be saved, pake UUID, untuk skg taro di uploads dlu - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Validate input data using struct tags - var achievements models.Achievement - achievements.Nama = ctx.PostForm("nama") - achievements.Pencapaian = ctx.PostForm("pencapaian") - achievements.Link = ctx.PostForm("link") - achievements.Kategori = ctx.PostForm("kategori") - achievements.Foto = fileName - achievements.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) - - // Validate the achievements struct - validationErrors := utils.ValidateStruct(achievements) - if len(validationErrors) > 0 { - // Return validation errors without saving the file - ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) - return - } - - // Save the file to the defined path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - // Save achievement to database - if err := ac.DB.Create(&achievements).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save achievement to database"}) - return - } - - ctx.JSON(http.StatusOK, achievements) -} - -// EditAchievements adalah fungsi untuk mengedit Achievements -// @Summary Edit Achievements -// @Description Edits a Achievements by its ID -// @Tags Achievement -// @Accept multipart/form-data -// @Param id path string true "Achievement ID" -// @Param nama formData string true "Nama peraih achievement" -// @Param pencapaian formData string true "Pencapaian yang diraih" -// @Param link formData string true "link ke Achievementnya" -// @Param kategori formData string true "Kategori Achievement" -// @Param foto formData file true "Foto peraih Achievement" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {object} models.Achievement -// @Router /achievements/{id} [put] -func (ac *AchievementsController) EditAchievements(ctx *gin.Context) { - // Get achievement ID from URL path parameter - achievementsID := ctx.Param("id") - - // Retrieve achievement from the database by its ID - var achievements models.Achievement - if err := ac.DB.Where("id = ?", achievementsID).First(&achievements).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "achievements not found"}) - return - } - - // Update achievements fields from form data - achievements.Nama = ctx.PostForm("nama") - achievements.Pencapaian = ctx.PostForm("pencapaian") - achievements.Link = ctx.PostForm("link") - achievements.Kategori = ctx.PostForm("kategori") - - // Validate the achievement struct - validationErrors := utils.ValidateStruct(achievements) - if len(validationErrors) > 0 { - ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) - return - } - - // Update the file if a new one is uploaded - file, err := ctx.FormFile("foto") - if err == nil { - // Define the path where the new file will be saved - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) + // Validasi file + if err := utils.ValidateFile(file); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } - // Save the new file to the defined path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } + // Generate file name dan simpan + fileName := uuid.New().String() + ".png" + filePath := "uploads/" + fileName - // Delete the old file - oldFilePath := filepath.Join("uploads", achievements.Foto) - if err := os.Remove(oldFilePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete old file"}) - return - } + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } - // Update the Foto field with the new file name and update ImageURL - achievements.Foto = fileName - achievements.ImageURL = fmt.Sprintf("http://localhost:8080/uploads/%s", fileName) - } + // Dapatkan ID dari URL parameter + id := ctx.Param("id") - // Save updated achievement to the database - if err := ac.DB.Save(&achievements).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update achievements in database"}) - return - } + // Update path gambar di database + if err := ac.Service.UpdateAchievementImage(id, fileName, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update image path"}) + return + } - ctx.JSON(http.StatusOK, achievements) + ctx.JSON(http.StatusOK, gin.H{"message": "Image uploaded successfully", "path": filePath}) } -// DeleteAchievements adalah fungsi untuk menghapus Achievements dan gambar-nya dari database. -// @Summary Delete Achievements -// @Description Delete a Achievements and its thumbnail from the database and storage. -// @Tags Achievement -// @Param id path string true "Achievements ID" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {string} string "Achievements deleted successfully" -// @Router /achievements/{id} [delete] -func (ac *AchievementsController) DeleteAchievements(ctx *gin.Context) { - achievementId := ctx.Param("id") +// UpdateAchievement mengupdate achievement berdasarkan ID +func (ac *AchievementController) UpdateAchievement(ctx *gin.Context) { + id := ctx.Param("id") + var updatedData models.Achievement - // Cari achievements dari id - var achievements models.Achievement - if err := ac.DB.Where("id = ?", achievementId).First(&achievements).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) + // Bind input + if err := ctx.ShouldBindJSON(&updatedData); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) return } - // tentuin file path dari file yg mau didelete - filePath := filepath.Join("uploads", achievements.Foto) - - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - // File doesn't exist, still delete the achievements from database - if err := ac.DB.Delete(&achievements).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete achievements from database"}) - return - } - ctx.JSON(http.StatusOK, "Achievement deleted successfully") + // Update achievement + if err := ac.Service.UpdateAchievement(id, &updatedData); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update achievement"}) return } - // Delete file - if err := os.Remove(filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) - return - } + ctx.JSON(http.StatusOK, gin.H{"message": "Achievement updated successfully", "data": updatedData}) +} + +// DeleteAchievement menghapus achievement berdasarkan ID +func (ac *AchievementController) DeleteAchievement(ctx *gin.Context) { + id := ctx.Param("id") - // Delete achievement from database - if err := ac.DB.Delete(&achievements).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete achievements from database"}) + // Hapus achievement + if err := ac.Service.DeleteAchievement(id); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete achievement"}) return } - ctx.JSON(http.StatusOK, "Achievement deleted successfully") + ctx.JSON(http.StatusOK, gin.H{"message": "Achievement deleted successfully"}) } diff --git a/controllers/activityController.go b/controllers/activityController.go index 8073b36..1304937 100644 --- a/controllers/activityController.go +++ b/controllers/activityController.go @@ -2,286 +2,140 @@ package controllers import ( "backend/models" + "backend/services" "backend/utils" "fmt" "net/http" - "os" - - "github.com/gin-gonic/gin" - "path/filepath" + "github.com/gin-gonic/gin" "github.com/google/uuid" - "gorm.io/gorm" ) +// ActivitiesController menangani request terkait activities type ActivitiesController struct { - DB *gorm.DB + Service services.ActivityService } -// UploadActivity adalah fungsi untuk mengupload activity beserta file-nya. -// @Summary Upload an activity with File -// @Description Uploads a Activities along with its file and saves them to the database. -// @Tags Activities -// @Accept multipart/form-data -// @Param title formData string true "Title Activities" -// @Param tanggal formData string true "Tanggal Activities" -// @Param gambar formData file true "File gambar" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {object} models.Activities -// @Router /activities [post] -func (ac *ActivitiesController) UploadActivity(ctx *gin.Context) { - // Get the file from the form data - file, err := ctx.FormFile("gambar") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - host := utils.Getenv("ENV_HOST", "localhost") - - // Define the path where the file will be saved, pake UUID, untuk skg taro di uploads dlu - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Save the file to the defined path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - title := ctx.PostForm("title") - tanggal := ctx.PostForm("tanggal") - - // Create Activity object - activity := models.Activities{ - Title: title, - Tanggal: tanggal, - Gambar: fileName, - ImageURL: fmt.Sprintf("https://%s/uploads/%s", host, fileName), - } - - // Save activity to database - if err := ac.DB.Create(&activity).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save activity to database"}) - return - } - - ctx.JSON(http.StatusOK, activity) +// NewActivitiesController membuat instance ActivitiesController +func NewActivitiesController(service services.ActivityService) *ActivitiesController { + return &ActivitiesController{Service: service} } -// GetAllActivities adalah fungsi untuk mendapatkan semua activity dari database. -// @Summary Get All Activities -// @Description Retrieves all Activities from the database. -// @Tags Activities -// @Produce json -// @Success 200 {array} models.Activities -// @Router /activities [get] -func (ac *ActivitiesController) GetAllActivities(ctx *gin.Context) { - var activities []models.Activities - - // Retrieve all activities from the database, masalahnyaaa gambarnya ga ngikut gmn ya infoo helppp - if err := ac.DB.Find(&activities).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get activitiess from database"}) +// UploadActivity mengunggah aktivitas beserta file gambar +func (ac *ActivitiesController) UploadActivity(ctx *gin.Context) { + // Ambil file dari form-data + file, err := ctx.FormFile("gambar") + if err != nil { + utils.HandleError(ctx, http.StatusBadRequest, "File gambar is required") return } - // apa gini aja dah bener? - - // Response with activities - ctx.JSON(http.StatusOK, activities) -} -// GetActivityById adalah fungsi untuk mengambil activity berdasarkan ID. -// @Summary Get Activity By ID -// @Description Retrieves Activity data by its ID. -// @Tags Activities -// @Param id path string true "Activity ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /activities/{id} [get] -func (ac *ActivitiesController) GetActivityById(ctx *gin.Context) { - // Get activity ID from URL path parameter - activityId := ctx.Param("id") - - // Retrieve activity from the database by its ID - var activity models.Activities - if err := ac.DB.Where("id = ?", activityId).First(&activity).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) + // Validasi file gambar + if err := utils.ValidateFile(file); err != nil { + utils.HandleError(ctx, http.StatusBadRequest, err.Error()) return } - // Response with activity - ctx.JSON(http.StatusOK, activity) -} - -// GetGambar adalah fungsi untuk mengambil gambar activity berdasarkan ID. -// @Summary Get Gambar Activity -// @Description Retrieves the image of a activity by its ID. -// @Tags Activities -// @Param id path string true "Activity ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /activities/file/{id} [get] -func (ac *ActivitiesController) GetGambarActivities(ctx *gin.Context) { - // Get activity image ID from URL path parameter, pakenya param klo mau diubah ke yg laen sok - activityID := ctx.Param("id") - - // Retrieve gambar from the database by its ID - var activity models.Activities - if err := ac.DB.Where("id = ?", activityID).First(&activity).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) + // Simpan file ke folder uploads + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, "Failed to save file") return } - // Define the file path - filePath := filepath.Join("uploads", activity.Gambar) + // Ambil input form lainnya + title := ctx.PostForm("title") + tanggal := ctx.PostForm("tanggal") + host := utils.Getenv("ENV_HOST", "localhost") - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) - return + // Buat object activity + activity := models.Activities{ + Title: title, + Date: tanggal, + Image: fileName, + ImageURL: fmt.Sprintf("https://%s/uploads/%s", host, fileName), } - // Set the headers for the file transfer and return the file - ctx.Header("Content-Description", "File Transfer") - ctx.Header("Content-Transfer-Encoding", "binary") - ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", activity.Gambar)) - - // switch case buat content type, sumpah klo ga gini gw gtw gimana biar semua filetype bisa pls help - ext := filepath.Ext(activity.Gambar) - switch ext { - case ".png": - ctx.Header("Content-Type", "image/png") - case ".jpg", ".jpeg": - ctx.Header("Content-Type", "image/jpeg") - case ".gif": - ctx.Header("Content-Type", "image/gif") - case ".pdf": - ctx.Header("Content-Type", "application/pdf") - default: - ctx.Header("Content-Type", "application/octet-stream") + // Simpan ke database + if err := ac.Service.CreateActivity(&activity); err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, "Failed to save activity to database") + return } - ctx.File(filePath) + ctx.JSON(http.StatusOK, gin.H{"message": "Activity uploaded successfully", "data": activity}) } -// EditActivity adalah fungsi untuk mengedit Activity, termasuk kemampuan untuk mengganti file Activity. -// @Summary Edit Activity -// @Description Edits a Activity including the ability to replace its file. -// @Tags Activities -// @Accept multipart/form-data -// @Param id path string true "Activity ID" -// @Param title formData string true "Title Activity" -// @Param tanggal formData string true "Tanggal Activity" -// @Param gambar formData file false "Gambar Activity (optional)" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {object} models.Activities -// @Router /activities/{id} [put] -func (ac *ActivitiesController) EditActivity(ctx *gin.Context) { - // Get activity ID from URL path parameter - activityID := ctx.Param("id") - - // Retrieve activity from the database by its ID - var activity models.Activities - if err := ac.DB.Where("id = ?", activityID).First(&activity).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) - return - } - - title := ctx.PostForm("title") - tanggal := ctx.PostForm("tanggal") - host := utils.Getenv("ENV_HOST", "localhost") - - activity.Title = title - activity.Tanggal = tanggal - - // Cek apakah file diganti - file, err := ctx.FormFile("gambar") - if err != nil && err != http.ErrMissingFile { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Jika iya, maka di save - if file != nil { - // Tentuin tempat nge save - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Save the file to the path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - // Remove old file - oldFilePath := filepath.Join("uploads", activity.Gambar) - if err := os.Remove(oldFilePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove old file"}) - } - - // Update file gambar field in the database - activity.Gambar = fileName - activity.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) - } - - // Save updated activity to database - if err := ac.DB.Save(&activity).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save updated activity to database"}) - return - } +// GetAllActivities mengambil semua aktivitas +func (ac *ActivitiesController) GetAllActivities(ctx *gin.Context) { + activities, err := ac.Service.GetAllActivities() + if err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, "Failed to get activities") + return + } - // Response success - ctx.JSON(http.StatusOK, activity) + ctx.JSON(http.StatusOK, activities) } -// DeleteActivity adalah fungsi untuk menghapus Activity dan gambar-nya dari database. -// @Summary Delete Activity -// @Description Deletes a Activity and its gambar from the database and storage. -// @Tags Activities -// @Param id path string true "Activity ID" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {string} string "Activity deleted successfully" -// @Router /activities/{id} [delete] -func (ac *ActivitiesController) DeleteActivity(ctx *gin.Context) { - activityID := ctx.Param("id") - - // Cari activity dari id - var activity models.Activities - if err := ac.DB.Where("id = ?", activityID).First(&activity).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) +// GetActivityByID mengambil aktivitas berdasarkan ID +func (ac *ActivitiesController) GetActivityByID(ctx *gin.Context) { + id := ctx.Param("id") + activity, err := ac.Service.GetActivityByID(id) + if err != nil { + utils.HandleError(ctx, http.StatusNotFound, "Activity not found") return } - // tentuin file path dari file yg mau didelete - filePath := filepath.Join("uploads", activity.Gambar) + ctx.JSON(http.StatusOK, activity) +} - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - // File doesn't exist, still delete the activity from database - if err := ac.DB.Delete(&activity).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete activity from database"}) +// EditActivity mengupdate data aktivitas +func (ac *ActivitiesController) EditActivity(ctx *gin.Context) { + id := ctx.Param("id") + + // Ambil data form + title := ctx.PostForm("title") + tanggal := ctx.PostForm("tanggal") + + // Ambil file (jika ada) + file, err := ctx.FormFile("gambar") + var fileName string + if err == nil { // File ditemukan + // Validasi file + if err := utils.ValidateFile(file); err != nil { + utils.HandleError(ctx, http.StatusBadRequest, err.Error()) + return + } + + // Simpan file baru + fileName = uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, "Failed to save file") return } - ctx.JSON(http.StatusOK, "Activity deleted successfully") - return } - // Delete file - if err := os.Remove(filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) + // Panggil service untuk update + updatedActivity, err := ac.Service.UpdateActivity(id, title, tanggal, fileName) + if err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, "Failed to update activity") return } - // Delete activity from database - if err := ac.DB.Delete(&activity).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete activity from database"}) + ctx.JSON(http.StatusOK, gin.H{"message": "Activity updated successfully", "data": updatedActivity}) +} + +// DeleteActivity menghapus aktivitas dan gambarnya +func (ac *ActivitiesController) DeleteActivity(ctx *gin.Context) { + id := ctx.Param("id") + + // Panggil service untuk hapus + if err := ac.Service.DeleteActivity(id); err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, "Failed to delete activity") return } - ctx.JSON(http.StatusOK, "Activity and file deleted successfully") + ctx.JSON(http.StatusOK, gin.H{"message": "Activity deleted successfully"}) } diff --git a/controllers/authAdminController.go b/controllers/authAdminController.go new file mode 100644 index 0000000..fc1b416 --- /dev/null +++ b/controllers/authAdminController.go @@ -0,0 +1,58 @@ +package controllers + +import ( + "backend/services" + "backend/utils" + "net/http" + + "github.com/gin-gonic/gin" +) + +// KodeInput - struct untuk input kode login +type KodeInput struct { + Kode string `json:"kode" binding:"required,min=5,max=30"` +} + +// AuthAdminController - controller untuk autentikasi admin +type AuthAdminController struct { + AuthService services.AuthAdminService +} + +// NewAuthAdminController - inisialisasi controller baru +func NewAuthAdminController(authService services.AuthAdminService) *AuthAdminController { + return &AuthAdminController{AuthService: authService} +} + +// LoginAdmin - endpoint untuk autentikasi admin +// @Summary Login as admin. +// @Description Logs in an admin and returns a JWT token. +// @Tags Auth +// @Accept json +// @Param Body KodeInput true "Admin code for login" +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Failure 401 {object} map[string]interface{} +// @Router /auth/login-admin [post] +func (ac *AuthAdminController) LoginAdmin(ctx *gin.Context) { + var input KodeInput + + // Validasi input + if err := ctx.ShouldBindJSON(&input); err != nil { + utils.HandleError(ctx, http.StatusBadRequest, "Invalid input format") + return + } + + // Panggil service untuk autentikasi + token, err := ac.AuthService.LoginAdmin(input.Kode) + if err != nil { + utils.HandleError(ctx, http.StatusUnauthorized, "Invalid credentials") + return + } + + // Respons sukses + ctx.JSON(http.StatusOK, gin.H{ + "message": "Login successful", + "token": token, + }) +} diff --git a/controllers/newsController.go b/controllers/newsController.go index 9644801..03a09b6 100644 --- a/controllers/newsController.go +++ b/controllers/newsController.go @@ -2,475 +2,83 @@ package controllers import ( "backend/models" + "backend/services" "backend/utils" - "encoding/json" - "fmt" + "context" "net/http" - "os" - "path/filepath" + "strings" + "time" "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" ) -type NewsResponse struct { - ID uint `json:"id"` - Title string `json:"title"` - Kategori string `json:"kategori"` - Thumbnail string `json:"thumbnail"` - IsiKonten interface{} `json:"isi_konten"` - NamaPenulis string `json:"nama_penulis"` - Link string `json:"link"` - ImageURL string `json:"image_url"` - Date string `json:"date"` -} - type NewsController struct { - DB *gorm.DB -} - -// GetAllNews adalah fungsi untuk mendapatkan semua news dari database. -// @Summary Get All News -// @Description Retrieves all news from the database. -// @Tags News -// @Produce json -// @Success 200 {array} models.News -// @Router /news [get] -func (nc *NewsController) GetAllNews(ctx *gin.Context) { - var news []models.News - - // Retrieve all news from the database - if err := nc.DB.Find(&news).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get news from database"}) - return - } - - var response []NewsResponse - - for _, n := range news { - var isiKontenDecoded interface{} - if err := json.Unmarshal([]byte(n.IsiKonten), &isiKontenDecoded); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) - return - } - - response = append(response, NewsResponse{ - ID: n.ID, - Title: n.Title, - Kategori: n.Kategori, - Thumbnail: n.Thumbnail, - IsiKonten: isiKontenDecoded, - NamaPenulis: n.NamaPenulis, - Link: n.Link, - ImageURL: n.ImageURL, - Date: n.Date, - }) - } - - // Response with news - ctx.JSON(http.StatusOK, response) + NewsService services.NewsService } -// GetNewsById adalah fungsi untuk mengambil news berdasarkan ID. -// @Summary Get News By ID -// @Description Retrieves news data by its ID. -// @Tags News -// @Param id path string true "News ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /news/{id} [get] -func (nc *NewsController) GetNewsById(ctx *gin.Context) { - // Get news ID from URL path parameter - newsId := ctx.Param("id") - - // Retrieve news from the database by its ID - var news models.News - if err := nc.DB.Where("id = ?", newsId).First(&news).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) - return - } - - var isiKontenDecoded interface{} - if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) - return - } - - // Create response with decoded isi_konten - response := NewsResponse{ - ID: news.ID, - Title: news.Title, - Kategori: news.Kategori, - Thumbnail: news.Thumbnail, - IsiKonten: isiKontenDecoded, - NamaPenulis: news.NamaPenulis, - Link: news.Link, - ImageURL: news.ImageURL, - Date: news.Date, - } - - // Response with news - ctx.JSON(http.StatusOK, response) +func NewNewsController(newsService services.NewsService) *NewsController { + return &NewsController{NewsService: newsService} } -// GetThumbnailNews adalah fungsi untuk mengambil Thumbnail News berdasarkan ID. -// @Summary Get Thumbnail News -// @Description Retrieves the image of a News by its ID. -// @Tags News -// @Param id path string true "News ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /news/file/{id} [get] -func (nc *NewsController) GetThumbnailNews(ctx *gin.Context) { - // Get News image ID from URL path parameter, pakenya param klo mau diubah ke yg laen sok - newsID := ctx.Param("id") - - // Retrieve Thumbnail from the database by its ID - var news models.News - if err := nc.DB.Where("id = ?", newsID).First(&news).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) - return - } - // Define the file path - filePath := filepath.Join("uploads", news.Thumbnail) +// GetAllNews mengambil seluruh berita +func (nc *NewsController) GetAllNews(ctx *gin.Context) { + // Set timeout untuk konteks request + c, cancel := context.WithTimeout(ctx.Request.Context(), 10*time.Second) + defer cancel() - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + news, err := nc.NewsService.GetAllNews(c) + if err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, err.Error()) return } - // Set the headers for the file transfer and return the file - ctx.Header("Content-Description", "File Transfer") - ctx.Header("Content-Transfer-Encoding", "binary") - ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", news.Thumbnail)) - - // switch case buat content type - ext := filepath.Ext(news.Thumbnail) - switch ext { - case ".png": - ctx.Header("Content-Type", "image/png") - case ".jpg", ".jpeg": - ctx.Header("Content-Type", "image/jpeg") - case ".gif": - ctx.Header("Content-Type", "image/gif") - case ".pdf": - ctx.Header("Content-Type", "application/pdf") - default: - ctx.Header("Content-Type", "application/octet-stream") - } - - ctx.File(filePath) + ctx.JSON(http.StatusOK, news) } -// GetNewsByCategory adalah fungsi untuk mengambil news berdasarkan Category. -// @Summary Get All News By Category -// @Description Retrieves news data by its Category. -// @Tags News -// @Param category path string true "News Category" -// @Produce json -// @Success 200 {array} NewsResponse -// @Router /news/category/{category} [get] -func (nc *NewsController) GetNewsByCategory(ctx *gin.Context) { - // Get news category from URL path parameter - newsCategory := ctx.Param("category") - - // Retrieve all news from the database with the specified category - var newsList []models.News - if err := nc.DB.Where("kategori = ?", newsCategory).Find(&newsList).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) - return - } - - // Prepare the response - var response []NewsResponse - for _, news := range newsList { - var isiKontenDecoded interface{} - if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) - return - } - - response = append(response, NewsResponse{ - ID: news.ID, - Title: news.Title, - Kategori: news.Kategori, - Thumbnail: news.Thumbnail, - IsiKonten: isiKontenDecoded, - NamaPenulis: news.NamaPenulis, - Link: news.Link, - ImageURL: news.ImageURL, - Date: news.Date, - }) - } - - // Response with list of news - ctx.JSON(http.StatusOK, response) -} - -// InsertNews adalah fungsi untuk membuat post news terbaru. -// @Summary Insert a new news -// @Description Insert a news and saves them to the database. -// @Tags News -// @Accept multipart/form-data -// @Param title formData string true "Judul news" -// @Param kategori formData string true "Kategori news" -// @Param thumbnail formData file true "Thumbnail news" -// @Param isi_konten formData string true "Isi konten news" -// @Param nama_penulis formData string true "Nama penulis news" -// @Param link formData string true "Link news" -// @Param date formData string true "Date news" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {object} models.News -// @Router /news [post] +// InsertNews menyimpan berita baru beserta thumbnail func (nc *NewsController) InsertNews(ctx *gin.Context) { - // Get the file from the form data - file, err := ctx.FormFile("thumbnail") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - host := utils.Getenv("ENV_HOST", "localhost") - - // Define the path where the file will be saved, using UUID for uniqueness - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Validate input data using struct tags - var news models.News - news.Title = ctx.PostForm("title") - news.Kategori = ctx.PostForm("kategori") - news.Date = ctx.PostForm("date") - isiKonten := ctx.PostForm("isi_konten") - - // Parse isi_konten into JSON - var isiKontenJSON interface{} - if err := json.Unmarshal([]byte(isiKonten), &isiKontenJSON); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format for isi_konten"}) - return - } + // Set timeout untuk konteks request + c, cancel := context.WithTimeout(ctx.Request.Context(), 15*time.Second) + defer cancel() - // Convert isiKontenJSON back to string to store in News struct - isiKontenBytes, err := json.Marshal(isiKontenJSON) + // Validasi file input + file, err := ctx.FormFile("thumbnail") if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process isi_konten"}) - return - } - news.IsiKonten = string(isiKontenBytes) - news.NamaPenulis = ctx.PostForm("nama_penulis") - news.Link = ctx.PostForm("link") - news.Thumbnail = fileName - news.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) - - // Validate the News struct - validationErrors := utils.ValidateStruct(news) - if len(validationErrors) > 0 { - // Return validation errors without saving the file - ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) - return - } - - // Save the file to the defined path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - // Save news to database - if err := nc.DB.Create(&news).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save news to database"}) + utils.HandleError(ctx, http.StatusBadRequest, "Thumbnail is required") return } - var isiKontenDecoded interface{} - if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) - return - } - - response := NewsResponse{ - ID: news.ID, - Title: news.Title, - Kategori: news.Kategori, - Thumbnail: news.Thumbnail, - IsiKonten: isiKontenDecoded, - NamaPenulis: news.NamaPenulis, - Link: news.Link, - ImageURL: news.ImageURL, - Date: news.Date, - } - - ctx.JSON(http.StatusOK, response) -} - -// EditNews adalah fungsi untuk mengedit News -// @Summary Edit News -// @Description Edits a News by its ID -// @Tags News -// @Accept multipart/form-data -// @Param id path string true "News ID" -// @Param title formData string true "Title News" -// @Param kategori formData string true "Kategori News" -// @Param thumbnail formData file false "Thumbnail News" -// @Param isi_konten formData string true "Isi Konten News" -// @Param nama_penulis formData string true "Nama Penulis News" -// @Param link formData string true "Link News" -// @Param date formData string true "Date News" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {object} models.News -// @Router /news/{id} [put] -func (nc *NewsController) EditNews(ctx *gin.Context) { - // Get news ID from URL path parameter - newsID := ctx.Param("id") - host := utils.Getenv("ENV_HOST", "localhost") - // Retrieve news from the database by its ID - var news models.News - if err := nc.DB.Where("id = ?", newsID).First(&news).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) - return + // Validasi data form lainnya + news := models.News{ + Title: ctx.PostForm("title"), + Kategori: ctx.PostForm("kategori"), + Date: ctx.PostForm("date"), + IsiKonten: ctx.PostForm("isi_konten"), + NamaPenulis: ctx.PostForm("nama_penulis"), + Link: ctx.PostForm("link"), } - // Update news fields from form data - news.Title = ctx.PostForm("title") - news.Kategori = ctx.PostForm("kategori") - news.Date = ctx.PostForm("date") - isiKonten := ctx.PostForm("isi_konten") - - if isiKonten != "" { - var isiKontenJSON interface{} - if err := json.Unmarshal([]byte(isiKonten), &isiKontenJSON); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format for isi_konten"}) - return - } - - isiKontenBytes, err := json.Marshal(isiKontenJSON) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process isi_konten"}) - return - } - news.IsiKonten = string(isiKontenBytes) - } - news.NamaPenulis = ctx.PostForm("nama_penulis") - news.Link = ctx.PostForm("link") - - // Validate the News struct + // Validasi struct berita validationErrors := utils.ValidateStruct(news) if len(validationErrors) > 0 { - // Return validation errors without saving the file - ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) - return - } - - // Check if a new thumbnail file is uploaded - file, err := ctx.FormFile("thumbnail") - if err != nil && err != http.ErrMissingFile { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // If a new file is uploaded, save it and update the thumbnail field - if file != nil { - // Define the path where the file will be saved, using UUID for uniqueness - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Save the file to the defined path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return + // Menggabungkan kesalahan validasi menjadi satu string + var errorMessages []string + for _, message := range validationErrors { + errorMessages = append(errorMessages, message) } - - // Remove old file - oldFilePath := filepath.Join("uploads", news.Thumbnail) - if err := os.Remove(oldFilePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove old file"}) - return - } - - // Update the thumbnail field in the database - news.Thumbnail = fileName - news.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) - } - - // Save updated news to the database - if err := nc.DB.Save(&news).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save updated news to database"}) + utils.HandleError(ctx, http.StatusBadRequest, strings.Join(errorMessages, ", ")) return } - // Decode isi_konten for response - var isiKontenDecoded interface{} - if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) - return - } - - // Create response with decoded isi_konten - response := NewsResponse{ - ID: news.ID, - Title: news.Title, - Kategori: news.Kategori, - Thumbnail: news.Thumbnail, - IsiKonten: isiKontenDecoded, - NamaPenulis: news.NamaPenulis, - Link: news.Link, - ImageURL: news.ImageURL, - Date: news.Date, - } - - // Response success - ctx.JSON(http.StatusOK, response) -} - -// DeleteNews adalah fungsi untuk menghapus News dan gambar-nya dari database. -// @Summary Delete News -// @Description Delete a News and its thumbnail from the database and storage. -// @Tags News -// @Param id path string true "News ID" -// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" -// @Produce json -// @Success 200 {string} string "News deleted successfully" -// @Router /news/{id} [delete] -func (nc *NewsController) DeleteNews(ctx *gin.Context) { - newsId := ctx.Param("id") - - // Cari news dari id - var news models.News - if err := nc.DB.Where("id = ?", newsId).First(&news).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) - return - } - - // tentuin file path dari file yg mau didelete - filePath := filepath.Join("uploads", news.Thumbnail) - - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - // File doesn't exist, still delete the news from database - if err := nc.DB.Delete(&news).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete news from database"}) - return - } - ctx.JSON(http.StatusOK, "News deleted successfully") - return - } - - // Delete file - if err := os.Remove(filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) - return - } - - // Delete news from database - if err := nc.DB.Delete(&news).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete news from database"}) + // Panggil service untuk menyimpan berita + response, err := nc.NewsService.InsertNews(c, news, file, ctx) + if err != nil { + utils.HandleError(ctx, http.StatusInternalServerError, err.Error()) return } - ctx.JSON(http.StatusOK, "News deleted successfully") + ctx.JSON(http.StatusOK, gin.H{ + "message": "News successfully inserted", + "data": response, + }) } diff --git a/middlewares/auth.go b/middlewares/auth.go new file mode 100644 index 0000000..628e33a --- /dev/null +++ b/middlewares/auth.go @@ -0,0 +1,45 @@ +package middlewares + +import ( + "backend/utils" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// AuthMiddleware - middleware untuk verifikasi JWT +func AuthMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // Ambil header Authorization + authHeader := ctx.GetHeader("Authorization") + if authHeader == "" { + // Jika tidak ada header Authorization, kembalikan error + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + ctx.Abort() + return + } + + // Ekstrak token dari header Authorization + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == "" { + // Jika token tidak ada setelah prefix "Bearer ", kembalikan error + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token is missing"}) + ctx.Abort() + return + } + + // Validasi token dan ambil userID dari token + userID, err := utils.ValidateToken(tokenString) + if err != nil { + // Jika token tidak valid atau ada masalah, kembalikan error + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token", "details": err.Error()}) + ctx.Abort() + return + } + + // Attach userID ke context, supaya bisa diakses di handler selanjutnya + ctx.Set("userID", userID) + ctx.Next() + } +} diff --git a/middlewares/middleware.go b/middlewares/middleware.go index 21241c9..d177308 100644 --- a/middlewares/middleware.go +++ b/middlewares/middleware.go @@ -1,47 +1,47 @@ -package middlewares - -import ( - "backend/utils/token" - "net/http" - "time" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -func CorsMiddleware() gin.HandlerFunc { - return cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - MaxAge: 7 * time.Hour, - }) -} - -func AdminCheckMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - err := token.TokenValid(c) - if err != nil { - c.String(http.StatusUnauthorized, err.Error()) - c.Abort() - return - } - - // Setelah token divalidasi, kita dapat memeriksa role dari user yang terautentikasi. - role, err := token.ExtractUserRole(c) - if err != nil { - c.String(http.StatusUnauthorized, err.Error()) - c.Abort() - return - } - - // Jika role tidak sesuai, berikan pesan error dan hentikan proses. - if role != "admin" { - c.String(http.StatusForbidden, "Access denied. Insufficient role.") - c.Abort() - return - } - } -} +package middlewares + +import ( + "backend/utils/token" + "net/http" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func CorsMiddleware() gin.HandlerFunc { + return cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 7 * time.Hour, + }) +} + +func AdminCheckMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + err := token.TokenValid(c) + if err != nil { + c.String(http.StatusUnauthorized, err.Error()) + c.Abort() + return + } + + // Setelah token divalidasi, kita dapat memeriksa role dari user yang terautentikasi. + role, err := token.ExtractUserRole(c) + if err != nil { + c.String(http.StatusUnauthorized, err.Error()) + c.Abort() + return + } + + // Jika role tidak sesuai, berikan pesan error dan hentikan proses. + if role != "admin" { + c.String(http.StatusForbidden, "Access denied. Insufficient role.") + c.Abort() + return + } + } +} diff --git a/models/achievement.go b/models/achievement.go new file mode 100644 index 0000000..658fb1e --- /dev/null +++ b/models/achievement.go @@ -0,0 +1,11 @@ +package models + +type Achievement struct { + ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` + Nama string `json:"nama"` + Pencapaian string `json:"pencapaian"` + Link string `json:"link"` + Kategori string `json:"kategori" validate:"required,oneof='International' 'National' 'Campus'"` + Foto string `json:"foto"` + ImageURL string `json:"image_url"` +} diff --git a/models/activities.go b/models/activities.go new file mode 100644 index 0000000..8206861 --- /dev/null +++ b/models/activities.go @@ -0,0 +1,9 @@ +package models + +type Activities struct { + ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` + Title string `json:"title"` + Date string `json:"tanggal"` + Image string `json:"gambar"` + ImageURL string `json:"image_url"` +} diff --git a/models/news.go b/models/news.go new file mode 100644 index 0000000..c31e74f --- /dev/null +++ b/models/news.go @@ -0,0 +1,13 @@ +package models + +type News struct { + ID uint `gorm:"primaryKey" json:"id"` + Title string `json:"title" validate:"required"` + Kategori string `json:"kategori" validate:"required,oneof=News Event"` + Date string `json:"date" validate:"required"` + IsiKonten string `json:"isi_konten" validate:"required"` + NamaPenulis string `json:"nama_penulis" validate:"required"` + Link string `json:"link"` + Thumbnail string `json:"thumbnail"` + ImageURL string `json:"image_url"` +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..7f9a9df --- /dev/null +++ b/models/user.go @@ -0,0 +1,40 @@ +package models + +import ( + "backend/utils/token" + "errors" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// User - model untuk user +type User struct { + ID uint `gorm:"primaryKey;autoIncrement:true"` + Role string `json:"role"` + Code string `json:"code"` +} + +// GenerateJWT - generate token JWT untuk user +func (u *User) GenerateJWT() (string, error) { + if u.ID == 0 || u.Role == "" { + return "", errors.New("invalid user data for token generation") + } + return token.GenerateToken(u.ID, u.Role) +} + +// LoginCheckAdmin - verifikasi kode admin +func (u *User) LoginCheckAdmin(db *gorm.DB) (string, error) { + var admin User + if err := db.Where("code = ?", u.Code).First(&admin).Error; err != nil { + return "", errors.New("admin not found") + } + + // Validasi kode dengan bcrypt + if err := bcrypt.CompareHashAndPassword([]byte(admin.Code), []byte(u.Code)); err != nil { + return "", errors.New("invalid code") + } + + // Generate token JWT + return admin.GenerateJWT() +} diff --git a/repositories/achievement_repository.go b/repositories/achievement_repository.go new file mode 100644 index 0000000..ff12007 --- /dev/null +++ b/repositories/achievement_repository.go @@ -0,0 +1,107 @@ +package repositories + +import ( + "backend/models" + "errors" + + "gorm.io/gorm" +) + +// AchievementRepository mendefinisikan metode untuk interaksi database +type AchievementRepository interface { + FindAll() ([]models.Achievement, error) + FindByID(id string) (models.Achievement, error) + Create(achievement *models.Achievement) error + Update(id string, updatedData *models.Achievement) error + UpdateImagePath(id string, filePath string) error + Delete(id string) error +} + +type achievementRepo struct { + db *gorm.DB +} + +// NewAchievementRepository membuat instance baru dari AchievementRepository +func NewAchievementRepository(db *gorm.DB) AchievementRepository { + return &achievementRepo{db: db} +} + +// FindAll mengambil semua data achievement dari database +func (repo *achievementRepo) FindAll() ([]models.Achievement, error) { + var achievements []models.Achievement + if err := repo.db.Find(&achievements).Error; err != nil { + return nil, err + } + return achievements, nil +} + +// FindByID mencari achievement berdasarkan ID +func (repo *achievementRepo) FindByID(id string) (models.Achievement, error) { + var achievement models.Achievement + if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return achievement, errors.New("achievement not found") + } + return achievement, err + } + return achievement, nil +} + +// Create menambahkan data achievement ke database +func (repo *achievementRepo) Create(achievement *models.Achievement) error { + if err := repo.db.Create(achievement).Error; err != nil { + return err + } + return nil +} + +// Update memperbarui data achievement berdasarkan ID +func (repo *achievementRepo) Update(id string, updatedData *models.Achievement) error { + var achievement models.Achievement + if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("achievement not found") + } + return err + } + + // Update field yang diubah + if err := repo.db.Model(&achievement).Updates(updatedData).Error; err != nil { + return err + } + return nil +} + +// UpdateImagePath memperbarui path file gambar dari achievement +func (repo *achievementRepo) UpdateImagePath(id string, filePath string) error { + var achievement models.Achievement + if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { + return err + } + + // Update field foto dan image_url + achievement.Foto = filePath + achievement.ImageURL = filePath + + if err := repo.db.Save(&achievement).Error; err != nil { + return err + } + return nil +} + +// Delete menghapus data achievement dari database +func (repo *achievementRepo) Delete(id string) error { + var achievement models.Achievement + if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("achievement not found") + } + return err + } + + // Hapus dari database + if err := repo.db.Delete(&achievement).Error; err != nil { + return err + } + return nil +} diff --git a/repositories/activity_repository.go b/repositories/activity_repository.go new file mode 100644 index 0000000..2f20b2a --- /dev/null +++ b/repositories/activity_repository.go @@ -0,0 +1,62 @@ +package repositories + +import ( + "backend/models" + "errors" + "gorm.io/gorm" +) + +type ActivityRepository interface { + Create(activity *models.Activities) error + FindAll() ([]models.Activities, error) + FindByID(id string) (models.Activities, error) + Update(id, title, tanggal, fileName string) (models.Activities, error) + Delete(id string) error +} + +type activityRepo struct { + db *gorm.DB +} + +func NewActivityRepository(db *gorm.DB) ActivityRepository { + return &activityRepo{db: db} +} + +func (r *activityRepo) Create(activity *models.Activities) error { + return r.db.Create(activity).Error +} + +func (r *activityRepo) FindAll() ([]models.Activities, error) { + var activities []models.Activities + err := r.db.Find(&activities).Error + return activities, err +} + +func (r *activityRepo) FindByID(id string) (models.Activities, error) { + var activity models.Activities + err := r.db.Where("id = ?", id).First(&activity).Error + return activity, err +} + +func (r *activityRepo) Update(id, title, tanggal, fileName string) (models.Activities, error) { + var activity models.Activities + if err := r.db.Where("id = ?", id).First(&activity).Error; err != nil { + return activity, errors.New("activity not found") + } + + activity.Title = title + activity.Date = tanggal + if fileName != "" { + activity.Image = fileName + activity.ImageURL = "uploads/" + fileName + } + + if err := r.db.Save(&activity).Error; err != nil { + return activity, err + } + return activity, nil +} + +func (r *activityRepo) Delete(id string) error { + return r.db.Where("id = ?", id).Delete(&models.Activities{}).Error +} diff --git a/repositories/news_repository.go b/repositories/news_repository.go new file mode 100644 index 0000000..572f664 --- /dev/null +++ b/repositories/news_repository.go @@ -0,0 +1,64 @@ +package repositories + +import ( + "backend/models" + "context" + "fmt" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "mime/multipart" + "os" + "path/filepath" + "time" +) + +// NewsRepository - interface untuk operasi pada model News +type NewsRepository interface { + FindAll(ctx context.Context) ([]models.News, error) + Save(ctx context.Context, news *models.News) error + SaveFile(file *multipart.FileHeader, ginContext *gin.Context) (string, error) // Menggunakan ginContext +} + +type newsRepository struct { + db *gorm.DB +} + +func NewNewsRepository(db *gorm.DB) NewsRepository { + return &newsRepository{db: db} +} + +func (r *newsRepository) FindAll(ctx context.Context) ([]models.News, error) { + var news []models.News + if err := r.db.Find(&news).Error; err != nil { + return nil, err + } + return news, nil +} + +func (r *newsRepository) Save(ctx context.Context, news *models.News) error { + if err := r.db.Create(news).Error; err != nil { + return err + } + return nil +} + +// SaveFile menyimpan file gambar dan mengembalikan nama file +func (r *newsRepository) SaveFile(file *multipart.FileHeader, ginContext *gin.Context) (string, error) { + // Tentukan direktori tempat file akan disimpan + dir := "uploads" + // Membuat folder jika belum ada + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return "", err + } + + // Menghasilkan nama file unik + fileName := fmt.Sprintf("%d-%s", time.Now().UnixNano(), file.Filename) + filePath := filepath.Join(dir, fileName) // Menggunakan filePath untuk menentukan lokasi penyimpanan + + // Menggunakan ginContext untuk menyimpan file + if err := ginContext.SaveUploadedFile(file, filePath); err != nil { + return "", err + } + + return fileName, nil +} diff --git a/repositories/user_repository.go b/repositories/user_repository.go new file mode 100644 index 0000000..efc2653 --- /dev/null +++ b/repositories/user_repository.go @@ -0,0 +1,40 @@ +package repositories + +import ( + "backend/models" + + "gorm.io/gorm" +) + +// UserRepository - interface untuk operasi pada model User +type UserRepository interface { + FindByCode(code string) (*models.User, error) + FindByID(id uint) (*models.User, error) +} + +type userRepo struct { + db *gorm.DB +} + +// NewUserRepository - inisialisasi UserRepository baru +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepo{db: db} +} + +// FindByCode - mencari user berdasarkan kode unik (code) +func (r *userRepo) FindByCode(code string) (*models.User, error) { + var user models.User + if err := r.db.Where("code = ?", code).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +// FindByID - mencari user berdasarkan ID +func (r *userRepo) FindByID(id uint) (*models.User, error) { + var user models.User + if err := r.db.First(&user, id).Error; err != nil { + return nil, err + } + return &user, nil +} diff --git a/routes/achievement_routes.go b/routes/achievement_routes.go new file mode 100644 index 0000000..20e6a86 --- /dev/null +++ b/routes/achievement_routes.go @@ -0,0 +1,19 @@ +package routes + +import ( + "backend/controllers" + + "github.com/gin-gonic/gin" +) + +func RegisterAchievementRoutes(router *gin.Engine, controller *controllers.AchievementController) { + achievements := router.Group("/achievements") + { + achievements.GET("/", controller.GetAllAchievements) + achievements.GET("/:id", controller.GetAchievementByID) + achievements.POST("/", controller.CreateAchievement) + achievements.POST("/:id/upload-image", controller.UploadAchievementImage) + achievements.PUT("/:id", controller.UpdateAchievement) + achievements.DELETE("/:id", controller.DeleteAchievement) + } +} diff --git a/routes/activity_routes.go b/routes/activity_routes.go new file mode 100644 index 0000000..f7473bf --- /dev/null +++ b/routes/activity_routes.go @@ -0,0 +1,27 @@ +package routes + +import ( + "backend/controllers" + "github.com/gin-gonic/gin" +) + +// RegisterActivityRoutes untuk mendaftarkan semua routes terkait aktivitas +func RegisterActivityRoutes(router *gin.Engine, activitiesController *controllers.ActivitiesController) { + activity := router.Group("/activities") // grup route untuk activities + { + // Route untuk upload aktivitas (POST) + activity.POST("/upload", activitiesController.UploadActivity) + + // Route untuk mendapatkan semua aktivitas (GET) + activity.GET("/", activitiesController.GetAllActivities) + + // Route untuk mendapatkan aktivitas berdasarkan ID (GET) + activity.GET("/:id", activitiesController.GetActivityByID) + + // Route untuk mengedit aktivitas (PUT) + activity.PUT("/:id", activitiesController.EditActivity) + + // Route untuk menghapus aktivitas (DELETE) + activity.DELETE("/:id", activitiesController.DeleteActivity) + } +} diff --git a/routes/auth_routes.go b/routes/auth_routes.go new file mode 100644 index 0000000..5965046 --- /dev/null +++ b/routes/auth_routes.go @@ -0,0 +1,13 @@ +package routes + +import ( + "backend/controllers" + "github.com/gin-gonic/gin" +) + +func RegisterAuthRoutes(router *gin.Engine, authController *controllers.AuthAdminController) { + auth := router.Group("/auth") + { + auth.POST("/login-admin", authController.LoginAdmin) + } +} diff --git a/routes/news_routes.go b/routes/news_routes.go new file mode 100644 index 0000000..fff4bb4 --- /dev/null +++ b/routes/news_routes.go @@ -0,0 +1,15 @@ +package routes + +import ( + "backend/controllers" + + "github.com/gin-gonic/gin" +) + +func RegisterNewsRoutes(router *gin.Engine, newsController *controllers.NewsController) { + news := router.Group("/news") + { + news.GET("/", newsController.GetAllNews) + news.POST("/", newsController.InsertNews) + } +} diff --git a/routes/routes.go b/routes/routes.go index 0b2d393..92cfec9 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -1,79 +1,47 @@ package routes import ( - "github.com/gin-gonic/gin" - "gorm.io/gorm" - "backend/controllers" "backend/middlewares" - - swaggerFiles "github.com/swaggo/files" // swagger embed files - ginSwagger "github.com/swaggo/gin-swagger" + "backend/repositories" + "backend/services" + "github.com/gin-gonic/gin" + "gorm.io/gorm" ) -func SetupRouter(db *gorm.DB) *gin.Engine { - r := gin.Default() - - r.Use(func(c *gin.Context) { - c.Set("db", db) - }) - - paperController := &controllers.PaperController{DB: db} - activityController := &controllers.ActivitiesController{DB: db} - achievementController := &controllers.AchievementsController{DB: db} - newsController := &controllers.NewsController{DB: db} - r.Use(middlewares.CorsMiddleware()) - - // Jika bukan GET , Cek token dulu - r.Use(func(c *gin.Context) { - if c.Request.Method != "GET" && c.Request.URL.Path != "/login-admin" { - middlewares.AdminCheckMiddleware()(c) - } - c.Next() - }) - - r.POST("/login-admin", controllers.LoginAdmin) - - r.GET("/papers", paperController.GetAllPapers) - r.GET("/papers/file/:id", paperController.GetPaperFile) - - //activitiy - r.GET("/activities", activityController.GetAllActivities) - r.GET("/activities/:id", activityController.GetActivityById) - r.GET("/activities/file/:id", activityController.GetGambarActivities) - //news - r.GET("/news", newsController.GetAllNews) - r.GET("/news/:id", newsController.GetNewsById) - r.GET("/news/file/:id", newsController.GetThumbnailNews) - r.GET("/news/category/:category", newsController.GetNewsByCategory) - - //achievement - r.GET("/achievements", achievementController.GetAllAchievement) - r.GET("/achievements/:id", achievementController.GetAchievementById) - r.GET("/achievements/foto/:id", achievementController.GetFotoAchievement) - r.GET("/achievements/category/:category", achievementController.GetAchievementsByCategory) - - //papers - r.POST("/papers", paperController.UploadPaper) - r.PUT("/papers/:id", paperController.EditPaper) - r.DELETE("/papers/:id", paperController.DeletePaper) - - //activities - r.POST("/activities", activityController.UploadActivity) - r.DELETE("/activities/:id", activityController.DeleteActivity) - r.PUT("/activities/:id", activityController.EditActivity) - - //news - r.POST("/news", newsController.InsertNews) - r.PUT("/news/:id", newsController.EditNews) - r.DELETE("/news/:id", newsController.DeleteNews) - - //achievements - r.POST("/achievements", achievementController.InsertAchievement) - r.PUT("/achievements/:id", achievementController.EditAchievements) - r.DELETE("/achievements/:id", achievementController.DeleteAchievements) - - r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL("/swagger/doc.json"))) - r.Static("/uploads", "./uploads") - return r +// SetupRouter mengatur semua rute utama aplikasi +func SetupRouter(router *gin.Engine, db *gorm.DB) { + // Middleware global + router.Use(gin.Logger()) + router.Use(gin.Recovery()) + + // Inisialisasi service dan controller untuk Authentication + authRepo := repositories.NewUserRepository(db) + authService := services.NewAuthAdminService(authRepo) + authController := controllers.NewAuthAdminController(authService) + + // Inisialisasi repository dan service untuk Activities + activityRepo := repositories.NewActivityRepository(db) // Inisialisasi repository terlebih dahulu + activityService := services.NewActivityService(activityRepo) // Gunakan repository untuk service + activityController := controllers.NewActivitiesController(activityService) + + // Inisialisasi repository dan service untuk News + newsRepo := repositories.NewNewsRepository(db) + newsService := services.NewNewsService(newsRepo) + newsController := controllers.NewNewsController(newsService) + + // Pendaftaran routes modular + RegisterAuthRoutes(router, authController) + RegisterActivityRoutes(router, activityController) + RegisterNewsRoutes(router, newsController) + + // Middleware tambahan (contoh: middleware autentikasi untuk protected routes) + protected := router.Group("/protected") + protected.Use(middlewares.AuthMiddleware()) // Hanya untuk rute yang membutuhkan autentikasi + { + // Misalnya, kita ingin menambahkan rute yang memerlukan autentikasi + protected.GET("/secure-data", func(ctx *gin.Context) { + ctx.JSON(200, gin.H{"message": "This is protected data!"}) + }) + } } diff --git a/services/achievement_service.go b/services/achievement_service.go new file mode 100644 index 0000000..fdc942f --- /dev/null +++ b/services/achievement_service.go @@ -0,0 +1,47 @@ +package services + +import ( + "backend/models" + "backend/repositories" +) + +type AchievementService interface { + GetAllAchievements() ([]models.Achievement, error) + GetAchievementByID(id string) (models.Achievement, error) + CreateAchievement(achievement *models.Achievement) error + UpdateAchievement(id string, updatedData *models.Achievement) error + UpdateAchievementImage(id, fileName, filePath string) error + DeleteAchievement(id string) error +} + +type achievementService struct { + repo repositories.AchievementRepository +} + +func NewAchievementService(repo repositories.AchievementRepository) AchievementService { + return &achievementService{repo: repo} +} + +func (s *achievementService) GetAllAchievements() ([]models.Achievement, error) { + return s.repo.FindAll() +} + +func (s *achievementService) GetAchievementByID(id string) (models.Achievement, error) { + return s.repo.FindByID(id) +} + +func (s *achievementService) CreateAchievement(achievement *models.Achievement) error { + return s.repo.Create(achievement) +} + +func (s *achievementService) UpdateAchievement(id string, updatedData *models.Achievement) error { + return s.repo.Update(id, updatedData) +} + +func (s *achievementService) UpdateAchievementImage(id, fileName, filePath string) error { + return s.repo.UpdateImagePath(id, filePath) +} + +func (s *achievementService) DeleteAchievement(id string) error { + return s.repo.Delete(id) +} diff --git a/services/activity_service.go b/services/activity_service.go new file mode 100644 index 0000000..3647c7d --- /dev/null +++ b/services/activity_service.go @@ -0,0 +1,42 @@ +package services + +import ( + "backend/models" + "backend/repositories" +) + +type ActivityService interface { + CreateActivity(activity *models.Activities) error + GetAllActivities() ([]models.Activities, error) + GetActivityByID(id string) (models.Activities, error) + UpdateActivity(id, title, tanggal, fileName string) (models.Activities, error) + DeleteActivity(id string) error +} + +type activityService struct { + repo repositories.ActivityRepository +} + +func NewActivityService(repo repositories.ActivityRepository) ActivityService { + return &activityService{repo: repo} +} + +func (s *activityService) CreateActivity(activity *models.Activities) error { + return s.repo.Create(activity) +} + +func (s *activityService) GetAllActivities() ([]models.Activities, error) { + return s.repo.FindAll() +} + +func (s *activityService) GetActivityByID(id string) (models.Activities, error) { + return s.repo.FindByID(id) +} + +func (s *activityService) UpdateActivity(id, title, tanggal, fileName string) (models.Activities, error) { + return s.repo.Update(id, title, tanggal, fileName) +} + +func (s *activityService) DeleteActivity(id string) error { + return s.repo.Delete(id) +} diff --git a/services/auth_service.go b/services/auth_service.go new file mode 100644 index 0000000..e652392 --- /dev/null +++ b/services/auth_service.go @@ -0,0 +1,37 @@ +package services + +import ( + "backend/repositories" + "errors" +) + +// AuthAdminService - interface untuk autentikasi admin +type AuthAdminService interface { + LoginAdmin(kode string) (string, error) +} + +type authAdminService struct { + repo repositories.UserRepository +} + +// NewAuthAdminService - inisialisasi AuthAdminService baru +func NewAuthAdminService(repo repositories.UserRepository) AuthAdminService { + return &authAdminService{repo} // Mengembalikan instance authAdminService sebagai AuthAdminService +} + +// LoginAdmin - implementasi autentikasi admin +func (s *authAdminService) LoginAdmin(kode string) (string, error) { + // Cari user berdasarkan kode + user, err := s.repo.FindByCode(kode) + if err != nil { + return "", errors.New("invalid credentials") + } + + // Generate token JWT + token, err := user.GenerateJWT() + if err != nil { + return "", errors.New("failed to generate token") + } + + return token, nil +} diff --git a/services/news_service.go b/services/news_service.go new file mode 100644 index 0000000..ec8a569 --- /dev/null +++ b/services/news_service.go @@ -0,0 +1,49 @@ +package services + +import ( + "backend/models" + "backend/repositories" + "context" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "mime/multipart" +) + +type NewsService interface { + GetAllNews(ctx context.Context) ([]models.News, error) + InsertNews(ctx context.Context, news models.News, file *multipart.FileHeader, ginContext *gin.Context) (models.News, error) +} + +type newsService struct { + repo repositories.NewsRepository +} + +func NewNewsService(repo repositories.NewsRepository) NewsService { + return &newsService{repo: repo} +} + +// GetAllNews mengambil seluruh berita +func (s *newsService) GetAllNews(ctx context.Context) ([]models.News, error) { + return s.repo.FindAll(ctx) +} + +// InsertNews menyimpan berita dan thumbnail ke database +func (s *newsService) InsertNews(ctx context.Context, news models.News, file *multipart.FileHeader, ginContext *gin.Context) (models.News, error) { + // Simpan file thumbnail dan dapatkan nama file + fileName, err := s.repo.SaveFile(file, ginContext) // Menggunakan ginContext + if err != nil { + return models.News{}, errors.New("failed to save file") + } + + // Update properti gambar pada berita + news.Thumbnail = fileName + news.ImageURL = fmt.Sprintf("%s/uploads/%s", ginContext.Request.Host, fileName) + + // Simpan berita ke database + if err := s.repo.Save(ctx, &news); err != nil { + return models.News{}, errors.New("failed to save news") + } + + return news, nil +} diff --git a/utils/auth.go b/utils/auth.go new file mode 100644 index 0000000..eaf6fdc --- /dev/null +++ b/utils/auth.go @@ -0,0 +1,31 @@ +package utils + +// +//import ( +// "fmt" +// "github.com/dgrijalva/jwt-go" +//) +// +//// ValidateToken validates the JWT token and returns the user ID +//func ValidateToken(tokenString string) (string, error) { +// // Example secret key, replace with your own secret +// secretKey := "your-secret-key" +// +// // Parse the token +// token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { +// return []byte(secretKey), nil +// }) +// if err != nil || !token.Valid { +// return "", fmt.Errorf("invalid token") +// } +// +// // Extract userID from the token claims +// claims, ok := token.Claims.(jwt.MapClaims) +// if !ok || !token.Valid { +// return "", fmt.Errorf("invalid token claims") +// } +// +// // Assuming userID is in the claims +// userID := claims["userID"].(string) +// return userID, nil +//} diff --git a/utils/error.go b/utils/error.go new file mode 100644 index 0000000..e10c1dd --- /dev/null +++ b/utils/error.go @@ -0,0 +1,12 @@ +package utils + +import ( + "github.com/gin-gonic/gin" +) + +// HandleError - helper untuk mengirim error response +func HandleError(ctx *gin.Context, statusCode int, message string) { + ctx.JSON(statusCode, gin.H{ + "error": message, + }) +} diff --git a/utils/file_validation.go b/utils/file_validation.go new file mode 100644 index 0000000..653400b --- /dev/null +++ b/utils/file_validation.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "mime/multipart" +) + +const maxSize = 5 * 1024 * 1024 // 5MB + +// ValidateFile checks if the uploaded file is valid (image type, size) +func ValidateFile(file *multipart.FileHeader) error { + // Check file size + if file.Size > maxSize { + return fmt.Errorf("file size exceeds the limit of 5MB") + } + + // Check file type + allowedTypes := []string{"image/png", "image/jpeg", "image/jpg"} + if !Contains(allowedTypes, file.Header.Get("Content-Type")) { + return fmt.Errorf("invalid file type. Only PNG, JPG, and JPEG are allowed") + } + + return nil +} + +// Contains checks if a value is present in a slice +func Contains(slice []string, value string) bool { + for _, item := range slice { + if item == value { + return true + } + } + return false +} diff --git a/utils/helper.go b/utils/helper.go index 3aefd6c..293336c 100644 --- a/utils/helper.go +++ b/utils/helper.go @@ -1,53 +1,53 @@ -package utils - -import ( - "os" - "path/filepath" - "strings" - - "github.com/go-playground/validator/v10" -) - -// ga usah dipake gpp si cuma enak aja klo ada -func Getenv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -func GetFileType(filePath string) string { - ext := strings.ToLower(filepath.Ext(filePath)) - switch ext { - case ".jpg", ".jpeg": - return "image/jpeg" - case ".png": - return "image/png" - case ".gif": - return "image/gif" - default: - return "" - } -} - -func ValidateStruct(data interface{}) map[string]string { - // Create a new validator instance - validate := validator.New() - - // Map to store validation errors - validationErrors := make(map[string]string) - - // Validate the data - if err := validate.Struct(data); err != nil { - // Type assert to get the validation errors - for _, err := range err.(validator.ValidationErrors) { - // Extract field name from struct tag - fieldName := strings.ToLower(err.Field()) - - // Store validation error - validationErrors[fieldName] = err.Tag() - } - } - - return validationErrors -} +package utils + +import ( + "os" + "path/filepath" + "strings" + + "github.com/go-playground/validator/v10" +) + +// ga usah dipake gpp si cuma enak aja klo ada +func Getenv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func GetFileType(filePath string) string { + ext := strings.ToLower(filepath.Ext(filePath)) + switch ext { + case ".jpg", ".jpeg": + return "image/jpeg" + case ".png": + return "image/png" + case ".gif": + return "image/gif" + default: + return "" + } +} + +func ValidateStruct(data interface{}) map[string]string { + // Create a new validator instance + validate := validator.New() + + // Map to store validation errors + validationErrors := make(map[string]string) + + // Validate the data + if err := validate.Struct(data); err != nil { + // Type assert to get the validation errors + for _, err := range err.(validator.ValidationErrors) { + // Extract field name from struct tag + fieldName := strings.ToLower(err.Field()) + + // Store validation error + validationErrors[fieldName] = err.Tag() + } + } + + return validationErrors +} diff --git a/utils/token.go b/utils/token.go new file mode 100644 index 0000000..4a0418a --- /dev/null +++ b/utils/token.go @@ -0,0 +1,38 @@ +package utils + +import ( + "errors" + "github.com/dgrijalva/jwt-go" +) + +// Secret key untuk menandatangani token +var secretKey = []byte("your_secret_key") + +// ValidateToken akan memverifikasi dan mengembalikan userID dari JWT yang valid +func ValidateToken(tokenString string) (uint, error) { + // Parse token menggunakan secret key + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Validasi bahwa token menggunakan algoritma HS256 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return secretKey, nil + }) + + // Jika terjadi error parsing, kembalikan error + if err != nil { + return 0, err + } + + // Pastikan token valid + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + // Mengambil userID dari claims + userID, ok := claims["user_id"].(float64) + if !ok { + return 0, errors.New("user_id not found in token") + } + return uint(userID), nil + } + + return 0, errors.New("invalid token") +} diff --git a/utils/token/token.go b/utils/token/token.go index ba58802..5a42875 100644 --- a/utils/token/token.go +++ b/utils/token/token.go @@ -1,103 +1,103 @@ -package token - -import ( - "backend/utils" - "fmt" - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt" -) - -var API_SECRET = utils.Getenv("API_SECRET", "xxx") - -func GenerateToken(user_id uint, role string) (string, error) { - token_lifespan, err := strconv.Atoi(utils.Getenv("TOKEN_HOUR_LIFESPAN", "168")) - - if err != nil { - return "", err - } - - claims := jwt.MapClaims{} - claims["authorized"] = true - claims["user_id"] = user_id - claims["role"] = role - claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix() - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - return token.SignedString([]byte(API_SECRET)) - -} - -func TokenValid(c *gin.Context) error { - tokenString := ExtractToken(c) - _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(API_SECRET), nil - }) - if err != nil { - return err - } - return nil -} - -func ExtractToken(c *gin.Context) string { - token := c.Query("token") - if token != "" { - return token - } - bearerToken := c.Request.Header.Get("Authorization") - if len(strings.Split(bearerToken, " ")) == 2 { - return strings.Split(bearerToken, " ")[1] - } - return "" -} - -func ExtractTokenID(c *gin.Context) (uint, error) { - - tokenString := ExtractToken(c) - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(API_SECRET), nil - }) - if err != nil { - return 0, err - } - claims, ok := token.Claims.(jwt.MapClaims) - if ok && token.Valid { - uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32) - if err != nil { - return 0, err - } - return uint(uid), nil - } - return 0, nil -} - -func ExtractUserRole(c *gin.Context) (string, error) { - tokenString := ExtractToken(c) - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(API_SECRET), nil - }) - if err != nil { - return "", err - } - claims, ok := token.Claims.(jwt.MapClaims) - if ok && token.Valid { - role, ok := claims["role"].(string) - if !ok { - return "", fmt.Errorf("unable to extract role from token") - } - return role, nil - } - return "", nil -} +package token + +import ( + "backend/utils" + "fmt" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" +) + +var API_SECRET = utils.Getenv("API_SECRET", "xxx") + +func GenerateToken(user_id uint, role string) (string, error) { + token_lifespan, err := strconv.Atoi(utils.Getenv("TOKEN_HOUR_LIFESPAN", "168")) + + if err != nil { + return "", err + } + + claims := jwt.MapClaims{} + claims["authorized"] = true + claims["user_id"] = user_id + claims["role"] = role + claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString([]byte(API_SECRET)) + +} + +func TokenValid(c *gin.Context) error { + tokenString := ExtractToken(c) + _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(API_SECRET), nil + }) + if err != nil { + return err + } + return nil +} + +func ExtractToken(c *gin.Context) string { + token := c.Query("token") + if token != "" { + return token + } + bearerToken := c.Request.Header.Get("Authorization") + if len(strings.Split(bearerToken, " ")) == 2 { + return strings.Split(bearerToken, " ")[1] + } + return "" +} + +func ExtractTokenID(c *gin.Context) (uint, error) { + + tokenString := ExtractToken(c) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(API_SECRET), nil + }) + if err != nil { + return 0, err + } + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32) + if err != nil { + return 0, err + } + return uint(uid), nil + } + return 0, nil +} + +func ExtractUserRole(c *gin.Context) (string, error) { + tokenString := ExtractToken(c) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(API_SECRET), nil + }) + if err != nil { + return "", err + } + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + role, ok := claims["role"].(string) + if !ok { + return "", fmt.Errorf("unable to extract role from token") + } + return role, nil + } + return "", nil +} From 8994641b6f0b4e3a00afefd8cb3d28a515869523 Mon Sep 17 00:00:00 2001 From: Fatih Maulana <85884851+NoBody313@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:41:25 +0700 Subject: [PATCH 2/4] Update main.go --- main.go | 67 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index ddbf64c..bbdd570 100644 --- a/main.go +++ b/main.go @@ -1,43 +1,58 @@ package main import ( - "log" - "backend/config" - "backend/docs" + "backend/controllers" + "backend/repositories" "backend/routes" + "backend/services" "backend/utils" + "fmt" + "log" + "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @termsOfService http://swagger.io/terms/ - func main() { - // Load environment variables from .env file - err := godotenv.Load(".env") - if err != nil { + // Load environment variables + if err := godotenv.Load(".env"); err != nil { log.Fatal("Error loading .env file") } - docs.SwaggerInfo.Title = "Swagger Files API" - docs.SwaggerInfo.Description = "This is a simple Files." - docs.SwaggerInfo.Version = "1.0" - envHost := utils.Getenv("ENV_HOST", "localhost:8080") - docs.SwaggerInfo.Host = envHost - docs.SwaggerInfo.Schemes = []string{"http", "https"} - - // Get the global database instance + // Connect to the database db := config.ConnectDatabase() + if db == nil { + log.Fatal("Failed to connect to the database") + } - // Create a new gin router with default middleware - r := routes.SetupRouter(db) - r.Run() + // Initialize Gin router + router := gin.Default() + + // Initialize repositories and services + userRepo := repositories.NewUserRepository(db) + authService := services.NewAuthAdminService(userRepo) + authController := controllers.NewAuthAdminController(authService) + + activityService := services.NewActivityService(db) + activityController := controllers.NewActivitiesController(activityService) + + newsRepo := repositories.NewNewsRepository(db) + newsService := services.NewNewsService(newsRepo) + newsController := controllers.NewNewsController(newsService) + + // Setup routes + routes.RegisterAuthRoutes(router, authController) + routes.RegisterActivityRoutes(router, activityController) + routes.RegisterNewsRoutes(router, newsController) + + // Run the server + host := utils.Getenv("ENV_HOST", "localhost") + port := utils.Getenv("ENV_PORT", "8080") + serverAddr := fmt.Sprintf("%s:%s", host, port) + + log.Printf("Server is running on %s", serverAddr) + if err := router.Run(serverAddr); err != nil { + log.Fatalf("Failed to start server: %v", err) + } } From 64139a4e2df9be2d4a1a1dc20bf92bc66ee29e24 Mon Sep 17 00:00:00 2001 From: Wildan Khalid <91074121+ShowMeMyCent@users.noreply.github.com> Date: Tue, 7 Jan 2025 00:25:11 +0700 Subject: [PATCH 3/4] rework --- app/controllers/achievementController.go | 278 ++++++++++++++ app/controllers/activityController.go | 253 +++++++++++++ app/controllers/newsController.go | 428 ++++++++++++++++++++++ app/repositories/achievment_repository.go | 46 +++ app/repositories/activity_controller.go | 34 ++ app/repositories/news_repository.go | 40 ++ app/services/achievment_service.go | 34 ++ app/services/activity_service.go | 30 ++ app/services/news_service.go | 34 ++ routes/achievment.go | 25 ++ routes/activities.go | 24 ++ routes/news.go | 25 ++ 12 files changed, 1251 insertions(+) create mode 100644 app/controllers/achievementController.go create mode 100644 app/controllers/activityController.go create mode 100644 app/controllers/newsController.go create mode 100644 app/repositories/achievment_repository.go create mode 100644 app/repositories/activity_controller.go create mode 100644 app/repositories/news_repository.go create mode 100644 app/services/achievment_service.go create mode 100644 app/services/activity_service.go create mode 100644 app/services/news_service.go create mode 100644 routes/achievment.go create mode 100644 routes/activities.go create mode 100644 routes/news.go diff --git a/app/controllers/achievementController.go b/app/controllers/achievementController.go new file mode 100644 index 0000000..2319d35 --- /dev/null +++ b/app/controllers/achievementController.go @@ -0,0 +1,278 @@ +package controllers + +import ( + "backend/app/models" + "backend/app/services" + "backend/utils" + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type AchievementsController struct { + DB *gorm.DB + Service *services.AchievementService +} + +// GetAllAchievements adalah fungsi untuk mendapatkan semua achievements dari database. +// @Summary Get All Achievement +// @Description Retrieves all achievements from the database. +// @Tags Achievement +// @Produce json +// @Success 200 {array} models.Achievement +// @Router /achievements [get] +func (ac *AchievementsController) GetAllAchievement(ctx *gin.Context) { + var achievements []models.Achievement + + // Retrieve all achievementsfrom the database + if err := ac.DB.Find(&achievements).Error; err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get achievements from database"}) + return + } + + // Response with achievements + ctx.JSON(http.StatusOK, achievements) +} + +// GetAchievementById adalah fungsi untuk mengambil achievements berdasarkan ID. +// @Summary Get achievement By ID +// @Description Retrieves achievement data by its ID. +// @Tags Achievement +// @Param id path string true "achievement ID" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /achievements/{id} [get] +func (ac *AchievementsController) GetAchievementById(ctx *gin.Context) { + achievementId := ctx.Param("id") + achievement, err := ac.Service.GetAchievementById(achievementId) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) + return + } + ctx.JSON(http.StatusOK, achievement) +} + +// GetFotoAchievement adalah fungsi untuk mengambil Foto Achievement berdasarkan ID. +// @Summary Get Foto Achievement +// @Description Retrieves the image of an Achievement by its ID. +// @Tags Achievement +// @Param id path string true "Achievement ID" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /achievements/foto/{id} [get] +func (ac *AchievementsController) GetFotoAchievement(ctx *gin.Context) { + // Get achievements image ID from URL path parameter, pakenya param klo mau diubah ke yg laen sok + achievementsID := ctx.Param("id") + + // Retrieve Thumbnail from the database by its ID + var achievements models.Achievement + if err := ac.DB.Where("id = ?", achievementsID).First(&achievements).Error; err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) + return + } + + // Define the file path + filePath := filepath.Join("uploads", achievements.Foto) + + // Check if the file exists + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + return + } + + // Set the headers for the file transfer and return the file + ctx.Header("Content-Description", "File Transfer") + ctx.Header("Content-Transfer-Encoding", "binary") + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", achievements.Foto)) + + // switch case buat content type + ext := filepath.Ext(achievements.Foto) + switch ext { + case ".png": + ctx.Header("Content-Type", "image/png") + case ".jpg", ".jpeg": + ctx.Header("Content-Type", "image/jpeg") + case ".gif": + ctx.Header("Content-Type", "image/gif") + case ".pdf": + ctx.Header("Content-Type", "application/pdf") + default: + ctx.Header("Content-Type", "application/octet-stream") + } + + ctx.File(filePath) +} + +// GetAchievementsByCategory adalah fungsi untuk mengambil Achievements berdasarkan Category. +// @Summary Get All Achievements By Category +// @Description Retrieves Achievements data by its Category. +// @Tags Achievement +// @Param category path string true "Achievements Category" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /achievements/category/{category} [get] +func (ac *AchievementsController) GetAchievementsByCategory(ctx *gin.Context) { + category := ctx.Param("category") + achievements, err := ac.Service.GetAchievementsByCategory(category) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievements not found"}) + return + } + ctx.JSON(http.StatusOK, achievements) +} + +// InsertAchievement adalah fungsi untuk membuat post Achievements terbaru. +// @Summary Insert a new Achievement +// @Description Insert a Achievements and saves them to the database. +// @Tags Achievement +// @Accept multipart/form-data +// @Param nama formData string true "Nama peraih achievement" +// @Param pencapaian formData string true "Pencapaian yang diraih" +// @Param link formData string true "link ke Achievementnya" +// @Param kategori formData string true "Kategori Achievement" +// @Param foto formData file true "Foto peraih Achievement" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {object} models.Achievement +// @Router /achievements [post] +func (ac *AchievementsController) InsertAchievement(ctx *gin.Context) { + file, err := ctx.FormFile("foto") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + host := utils.Getenv("ENV_HOST", "localhost") + + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + + var achievement models.Achievement + achievement.Nama = ctx.PostForm("nama") + achievement.Pencapaian = ctx.PostForm("pencapaian") + achievement.Link = ctx.PostForm("link") + achievement.Kategori = ctx.PostForm("kategori") + achievement.Foto = fileName + achievement.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) + + validationErrors := utils.ValidateStruct(achievement) + if len(validationErrors) > 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) + return + } + + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + if err := ac.Service.InsertAchievement(&achievement); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save achievement to database"}) + return + } + + ctx.JSON(http.StatusOK, achievement) +} + +// EditAchievements adalah fungsi untuk mengedit Achievements +// @Summary Edit Achievements +// @Description Edits a Achievements by its ID +// @Tags Achievement +// @Accept multipart/form-data +// @Param id path string true "Achievement ID" +// @Param nama formData string true "Nama peraih achievement" +// @Param pencapaian formData string true "Pencapaian yang diraih" +// @Param link formData string true "link ke Achievementnya" +// @Param kategori formData string true "Kategori Achievement" +// @Param foto formData file true "Foto peraih Achievement" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {object} models.Achievement +// @Router /achievements/{id} [put] +func (ac *AchievementsController) EditAchievement(ctx *gin.Context) { + achievementId := ctx.Param("id") + + var achievement *models.Achievement + achievement, err := ac.Service.GetAchievementById(achievementId) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) + return + } + + achievement.Nama = ctx.PostForm("nama") + achievement.Pencapaian = ctx.PostForm("pencapaian") + achievement.Link = ctx.PostForm("link") + achievement.Kategori = ctx.PostForm("kategori") + + validationErrors := utils.ValidateStruct(achievement) + if len(validationErrors) > 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) + return + } + + file, err := ctx.FormFile("foto") + if err == nil { + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + oldFilePath := filepath.Join("uploads", achievement.Foto) + if err := os.Remove(oldFilePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete old file"}) + return + } + + achievement.Foto = fileName + achievement.ImageURL = fmt.Sprintf("http://localhost:8080/uploads/%s", fileName) + } + + if err := ac.Service.UpdateAchievement(achievement); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update achievement in database"}) + return + } + + ctx.JSON(http.StatusOK, achievement) +} + +// DeleteAchievements adalah fungsi untuk menghapus Achievements dan gambar-nya dari database. +// @Summary Delete Achievements +// @Description Delete a Achievements and its thumbnail from the database and storage. +// @Tags Achievement +// @Param id path string true "Achievements ID" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {string} string "Achievements deleted successfully" +// @Router /achievements/{id} [delete] +func (ac *AchievementsController) DeleteAchievement(ctx *gin.Context) { + achievementId := ctx.Param("id") + + achievement, err := ac.Service.GetAchievementById(achievementId) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) + return + } + + filePath := filepath.Join("uploads", achievement.Foto) + if _, err := os.Stat(filePath); err == nil { + if err := os.Remove(filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) + return + } + } + + if err := ac.Service.DeleteAchievementById(achievementId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete achievement from database"}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "Achievement deleted successfully"}) +} diff --git a/app/controllers/activityController.go b/app/controllers/activityController.go new file mode 100644 index 0000000..4ef7908 --- /dev/null +++ b/app/controllers/activityController.go @@ -0,0 +1,253 @@ +package controllers + +import ( + "backend/app/models" + "backend/app/services" + "backend/utils" + "fmt" + "net/http" + "os" + + "github.com/gin-gonic/gin" + + "path/filepath" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ActivitiesController struct { + DB *gorm.DB + Service *services.ActivityService +} + +// UploadActivity adalah fungsi untuk mengupload activity beserta file-nya. +// @Summary Upload an activity with File +// @Description Uploads a Activities along with its file and saves them to the database. +// @Tags Activities +// @Accept multipart/form-data +// @Param title formData string true "Title Activities" +// @Param tanggal formData string true "Tanggal Activities" +// @Param gambar formData file true "File gambar" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {object} models.Activities +// @Router /activities [post] +func (ac *ActivitiesController) UploadActivity(ctx *gin.Context) { + file, err := ctx.FormFile("gambar") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + host := utils.Getenv("ENV_HOST", "localhost") + + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + title := ctx.PostForm("title") + tanggal := ctx.PostForm("tanggal") + + activity := models.Activities{ + Title: title, + Tanggal: tanggal, + Gambar: fileName, + ImageURL: fmt.Sprintf("https://%s/uploads/%s", host, fileName), + } + + if err := ac.Service.CreateActivity(&activity); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save activity to database"}) + return + } + + ctx.JSON(http.StatusOK, activity) +} + +// GetAllActivities adalah fungsi untuk mendapatkan semua activity dari database. +// @Summary Get All Activities +// @Description Retrieves all Activities from the database. +// @Tags Activities +// @Produce json +// @Success 200 {array} models.Activities +// @Router /activities [get] +func (ac *ActivitiesController) GetAllActivities(ctx *gin.Context) { + activities, err := ac.Service.GetAllActivities() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get activities from database"}) + return + } + + ctx.JSON(http.StatusOK, activities) +} + +// GetActivityById adalah fungsi untuk mengambil activity berdasarkan ID. +// @Summary Get Activity By ID +// @Description Retrieves Activity data by its ID. +// @Tags Activities +// @Param id path string true "Activity ID" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /activities/{id} [get] +func (ac *ActivitiesController) GetActivityById(ctx *gin.Context) { + activityId := ctx.Param("id") + + activity, err := ac.Service.GetActivityById(activityId) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) + return + } + + ctx.JSON(http.StatusOK, activity) +} + +// GetGambar adalah fungsi untuk mengambil gambar activity berdasarkan ID. +// @Summary Get Gambar Activity +// @Description Retrieves the image of a activity by its ID. +// @Tags Activities +// @Param id path string true "Activity ID" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /activities/file/{id} [get] +func (ac *ActivitiesController) GetGambarActivities(ctx *gin.Context) { + activityID := ctx.Param("id") + + activity, err := ac.Service.GetActivityById(activityID) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) + return + } + + filePath := filepath.Join("uploads", activity.Gambar) + _, err = os.Stat(filePath) + if os.IsNotExist(err) { + ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + return + } + + ctx.Header("Content-Description", "File Transfer") + ctx.Header("Content-Transfer-Encoding", "binary") + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", activity.Gambar)) + + ext := filepath.Ext(activity.Gambar) + switch ext { + case ".png": + ctx.Header("Content-Type", "image/png") + case ".jpg", ".jpeg": + ctx.Header("Content-Type", "image/jpeg") + case ".gif": + ctx.Header("Content-Type", "image/gif") + case ".pdf": + ctx.Header("Content-Type", "application/pdf") + default: + ctx.Header("Content-Type", "application/octet-stream") + } + + ctx.File(filePath) +} + +// EditActivity adalah fungsi untuk mengedit Activity, termasuk kemampuan untuk mengganti file Activity. +// @Summary Edit Activity +// @Description Edits a Activity including the ability to replace its file. +// @Tags Activities +// @Accept multipart/form-data +// @Param id path string true "Activity ID" +// @Param title formData string true "Title Activity" +// @Param tanggal formData string true "Tanggal Activity" +// @Param gambar formData file false "Gambar Activity (optional)" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {object} models.Activities +// @Router /activities/{id} [put] +func (ac *ActivitiesController) EditActivity(ctx *gin.Context) { + activityID := ctx.Param("id") + + activity, err := ac.Service.GetActivityById(activityID) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) + return + } + + title := ctx.PostForm("title") + tanggal := ctx.PostForm("tanggal") + host := utils.Getenv("ENV_HOST", "localhost") + + activity.Title = title + activity.Tanggal = tanggal + + file, err := ctx.FormFile("gambar") + if err != nil && err != http.ErrMissingFile { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if file != nil { + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + oldFilePath := filepath.Join("uploads", activity.Gambar) + if err := os.Remove(oldFilePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove old file"}) + } + + activity.Gambar = fileName + activity.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) + } + + if err := ac.Service.UpdateActivity(activity); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save updated activity to database"}) + return + } + + ctx.JSON(http.StatusOK, activity) +} + +// DeleteActivity adalah fungsi untuk menghapus Activity dan gambar-nya dari database. +// @Summary Delete Activity +// @Description Deletes a Activity and its gambar from the database and storage. +// @Tags Activities +// @Param id path string true "Activity ID" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {string} string "Activity deleted successfully" +// @Router /activities/{id} [delete] +func (ac *ActivitiesController) DeleteActivity(ctx *gin.Context) { + activityID := ctx.Param("id") + + activity, err := ac.Service.GetActivityById(activityID) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) + return + } + + filePath := filepath.Join("uploads", activity.Gambar) + _, err = os.Stat(filePath) + if os.IsNotExist(err) { + if err := ac.Service.DeleteActivity(activityID); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete activity from database"}) + return + } + ctx.JSON(http.StatusOK, "Activity deleted successfully") + return + } + + if err := os.Remove(filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) + return + } + + if err := ac.Service.DeleteActivity(activityID); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete activity from database"}) + return + } + + ctx.JSON(http.StatusOK, "Activity and file deleted successfully") +} diff --git a/app/controllers/newsController.go b/app/controllers/newsController.go new file mode 100644 index 0000000..4fc4ca9 --- /dev/null +++ b/app/controllers/newsController.go @@ -0,0 +1,428 @@ +package controllers + +import ( + "backend/app/models" + "backend/app/services" + "backend/utils" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type NewsResponse struct { + ID uint `json:"id"` + Title string `json:"title"` + Kategori string `json:"kategori"` + Thumbnail string `json:"thumbnail"` + IsiKonten interface{} `json:"isi_konten"` + NamaPenulis string `json:"nama_penulis"` + Link string `json:"link"` + ImageURL string `json:"image_url"` + Date string `json:"date"` +} + +type NewsController struct { + DB *gorm.DB + Service *services.NewsService +} + +// GetAllNews adalah fungsi untuk mendapatkan semua news dari database. +// @Summary Get All News +// @Description Retrieves all news from the database. +// @Tags News +// @Produce json +// @Success 200 {array} models.News +// @Router /news [get] +func (nc *NewsController) GetAllNews(ctx *gin.Context) { + news, err := nc.Service.GetAllNews() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get news from database"}) + return + } + + var response []NewsResponse + for _, n := range news { + var isiKontenDecoded interface{} + if err := json.Unmarshal([]byte(n.IsiKonten), &isiKontenDecoded); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) + return + } + + response = append(response, NewsResponse{ + ID: n.ID, + Title: n.Title, + Kategori: n.Kategori, + Thumbnail: n.Thumbnail, + IsiKonten: isiKontenDecoded, + NamaPenulis: n.NamaPenulis, + Link: n.Link, + ImageURL: n.ImageURL, + Date: n.Date, + }) + } + + ctx.JSON(http.StatusOK, response) +} + +// GetNewsById adalah fungsi untuk mengambil news berdasarkan ID. +// @Summary Get News By ID +// @Description Retrieves news data by its ID. +// @Tags News +// @Param id path string true "News ID" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /news/{id} [get] +func (nc *NewsController) GetNewsById(ctx *gin.Context) { + newsId := ctx.Param("id") + news, err := nc.Service.GetNewsById(newsId) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) + return + } + + var isiKontenDecoded interface{} + if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) + return + } + + response := NewsResponse{ + ID: news.ID, + Title: news.Title, + Kategori: news.Kategori, + Thumbnail: news.Thumbnail, + IsiKonten: isiKontenDecoded, + NamaPenulis: news.NamaPenulis, + Link: news.Link, + ImageURL: news.ImageURL, + Date: news.Date, + } + + ctx.JSON(http.StatusOK, response) +} + +// GetThumbnailNews adalah fungsi untuk mengambil Thumbnail News berdasarkan ID. +// @Summary Get Thumbnail News +// @Description Retrieves the image of a News by its ID. +// @Tags News +// @Param id path string true "News ID" +// @Produce octet-stream +// @Success 200 {file} octet-stream +// @Router /news/file/{id} [get] +func (nc *NewsController) GetThumbnailNews(ctx *gin.Context) { + newsID := ctx.Param("id") + news, err := nc.Service.GetNewsById(newsID) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) + return + } + + filePath := filepath.Join("uploads", news.Thumbnail) + _, err = os.Stat(filePath) + if os.IsNotExist(err) { + ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + return + } + + ctx.Header("Content-Description", "File Transfer") + ctx.Header("Content-Transfer-Encoding", "binary") + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", news.Thumbnail)) + + ext := filepath.Ext(news.Thumbnail) + switch ext { + case ".png": + ctx.Header("Content-Type", "image/png") + case ".jpg", ".jpeg": + ctx.Header("Content-Type", "image/jpeg") + case ".gif": + ctx.Header("Content-Type", "image/gif") + case ".pdf": + ctx.Header("Content-Type", "application/pdf") + default: + ctx.Header("Content-Type", "application/octet-stream") + } + + ctx.File(filePath) +} + +// GetNewsByCategory adalah fungsi untuk mengambil news berdasarkan Category. +// @Summary Get All News By Category +// @Description Retrieves news data by its Category. +// @Tags News +// @Param category path string true "News Category" +// @Produce json +// @Success 200 {array} NewsResponse +// @Router /news/category/{category} [get] +func (nc *NewsController) GetNewsByCategory(ctx *gin.Context) { + newsCategory := ctx.Param("category") + newsList, err := nc.Service.GetNewsByCategory(newsCategory) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) + return + } + + var response []NewsResponse + for _, news := range newsList { + var isiKontenDecoded interface{} + if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) + return + } + + response = append(response, NewsResponse{ + ID: news.ID, + Title: news.Title, + Kategori: news.Kategori, + Thumbnail: news.Thumbnail, + IsiKonten: isiKontenDecoded, + NamaPenulis: news.NamaPenulis, + Link: news.Link, + ImageURL: news.ImageURL, + Date: news.Date, + }) + } + + ctx.JSON(http.StatusOK, response) +} + +// InsertNews adalah fungsi untuk membuat post news terbaru. +// @Summary Insert a new news +// @Description Insert a news and saves them to the database. +// @Tags News +// @Accept multipart/form-data +// @Param title formData string true "Judul news" +// @Param kategori formData string true "Kategori news" +// @Param thumbnail formData file true "Thumbnail news" +// @Param isi_konten formData string true "Isi konten news" +// @Param nama_penulis formData string true "Nama penulis news" +// @Param link formData string true "Link news" +// @Param date formData string true "Date news" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {object} models.News +// @Router /news [post] +func (nc *NewsController) InsertNews(ctx *gin.Context) { + file, err := ctx.FormFile("thumbnail") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + host := utils.Getenv("ENV_HOST", "localhost") + + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + + var news models.News + news.Title = ctx.PostForm("title") + news.Kategori = ctx.PostForm("kategori") + news.Date = ctx.PostForm("date") + isiKonten := ctx.PostForm("isi_konten") + + var isiKontenJSON interface{} + if err := json.Unmarshal([]byte(isiKonten), &isiKontenJSON); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format for isi_konten"}) + return + } + + isiKontenBytes, err := json.Marshal(isiKontenJSON) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process isi_konten"}) + return + } + news.IsiKonten = string(isiKontenBytes) + news.NamaPenulis = ctx.PostForm("nama_penulis") + news.Link = ctx.PostForm("link") + news.Thumbnail = fileName + news.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) + + validationErrors := utils.ValidateStruct(news) + if len(validationErrors) > 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) + return + } + + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + if err := nc.Service.CreateNews(&news); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save news to database"}) + return + } + + var isiKontenDecoded interface{} + if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) + return + } + + response := NewsResponse{ + ID: news.ID, + Title: news.Title, + Kategori: news.Kategori, + Thumbnail: news.Thumbnail, + IsiKonten: isiKontenDecoded, + NamaPenulis: news.NamaPenulis, + Link: news.Link, + ImageURL: news.ImageURL, + Date: news.Date, + } + + ctx.JSON(http.StatusOK, response) +} + +// EditNews adalah fungsi untuk mengedit News +// @Summary Edit News +// @Description Edits a News by its ID +// @Tags News +// @Accept multipart/form-data +// @Param id path string true "News ID" +// @Param title formData string true "Title News" +// @Param kategori formData string true "Kategori News" +// @Param thumbnail formData file false "Thumbnail News" +// @Param isi_konten formData string true "Isi Konten News" +// @Param nama_penulis formData string true "Nama Penulis News" +// @Param link formData string true "Link News" +// @Param date formData string true "Date News" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {object} models.News +// @Router /news/{id} [put] + +func (nc *NewsController) EditNews(ctx *gin.Context) { + newsID := ctx.Param("id") + host := utils.Getenv("ENV_HOST", "localhost") + + news, err := nc.Service.GetNewsById(newsID) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) + return + } + + news.Title = ctx.PostForm("title") + news.Kategori = ctx.PostForm("kategori") + news.Date = ctx.PostForm("date") + isiKonten := ctx.PostForm("isi_konten") + + if isiKonten != "" { + var isiKontenJSON interface{} + if err := json.Unmarshal([]byte(isiKonten), &isiKontenJSON); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format for isi_konten"}) + return + } + + isiKontenBytes, err := json.Marshal(isiKontenJSON) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process isi_konten"}) + return + } + news.IsiKonten = string(isiKontenBytes) + } + news.NamaPenulis = ctx.PostForm("nama_penulis") + news.Link = ctx.PostForm("link") + + validationErrors := utils.ValidateStruct(news) + if len(validationErrors) > 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": validationErrors}) + return + } + + file, err := ctx.FormFile("thumbnail") + if err != nil && err != http.ErrMissingFile { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if file != nil { + fileName := uuid.New().String() + filepath.Ext(file.Filename) + filePath := filepath.Join("uploads", fileName) + + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + oldFilePath := filepath.Join("uploads", news.Thumbnail) + if err := os.Remove(oldFilePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove old file"}) + return + } + + news.Thumbnail = fileName + news.ImageURL = fmt.Sprintf("https://%s/uploads/%s", host, fileName) + } + + if err := nc.Service.UpdateNews(news); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save updated news to database"}) + return + } + + var isiKontenDecoded interface{} + if err := json.Unmarshal([]byte(news.IsiKonten), &isiKontenDecoded); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode isi_konten"}) + return + } + + response := NewsResponse{ + ID: news.ID, + Title: news.Title, + Kategori: news.Kategori, + Thumbnail: news.Thumbnail, + IsiKonten: isiKontenDecoded, + NamaPenulis: news.NamaPenulis, + Link: news.Link, + ImageURL: news.ImageURL, + Date: news.Date, + } + + ctx.JSON(http.StatusOK, response) +} + +// DeleteNews adalah fungsi untuk menghapus News dan gambar-nya dari database. +// @Summary Delete News +// @Description Delete a News and its thumbnail from the database and storage. +// @Tags News +// @Param id path string true "News ID" +// @Param Authorization header string true "Authorization. How to input in swagger : 'Bearer '" +// @Produce json +// @Success 200 {string} string "News deleted successfully" +// @Router /news/{id} [delete] +func (nc *NewsController) DeleteNews(ctx *gin.Context) { + newsId := ctx.Param("id") + + news, err := nc.Service.GetNewsById(newsId) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "News not found"}) + return + } + + filePath := filepath.Join("uploads", news.Thumbnail) + _, err = os.Stat(filePath) + if os.IsNotExist(err) { + if err := nc.Service.DeleteNewsById(newsId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete news from database"}) + return + } + ctx.JSON(http.StatusOK, "News deleted successfully") + return + } + + if err := os.Remove(filePath); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) + return + } + + if err := nc.Service.DeleteNewsById(newsId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete news from database"}) + return + } + + ctx.JSON(http.StatusOK, "News deleted successfully") +} diff --git a/app/repositories/achievment_repository.go b/app/repositories/achievment_repository.go new file mode 100644 index 0000000..97619d2 --- /dev/null +++ b/app/repositories/achievment_repository.go @@ -0,0 +1,46 @@ +package repositories + +import ( + "backend/app/models" + "gorm.io/gorm" +) + +type AchievementRepository struct { + DB *gorm.DB +} + +func (ar *AchievementRepository) GetAllAchievements() ([]models.Achievement, error) { + var achievements []models.Achievement + if err := ar.DB.Find(&achievements).Error; err != nil { + return nil, err + } + return achievements, nil +} + +func (ar *AchievementRepository) GetAchievementById(id string) (*models.Achievement, error) { + var achievement models.Achievement + if err := ar.DB.Where("id = ?", id).First(&achievement).Error; err != nil { + return nil, err + } + return &achievement, nil +} + +func (ar *AchievementRepository) GetAchievementsByCategory(category string) ([]models.Achievement, error) { + var achievements []models.Achievement + if err := ar.DB.Where("kategori = ?", category).Find(&achievements).Error; err != nil { + return nil, err + } + return achievements, nil +} + +func (ar *AchievementRepository) InsertAchievement(achievement *models.Achievement) error { + return ar.DB.Create(achievement).Error +} + +func (ar *AchievementRepository) UpdateAchievement(achievement *models.Achievement) error { + return ar.DB.Save(achievement).Error +} + +func (ar *AchievementRepository) DeleteAchievementById(id string) error { + return ar.DB.Where("id = ?", id).Delete(&models.Achievement{}).Error +} diff --git a/app/repositories/activity_controller.go b/app/repositories/activity_controller.go new file mode 100644 index 0000000..b8f7608 --- /dev/null +++ b/app/repositories/activity_controller.go @@ -0,0 +1,34 @@ +package repositories + +import ( + "backend/app/models" + "gorm.io/gorm" +) + +type ActivityRepository struct { + DB *gorm.DB +} + +func (ar *ActivityRepository) CreateActivity(activity *models.Activities) error { + return ar.DB.Create(activity).Error +} + +func (ar *ActivityRepository) GetAllActivities() ([]models.Activities, error) { + var activities []models.Activities + err := ar.DB.Find(&activities).Error + return activities, err +} + +func (ar *ActivityRepository) GetActivityById(id string) (*models.Activities, error) { + var activity models.Activities + err := ar.DB.Where("id = ?", id).First(&activity).Error + return &activity, err +} + +func (ar *ActivityRepository) UpdateActivity(activity *models.Activities) error { + return ar.DB.Save(activity).Error +} + +func (ar *ActivityRepository) DeleteActivity(id string) error { + return ar.DB.Where("id = ?", id).Delete(&models.Activities{}).Error +} diff --git a/app/repositories/news_repository.go b/app/repositories/news_repository.go new file mode 100644 index 0000000..59f45ec --- /dev/null +++ b/app/repositories/news_repository.go @@ -0,0 +1,40 @@ +package repositories + +import ( + "backend/app/models" + "gorm.io/gorm" +) + +type NewsRepository struct { + DB *gorm.DB +} + +func (nr *NewsRepository) GetAllNews() ([]models.News, error) { + var news []models.News + err := nr.DB.Find(&news).Error + return news, err +} + +func (nr *NewsRepository) GetNewsById(id string) (*models.News, error) { + var news models.News + err := nr.DB.Where("id = ?", id).First(&news).Error + return &news, err +} + +func (nr *NewsRepository) GetNewsByCategory(category string) ([]models.News, error) { + var news []models.News + err := nr.DB.Where("kategori = ?", category).Find(&news).Error + return news, err +} + +func (nr *NewsRepository) CreateNews(news *models.News) error { + return nr.DB.Create(news).Error +} + +func (nr *NewsRepository) UpdateNews(news *models.News) error { + return nr.DB.Save(news).Error +} + +func (nr *NewsRepository) DeleteNewsById(id string) error { + return nr.DB.Where("id = ?", id).Delete(&models.News{}).Error +} diff --git a/app/services/achievment_service.go b/app/services/achievment_service.go new file mode 100644 index 0000000..e74f9bc --- /dev/null +++ b/app/services/achievment_service.go @@ -0,0 +1,34 @@ +package services + +import ( + "backend/app/models" + "backend/app/repositories" +) + +type AchievementService struct { + Repo *repositories.AchievementRepository +} + +func (as *AchievementService) GetAllAchievements() ([]models.Achievement, error) { + return as.Repo.GetAllAchievements() +} + +func (as *AchievementService) GetAchievementById(id string) (*models.Achievement, error) { + return as.Repo.GetAchievementById(id) +} + +func (as *AchievementService) GetAchievementsByCategory(category string) ([]models.Achievement, error) { + return as.Repo.GetAchievementsByCategory(category) +} + +func (as *AchievementService) InsertAchievement(achievement *models.Achievement) error { + return as.Repo.InsertAchievement(achievement) +} + +func (as *AchievementService) UpdateAchievement(achievement *models.Achievement) error { + return as.Repo.UpdateAchievement(achievement) +} + +func (as *AchievementService) DeleteAchievementById(id string) error { + return as.Repo.DeleteAchievementById(id) +} diff --git a/app/services/activity_service.go b/app/services/activity_service.go new file mode 100644 index 0000000..0888522 --- /dev/null +++ b/app/services/activity_service.go @@ -0,0 +1,30 @@ +package services + +import ( + "backend/app/models" + "backend/app/repositories" +) + +type ActivityService struct { + Repo *repositories.ActivityRepository +} + +func (as *ActivityService) CreateActivity(activity *models.Activities) error { + return as.Repo.CreateActivity(activity) +} + +func (as *ActivityService) GetAllActivities() ([]models.Activities, error) { + return as.Repo.GetAllActivities() +} + +func (as *ActivityService) GetActivityById(id string) (*models.Activities, error) { + return as.Repo.GetActivityById(id) +} + +func (as *ActivityService) UpdateActivity(activity *models.Activities) error { + return as.Repo.UpdateActivity(activity) +} + +func (as *ActivityService) DeleteActivity(id string) error { + return as.Repo.DeleteActivity(id) +} diff --git a/app/services/news_service.go b/app/services/news_service.go new file mode 100644 index 0000000..df2babb --- /dev/null +++ b/app/services/news_service.go @@ -0,0 +1,34 @@ +package services + +import ( + "backend/app/models" + "backend/app/repositories" +) + +type NewsService struct { + Repo *repositories.NewsRepository +} + +func (ns *NewsService) GetAllNews() ([]models.News, error) { + return ns.Repo.GetAllNews() +} + +func (ns *NewsService) GetNewsById(id string) (*models.News, error) { + return ns.Repo.GetNewsById(id) +} + +func (ns *NewsService) GetNewsByCategory(category string) ([]models.News, error) { + return ns.Repo.GetNewsByCategory(category) +} + +func (ns *NewsService) CreateNews(news *models.News) error { + return ns.Repo.CreateNews(news) +} + +func (ns *NewsService) UpdateNews(news *models.News) error { + return ns.Repo.UpdateNews(news) +} + +func (ns *NewsService) DeleteNewsById(id string) error { + return ns.Repo.DeleteNewsById(id) +} diff --git a/routes/achievment.go b/routes/achievment.go new file mode 100644 index 0000000..7d1c38e --- /dev/null +++ b/routes/achievment.go @@ -0,0 +1,25 @@ +package routes + +import ( + "backend/app/controllers" + "backend/app/repositories" + "backend/app/services" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterAchievementRoutes(r *gin.Engine, db *gorm.DB) { + achievementRepo := &repositories.AchievementRepository{DB: db} + achievementService := &services.AchievementService{Repo: achievementRepo} + achievementController := &controllers.AchievementsController{Service: achievementService} + + r.GET("/achievements", achievementController.GetAllAchievement) + r.GET("/achievements/:id", achievementController.GetAchievementById) + r.GET("/achievements/foto/:id", achievementController.GetFotoAchievement) + r.GET("/achievements/category/:category", achievementController.GetAchievementsByCategory) + + authGroup := AuthGroup(r) + authGroup.POST("/achievements", achievementController.InsertAchievement) + authGroup.PUT("/achievements/:id", achievementController.EditAchievement) + authGroup.DELETE("/achievements/:id", achievementController.DeleteAchievement) +} diff --git a/routes/activities.go b/routes/activities.go new file mode 100644 index 0000000..b8a851c --- /dev/null +++ b/routes/activities.go @@ -0,0 +1,24 @@ +package routes + +import ( + "backend/app/controllers" + "backend/app/repositories" + "backend/app/services" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterActivityRoutes(r *gin.Engine, db *gorm.DB) { + activityRepo := &repositories.ActivityRepository{DB: db} + activityService := &services.ActivityService{Repo: activityRepo} + activityController := &controllers.ActivitiesController{Service: activityService} + + r.GET("/activities", activityController.GetAllActivities) + r.GET("/activities/:id", activityController.GetActivityById) + r.GET("/activities/file/:id", activityController.GetGambarActivities) + + authGroup := AuthGroup(r) + authGroup.POST("/activities", activityController.UploadActivity) + authGroup.PUT("/activities/:id", activityController.EditActivity) + authGroup.DELETE("/activities/:id", activityController.DeleteActivity) +} diff --git a/routes/news.go b/routes/news.go new file mode 100644 index 0000000..1e029c2 --- /dev/null +++ b/routes/news.go @@ -0,0 +1,25 @@ +package routes + +import ( + "backend/app/controllers" + "backend/app/repositories" + "backend/app/services" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterNewsRoutes(r *gin.Engine, db *gorm.DB) { + newsRepo := &repositories.NewsRepository{DB: db} + newsService := &services.NewsService{Repo: newsRepo} + newsController := &controllers.NewsController{Service: newsService} + + r.GET("/news", newsController.GetAllNews) + r.GET("/news/:id", newsController.GetNewsById) + r.GET("/news/file/:id", newsController.GetThumbnailNews) + r.GET("/news/category/:category", newsController.GetNewsByCategory) + + authGroup := AuthGroup(r) + authGroup.POST("/news", newsController.InsertNews) + authGroup.PUT("/news/:id", newsController.EditNews) + authGroup.DELETE("/news/:id", newsController.DeleteNews) +} From aae4b0284f6254bf68bc1cdbfa5fe32857fc495d Mon Sep 17 00:00:00 2001 From: Wildan Khalid <91074121+ShowMeMyCent@users.noreply.github.com> Date: Tue, 7 Jan 2025 01:09:16 +0700 Subject: [PATCH 4/4] rework --- .idea/vcs.xml | 1 + app/controllers/activityController.go | 9 +- {controllers => app/controllers}/authAdmin.go | 11 +- {middlewares => app/middlewares}/auth.go | 0 .../middlewares}/middleware.go | 94 +++--- {models => app/models}/achievement.go | 0 app/models/activities.go | 9 + {models => app/models}/news.go | 0 {models => app/models}/user.go | 0 app/services/auth_service.go | 30 ++ config/automigrate.go | 21 ++ config/database.go | 15 - config/db.go | 33 --- controllers/achievementController.go | 131 --------- controllers/activityController.go | 141 --------- controllers/authAdminController.go | 58 ---- controllers/newsController.go | 84 ------ controllers/paperController.go | 273 ------------------ go.mod | 1 + go.sum | 2 + main.go | 67 ++--- models/activities.go | 9 - models/model.go | 90 ------ repositories/achievement_repository.go | 107 ------- repositories/activity_repository.go | 62 ---- repositories/news_repository.go | 64 ---- repositories/user_repository.go | 40 --- routes/achievement_routes.go | 19 -- routes/activity_routes.go | 27 -- routes/auth_routes.go | 10 +- routes/init_routes.go | 36 +++ routes/news.go | 1 + routes/news_routes.go | 15 - routes/routes.go | 47 --- services/achievement_service.go | 47 --- services/activity_service.go | 42 --- services/auth_service.go | 37 --- services/news_service.go | 49 ---- 38 files changed, 185 insertions(+), 1497 deletions(-) rename {controllers => app/controllers}/authAdmin.go (87%) rename {middlewares => app/middlewares}/auth.go (100%) rename {middlewares => app/middlewares}/middleware.go (95%) rename {models => app/models}/achievement.go (100%) create mode 100644 app/models/activities.go rename {models => app/models}/news.go (100%) rename {models => app/models}/user.go (100%) create mode 100644 app/services/auth_service.go create mode 100644 config/automigrate.go delete mode 100644 config/db.go delete mode 100644 controllers/achievementController.go delete mode 100644 controllers/activityController.go delete mode 100644 controllers/authAdminController.go delete mode 100644 controllers/newsController.go delete mode 100644 controllers/paperController.go delete mode 100644 models/activities.go delete mode 100644 models/model.go delete mode 100644 repositories/achievement_repository.go delete mode 100644 repositories/activity_repository.go delete mode 100644 repositories/news_repository.go delete mode 100644 repositories/user_repository.go delete mode 100644 routes/achievement_routes.go delete mode 100644 routes/activity_routes.go create mode 100644 routes/init_routes.go delete mode 100644 routes/news_routes.go delete mode 100644 routes/routes.go delete mode 100644 services/achievement_service.go delete mode 100644 services/activity_service.go delete mode 100644 services/auth_service.go delete mode 100644 services/news_service.go diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 6c0b863..288b36b 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/app/controllers/activityController.go b/app/controllers/activityController.go index 4ef7908..1404fc7 100644 --- a/app/controllers/activityController.go +++ b/app/controllers/activityController.go @@ -5,15 +5,12 @@ import ( "backend/app/services" "backend/utils" "fmt" - "net/http" - "os" - "github.com/gin-gonic/gin" - - "path/filepath" - "github.com/google/uuid" "gorm.io/gorm" + "net/http" + "os" + "path/filepath" ) type ActivitiesController struct { diff --git a/controllers/authAdmin.go b/app/controllers/authAdmin.go similarity index 87% rename from controllers/authAdmin.go rename to app/controllers/authAdmin.go index dccf5ab..539d75f 100644 --- a/controllers/authAdmin.go +++ b/app/controllers/authAdmin.go @@ -1,7 +1,8 @@ +// app/controllers/authAdmin.go package controllers import ( - "backend/models" + "backend/app/services" "fmt" "net/http" @@ -30,10 +31,7 @@ func LoginAdmin(c *gin.Context) { return } - u := models.User{} - u.Code = input.Kode - - token, err := u.LoginCheckAdmin(db) + token, err := services.LoginCheckAdmin(db, input.Kode) if err != nil { fmt.Println(err) @@ -42,9 +40,8 @@ func LoginAdmin(c *gin.Context) { } admin := map[string]string{ - "kode": u.Code, + "kode": input.Kode, } c.JSON(http.StatusOK, gin.H{"message": "login success", "user": admin, "token": token}) - } diff --git a/middlewares/auth.go b/app/middlewares/auth.go similarity index 100% rename from middlewares/auth.go rename to app/middlewares/auth.go diff --git a/middlewares/middleware.go b/app/middlewares/middleware.go similarity index 95% rename from middlewares/middleware.go rename to app/middlewares/middleware.go index d177308..21241c9 100644 --- a/middlewares/middleware.go +++ b/app/middlewares/middleware.go @@ -1,47 +1,47 @@ -package middlewares - -import ( - "backend/utils/token" - "net/http" - "time" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -func CorsMiddleware() gin.HandlerFunc { - return cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - MaxAge: 7 * time.Hour, - }) -} - -func AdminCheckMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - err := token.TokenValid(c) - if err != nil { - c.String(http.StatusUnauthorized, err.Error()) - c.Abort() - return - } - - // Setelah token divalidasi, kita dapat memeriksa role dari user yang terautentikasi. - role, err := token.ExtractUserRole(c) - if err != nil { - c.String(http.StatusUnauthorized, err.Error()) - c.Abort() - return - } - - // Jika role tidak sesuai, berikan pesan error dan hentikan proses. - if role != "admin" { - c.String(http.StatusForbidden, "Access denied. Insufficient role.") - c.Abort() - return - } - } -} +package middlewares + +import ( + "backend/utils/token" + "net/http" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func CorsMiddleware() gin.HandlerFunc { + return cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 7 * time.Hour, + }) +} + +func AdminCheckMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + err := token.TokenValid(c) + if err != nil { + c.String(http.StatusUnauthorized, err.Error()) + c.Abort() + return + } + + // Setelah token divalidasi, kita dapat memeriksa role dari user yang terautentikasi. + role, err := token.ExtractUserRole(c) + if err != nil { + c.String(http.StatusUnauthorized, err.Error()) + c.Abort() + return + } + + // Jika role tidak sesuai, berikan pesan error dan hentikan proses. + if role != "admin" { + c.String(http.StatusForbidden, "Access denied. Insufficient role.") + c.Abort() + return + } + } +} diff --git a/models/achievement.go b/app/models/achievement.go similarity index 100% rename from models/achievement.go rename to app/models/achievement.go diff --git a/app/models/activities.go b/app/models/activities.go new file mode 100644 index 0000000..a2e18ea --- /dev/null +++ b/app/models/activities.go @@ -0,0 +1,9 @@ +package models + +type Activities struct { + ID uint `gorm:"primaryKey"` + Title string `json:"title"` + Tanggal string `json:"tanggal"` + Gambar string `json:"gambar"` + ImageURL string `json:"image_url"` +} diff --git a/models/news.go b/app/models/news.go similarity index 100% rename from models/news.go rename to app/models/news.go diff --git a/models/user.go b/app/models/user.go similarity index 100% rename from models/user.go rename to app/models/user.go diff --git a/app/services/auth_service.go b/app/services/auth_service.go new file mode 100644 index 0000000..e3a83f6 --- /dev/null +++ b/app/services/auth_service.go @@ -0,0 +1,30 @@ +package services + +import ( + "backend/app/models" + "backend/utils/token" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func LoginCheckAdmin(db *gorm.DB, code string) (string, error) { + var err error + + adminFromDB := models.User{} + err = db.Model(&models.User{}).Where("code = ?", code).Take(&adminFromDB).Error + if err != nil { + return "", err + } + + err = bcrypt.CompareHashAndPassword([]byte(adminFromDB.Code), []byte(code)) + if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { + return "", err + } + + token, err := token.GenerateToken(adminFromDB.ID, adminFromDB.Role) + if err != nil { + return "", err + } + + return token, nil +} diff --git a/config/automigrate.go b/config/automigrate.go new file mode 100644 index 0000000..f199114 --- /dev/null +++ b/config/automigrate.go @@ -0,0 +1,21 @@ +package config + +import ( + "backend/app/models" + "gorm.io/gorm" + "log" +) + +// AutoMigrate performs automatic migration of database schema +func AutoMigrate(db *gorm.DB) { + // Run migrations for the models + err := db.AutoMigrate( + &models.Achievement{}, + &models.Activities{}, + &models.News{}, + &models.User{}, + ) + if err != nil { + log.Fatal("Failed to auto migrate: ", err) + } +} diff --git a/config/database.go b/config/database.go index 2403eb8..135dadf 100644 --- a/config/database.go +++ b/config/database.go @@ -1,7 +1,6 @@ package config import ( - "backend/models" "backend/utils" "fmt" "gorm.io/driver/mysql" @@ -52,17 +51,3 @@ func ConnectDatabase() *gorm.DB { return db } - -// AutoMigrate performs automatic migration of database schema -func AutoMigrate(db *gorm.DB) { - // Run migrations for the models - err := db.AutoMigrate( - &models.Achievement{}, - &models.Activities{}, - &models.News{}, - &models.User{}, - ) - if err != nil { - log.Fatal("Failed to auto migrate: ", err) - } -} diff --git a/config/db.go b/config/db.go deleted file mode 100644 index 5cb327b..0000000 --- a/config/db.go +++ /dev/null @@ -1,33 +0,0 @@ -package config - -import ( - "backend/models" - "backend/utils" - "fmt" - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -func ConnectDatabase() *gorm.DB { - // EDIT SESUAI DB LOKAL - username := utils.Getenv("DB_USERNAME", "root") - password := utils.Getenv("DB_PASSWORD", "12345") - database := utils.Getenv("DB_DATABASE", "yourdb") - host := utils.Getenv("DB_HOST", "127.0.0.1") - - dsn := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", - username, - password, - host, - database, - ) - - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - panic(err.Error()) - } - - db.AutoMigrate(&models.Paper{}, &models.Achievement{}, &models.Activities{}, &models.News{}, &models.User{}) - - return db -} diff --git a/controllers/achievementController.go b/controllers/achievementController.go deleted file mode 100644 index 4cfdc2a..0000000 --- a/controllers/achievementController.go +++ /dev/null @@ -1,131 +0,0 @@ -package controllers - -import ( - "backend/models" - "backend/services" - "backend/utils" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -// AchievementController menangani request terkait achievements -type AchievementController struct { - Service services.AchievementService -} - -// NewAchievementController membuat instance baru dari AchievementController -func NewAchievementController(service services.AchievementService) *AchievementController { - return &AchievementController{Service: service} -} - -// GetAllAchievements mengambil semua achievement -func (ac *AchievementController) GetAllAchievements(ctx *gin.Context) { - achievements, err := ac.Service.GetAllAchievements() - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve achievements"}) - return - } - ctx.JSON(http.StatusOK, achievements) -} - -// GetAchievementByID mengambil achievement berdasarkan ID -func (ac *AchievementController) GetAchievementByID(ctx *gin.Context) { - id := ctx.Param("id") - - achievement, err := ac.Service.GetAchievementByID(id) - if err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Achievement not found"}) - return - } - ctx.JSON(http.StatusOK, achievement) -} - -// CreateAchievement menambahkan achievement baru -func (ac *AchievementController) CreateAchievement(ctx *gin.Context) { - var achievement models.Achievement - - // Bind JSON ke struct Achievement - if err := ctx.ShouldBindJSON(&achievement); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) - return - } - - // Simpan achievement - if err := ac.Service.CreateAchievement(&achievement); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create achievement"}) - return - } - - ctx.JSON(http.StatusCreated, gin.H{"message": "Achievement created successfully", "data": achievement}) -} - -// UploadAchievementImage mengunggah foto untuk achievement -func (ac *AchievementController) UploadAchievementImage(ctx *gin.Context) { - // Ambil file dari form-data - file, err := ctx.FormFile("foto") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "File is required"}) - return - } - - // Validasi file - if err := utils.ValidateFile(file); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Generate file name dan simpan - fileName := uuid.New().String() + ".png" - filePath := "uploads/" + fileName - - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - // Dapatkan ID dari URL parameter - id := ctx.Param("id") - - // Update path gambar di database - if err := ac.Service.UpdateAchievementImage(id, fileName, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update image path"}) - return - } - - ctx.JSON(http.StatusOK, gin.H{"message": "Image uploaded successfully", "path": filePath}) -} - -// UpdateAchievement mengupdate achievement berdasarkan ID -func (ac *AchievementController) UpdateAchievement(ctx *gin.Context) { - id := ctx.Param("id") - var updatedData models.Achievement - - // Bind input - if err := ctx.ShouldBindJSON(&updatedData); err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) - return - } - - // Update achievement - if err := ac.Service.UpdateAchievement(id, &updatedData); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update achievement"}) - return - } - - ctx.JSON(http.StatusOK, gin.H{"message": "Achievement updated successfully", "data": updatedData}) -} - -// DeleteAchievement menghapus achievement berdasarkan ID -func (ac *AchievementController) DeleteAchievement(ctx *gin.Context) { - id := ctx.Param("id") - - // Hapus achievement - if err := ac.Service.DeleteAchievement(id); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete achievement"}) - return - } - - ctx.JSON(http.StatusOK, gin.H{"message": "Achievement deleted successfully"}) -} diff --git a/controllers/activityController.go b/controllers/activityController.go deleted file mode 100644 index 1304937..0000000 --- a/controllers/activityController.go +++ /dev/null @@ -1,141 +0,0 @@ -package controllers - -import ( - "backend/models" - "backend/services" - "backend/utils" - "fmt" - "net/http" - "path/filepath" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -// ActivitiesController menangani request terkait activities -type ActivitiesController struct { - Service services.ActivityService -} - -// NewActivitiesController membuat instance ActivitiesController -func NewActivitiesController(service services.ActivityService) *ActivitiesController { - return &ActivitiesController{Service: service} -} - -// UploadActivity mengunggah aktivitas beserta file gambar -func (ac *ActivitiesController) UploadActivity(ctx *gin.Context) { - // Ambil file dari form-data - file, err := ctx.FormFile("gambar") - if err != nil { - utils.HandleError(ctx, http.StatusBadRequest, "File gambar is required") - return - } - - // Validasi file gambar - if err := utils.ValidateFile(file); err != nil { - utils.HandleError(ctx, http.StatusBadRequest, err.Error()) - return - } - - // Simpan file ke folder uploads - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, "Failed to save file") - return - } - - // Ambil input form lainnya - title := ctx.PostForm("title") - tanggal := ctx.PostForm("tanggal") - host := utils.Getenv("ENV_HOST", "localhost") - - // Buat object activity - activity := models.Activities{ - Title: title, - Date: tanggal, - Image: fileName, - ImageURL: fmt.Sprintf("https://%s/uploads/%s", host, fileName), - } - - // Simpan ke database - if err := ac.Service.CreateActivity(&activity); err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, "Failed to save activity to database") - return - } - - ctx.JSON(http.StatusOK, gin.H{"message": "Activity uploaded successfully", "data": activity}) -} - -// GetAllActivities mengambil semua aktivitas -func (ac *ActivitiesController) GetAllActivities(ctx *gin.Context) { - activities, err := ac.Service.GetAllActivities() - if err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, "Failed to get activities") - return - } - - ctx.JSON(http.StatusOK, activities) -} - -// GetActivityByID mengambil aktivitas berdasarkan ID -func (ac *ActivitiesController) GetActivityByID(ctx *gin.Context) { - id := ctx.Param("id") - activity, err := ac.Service.GetActivityByID(id) - if err != nil { - utils.HandleError(ctx, http.StatusNotFound, "Activity not found") - return - } - - ctx.JSON(http.StatusOK, activity) -} - -// EditActivity mengupdate data aktivitas -func (ac *ActivitiesController) EditActivity(ctx *gin.Context) { - id := ctx.Param("id") - - // Ambil data form - title := ctx.PostForm("title") - tanggal := ctx.PostForm("tanggal") - - // Ambil file (jika ada) - file, err := ctx.FormFile("gambar") - var fileName string - if err == nil { // File ditemukan - // Validasi file - if err := utils.ValidateFile(file); err != nil { - utils.HandleError(ctx, http.StatusBadRequest, err.Error()) - return - } - - // Simpan file baru - fileName = uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, "Failed to save file") - return - } - } - - // Panggil service untuk update - updatedActivity, err := ac.Service.UpdateActivity(id, title, tanggal, fileName) - if err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, "Failed to update activity") - return - } - - ctx.JSON(http.StatusOK, gin.H{"message": "Activity updated successfully", "data": updatedActivity}) -} - -// DeleteActivity menghapus aktivitas dan gambarnya -func (ac *ActivitiesController) DeleteActivity(ctx *gin.Context) { - id := ctx.Param("id") - - // Panggil service untuk hapus - if err := ac.Service.DeleteActivity(id); err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, "Failed to delete activity") - return - } - - ctx.JSON(http.StatusOK, gin.H{"message": "Activity deleted successfully"}) -} diff --git a/controllers/authAdminController.go b/controllers/authAdminController.go deleted file mode 100644 index fc1b416..0000000 --- a/controllers/authAdminController.go +++ /dev/null @@ -1,58 +0,0 @@ -package controllers - -import ( - "backend/services" - "backend/utils" - "net/http" - - "github.com/gin-gonic/gin" -) - -// KodeInput - struct untuk input kode login -type KodeInput struct { - Kode string `json:"kode" binding:"required,min=5,max=30"` -} - -// AuthAdminController - controller untuk autentikasi admin -type AuthAdminController struct { - AuthService services.AuthAdminService -} - -// NewAuthAdminController - inisialisasi controller baru -func NewAuthAdminController(authService services.AuthAdminService) *AuthAdminController { - return &AuthAdminController{AuthService: authService} -} - -// LoginAdmin - endpoint untuk autentikasi admin -// @Summary Login as admin. -// @Description Logs in an admin and returns a JWT token. -// @Tags Auth -// @Accept json -// @Param Body KodeInput true "Admin code for login" -// @Produce json -// @Success 200 {object} map[string]interface{} -// @Failure 400 {object} map[string]interface{} -// @Failure 401 {object} map[string]interface{} -// @Router /auth/login-admin [post] -func (ac *AuthAdminController) LoginAdmin(ctx *gin.Context) { - var input KodeInput - - // Validasi input - if err := ctx.ShouldBindJSON(&input); err != nil { - utils.HandleError(ctx, http.StatusBadRequest, "Invalid input format") - return - } - - // Panggil service untuk autentikasi - token, err := ac.AuthService.LoginAdmin(input.Kode) - if err != nil { - utils.HandleError(ctx, http.StatusUnauthorized, "Invalid credentials") - return - } - - // Respons sukses - ctx.JSON(http.StatusOK, gin.H{ - "message": "Login successful", - "token": token, - }) -} diff --git a/controllers/newsController.go b/controllers/newsController.go deleted file mode 100644 index 03a09b6..0000000 --- a/controllers/newsController.go +++ /dev/null @@ -1,84 +0,0 @@ -package controllers - -import ( - "backend/models" - "backend/services" - "backend/utils" - "context" - "net/http" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -type NewsController struct { - NewsService services.NewsService -} - -func NewNewsController(newsService services.NewsService) *NewsController { - return &NewsController{NewsService: newsService} -} - -// GetAllNews mengambil seluruh berita -func (nc *NewsController) GetAllNews(ctx *gin.Context) { - // Set timeout untuk konteks request - c, cancel := context.WithTimeout(ctx.Request.Context(), 10*time.Second) - defer cancel() - - news, err := nc.NewsService.GetAllNews(c) - if err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, err.Error()) - return - } - - ctx.JSON(http.StatusOK, news) -} - -// InsertNews menyimpan berita baru beserta thumbnail -func (nc *NewsController) InsertNews(ctx *gin.Context) { - // Set timeout untuk konteks request - c, cancel := context.WithTimeout(ctx.Request.Context(), 15*time.Second) - defer cancel() - - // Validasi file input - file, err := ctx.FormFile("thumbnail") - if err != nil { - utils.HandleError(ctx, http.StatusBadRequest, "Thumbnail is required") - return - } - - // Validasi data form lainnya - news := models.News{ - Title: ctx.PostForm("title"), - Kategori: ctx.PostForm("kategori"), - Date: ctx.PostForm("date"), - IsiKonten: ctx.PostForm("isi_konten"), - NamaPenulis: ctx.PostForm("nama_penulis"), - Link: ctx.PostForm("link"), - } - - // Validasi struct berita - validationErrors := utils.ValidateStruct(news) - if len(validationErrors) > 0 { - // Menggabungkan kesalahan validasi menjadi satu string - var errorMessages []string - for _, message := range validationErrors { - errorMessages = append(errorMessages, message) - } - utils.HandleError(ctx, http.StatusBadRequest, strings.Join(errorMessages, ", ")) - return - } - - // Panggil service untuk menyimpan berita - response, err := nc.NewsService.InsertNews(c, news, file, ctx) - if err != nil { - utils.HandleError(ctx, http.StatusInternalServerError, err.Error()) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "News successfully inserted", - "data": response, - }) -} diff --git a/controllers/paperController.go b/controllers/paperController.go deleted file mode 100644 index 2247abc..0000000 --- a/controllers/paperController.go +++ /dev/null @@ -1,273 +0,0 @@ -package controllers - -import ( - "backend/models" - "fmt" - "net/http" - "os" - - "github.com/gin-gonic/gin" - - "path/filepath" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -type PaperController struct { - DB *gorm.DB -} - -// UploadPaper adalah fungsi untuk mengupload paper beserta file-nya. -// @Summary Upload Paper with File -// @Description Uploads a paper along with its file and saves them to the database. -// @Tags Papers -// @Accept multipart/form-data -// @Param judul formData string true "Judul paper" -// @Param abstrak formData string true "Abstrak paper" -// @Param link formData string true "Link paper" -// @Param file_paper formData file true "File paper" -// @Param author formData string true "Author paper" -// @Param tanggal_terbit formData string true "Tanggal terbit paper" -// @Produce json -// @Success 200 {object} models.Paper -// @Router /papers [post] -func (pc *PaperController) UploadPaper(ctx *gin.Context) { - // Get the file from the form data - file, err := ctx.FormFile("file_paper") - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Define the path where the file will be saved, pake UUID, untuk skg taro di uploads dlu - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Save the file to the defined path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - judul := ctx.PostForm("judul") - abstrak := ctx.PostForm("abstrak") - link := ctx.PostForm("link") - author := ctx.PostForm("author") - tanggalTerbit := ctx.PostForm("tanggal_terbit") - - // Create Paper object - paper := models.Paper{ - Judul: judul, - Abstrak: abstrak, - Link: link, - FilePaper: fileName, - Author: author, - TanggalTerbit: tanggalTerbit, - } - - // Save paper to database - if err := pc.DB.Create(&paper).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save paper to database"}) - return - } - - ctx.JSON(http.StatusOK, paper) -} - -// GetAllPapers adalah fungsi untuk mendapatkan semua paper dari database. -// @Summary Get All Papers -// @Description Retrieves all papers from the database. -// @Tags Papers -// @Produce json -// @Success 200 {array} models.Paper -// @Router /papers [get] -func (pc *PaperController) GetAllPapers(ctx *gin.Context) { - var papers []models.Paper - - // Retrieve all papers from the database, masalahnyaaa gambarnya ga ngikut gmn ya infoo helppp - if err := pc.DB.Find(&papers).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get papers from database"}) - return - } - - // Response with papers - ctx.JSON(http.StatusOK, papers) -} - -// GetPaperFile adalah fungsi untuk mengambil file paper berdasarkan ID. -// @Summary Get Paper File -// @Description Retrieves the file of a paper by its ID. -// @Tags Papers -// @Param id path string true "Paper ID" -// @Produce octet-stream -// @Success 200 {file} octet-stream -// @Router /papers/file/{id} [get] -func (pc *PaperController) GetPaperFile(ctx *gin.Context) { - // Get paper ID from URL path parameter, pakenya param klo mau diubah ke yg laen sok - paperID := ctx.Param("id") - - // Retrieve paper from the database by its ID - var paper models.Paper - if err := pc.DB.Where("id = ?", paperID).First(&paper).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Paper not found"}) - return - } - - // Define the file path - filePath := filepath.Join("uploads", paper.FilePaper) - - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) - return - } - - // Set the headers for the file transfer and return the file - ctx.Header("Content-Description", "File Transfer") - ctx.Header("Content-Transfer-Encoding", "binary") - ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", paper.FilePaper)) - - // switch case buat content type, sumpah klo ga gini gw gtw gimana biar semua filetype bisa pls help - ext := filepath.Ext(paper.FilePaper) - switch ext { - case ".png": - ctx.Header("Content-Type", "image/png") - case ".jpg", ".jpeg": - ctx.Header("Content-Type", "image/jpeg") - case ".gif": - ctx.Header("Content-Type", "image/gif") - case ".pdf": - ctx.Header("Content-Type", "application/pdf") - default: - ctx.Header("Content-Type", "application/octet-stream") - } - - ctx.File(filePath) -} - -// EditPaper adalah fungsi untuk mengedit paper, termasuk kemampuan untuk mengganti file paper. -// @Summary Edit Paper -// @Description Edits a paper including the ability to replace its file. -// @Tags Papers -// @Accept multipart/form-data -// @Param id path string true "Paper ID" -// @Param judul formData string true "Judul paper" -// @Param abstrak formData string true "Abstrak paper" -// @Param link formData string true "Link paper" -// @Param file_paper formData file false "File paper (optional)" -// @Param author formData string true "Author paper" -// @Param tanggal_terbit formData string true "Tanggal terbit paper" -// @Produce json -// @Success 200 {object} models.Paper -// @Router /papers/{id} [put] -func (pc *PaperController) EditPaper(ctx *gin.Context) { - // Get paper ID from URL path parameter - paperID := ctx.Param("id") - - // Retrieve paper from the database by its ID - var paper models.Paper - if err := pc.DB.Where("id = ?", paperID).First(&paper).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Paper not found"}) - return - } - - judul := ctx.PostForm("judul") - abstrak := ctx.PostForm("abstrak") - link := ctx.PostForm("link") - author := ctx.PostForm("author") - tanggalTerbit := ctx.PostForm("tanggal_terbit") - - paper.Judul = judul - paper.Abstrak = abstrak - paper.Link = link - paper.Author = author - paper.TanggalTerbit = tanggalTerbit - - // Cek apakah file diganti - file, err := ctx.FormFile("file_paper") - if err != nil && err != http.ErrMissingFile { - ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Jika iya, maka di save - if file != nil { - // Tentuin tempat nge save - fileName := uuid.New().String() + filepath.Ext(file.Filename) - filePath := filepath.Join("uploads", fileName) - - // Save the file to the path - if err := ctx.SaveUploadedFile(file, filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) - return - } - - // Remove old file - oldFilePath := filepath.Join("uploads", paper.FilePaper) - if err := os.Remove(oldFilePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove old file"}) - } - - // Update file paper field in the database - paper.FilePaper = fileName - } - - // Save updated paper to database - if err := pc.DB.Save(&paper).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save updated paper to database"}) - return - } - - // Response success - ctx.JSON(http.StatusOK, paper) -} - -// DeletePaper adalah fungsi untuk menghapus paper dan file-nya dari database dan sistem penyimpanan. -// @Summary Delete Paper -// @Description Deletes a paper and its file from the database and storage. -// @Tags Papers -// @Param id path string true "Paper ID" -// @Produce json -// @Success 200 {string} string "Paper deleted successfully" -// @Router /papers/{id} [delete] -func (pc *PaperController) DeletePaper(ctx *gin.Context) { - paperID := ctx.Param("id") - - // Cari paper dari id - var paper models.Paper - if err := pc.DB.Where("id = ?", paperID).First(&paper).Error; err != nil { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Paper not found"}) - return - } - - // tentuin file path dari file yg mau didelete - filePath := filepath.Join("uploads", paper.FilePaper) - - // Check if the file exists - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - // File doesn't exist, still delete the paper from database - if err := pc.DB.Delete(&paper).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete paper from database"}) - return - } - ctx.JSON(http.StatusOK, "Paper deleted successfully") - return - } - - // Delete file - if err := os.Remove(filePath); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) - return - } - - // Delete paper from database - if err := pc.DB.Delete(&paper).Error; err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete paper from database"}) - return - } - - ctx.JSON(http.StatusOK, "Paper and file deleted successfully") -} diff --git a/go.mod b/go.mod index 5d1a67b..edfe5f3 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 toolchain go1.23.0 require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-contrib/cors v1.5.0 github.com/gin-gonic/gin v1.9.1 github.com/go-playground/validator/v10 v10.18.0 diff --git a/go.sum b/go.sum index 7243a9f..7436af5 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= diff --git a/main.go b/main.go index bbdd570..6312b7b 100644 --- a/main.go +++ b/main.go @@ -1,58 +1,43 @@ package main import ( + "log" + "backend/config" - "backend/controllers" - "backend/repositories" + "backend/docs" "backend/routes" - "backend/services" "backend/utils" - "fmt" - "log" - "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) -func main() { - // Load environment variables - if err := godotenv.Load(".env"); err != nil { - log.Fatal("Error loading .env file") - } +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io - // Connect to the database - db := config.ConnectDatabase() - if db == nil { - log.Fatal("Failed to connect to the database") - } +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - // Initialize Gin router - router := gin.Default() +// @termsOfService http://swagger.io/terms/ - // Initialize repositories and services - userRepo := repositories.NewUserRepository(db) - authService := services.NewAuthAdminService(userRepo) - authController := controllers.NewAuthAdminController(authService) - - activityService := services.NewActivityService(db) - activityController := controllers.NewActivitiesController(activityService) - - newsRepo := repositories.NewNewsRepository(db) - newsService := services.NewNewsService(newsRepo) - newsController := controllers.NewNewsController(newsService) +func main() { + // Load environment variables from .env file + err := godotenv.Load(".env") + if err != nil { + log.Fatal("Error loading .env file") + } - // Setup routes - routes.RegisterAuthRoutes(router, authController) - routes.RegisterActivityRoutes(router, activityController) - routes.RegisterNewsRoutes(router, newsController) + docs.SwaggerInfo.Title = "Swagger Files API" + docs.SwaggerInfo.Description = "This is a simple Files." + docs.SwaggerInfo.Version = "1.0" + envHost := utils.Getenv("ENV_HOST", "localhost:8080") + docs.SwaggerInfo.Host = envHost + docs.SwaggerInfo.Schemes = []string{"http", "https"} - // Run the server - host := utils.Getenv("ENV_HOST", "localhost") - port := utils.Getenv("ENV_PORT", "8080") - serverAddr := fmt.Sprintf("%s:%s", host, port) + // Get the global database instance + db := config.ConnectDatabase() - log.Printf("Server is running on %s", serverAddr) - if err := router.Run(serverAddr); err != nil { - log.Fatalf("Failed to start server: %v", err) - } + // Create a new gin router with default middleware + r := routes.InitRouter(db) + r.Run() } diff --git a/models/activities.go b/models/activities.go deleted file mode 100644 index 8206861..0000000 --- a/models/activities.go +++ /dev/null @@ -1,9 +0,0 @@ -package models - -type Activities struct { - ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` - Title string `json:"title"` - Date string `json:"tanggal"` - Image string `json:"gambar"` - ImageURL string `json:"image_url"` -} diff --git a/models/model.go b/models/model.go deleted file mode 100644 index eb8eb3a..0000000 --- a/models/model.go +++ /dev/null @@ -1,90 +0,0 @@ -package models - -import ( - "backend/utils/token" - "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" -) - -type Paper struct { - ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` - Judul string `json:"judul"` - Abstrak string `json:"abstrak"` - Link string `json:"link"` - FilePaper string `json:"file_paper"` - Author string `json:"author"` - TanggalTerbit string `json:"tanggal_terbit"` -} - -type Activities struct { - ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` - Title string `json:"title"` - Tanggal string `json:"tanggal"` - Gambar string `json:"gambar"` - ImageURL string `json:"image_url"` -} - -type News struct { - ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` - Title string `json:"title" validate:"required"` - Kategori string `json:"kategori" validate:"required,oneof=News Event"` - Thumbnail string `json:"thumbnail"` - IsiKonten string `json:"isi_konten" gorm:"type:json"` - NamaPenulis string `json:"nama_penulis" validate:"required"` - Link string `json:"link"` - ImageURL string `json:"image_url"` - Date string `json:"date"` -} - -type Achievement struct { - ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` - Nama string `json:"nama"` - Pencapaian string `json:"pencapaian"` - Link string `json:"link"` - Kategori string `json:"kategori" validate:"required,oneof='International' 'National' 'Campus'"` - Foto string `json:"foto"` - ImageURL string `json:"image_url"` -} - -type User struct { - ID uint `gorm:"primaryKey;autoIncrement:true"` - Role string `json:"role"` - Code string `json:"code"` -} - -func (u *User) LoginCheckAdmin(db *gorm.DB) (string, error) { - var err error - - adminFromDB := User{} - err = db.Model(&User{}).Where("code = ?", u.Code).Take(&adminFromDB).Error - if err != nil { - return "", err - } - - err = bcrypt.CompareHashAndPassword([]byte(adminFromDB.Code), []byte(u.Code)) - if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { - return "", err - } - - token, err := token.GenerateToken(adminFromDB.ID, adminFromDB.Role) - if err != nil { - return "", err - } - - return token, nil -} - -func (u *User) SaveAdmin(db *gorm.DB) (*User, error) { - // Turn kode into hash - hashedKode, err := bcrypt.GenerateFromPassword([]byte(u.Code), bcrypt.DefaultCost) - if err != nil { - return &User{}, err - } - u.Code = string(hashedKode) - - err = db.Create(&u).Error - if err != nil { - return &User{}, err - } - return u, nil -} diff --git a/repositories/achievement_repository.go b/repositories/achievement_repository.go deleted file mode 100644 index ff12007..0000000 --- a/repositories/achievement_repository.go +++ /dev/null @@ -1,107 +0,0 @@ -package repositories - -import ( - "backend/models" - "errors" - - "gorm.io/gorm" -) - -// AchievementRepository mendefinisikan metode untuk interaksi database -type AchievementRepository interface { - FindAll() ([]models.Achievement, error) - FindByID(id string) (models.Achievement, error) - Create(achievement *models.Achievement) error - Update(id string, updatedData *models.Achievement) error - UpdateImagePath(id string, filePath string) error - Delete(id string) error -} - -type achievementRepo struct { - db *gorm.DB -} - -// NewAchievementRepository membuat instance baru dari AchievementRepository -func NewAchievementRepository(db *gorm.DB) AchievementRepository { - return &achievementRepo{db: db} -} - -// FindAll mengambil semua data achievement dari database -func (repo *achievementRepo) FindAll() ([]models.Achievement, error) { - var achievements []models.Achievement - if err := repo.db.Find(&achievements).Error; err != nil { - return nil, err - } - return achievements, nil -} - -// FindByID mencari achievement berdasarkan ID -func (repo *achievementRepo) FindByID(id string) (models.Achievement, error) { - var achievement models.Achievement - if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return achievement, errors.New("achievement not found") - } - return achievement, err - } - return achievement, nil -} - -// Create menambahkan data achievement ke database -func (repo *achievementRepo) Create(achievement *models.Achievement) error { - if err := repo.db.Create(achievement).Error; err != nil { - return err - } - return nil -} - -// Update memperbarui data achievement berdasarkan ID -func (repo *achievementRepo) Update(id string, updatedData *models.Achievement) error { - var achievement models.Achievement - if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("achievement not found") - } - return err - } - - // Update field yang diubah - if err := repo.db.Model(&achievement).Updates(updatedData).Error; err != nil { - return err - } - return nil -} - -// UpdateImagePath memperbarui path file gambar dari achievement -func (repo *achievementRepo) UpdateImagePath(id string, filePath string) error { - var achievement models.Achievement - if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { - return err - } - - // Update field foto dan image_url - achievement.Foto = filePath - achievement.ImageURL = filePath - - if err := repo.db.Save(&achievement).Error; err != nil { - return err - } - return nil -} - -// Delete menghapus data achievement dari database -func (repo *achievementRepo) Delete(id string) error { - var achievement models.Achievement - if err := repo.db.Where("id = ?", id).First(&achievement).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("achievement not found") - } - return err - } - - // Hapus dari database - if err := repo.db.Delete(&achievement).Error; err != nil { - return err - } - return nil -} diff --git a/repositories/activity_repository.go b/repositories/activity_repository.go deleted file mode 100644 index 2f20b2a..0000000 --- a/repositories/activity_repository.go +++ /dev/null @@ -1,62 +0,0 @@ -package repositories - -import ( - "backend/models" - "errors" - "gorm.io/gorm" -) - -type ActivityRepository interface { - Create(activity *models.Activities) error - FindAll() ([]models.Activities, error) - FindByID(id string) (models.Activities, error) - Update(id, title, tanggal, fileName string) (models.Activities, error) - Delete(id string) error -} - -type activityRepo struct { - db *gorm.DB -} - -func NewActivityRepository(db *gorm.DB) ActivityRepository { - return &activityRepo{db: db} -} - -func (r *activityRepo) Create(activity *models.Activities) error { - return r.db.Create(activity).Error -} - -func (r *activityRepo) FindAll() ([]models.Activities, error) { - var activities []models.Activities - err := r.db.Find(&activities).Error - return activities, err -} - -func (r *activityRepo) FindByID(id string) (models.Activities, error) { - var activity models.Activities - err := r.db.Where("id = ?", id).First(&activity).Error - return activity, err -} - -func (r *activityRepo) Update(id, title, tanggal, fileName string) (models.Activities, error) { - var activity models.Activities - if err := r.db.Where("id = ?", id).First(&activity).Error; err != nil { - return activity, errors.New("activity not found") - } - - activity.Title = title - activity.Date = tanggal - if fileName != "" { - activity.Image = fileName - activity.ImageURL = "uploads/" + fileName - } - - if err := r.db.Save(&activity).Error; err != nil { - return activity, err - } - return activity, nil -} - -func (r *activityRepo) Delete(id string) error { - return r.db.Where("id = ?", id).Delete(&models.Activities{}).Error -} diff --git a/repositories/news_repository.go b/repositories/news_repository.go deleted file mode 100644 index 572f664..0000000 --- a/repositories/news_repository.go +++ /dev/null @@ -1,64 +0,0 @@ -package repositories - -import ( - "backend/models" - "context" - "fmt" - "github.com/gin-gonic/gin" - "gorm.io/gorm" - "mime/multipart" - "os" - "path/filepath" - "time" -) - -// NewsRepository - interface untuk operasi pada model News -type NewsRepository interface { - FindAll(ctx context.Context) ([]models.News, error) - Save(ctx context.Context, news *models.News) error - SaveFile(file *multipart.FileHeader, ginContext *gin.Context) (string, error) // Menggunakan ginContext -} - -type newsRepository struct { - db *gorm.DB -} - -func NewNewsRepository(db *gorm.DB) NewsRepository { - return &newsRepository{db: db} -} - -func (r *newsRepository) FindAll(ctx context.Context) ([]models.News, error) { - var news []models.News - if err := r.db.Find(&news).Error; err != nil { - return nil, err - } - return news, nil -} - -func (r *newsRepository) Save(ctx context.Context, news *models.News) error { - if err := r.db.Create(news).Error; err != nil { - return err - } - return nil -} - -// SaveFile menyimpan file gambar dan mengembalikan nama file -func (r *newsRepository) SaveFile(file *multipart.FileHeader, ginContext *gin.Context) (string, error) { - // Tentukan direktori tempat file akan disimpan - dir := "uploads" - // Membuat folder jika belum ada - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return "", err - } - - // Menghasilkan nama file unik - fileName := fmt.Sprintf("%d-%s", time.Now().UnixNano(), file.Filename) - filePath := filepath.Join(dir, fileName) // Menggunakan filePath untuk menentukan lokasi penyimpanan - - // Menggunakan ginContext untuk menyimpan file - if err := ginContext.SaveUploadedFile(file, filePath); err != nil { - return "", err - } - - return fileName, nil -} diff --git a/repositories/user_repository.go b/repositories/user_repository.go deleted file mode 100644 index efc2653..0000000 --- a/repositories/user_repository.go +++ /dev/null @@ -1,40 +0,0 @@ -package repositories - -import ( - "backend/models" - - "gorm.io/gorm" -) - -// UserRepository - interface untuk operasi pada model User -type UserRepository interface { - FindByCode(code string) (*models.User, error) - FindByID(id uint) (*models.User, error) -} - -type userRepo struct { - db *gorm.DB -} - -// NewUserRepository - inisialisasi UserRepository baru -func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepo{db: db} -} - -// FindByCode - mencari user berdasarkan kode unik (code) -func (r *userRepo) FindByCode(code string) (*models.User, error) { - var user models.User - if err := r.db.Where("code = ?", code).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -// FindByID - mencari user berdasarkan ID -func (r *userRepo) FindByID(id uint) (*models.User, error) { - var user models.User - if err := r.db.First(&user, id).Error; err != nil { - return nil, err - } - return &user, nil -} diff --git a/routes/achievement_routes.go b/routes/achievement_routes.go deleted file mode 100644 index 20e6a86..0000000 --- a/routes/achievement_routes.go +++ /dev/null @@ -1,19 +0,0 @@ -package routes - -import ( - "backend/controllers" - - "github.com/gin-gonic/gin" -) - -func RegisterAchievementRoutes(router *gin.Engine, controller *controllers.AchievementController) { - achievements := router.Group("/achievements") - { - achievements.GET("/", controller.GetAllAchievements) - achievements.GET("/:id", controller.GetAchievementByID) - achievements.POST("/", controller.CreateAchievement) - achievements.POST("/:id/upload-image", controller.UploadAchievementImage) - achievements.PUT("/:id", controller.UpdateAchievement) - achievements.DELETE("/:id", controller.DeleteAchievement) - } -} diff --git a/routes/activity_routes.go b/routes/activity_routes.go deleted file mode 100644 index f7473bf..0000000 --- a/routes/activity_routes.go +++ /dev/null @@ -1,27 +0,0 @@ -package routes - -import ( - "backend/controllers" - "github.com/gin-gonic/gin" -) - -// RegisterActivityRoutes untuk mendaftarkan semua routes terkait aktivitas -func RegisterActivityRoutes(router *gin.Engine, activitiesController *controllers.ActivitiesController) { - activity := router.Group("/activities") // grup route untuk activities - { - // Route untuk upload aktivitas (POST) - activity.POST("/upload", activitiesController.UploadActivity) - - // Route untuk mendapatkan semua aktivitas (GET) - activity.GET("/", activitiesController.GetAllActivities) - - // Route untuk mendapatkan aktivitas berdasarkan ID (GET) - activity.GET("/:id", activitiesController.GetActivityByID) - - // Route untuk mengedit aktivitas (PUT) - activity.PUT("/:id", activitiesController.EditActivity) - - // Route untuk menghapus aktivitas (DELETE) - activity.DELETE("/:id", activitiesController.DeleteActivity) - } -} diff --git a/routes/auth_routes.go b/routes/auth_routes.go index 5965046..a8a29ff 100644 --- a/routes/auth_routes.go +++ b/routes/auth_routes.go @@ -1,13 +1,11 @@ package routes import ( - "backend/controllers" + "backend/app/controllers" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) -func RegisterAuthRoutes(router *gin.Engine, authController *controllers.AuthAdminController) { - auth := router.Group("/auth") - { - auth.POST("/login-admin", authController.LoginAdmin) - } +func RegisterAuthRoutes(r *gin.Engine, db *gorm.DB) { + r.POST("/login-admin", controllers.LoginAdmin) } diff --git a/routes/init_routes.go b/routes/init_routes.go new file mode 100644 index 0000000..8a866d7 --- /dev/null +++ b/routes/init_routes.go @@ -0,0 +1,36 @@ +package routes + +import ( + "backend/app/middlewares" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "gorm.io/gorm" +) + +func AuthGroup(r *gin.Engine) *gin.RouterGroup { + authGroup := r.Group("/") + authGroup.Use(middlewares.AdminCheckMiddleware()) + return authGroup +} + +func InitRouter(db *gorm.DB) *gin.Engine { + r := gin.Default() + + r.Use(func(c *gin.Context) { + c.Set("db", db) + }) + + r.Use(middlewares.CorsMiddleware()) + + // Jika bukan GET , Cek token dulu + + RegisterAuthRoutes(r, db) + RegisterActivityRoutes(r, db) + RegisterNewsRoutes(r, db) + RegisterAchievementRoutes(r, db) + + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL("/swagger/doc.json"))) + r.Static("/uploads", "./uploads") + return r +} diff --git a/routes/news.go b/routes/news.go index 1e029c2..a01f85b 100644 --- a/routes/news.go +++ b/routes/news.go @@ -4,6 +4,7 @@ import ( "backend/app/controllers" "backend/app/repositories" "backend/app/services" + "github.com/gin-gonic/gin" "gorm.io/gorm" ) diff --git a/routes/news_routes.go b/routes/news_routes.go deleted file mode 100644 index fff4bb4..0000000 --- a/routes/news_routes.go +++ /dev/null @@ -1,15 +0,0 @@ -package routes - -import ( - "backend/controllers" - - "github.com/gin-gonic/gin" -) - -func RegisterNewsRoutes(router *gin.Engine, newsController *controllers.NewsController) { - news := router.Group("/news") - { - news.GET("/", newsController.GetAllNews) - news.POST("/", newsController.InsertNews) - } -} diff --git a/routes/routes.go b/routes/routes.go deleted file mode 100644 index 92cfec9..0000000 --- a/routes/routes.go +++ /dev/null @@ -1,47 +0,0 @@ -package routes - -import ( - "backend/controllers" - "backend/middlewares" - "backend/repositories" - "backend/services" - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -// SetupRouter mengatur semua rute utama aplikasi -func SetupRouter(router *gin.Engine, db *gorm.DB) { - // Middleware global - router.Use(gin.Logger()) - router.Use(gin.Recovery()) - - // Inisialisasi service dan controller untuk Authentication - authRepo := repositories.NewUserRepository(db) - authService := services.NewAuthAdminService(authRepo) - authController := controllers.NewAuthAdminController(authService) - - // Inisialisasi repository dan service untuk Activities - activityRepo := repositories.NewActivityRepository(db) // Inisialisasi repository terlebih dahulu - activityService := services.NewActivityService(activityRepo) // Gunakan repository untuk service - activityController := controllers.NewActivitiesController(activityService) - - // Inisialisasi repository dan service untuk News - newsRepo := repositories.NewNewsRepository(db) - newsService := services.NewNewsService(newsRepo) - newsController := controllers.NewNewsController(newsService) - - // Pendaftaran routes modular - RegisterAuthRoutes(router, authController) - RegisterActivityRoutes(router, activityController) - RegisterNewsRoutes(router, newsController) - - // Middleware tambahan (contoh: middleware autentikasi untuk protected routes) - protected := router.Group("/protected") - protected.Use(middlewares.AuthMiddleware()) // Hanya untuk rute yang membutuhkan autentikasi - { - // Misalnya, kita ingin menambahkan rute yang memerlukan autentikasi - protected.GET("/secure-data", func(ctx *gin.Context) { - ctx.JSON(200, gin.H{"message": "This is protected data!"}) - }) - } -} diff --git a/services/achievement_service.go b/services/achievement_service.go deleted file mode 100644 index fdc942f..0000000 --- a/services/achievement_service.go +++ /dev/null @@ -1,47 +0,0 @@ -package services - -import ( - "backend/models" - "backend/repositories" -) - -type AchievementService interface { - GetAllAchievements() ([]models.Achievement, error) - GetAchievementByID(id string) (models.Achievement, error) - CreateAchievement(achievement *models.Achievement) error - UpdateAchievement(id string, updatedData *models.Achievement) error - UpdateAchievementImage(id, fileName, filePath string) error - DeleteAchievement(id string) error -} - -type achievementService struct { - repo repositories.AchievementRepository -} - -func NewAchievementService(repo repositories.AchievementRepository) AchievementService { - return &achievementService{repo: repo} -} - -func (s *achievementService) GetAllAchievements() ([]models.Achievement, error) { - return s.repo.FindAll() -} - -func (s *achievementService) GetAchievementByID(id string) (models.Achievement, error) { - return s.repo.FindByID(id) -} - -func (s *achievementService) CreateAchievement(achievement *models.Achievement) error { - return s.repo.Create(achievement) -} - -func (s *achievementService) UpdateAchievement(id string, updatedData *models.Achievement) error { - return s.repo.Update(id, updatedData) -} - -func (s *achievementService) UpdateAchievementImage(id, fileName, filePath string) error { - return s.repo.UpdateImagePath(id, filePath) -} - -func (s *achievementService) DeleteAchievement(id string) error { - return s.repo.Delete(id) -} diff --git a/services/activity_service.go b/services/activity_service.go deleted file mode 100644 index 3647c7d..0000000 --- a/services/activity_service.go +++ /dev/null @@ -1,42 +0,0 @@ -package services - -import ( - "backend/models" - "backend/repositories" -) - -type ActivityService interface { - CreateActivity(activity *models.Activities) error - GetAllActivities() ([]models.Activities, error) - GetActivityByID(id string) (models.Activities, error) - UpdateActivity(id, title, tanggal, fileName string) (models.Activities, error) - DeleteActivity(id string) error -} - -type activityService struct { - repo repositories.ActivityRepository -} - -func NewActivityService(repo repositories.ActivityRepository) ActivityService { - return &activityService{repo: repo} -} - -func (s *activityService) CreateActivity(activity *models.Activities) error { - return s.repo.Create(activity) -} - -func (s *activityService) GetAllActivities() ([]models.Activities, error) { - return s.repo.FindAll() -} - -func (s *activityService) GetActivityByID(id string) (models.Activities, error) { - return s.repo.FindByID(id) -} - -func (s *activityService) UpdateActivity(id, title, tanggal, fileName string) (models.Activities, error) { - return s.repo.Update(id, title, tanggal, fileName) -} - -func (s *activityService) DeleteActivity(id string) error { - return s.repo.Delete(id) -} diff --git a/services/auth_service.go b/services/auth_service.go deleted file mode 100644 index e652392..0000000 --- a/services/auth_service.go +++ /dev/null @@ -1,37 +0,0 @@ -package services - -import ( - "backend/repositories" - "errors" -) - -// AuthAdminService - interface untuk autentikasi admin -type AuthAdminService interface { - LoginAdmin(kode string) (string, error) -} - -type authAdminService struct { - repo repositories.UserRepository -} - -// NewAuthAdminService - inisialisasi AuthAdminService baru -func NewAuthAdminService(repo repositories.UserRepository) AuthAdminService { - return &authAdminService{repo} // Mengembalikan instance authAdminService sebagai AuthAdminService -} - -// LoginAdmin - implementasi autentikasi admin -func (s *authAdminService) LoginAdmin(kode string) (string, error) { - // Cari user berdasarkan kode - user, err := s.repo.FindByCode(kode) - if err != nil { - return "", errors.New("invalid credentials") - } - - // Generate token JWT - token, err := user.GenerateJWT() - if err != nil { - return "", errors.New("failed to generate token") - } - - return token, nil -} diff --git a/services/news_service.go b/services/news_service.go deleted file mode 100644 index ec8a569..0000000 --- a/services/news_service.go +++ /dev/null @@ -1,49 +0,0 @@ -package services - -import ( - "backend/models" - "backend/repositories" - "context" - "errors" - "fmt" - "github.com/gin-gonic/gin" - "mime/multipart" -) - -type NewsService interface { - GetAllNews(ctx context.Context) ([]models.News, error) - InsertNews(ctx context.Context, news models.News, file *multipart.FileHeader, ginContext *gin.Context) (models.News, error) -} - -type newsService struct { - repo repositories.NewsRepository -} - -func NewNewsService(repo repositories.NewsRepository) NewsService { - return &newsService{repo: repo} -} - -// GetAllNews mengambil seluruh berita -func (s *newsService) GetAllNews(ctx context.Context) ([]models.News, error) { - return s.repo.FindAll(ctx) -} - -// InsertNews menyimpan berita dan thumbnail ke database -func (s *newsService) InsertNews(ctx context.Context, news models.News, file *multipart.FileHeader, ginContext *gin.Context) (models.News, error) { - // Simpan file thumbnail dan dapatkan nama file - fileName, err := s.repo.SaveFile(file, ginContext) // Menggunakan ginContext - if err != nil { - return models.News{}, errors.New("failed to save file") - } - - // Update properti gambar pada berita - news.Thumbnail = fileName - news.ImageURL = fmt.Sprintf("%s/uploads/%s", ginContext.Request.Host, fileName) - - // Simpan berita ke database - if err := s.repo.Save(ctx, &news); err != nil { - return models.News{}, errors.New("failed to save news") - } - - return news, nil -}