diff --git a/config/config.go b/config/config.go index 0ef42ce..14b022d 100644 --- a/config/config.go +++ b/config/config.go @@ -2,8 +2,10 @@ package config import ( "errors" - "github.com/joho/godotenv" "os" + "strconv" + + "github.com/joho/godotenv" ) type DbConfig struct { @@ -17,6 +19,7 @@ type LoginConfig struct { LoginAPI string AuthMeAPI string LogoutAPI string + SkipSSLVerification bool } type TokenConfig struct { @@ -67,10 +70,17 @@ func (c *Config) readConfig() error { TokenApiKey: os.Getenv("TOKEN_API_KEY"), } + skipSSL, err := strconv.ParseBool(os.Getenv("SKIP_SSL_VERIFICATION")) + if err != nil { + return errors.New("failed to read SKIP_SSL_VERIFICATION environment variable") + } + + c.LoginConfig = LoginConfig{ LoginAPI: os.Getenv("API_LOGIN_URL"), AuthMeAPI: os.Getenv("API_ME_URL"), LogoutAPI: os.Getenv("API_LOGOUT_URL"), + SkipSSLVerification: skipSSL, } if c.ApiConfig.ApiPort == "" { diff --git a/delivery/controller/activity_logs_controller.go b/delivery/controller/activity_logs_controller.go new file mode 100644 index 0000000..e99df34 --- /dev/null +++ b/delivery/controller/activity_logs_controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "net/http" + "strconv" + "users_management/m/middleware" + "users_management/m/usecase" + "users_management/m/utils/common" + "users_management/m/utils/helper" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ActivityLogController struct { + activityLogUC usecase.ActivityLogUseCase + rg *gin.RouterGroup +} + +func NewActivityLogController(activityLogUC usecase.ActivityLogUseCase, rg *gin.RouterGroup) *ActivityLogController { + return &ActivityLogController{ + activityLogUC: activityLogUC, + rg: rg, + } +} + +func (c *ActivityLogController) Route() { + logs := c.rg.Group("/logs") + { + // Users can see their own logs (simplified response) + logs.GET("/my-logs", c.getMyLogs) + + // Only admins and superadmins can see all logs (detailed response) + logs.GET("/all", middleware.RequireAdminRole(), c.getAllLogs) + + // Admins and superadmins can see teknisi logs specifically (detailed response) + logs.GET("/teknisi", middleware.RequireAdminRole(), c.getTeknisinLogs) + } +} + +func (c *ActivityLogController) getMyLogs(ctx *gin.Context) { + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + uid, ok := userID.(uuid.UUID) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + logs, total, err := c.activityLogUC.GetUserLogs(uid, page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + // Convert to simplified response using helper + simplifiedLogs := helper.ConvertToActivityLogResponses(logs) + + response := gin.H{ + "logs": simplifiedLogs, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "User logs retrieved successfully", response) +} + +func (c *ActivityLogController) getAllLogs(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + logs, total, err := c.activityLogUC.GetAllLogs(page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + // Convert to detailed response for admins using helper + detailedLogs := helper.ConvertToActivityLogDetailResponses(logs) + + response := gin.H{ + "logs": detailedLogs, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "All logs retrieved successfully", response) +} + +func (c *ActivityLogController) getTeknisinLogs(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + logs, total, err := c.activityLogUC.GetTeknisinLogs(page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + // Convert to detailed response for admins using helper + detailedLogs := helper.ConvertToActivityLogDetailResponses(logs) + + response := gin.H{ + "logs": detailedLogs, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "Teknisi logs retrieved successfully", response) +} \ No newline at end of file diff --git a/delivery/controller/backbone_controller.go b/delivery/controller/backbone_controller.go index f0b7344..4175ee2 100644 --- a/delivery/controller/backbone_controller.go +++ b/delivery/controller/backbone_controller.go @@ -18,15 +18,13 @@ type BackboneController struct { func (bc *BackboneController) Route() { rg := bc.rg.Group("/backbone") - - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.CORSMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) { rg.GET("", bc.GetBackbone()) - rg.POST("", bc.CreateBackbone()) rg.GET("/:uuid", bc.GetBackboneByID()) - rg.PUT("/:uuid", bc.UpdateBackbone()) + + rg.POST("", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), bc.CreateBackbone()) + rg.PUT("/:uuid", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), bc.UpdateBackbone()) + } } diff --git a/delivery/controller/count_assets_controller.go b/delivery/controller/count_assets_controller.go new file mode 100644 index 0000000..27d8bed --- /dev/null +++ b/delivery/controller/count_assets_controller.go @@ -0,0 +1,35 @@ +package controller + +import ( + "net/http" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +type CountAssetsController struct { + countAssetsUC usecase.CountAssetsUseCase + rg *gin.RouterGroup +} + +func NewCountAssetsController(countAssetsUC usecase.CountAssetsUseCase, rg *gin.RouterGroup) *CountAssetsController { + return &CountAssetsController{ + countAssetsUC: countAssetsUC, + rg: rg, + } +} + +func (c *CountAssetsController) Route() { + c.rg.GET("/count-assets", c.getCounts) +} + +func (c *CountAssetsController) getCounts(ctx *gin.Context) { + counts, err := c.countAssetsUC.GetCounts() + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "Count assets retrieved successfully", counts) +} \ No newline at end of file diff --git a/delivery/controller/devicePort_controller.go b/delivery/controller/devicePort_controller.go index 568c707..3e615a6 100644 --- a/delivery/controller/devicePort_controller.go +++ b/delivery/controller/devicePort_controller.go @@ -2,7 +2,6 @@ package controller import ( "net/http" - "users_management/m/middleware" "users_management/m/model/dto/req" "users_management/m/usecase" "users_management/m/utils/common" @@ -18,9 +17,6 @@ type DevicePortController struct { func (dc *DevicePortController) Route() { rg := dc.rg.Group("/device-port") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) { rg.GET("", dc.GetDevicePort()) rg.POST("", dc.CreateDevicePort()) diff --git a/delivery/controller/devices_controller.go b/delivery/controller/devices_controller.go index 4736c49..8d4860a 100644 --- a/delivery/controller/devices_controller.go +++ b/delivery/controller/devices_controller.go @@ -18,9 +18,7 @@ type DeviceController struct { func (dc *DeviceController) Route() { rg := dc.rg.Group("/devices") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) + rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) { rg.POST("", dc.CreateDevice()) rg.GET("", dc.GetAllDevices()) diff --git a/delivery/controller/fishbone_controller.go b/delivery/controller/fishbone_controller.go index 1bc0b44..55cfaee 100644 --- a/delivery/controller/fishbone_controller.go +++ b/delivery/controller/fishbone_controller.go @@ -1,6 +1,7 @@ package controller import ( + "log" "net/http" "users_management/m/middleware" "users_management/m/model/dto/req" @@ -18,9 +19,8 @@ type FishboneController struct { func (fc *FishboneController) Route() { rg := fc.rg.Group("/fishbone") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) + rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) + // Apply middleware to all routes { rg.GET("", fc.GetFishbone()) rg.POST("", fc.CreateFishbone()) @@ -37,82 +37,126 @@ func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup) *Fis } func (fc *FishboneController) GetFishbone() gin.HandlerFunc { - return func(c *gin.Context) { - fishbones, err := fc.fu.GetAllFishbone() + return func(c *gin.Context) { + log.Println("Fetching all fishbones") + fishbones, err := fc.fu.GetAllFishbone() - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - common.SingleResponses(c, "Success", fishbones) - } + common.SingleResponses(c, "Success", fishbones) + } } func (fc *FishboneController) CreateFishbone() gin.HandlerFunc { - return func(c *gin.Context) { - var fishboneDTO req.FishboneDTO - err := c.ShouldBindJSON(&fishboneDTO) + return func(c *gin.Context) { + var fishboneDTO req.FishboneDTO + err := c.ShouldBindJSON(&fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - err = fc.fu.CreateFishbone(fishboneDTO) + err = fc.fu.CreateFishbone(fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - common.SingleResponses(c, "Fishbone has been created", nil) - } + common.SingleResponses(c, "Fishbone has been created successfully", nil) + } } func (fc *FishboneController) GetFishboneByID() gin.HandlerFunc { - return func(c *gin.Context) { - id := c.Param("uuid") - uuid, err := uuid.Parse(id) - if err != nil{ - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } - fishbone, err := fc.fu.GetByID(uuid) + return func(c *gin.Context) { + id := c.Param("uuid") + fishboneUUID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format") + return + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + fishbone, err := fc.fu.GetByID(fishboneUUID) + if err != nil { + if err.Error() == "fishbone not found" { + common.ErrorResponses(c, http.StatusNotFound, err.Error()) + return + } + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } - common.SingleResponses(c, "Success", fishbone) - } + common.SingleResponses(c, "Success", fishbone) + } } func (fc *FishboneController) UpdateFishbone() gin.HandlerFunc { - return func(c *gin.Context) { - id := c.Param("uuid") - uuid, err := uuid.Parse(id) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + return func(c *gin.Context) { + id := c.Param("uuid") + fishboneUUID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format") + return + } - var fishboneDTO req.UpdateFishboneDTO - err = c.ShouldBindJSON(&fishboneDTO) + var fishboneDTO req.UpdateFishboneDTO + err = c.ShouldBindJSON(&fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - err = fc.fu.UpdateFishbone(uuid, fishboneDTO) + err = fc.fu.UpdateFishbone(fishboneUUID, fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + if err.Error() == "fishbone not found" { + common.ErrorResponses(c, http.StatusNotFound, err.Error()) + return + } + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - common.SingleResponses(c, "Fishbone has been updated", nil) - } + common.SingleResponses(c, "Fishbone has been updated successfully", nil) + } +} + +func (fc *FishboneController) DeleteFishbone() gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("uuid") + fishboneUUID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format") + return + } + + err = fc.fu.DeleteFishbone(fishboneUUID) + if err != nil { + if err.Error() == "fishbone not found" { + common.ErrorResponses(c, http.StatusNotFound, err.Error()) + return + } + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(c, "Fishbone has been deleted successfully", nil) + } +} + +func (fc *FishboneController) GetFishboneStats() gin.HandlerFunc { + return func(c *gin.Context) { + stats, err := fc.fu.GetFishboneStats() + if err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(c, "Success", stats) + } } \ No newline at end of file diff --git a/delivery/controller/tower_controller.go b/delivery/controller/tower_controller.go index c8b6332..6bbab91 100644 --- a/delivery/controller/tower_controller.go +++ b/delivery/controller/tower_controller.go @@ -2,6 +2,7 @@ package controller import ( "net/http" + "strconv" "users_management/m/middleware" "users_management/m/model/dto/req" "users_management/m/usecase" @@ -18,12 +19,11 @@ type TowerController struct { func (tc *TowerController) Route() { rg := tc.rg.Group("/tower") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) + rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) // Apply middleware to all routes { rg.GET("", tc.GetTower()) rg.POST("", tc.CreateTower()) + rg.GET("/:uuid", tc.GetTowerByID()) rg.PUT("/:uuid", tc.UpdateTower()) } @@ -50,24 +50,65 @@ func (tc *TowerController) GetTower() gin.HandlerFunc { } func (tc *TowerController) CreateTower() gin.HandlerFunc { - return func(c *gin.Context) { - var towerDTO req.TowerDTO - err := c.ShouldBindJSON(&towerDTO) + return func(c *gin.Context) { + // Parse multipart form + err := c.Request.ParseMultipartForm(10 << 20) // 10MB max + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form") + return + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + // Extract form data + deviceIDStr := c.PostForm("dev_id") + towerCode := c.PostForm("tower_code") + longitudeStr := c.PostForm("longitude") + latitudeStr := c.PostForm("latitude") - err = tc.tu.Post(towerDTO) + // Validate required fields + if deviceIDStr == "" || towerCode == "" || longitudeStr == "" || latitudeStr == "" { + common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields") + return + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + // Parse UUID + deviceID, err := uuid.Parse(deviceIDStr) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID") + return + } - common.SingleResponses(c, "Tower has been created", nil) - } + // Parse coordinates + longitude, err := strconv.ParseFloat(longitudeStr, 64) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid longitude") + return + } + + latitude, err := strconv.ParseFloat(latitudeStr, 64) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude") + return + } + + // Create DTO + towerDTO := req.TowerDTO{ + DeviceID: deviceID, + TowerCode: towerCode, + Longitude: longitude, + Latitude: latitude, + } + + // Get image file (optional) + imageFile, _ := c.FormFile("image") + + err = tc.tu.Post(towerDTO, imageFile) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(c, "Tower has been created", nil) + } } func (tc *TowerController) GetTowerByID() gin.HandlerFunc { @@ -90,27 +131,57 @@ func (tc *TowerController) GetTowerByID() gin.HandlerFunc { } func (tc *TowerController) UpdateTower() gin.HandlerFunc { - return func(c *gin.Context) { - id := c.Param("uuid") - uuid, err := uuid.Parse(id) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - } - var towerUpdateDTO req.UpdateTowerDTO - err = c.ShouldBindJSON(&towerUpdateDTO) + return func(c *gin.Context) { + id := c.Param("uuid") + + tower_uuid, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + // Parse multipart form + err = c.Request.ParseMultipartForm(10 << 20) // 10MB max + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form") + return + } - err = tc.tu.UpdateTower(uuid, towerUpdateDTO) + // Create update DTO + towerUpdateDTO := req.UpdateTowerDTO{} - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + // Optional fields + if deviceIDStr := c.PostForm("device_id"); deviceIDStr != "" { + if deviceID, err := uuid.Parse(deviceIDStr); err == nil { + towerUpdateDTO.DeviceID = &deviceID + } + } - common.SingleResponses(c, "Tower has been updated", nil) - } + if towerCode := c.PostForm("tower_code"); towerCode != "" { + towerUpdateDTO.TowerCode = &towerCode + } + + if longitudeStr := c.PostForm("longitude"); longitudeStr != "" { + if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil { + towerUpdateDTO.Longitude = &longitude + } + } + + if latitudeStr := c.PostForm("latitude"); latitudeStr != "" { + if latitude, err := strconv.ParseFloat(latitudeStr, 64); err == nil { + towerUpdateDTO.Latitude = &latitude + } + } + + // Get image file (optional) + imageFile, _ := c.FormFile("image") + + err = tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, imageFile) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(c, "Tower has been updated", nil) + } } \ No newline at end of file diff --git a/delivery/controller/user_management_controller.go b/delivery/controller/user_management_controller.go new file mode 100644 index 0000000..209915d --- /dev/null +++ b/delivery/controller/user_management_controller.go @@ -0,0 +1,89 @@ +package controller + +import ( + "net/http" + "users_management/m/middleware" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +type UserManagementController struct { + userUC usecase.UsersUsecase + rg *gin.RouterGroup +} + +func NewUserManagementController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *UserManagementController { + return &UserManagementController{ + userUC: userUC, + rg: rg, + } +} + +func (c *UserManagementController) Route() { + users := c.rg.Group("/user-management") + { + // Only superadmin can manage user roles + users.PUT("/role", middleware.RequireSuperAdminRole(), c.updateUserRole) + + // Admins and superadmins can view all users + users.GET("/users", middleware.RequireAdminRole(), c.getAllUsers) + + // Users can view their own profile + users.GET("/profile", c.getMyProfile) + } +} + +type UpdateRoleRequest struct { + NomorInduk string `json:"nomor_induk" binding:"required"` + RoleName string `json:"role_name" binding:"required"` +} + +func (c *UserManagementController) updateUserRole(ctx *gin.Context) { + var req UpdateRoleRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err := c.userUC.UpdateUserRole(req.NomorInduk, req.RoleName) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "User role updated successfully", nil) +} + +func (c *UserManagementController) getAllUsers(ctx *gin.Context) { + users, err := c.userUC.GetAllUsers() + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "Users retrieved successfully", users) +} + +func (c *UserManagementController) getMyProfile(ctx *gin.Context) { + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + nomorInduk, ok := userID.(string) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + user, err := c.userUC.GetUserByNomorInduk(nomorInduk) + if err != nil { + common.ErrorResponses(ctx, http.StatusNotFound, "User not found") + return + } + + common.SingleResponses(ctx, "User profile retrieved successfully", user) +} \ No newline at end of file diff --git a/delivery/server.go b/delivery/server.go index b6b0afa..0fd51b2 100644 --- a/delivery/server.go +++ b/delivery/server.go @@ -42,18 +42,29 @@ func NewServer() *Server { } func (s *Server) setupController() { + + s.engine.Static("/uploads", "./uploads") + s.engine.Use(middleware.CORSMiddleware()) + + rg := s.engine.Group("/api/v1") controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route() - rg.Use(middleware.AuthMiddleware()) + rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase())) + rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase())) + rg.Use(middleware.RateLimitMiddleware()) { controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route() controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), rg).Route() controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg).Route() controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg).Route() controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route() + controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route() + controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg).Route() } } + + func (s *Server) Run() { s.setupController() if err := s.engine.Run(s.host); err != nil { diff --git a/go.mod b/go.mod index c9385cf..319d1d7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/go-playground/validator/v10 v10.24.0 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + golang.org/x/crypto v0.33.0 + golang.org/x/time v0.10.0 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 ) @@ -15,7 +17,6 @@ require ( github.com/bytedance/sonic v1.12.8 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.0.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -39,12 +40,10 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.14.0 // indirect - golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.10.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b5f466a..ceb4f59 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= diff --git a/manager/infra_manager.go b/manager/infra_manager.go index 6ac151a..4b6b794 100644 --- a/manager/infra_manager.go +++ b/manager/infra_manager.go @@ -3,6 +3,8 @@ package manager import ( "fmt" "users_management/m/config" + "users_management/m/model/entity" + "gorm.io/driver/postgres" "gorm.io/gorm" @@ -24,10 +26,30 @@ func (im *infraManager) openConn() error { if err != nil { return err } + + err = im.autoMigrate(db) + if err != nil { + return fmt.Errorf("failed to migrate database schema: %w", err) + } + im.db = db return nil } +func (im *infraManager) autoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &entity.Role{}, + &entity.User{}, + &entity.Device{}, + &entity.Backbone{}, + &entity.Fishbone{}, + &entity.Tower{}, + &entity.DevicePort{}, + &entity.CountAssets{}, + &entity.ActivityLog{}, + ) +} + func NewInfraManager(cfg *config.Config) (InfraManager, error) { im := &infraManager{ cfg: cfg, diff --git a/manager/repository_manager.go b/manager/repository_manager.go index 283e91e..c07e3f0 100644 --- a/manager/repository_manager.go +++ b/manager/repository_manager.go @@ -10,6 +10,9 @@ type RepositoryManager interface { NewTowerRepository() repository.TowerRepo NewDevicePortRepository() repository.DevicePortRepo + NewCountAssetsRepository() repository.CountAssetsRepo + NewActivityLogRepository() repository.ActivityLogRepo + } type repositoryManager struct { @@ -44,4 +47,12 @@ func (rm *repositoryManager) NewTowerRepository() repository.TowerRepo { func (rm *repositoryManager) NewDevicePortRepository() repository.DevicePortRepo { return repository.NewDevicePortRepo(rm.infra.Conn()) +} + +func (rm *repositoryManager) NewCountAssetsRepository() repository.CountAssetsRepo { + return repository.NewCountAssetsRepo(rm.infra.Conn()) +} + +func (rm *repositoryManager) NewActivityLogRepository() repository.ActivityLogRepo { + return repository.NewActivityLogRepo(rm.infra.Conn()) } \ No newline at end of file diff --git a/manager/usecase_manager.go b/manager/usecase_manager.go index 62583c9..da1d3ea 100644 --- a/manager/usecase_manager.go +++ b/manager/usecase_manager.go @@ -16,6 +16,10 @@ type UsecaseManager interface { NewTowerUsecase() usecase.TowerUseCase NewDevicePortUsecase() usecase.DevicePortUseCase + NewCountAssetsUsecase() usecase.CountAssetsUseCase + + NewActivityLogUsecase() usecase.ActivityLogUseCase + } type usecaseManager struct { @@ -56,4 +60,13 @@ func (um *usecaseManager) NewTowerUsecase() usecase.TowerUseCase { func (um *usecaseManager) NewDevicePortUsecase() usecase.DevicePortUseCase { return usecase.NewDevicePortUseCase(um.repo.NewDevicePortRepository()) +} + + +func (um *usecaseManager) NewCountAssetsUsecase() usecase.CountAssetsUseCase { + return usecase.NewCountAssetsUseCase(um.repo.NewCountAssetsRepository()) +} + +func (um *usecaseManager) NewActivityLogUsecase() usecase.ActivityLogUseCase { + return usecase.NewActivityLogUseCase(um.repo.NewActivityLogRepository()) } \ No newline at end of file diff --git a/middleware/activity_logs_middleware.go b/middleware/activity_logs_middleware.go new file mode 100644 index 0000000..2531ed9 --- /dev/null +++ b/middleware/activity_logs_middleware.go @@ -0,0 +1,141 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "users_management/m/usecase" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type responseWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (r responseWriter) Write(b []byte) (int, error) { + r.body.Write(b) + return r.ResponseWriter.Write(b) +} + +func ActivityLoggingMiddleware(activityLogUC usecase.ActivityLogUseCase) gin.HandlerFunc { + return func(c *gin.Context) { + // Skip logging for certain endpoints + if shouldSkipLogging(c.Request.URL.Path) { + c.Next() + return + } + + // Get user info from context (set by auth middleware) + userID, userExists := c.Get("userID") + if !userExists { + c.Next() + return + } + + // Convert userID to UUID + uid, ok := userID.(uuid.UUID) + if !ok { + c.Next() + return + } + + // Read request body + var requestBody []byte + if c.Request.Body != nil { + requestBody, _ = io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + + // Wrap response writer to capture response + w := &responseWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer} + c.Writer = w + + // Process request + c.Next() + + // Log the activity in background + go func() { + action := getActionFromMethod(c.Request.Method) + resource := getResourceFromPath(c.Request.URL.Path) + resourceID := getResourceIDFromPath(c.Request.URL.Path) + + var oldData, newData interface{} + + // For updates, you might want to fetch old data before the operation + // This is a simplified version + if len(requestBody) > 0 { + json.Unmarshal(requestBody, &newData) + } + + details := "" + if c.Writer.Status() >= 400 { + details = fmt.Sprintf("Request failed with status: %d", c.Writer.Status()) + } + + activityLogUC.LogActivity( + uid, + action, + resource, + resourceID, + oldData, + newData, + c.ClientIP(), + c.Request.UserAgent(), + details, + ) + }() + } +} + +func shouldSkipLogging(path string) bool { + skipPaths := []string{ + "/api/v1/logs", + "/api/v1/users/login", + "/api/v1/users/logout", + "/uploads", + } + + for _, skipPath := range skipPaths { + if strings.HasPrefix(path, skipPath) { + return true + } + } + return false +} + +func getActionFromMethod(method string) string { + switch method { + case "POST": + return "CREATE" + case "PUT", "PATCH": + return "UPDATE" + case "DELETE": + return "DELETE" + case "GET": + return "READ" + default: + return method + } +} + +func getResourceFromPath(path string) string { + parts := strings.Split(path, "/") + if len(parts) >= 4 { + return parts[3] // /api/v1/devices -> devices + } + return "unknown" +} + +func getResourceIDFromPath(path string) *string { + parts := strings.Split(path, "/") + if len(parts) >= 5 { + id := parts[4] + return &id + } + return nil +} \ No newline at end of file diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index beaa0dd..f22032c 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,67 +1,96 @@ package middleware import ( + "crypto/tls" "encoding/json" + "log" "net/http" "strings" + "time" "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/usecase" "users_management/m/utils/common" "github.com/gin-gonic/gin" ) -func AuthMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("Authorization") +func AuthMiddleware(userUC usecase.UsersUsecase) gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") - if token == "" { - common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required") - c.Abort() - return - } + if token == "" { + common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required") + c.Abort() + return + } - token = strings.TrimPrefix(token, "Bearer ") + token = strings.TrimPrefix(token, "Bearer ") - c.Set("token", token) + c.Set("token", token) - req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil) + req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil) - if err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) - c.Abort() - return - } - - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - - client := &http.Client{} - - resp, err := client.Do(req) - if err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) - c.Abort() - return - } - - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - common.ErrorResponses(c, http.StatusUnauthorized, "Unauthroized") - c.Abort() - return - } - - var authResponse res.AuthMeResponse - if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil { + if err != nil { common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) c.Abort() return } - c.Set("userID", authResponse.Data.NomorInduk) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/json") + - c.Next() + var client *http.Client + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client = &http.Client{ + Transport: tr, + Timeout: 30 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + c.Abort() + return + } - } -} + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + + var authResponse res.AuthMeResponse + if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + c.Abort() + return + } + + // Set basic user info from external API + var user entity.User + + + // Check if user exists in local database + user, err = userUC.GetUserByUsername(strings.ToLower(authResponse.Data.Username)) + c.Set("userID", user.ID) + c.Set("userName", user.Username) + log.Println("User data from local DB:", user.ID) + if err != nil { + + defaultRole := "Teknisi" + c.Set("userRole", defaultRole) + } else { + // User exists in local DB, use their assigned role + c.Set("userRole", user.Role.Name) + } + + c.Next() + } +} \ No newline at end of file diff --git a/middleware/cors_middleware.go b/middleware/cors_middleware.go index a1ba6dc..9995708 100644 --- a/middleware/cors_middleware.go +++ b/middleware/cors_middleware.go @@ -1,23 +1,61 @@ package middleware import ( - "net/http" - - "github.com/gin-gonic/gin" + "net/http" + "strings" + "github.com/gin-gonic/gin" ) func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // Change to specific domains if needed - c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + return func(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + + // Define allowed origins (add your React app's URL) + allowedOrigins := []string{ + "http://localhost:3000", + "http://localhost:3001", + "http://127.0.0.1:3000", + "http://127.0.0.1:5173", // Add production URL + } + + // Check if origin is in allowed list + isAllowed := false + for _, allowed := range allowedOrigins { + if origin == allowed { + isAllowed = true + break + } + } + + // For development, also allow localhost variations + if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") { + isAllowed = true + } + + if origin != "" && isAllowed { + // For allowed origins, set specific origin and enable credentials + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + } else if origin == "" { + // For requests without origin (direct API calls, mobile apps, etc.) + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + // Don't set credentials for wildcard + } else { + // For disallowed origins, still set basic CORS but no credentials + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + } + + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE, PATCH") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type") + c.Writer.Header().Set("Access-Control-Max-Age", "86400") + + // Handle preflight requests + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } - // Allow OPTIONS method to pass through - if c.Request.Method == http.MethodOptions { - c.AbortWithStatus(http.StatusNoContent) - return - } - - c.Next() - } + c.Next() + } } \ No newline at end of file diff --git a/middleware/rate_limitter.go b/middleware/rate_limitter.go index 880a306..21219d5 100644 --- a/middleware/rate_limitter.go +++ b/middleware/rate_limitter.go @@ -1,18 +1,20 @@ package middleware import ( - "net/http" - "sync" - "time" - "users_management/m/utils/common" + "log" + "net/http" + "sync" + "time" + "users_management/m/utils/common" - "github.com/gin-gonic/gin" - "golang.org/x/time/rate" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "golang.org/x/time/rate" ) var ( - rateLimiters = make(map[string]*rate.Limiter) - mu sync.Mutex + rateLimiters = make(map[string]*rate.Limiter) + mu sync.Mutex ) func getRateLimiter(userID string) *rate.Limiter { @@ -21,7 +23,7 @@ func getRateLimiter(userID string) *rate.Limiter { limiter, exists := rateLimiters[userID] if !exists { - limiter = rate.NewLimiter(rate.Every(1*time.Minute), 50) // 1 request per second with a burst of 2 requests + limiter = rate.NewLimiter(rate.Every(1*time.Minute), 50) // 50 requests per minute rateLimiters[userID] = limiter } @@ -29,32 +31,49 @@ func getRateLimiter(userID string) *rate.Limiter { } func getLoginLimiter() *rate.Limiter { - mu.Lock() - defer mu.Unlock() + mu.Lock() + defer mu.Unlock() - limiter, exists := rateLimiters["login"] - if !exists { - limiter = rate.NewLimiter(rate.Every(1*time.Minute), 10) // 5 request per second with a burst of 10 requests - rateLimiters["login"] = limiter - } + limiter, exists := rateLimiters["login"] + if !exists { + limiter = rate.NewLimiter(rate.Every(1*time.Minute), 10) // 10 requests per minute for login + rateLimiters["login"] = limiter + } - return limiter + return limiter } func RateLimitMiddleware() gin.HandlerFunc{ - return func(c *gin.Context) { - if c.Request.Method == http.MethodOptions { - c.Next() - return - } - userID, exists := c.Get("userID") + return func(c *gin.Context) { + if c.Request.Method == http.MethodOptions { + c.Next() + return + } + + userID, exists := c.Get("userID") if !exists { common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized: No user ID found") c.Abort() return } - limiter := getRateLimiter(userID.(string)) + // Convert UUID to string for rate limiting + var userIDStr string + switch v := userID.(type) { + case uuid.UUID: + userIDStr = v.String() + case string: + userIDStr = v + default: + log.Printf("Unexpected userID type: %T", userID) + common.ErrorResponses(c, http.StatusInternalServerError, "Invalid user ID type") + c.Abort() + return + } + + log.Printf("User ID for rate limiting: %s", userIDStr) + + limiter := getRateLimiter(userIDStr) if !limiter.Allow() { common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests") @@ -62,24 +81,23 @@ func RateLimitMiddleware() gin.HandlerFunc{ return } - c.Next() - } + c.Next() + } } func RateLoginMiddleware() gin.HandlerFunc{ - return func(c *gin.Context) { - if c.Request.Method == http.MethodOptions { - c.Next() - return - } - limiter := getLoginLimiter() - - if !limiter.Allow() { - common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests") - c.Abort() - return - } - c.Next() - } -} + return func(c *gin.Context) { + if c.Request.Method == http.MethodOptions { + c.Next() + return + } + limiter := getLoginLimiter() + if !limiter.Allow() { + common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests") + c.Abort() + return + } + c.Next() + } +} \ No newline at end of file diff --git a/middleware/rbac.go b/middleware/rbac.go new file mode 100644 index 0000000..48cfc46 --- /dev/null +++ b/middleware/rbac.go @@ -0,0 +1,161 @@ +package middleware + +import ( + "net/http" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +// RequireRole middleware to check if user has required role +func RequireRole(allowedRoles ...string) gin.HandlerFunc { + return func(c *gin.Context) { + userRole, exists := c.Get("userRole") + if !exists { + common.ErrorResponses(c, http.StatusUnauthorized, "User role not found") + c.Abort() + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role") + c.Abort() + return + } + + // Check if user role is in allowed roles + for _, allowedRole := range allowedRoles { + if role == allowedRole { + c.Next() + return + } + } + + common.ErrorResponses(c, http.StatusForbidden, "Insufficient permissions") + c.Abort() + } +} + +// RequireAnyRole middleware - user needs at least one of the specified roles +func RequireAnyRole(roles ...string) gin.HandlerFunc { + return RequireRole(roles...) +} + +// RequireAdminRole middleware for admin-only access +func RequireAdminRole() gin.HandlerFunc { + return RequireRole("Admin", "Superadmin") +} + +// RequireSuperAdminRole middleware for superadmin-only access +func RequireSuperAdminRole() gin.HandlerFunc { + return RequireRole("Superadmin") +} + +// RequireTeknisiRole middleware for teknisi access +func RequireTeknisiRole() gin.HandlerFunc { + return RequireRole("Teknisi") +} + +// RequireNonTeknisiRole middleware to block teknisi users +func RequireNonTeknisiRole() gin.HandlerFunc { + return func(c *gin.Context) { + userRole, exists := c.Get("userRole") + if !exists { + common.ErrorResponses(c, http.StatusUnauthorized, "User role not found") + c.Abort() + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role") + c.Abort() + return + } + + if role == "Teknisi" { + common.ErrorResponses(c, http.StatusForbidden, "Teknisi users are not allowed to access this resource") + c.Abort() + return + } + + c.Next() + } +} + +// PermissionMiddleware for more granular permissions +func PermissionMiddleware(resource, action string) gin.HandlerFunc { + return func(c *gin.Context) { + userRole, exists := c.Get("userRole") + if !exists { + common.ErrorResponses(c, http.StatusUnauthorized, "User role not found") + c.Abort() + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role") + c.Abort() + return + } + + if !hasPermission(role, resource, action) { + common.ErrorResponses(c, http.StatusForbidden, "Insufficient permissions for this action") + c.Abort() + return + } + + c.Next() + } +} + +// Permission checker function +func hasPermission(role, resource, action string) bool { + permissions := getPermissions() + + rolePermissions, exists := permissions[role] + if !exists { + return false + } + + resourcePermissions, exists := rolePermissions[resource] + if !exists { + return false + } + + for _, allowedAction := range resourcePermissions { + if allowedAction == action || allowedAction == "*" { + return true + } + } + + return false +} + +// Define permissions for each role +func getPermissions() map[string]map[string][]string { + return map[string]map[string][]string{ + "Superadmin": { + "*": {"*"}, // Full access to everything + }, + "Admin": { + "backbone": {"CREATE", "READ", "UPDATE", "DELETE"}, + "fishbone": {"CREATE", "READ", "UPDATE", "DELETE"}, + "devices": {"CREATE", "READ", "UPDATE", "DELETE"}, + "towers": {"CREATE", "READ", "UPDATE", "DELETE"}, + "ports": {"CREATE", "READ", "UPDATE", "DELETE"}, + "logs": {"READ"}, + "users": {"READ"}, + }, + "Teknisi": { + "backbone": {"CREATE", "READ", "UPDATE"}, + "fishbone": {"CREATE", "READ", "UPDATE"}, + "devices": {"CREATE", "READ", "UPDATE"}, + "towers": {"CREATE", "READ", "UPDATE"}, + "ports": {"CREATE", "READ", "UPDATE"}, + "logs": {"READ"}, // Only own logs + }, + } +} \ No newline at end of file diff --git a/model/dto/req/device_dto.go b/model/dto/req/device_dto.go index 1259c07..9498304 100644 --- a/model/dto/req/device_dto.go +++ b/model/dto/req/device_dto.go @@ -8,6 +8,10 @@ type DeviceDTO struct { Latitude float64 `json:"latitude" validate:"required"` PortAmount int `json:"port_amount" validate:"required"` Status string `json:"status" validate:"required,oneof=active inactive maintenance"` + Region string `json:"region" validate:"required"` + Province string `json:"province" validate:"required"` + City string `json:"city" validate:"required"` + District string `json:"district" validate:"required"` } type UpdateDeviceDTO struct { @@ -17,4 +21,8 @@ type UpdateDeviceDTO struct { Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=1"` Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"` + Region *string `json:"region,omitempty" validate:"omitempty,min=3"` + Province *string `json:"province,omitempty" validate:"omitempty,min=3"` + City *string `json:"city,omitempty" validate:"omitempty,min=3"` + District *string `json:"district,omitempty" validate:"omitempty,min=3"` } \ No newline at end of file diff --git a/model/dto/req/fishbone_dto.go b/model/dto/req/fishbone_dto.go index 0c64f75..1a113dc 100644 --- a/model/dto/req/fishbone_dto.go +++ b/model/dto/req/fishbone_dto.go @@ -3,15 +3,17 @@ package req import "github.com/google/uuid" type FishboneDTO struct { - BackboneID uuid.UUID `json:"bb_id"` - DeviceStartID uuid.UUID `json:"dev_start_id"` - DeviceEndID uuid.UUID `json:"dev_end_id"` - CoreAmount int `json:"core_amount"` + FishboneCode string `json:"fishbone_code" validate:"required,min=3"` + BackboneID uuid.UUID `json:"bb_id" validate:"required"` + DeviceStartID uuid.UUID `json:"dev_start_id" validate:"required"` + DeviceEndID uuid.UUID `json:"dev_end_id" validate:"required"` + CoreAmount int `json:"core_amount" validate:"required,min=1"` } type UpdateFishboneDTO struct { - BackboneID *uuid.UUID `json:"bb_id,omitempty" validate:"omitempty,min=3"` - DeviceStartID *uuid.UUID `json:"dev_start_id,omitempty" validate:"omitempty,min=3"` - DeviceEndID *uuid.UUID `json:"dev_end_id,omitempty" validate:"omitempty,min=3"` - CoreAmount *int `json:"core_amount,omitempty" validate:"omitempty,min=1"` + FishboneCode *string `json:"fishbone_code,omitempty" validate:"omitempty,min=3"` + BackboneID *uuid.UUID `json:"bb_id,omitempty"` + DeviceStartID *uuid.UUID `json:"dev_start_id,omitempty"` + DeviceEndID *uuid.UUID `json:"dev_end_id,omitempty"` + CoreAmount *int `json:"core_amount,omitempty" validate:"omitempty,min=1"` } \ No newline at end of file diff --git a/model/dto/req/tower_dto.go b/model/dto/req/tower_dto.go index a91ddc2..5d7bfe5 100644 --- a/model/dto/req/tower_dto.go +++ b/model/dto/req/tower_dto.go @@ -4,14 +4,17 @@ import "github.com/google/uuid" type TowerDTO struct { DeviceID uuid.UUID `json:"dev_id"` + DeviceName string `json:"device_name"` TowerCode string `json:"tower_code"` Longitude float64 `json:"longitude"` Latitude float64 `json:"latitude"` } type UpdateTowerDTO struct { - DeviceID *string `json:"device_id,omitempty" validate:"omitempty,min=3"` + DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"` + DeviceName *string `json:"device_name,omitempty" validate:"omitempty,min=3"` TowerCode *string `json:"tower_code,omitempty" validate:"omitempty"` Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` + ImageURL *string `json:"image_url,omitempty" validate:"omitempty,url"` } \ No newline at end of file diff --git a/model/dto/res/activity_logs_res.go b/model/dto/res/activity_logs_res.go new file mode 100644 index 0000000..08f3993 --- /dev/null +++ b/model/dto/res/activity_logs_res.go @@ -0,0 +1,34 @@ +package res + +import ( + "time" + "github.com/google/uuid" +) + +// ActivityLogResponse - simplified response for users +type ActivityLogResponse struct { + ID uuid.UUID `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + Action string `json:"action"` + Resource string `json:"resource"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Timestamp time.Time `json:"timestamp"` +} + +// ActivityLogDetailResponse - detailed response for admins (if needed) +type ActivityLogDetailResponse struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + Action string `json:"action"` + Resource string `json:"resource"` + ResourceID *string `json:"resource_id,omitempty"` + OldData *string `json:"old_data,omitempty"` + NewData *string `json:"new_data,omitempty"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Timestamp time.Time `json:"timestamp"` +} \ No newline at end of file diff --git a/model/dto/res/fishbone_res.go b/model/dto/res/fishbone_res.go index ac39d51..d6105dd 100644 --- a/model/dto/res/fishbone_res.go +++ b/model/dto/res/fishbone_res.go @@ -1,17 +1,33 @@ package res import ( - "time" + "time" - "github.com/google/uuid" + "github.com/google/uuid" ) type FishboneResponse struct { - ID uuid.UUID `json:"id"` - DeviceName string `json:"device_name"` - TowerCode string `json:"tower_code"` - Longtitude float64 `json:"longtitude"` - Latitude float64 `json:"latitude"` - Address string `json:"address"` - CreatedAt time.Time `json:"created_at"` -} \ No newline at end of file + ID uuid.UUID `json:"id"` + FishboneCode string `json:"fishbone_code"` + BackboneCode string `json:"backbone_code"` + DeviceStart string `json:"device_start"` + DeviceEnd string `json:"device_end"` + CoreAmount int `json:"core_amount"` + TotalFishbone int `json:"total_fishbone"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type FishboneDetailResponse struct { + ID uuid.UUID `json:"id"` + FishboneCode string `json:"fishbone_code"` + Backbone BackboneInfo `json:"backbone"` + CoreAmount int `json:"core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type BackboneInfo struct { + ID uuid.UUID `json:"id"` + BackboneCode string `json:"backbone_code"` +} diff --git a/model/dto/res/tower_res.go b/model/dto/res/tower_res.go index 3447ea7..408991c 100644 --- a/model/dto/res/tower_res.go +++ b/model/dto/res/tower_res.go @@ -13,5 +13,6 @@ type TowerResponse struct { Longitude float64 `json:"longtitude"` Latitude float64 `json:"latitude"` Address string `json:"address"` + ImageURL string `json:"image_url"` CreatedAt time.Time `json:"created_at"` } diff --git a/model/entity/activity_logs.go b/model/entity/activity_logs.go new file mode 100644 index 0000000..e133898 --- /dev/null +++ b/model/entity/activity_logs.go @@ -0,0 +1,25 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +type ActivityLog struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + UserID uuid.UUID `json:"user_id" gorm:"type:uuid;not null"` + User User `json:"user" gorm:"foreignKey:UserID"` + Action string `json:"action" gorm:"not null"` // CREATE, UPDATE, DELETE, LOGIN, LOGOUT + Resource string `json:"resource" gorm:"not null"` // devices, backbone, fishbone, towers, etc. + ResourceID *string `json:"resource_id,omitempty"` // ID of the affected resource + OldData *string `json:"old_data,omitempty" gorm:"type:text"` // JSON of old data for updates + NewData *string `json:"new_data,omitempty" gorm:"type:text"` // JSON of new data + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Timestamp time.Time `json:"timestamp" gorm:"autoCreateTime"` + Details *string `json:"details,omitempty"` // Additional details +} + +func (ActivityLog) TableName() string { + return "activity_logs" +} \ No newline at end of file diff --git a/model/entity/backbone.go b/model/entity/backbone.go index c873157..d042a33 100644 --- a/model/entity/backbone.go +++ b/model/entity/backbone.go @@ -1,22 +1,21 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type Backbone struct { - ID uuid.UUID `json:"id"` - BackboneCode string `json:"backbone_code"` - DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"column:dev_start_id"` - DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"column:dev_end_id"` - CoreAmount int `json:"core_amount"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + BackboneCode string `json:"backbone_code" gorm:"unique"` + DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"` + DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"` + CoreAmount int `json:"core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - DeviceStart Device `gorm:"foreignKey:DeviceStartID"` - DeviceEnd Device `gorm:"foreignKey:DeviceEndID"` + DeviceStart Device `gorm:"foreignKey:DeviceStartID"` + DeviceEnd Device `gorm:"foreignKey:DeviceEndID"` } func (Backbone) TableName() string { diff --git a/model/entity/count_assets.go b/model/entity/count_assets.go new file mode 100644 index 0000000..0b0e06f --- /dev/null +++ b/model/entity/count_assets.go @@ -0,0 +1,20 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +type CountAssets struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + FishboneAmount int `json:"fishbone_amount" gorm:"column:fishbone_amount"` + BackboneAmount int `json:"backbone_amount" gorm:"column:backbone_amount"` + DeviceAmount int `json:"device_amount" gorm:"column:device_amount"` + TowerAmount int `json:"tower_amount" gorm:"column:tower_amount"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` +} + +func (CountAssets) TableName() string { + return "count_assets" +} \ No newline at end of file diff --git a/model/entity/device_port.go b/model/entity/device_port.go index 6e4e8a5..b1d70cb 100644 --- a/model/entity/device_port.go +++ b/model/entity/device_port.go @@ -1,21 +1,20 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type DevicePort struct { - ID uuid.UUID `json:"id"` - DeviceID uuid.UUID `json:"device_id" gorm:"column:device_id"` - PortNumber int `json:"port_number"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;column:device_id"` + PortNumber int `json:"port_number"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - Device Device `gorm:"foreignKey:DeviceID"` + Device Device `gorm:"foreignKey:DeviceID"` } func (DevicePort) TableName() string { - return "device_ports" + return "device_ports" } \ No newline at end of file diff --git a/model/entity/devices.go b/model/entity/devices.go index 1e36fff..e3e75a1 100644 --- a/model/entity/devices.go +++ b/model/entity/devices.go @@ -1,29 +1,36 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type DeviceType string type DeviceStatus string const ( - Odp DeviceType = "ODP" - activeDev DeviceStatus = "active" - inactiveDev DeviceStatus = "inactive" - maintenanceDev DeviceStatus = "maintenance" + Odp DeviceType = "ODP" + activeDev DeviceStatus = "active" + inactiveDev DeviceStatus = "inactive" + maintenanceDev DeviceStatus = "maintenance" ) type Device struct { - ID uuid.UUID `json:"id"` - DeviceCode string `json:"device_code"` - DeviceType DeviceType `json:"device_type"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - PortAmount int `json:"port_amount"` - Status DeviceStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceCode string `json:"device_code" gorm:"unique"` + DeviceType DeviceType `json:"device_type"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + PortAmount int `json:"port_amount"` + Status DeviceStatus `json:"status"` + Region string `json:"region"` + Province string `json:"province"` + City string `json:"city"` + District string `json:"district"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Device) TableName() string { + return "devices" } \ No newline at end of file diff --git a/model/entity/fishbone.go b/model/entity/fishbone.go index 59a3194..788d7aa 100644 --- a/model/entity/fishbone.go +++ b/model/entity/fishbone.go @@ -1,27 +1,26 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type Fishbone struct { - ID uuid.UUID `json:"id"` - BackboneID uuid.UUID `json:"backbone_id" gorm:"column:bb_id"` - DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"column:dev_start_id"` - DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"column:dev_end_id"` - CoreAmount int `json:"core_amount"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + FishboneCode string `json:"fishbone_code" gorm:"unique"` + BackboneID uuid.UUID `json:"bb_id" gorm:"type:uuid;column:bb_id"` + DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"` + DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"` + CoreAmount int `json:"core_amount" gorm:"column:core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - DeviceStart Device `gorm:"foreignKey:DeviceStartID"` - DeviceEnd Device `gorm:"foreignKey:DeviceEndID"` - - Backbone Backbone `gorm:"foreignKey:BackboneID"` - + // Relationships + Backbone Backbone `json:"backbone" gorm:"foreignKey:BackboneID"` + DeviceStart Device `json:"device_start" gorm:"foreignKey:DeviceStartID"` + DeviceEnd Device `json:"device_end" gorm:"foreignKey:DeviceEndID"` } func (Fishbone) TableName() string { - return "fishbone" + return "fishbone" } \ No newline at end of file diff --git a/model/entity/roles.go b/model/entity/roles.go index ee6089f..f5264df 100644 --- a/model/entity/roles.go +++ b/model/entity/roles.go @@ -1,10 +1,18 @@ package entity -import "github.com/google/uuid" +import ( + "time" + + "github.com/google/uuid" +) type Role struct { - Id uuid.UUID `json:"id"` - Name string `json:"name"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Name string `json:"name" gorm:"type:varchar(100);not null"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Role) TableName() string { + return "roles" } \ No newline at end of file diff --git a/model/entity/tower.go b/model/entity/tower.go index 69a0315..1265566 100644 --- a/model/entity/tower.go +++ b/model/entity/tower.go @@ -1,21 +1,21 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type Tower struct { - ID uuid.UUID `json:"id"` - DeviceID uuid.UUID`json:"dev_id" gorm:"column:dev_id"` - TowerCode string `json:"tower_code"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceID uuid.UUID `json:"dev_id" gorm:"type:uuid;column:dev_id"` + TowerCode string `json:"tower_code" gorm:"unique"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ImageURL string `json:"image_url" gorm:"column:image_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - Device Device `gorm:"foreignKey:DeviceID"` + Device Device `gorm:"foreignKey:DeviceID"` } func (Tower) TableName() string { diff --git a/model/entity/users.go b/model/entity/users.go index 9a962cb..29db6f3 100644 --- a/model/entity/users.go +++ b/model/entity/users.go @@ -2,17 +2,21 @@ package entity import ( "time" - "github.com/google/uuid" ) type User struct { - ID uuid.UUID `json:"id"` - RoleID uuid.UUID `json:"role_id"` - Role Role `json:"role"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + NomorInduk *string `json:"nomor_induk,omitempty" gorm:"unique"` // Add this field + RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"` + Role Role `json:"role" gorm:"foreignKey:RoleID"` + Name string `json:"name"` + Username string `json:"username" gorm:"unique"` + Password string `json:"password"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (User) TableName() string { + return "users" } \ No newline at end of file diff --git a/repository/activity_logs_repo.go b/repository/activity_logs_repo.go new file mode 100644 index 0000000..4f51bde --- /dev/null +++ b/repository/activity_logs_repo.go @@ -0,0 +1,90 @@ +package repository + +import ( + "users_management/m/model/entity" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ActivityLogRepo interface { + Create(log entity.ActivityLog) error + GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.ActivityLog, error) + GetAllForAdmins(limit, offset int) ([]entity.ActivityLog, error) + GetByUserRole(role string, limit, offset int) ([]entity.ActivityLog, error) + CountByUserID(userID uuid.UUID) (int64, error) + CountAll() (int64, error) + CountByUserRole(role string) (int64, error) +} + +type activityLogRepo struct { + db *gorm.DB +} + +func NewActivityLogRepo(db *gorm.DB) ActivityLogRepo { + return &activityLogRepo{ + db: db, + } +} + +func (r *activityLogRepo) Create(log entity.ActivityLog) error { + return r.db.Create(&log).Error +} + +func (r *activityLogRepo) GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.ActivityLog, error) { + var logs []entity.ActivityLog + err := r.db.Where("user_id = ?", userID). + Preload("User"). + Preload("User.Role"). + Order("timestamp DESC"). + Limit(limit). + Offset(offset). + Find(&logs).Error + return logs, err +} + +func (r *activityLogRepo) GetAllForAdmins(limit, offset int) ([]entity.ActivityLog, error) { + var logs []entity.ActivityLog + err := r.db.Preload("User"). + Preload("User.Role"). + Order("timestamp DESC"). + Limit(limit). + Offset(offset). + Find(&logs).Error + return logs, err +} + +func (r *activityLogRepo) GetByUserRole(role string, limit, offset int) ([]entity.ActivityLog, error) { + var logs []entity.ActivityLog + err := r.db.Joins("JOIN users ON users.id = activity_logs.user_id"). + Joins("JOIN roles ON roles.id = users.role_id"). + Where("roles.name = ?", role). + Preload("User"). + Preload("User.Role"). + Order("activity_logs.timestamp DESC"). + Limit(limit). + Offset(offset). + Find(&logs).Error + return logs, err +} + +func (r *activityLogRepo) CountByUserID(userID uuid.UUID) (int64, error) { + var count int64 + err := r.db.Model(&entity.ActivityLog{}).Where("user_id = ?", userID).Count(&count).Error + return count, err +} + +func (r *activityLogRepo) CountAll() (int64, error) { + var count int64 + err := r.db.Model(&entity.ActivityLog{}).Count(&count).Error + return count, err +} + +func (r *activityLogRepo) CountByUserRole(role string) (int64, error) { + var count int64 + err := r.db.Model(&entity.ActivityLog{}). + Joins("JOIN users ON users.id = activity_logs.user_id"). + Joins("JOIN roles ON roles.id = users.role_id"). + Where("roles.name = ?", role). + Count(&count).Error + return count, err +} \ No newline at end of file diff --git a/repository/count_assets.go b/repository/count_assets.go new file mode 100644 index 0000000..5b0165b --- /dev/null +++ b/repository/count_assets.go @@ -0,0 +1,136 @@ +package repository + +import ( + "users_management/m/model/entity" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CountAssetsRepo interface { + Get() (entity.CountAssets, error) + UpdateCounts() error +} + + +type countAssetsRepo struct { + db *gorm.DB +} + + +func NewCountAssetsRepo(db *gorm.DB) CountAssetsRepo { + return &countAssetsRepo{ + db: db, + } +} + +func (r *countAssetsRepo) Get() (entity.CountAssets, error) { + var countAssets entity.CountAssets + + // Try to get existing record, if not found create a new one + err := r.db.First(&countAssets).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // Create new record with current counts + return r.createNewCountRecord() + } + return countAssets, err + } + + return countAssets, nil +} + +func (r *countAssetsRepo) UpdateCounts() error { + var countAssets entity.CountAssets + + // Get current counts from each table + fishboneCount, err := r.countFishbones() + if err != nil { + return err + } + + backboneCount, err := r.countBackbones() + if err != nil { + return err + } + + deviceCount, err := r.countDevices() + if err != nil { + return err + } + + towerCount, err := r.countTowers() + if err != nil { + return err + } + + // Check if record exists + err = r.db.First(&countAssets).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // Create new record + countAssets = entity.CountAssets{ + ID: uuid.New(), + FishboneAmount: fishboneCount, + BackboneAmount: backboneCount, + DeviceAmount: deviceCount, + TowerAmount: towerCount, + } + return r.db.Create(&countAssets).Error + } + return err + } + + // Update existing record + updates := map[string]interface{}{ + "fishbone_amount": fishboneCount, + "backbone_amount": backboneCount, + "device_amount": deviceCount, + "tower_amount": towerCount, + } + + return r.db.Model(&countAssets).Updates(updates).Error +} + +func (r *countAssetsRepo) createNewCountRecord() (entity.CountAssets, error) { + fishboneCount, _ := r.countFishbones() + backboneCount, _ := r.countBackbones() + deviceCount, _ := r.countDevices() + towerCount, _ := r.countTowers() + + countAssets := entity.CountAssets{ + ID: uuid.New(), + FishboneAmount: fishboneCount, + BackboneAmount: backboneCount, + DeviceAmount: deviceCount, + TowerAmount: towerCount, + } + + err := r.db.Create(&countAssets).Error + return countAssets, err +} + +// Helper methods to count each entity +func (r *countAssetsRepo) countFishbones() (int, error) { + var count int64 + err := r.db.Model(&entity.Fishbone{}).Count(&count).Error + return int(count), err +} + +func (r *countAssetsRepo) countBackbones() (int, error) { + var count int64 + err := r.db.Model(&entity.Backbone{}).Count(&count).Error + return int(count), err +} + +func (r *countAssetsRepo) countDevices() (int, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Count(&count).Error + return int(count), err +} + +func (r *countAssetsRepo) countTowers() (int, error) { + var count int64 + err := r.db.Model(&entity.Tower{}).Count(&count).Error + return int(count), err +} \ No newline at end of file diff --git a/repository/fishbone_repo.go b/repository/fishbone_repo.go index 8659c1c..bb106f2 100644 --- a/repository/fishbone_repo.go +++ b/repository/fishbone_repo.go @@ -1,6 +1,7 @@ package repository import ( + "log" "users_management/m/model/entity" "github.com/google/uuid" @@ -16,6 +17,12 @@ type FishboneRepo interface { CountFishbone() (map[uuid.UUID]int, error) CountFishboneByBackboneID(backboneID uuid.UUID) (int, error) + GetAllWithRelations() ([]entity.Fishbone, error) + GetByIDWithRelations(id uuid.UUID) (entity.Fishbone, error) + CheckFishboneExists(id uuid.UUID) (bool, error) + CheckBackboneExists(id uuid.UUID) (bool, error) + CheckDeviceExists(id uuid.UUID) (bool, error) + Delete(id uuid.UUID) error } type fishboneRepo struct { @@ -37,11 +44,14 @@ func (r *fishboneRepo) Post(fishbone entity.Fishbone) error { } func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) { + log.Print("Fetching all fishbones with relations") var fishbones []entity.Fishbone - err := r.db.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Preload("Backbone.DeviceStart").Preload("Backbone.DeviceEnd").Find(&fishbones).Error + err := r.db.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Find(&fishbones).Error if err != nil { return fishbones, err } + + log.Print(fishbones) return fishbones, nil } @@ -93,4 +103,38 @@ func (r *fishboneRepo) CountFishboneByBackboneID(backboneID uuid.UUID) (int, err return 0, err } return int(count), nil +} + +func (r *fishboneRepo) GetAllWithRelations() ([]entity.Fishbone, error) { + var fishbones []entity.Fishbone + err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").Find(&fishbones).Error + return fishbones, err +} + +func (r *fishboneRepo) GetByIDWithRelations(id uuid.UUID) (entity.Fishbone, error) { + var fishbone entity.Fishbone + err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").Where("id = ?", id).First(&fishbone).Error + return fishbone, err +} + +func (r *fishboneRepo) CheckFishboneExists(id uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Count(&count).Error + return count > 0, err +} + +func (r *fishboneRepo) CheckBackboneExists(id uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Backbone{}).Where("id = ?", id).Count(&count).Error + return count > 0, err +} + +func (r *fishboneRepo) CheckDeviceExists(id uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Where("id = ?", id).Count(&count).Error + return count > 0, err +} + +func (r *fishboneRepo) Delete(id uuid.UUID) error { + return r.db.Where("id = ?", id).Delete(&entity.Fishbone{}).Error } \ No newline at end of file diff --git a/repository/users_repo.go b/repository/users_repo.go index cd2ca6d..055f3bd 100644 --- a/repository/users_repo.go +++ b/repository/users_repo.go @@ -1,50 +1,75 @@ package repository import ( - "users_management/m/model/entity" + "users_management/m/model/entity" - "gorm.io/gorm" + "github.com/google/uuid" + "gorm.io/gorm" ) type UsersRepo interface { - Post(user entity.User) error - GetRoleByDepartment(departmentName string) (entity.Role, error) - GetUserByUsername(username string) (entity.User, error) + Post(user entity.User) error + GetRoleByDepartment(departmentName string) (entity.Role, error) + GetUserByUsername(username string) (entity.User, error) + GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this + CreateUserFromExternal(user entity.User) error // Add this + UpdateUserRole(nomorInduk string, roleID uuid.UUID) error // Add this + GetAllUsers() ([]entity.User, error) // Add this } type usersRepo struct { - db *gorm.DB + db *gorm.DB } func NewUsersRepo(db *gorm.DB) UsersRepo { - return &usersRepo{ - db: db, - } + return &usersRepo{ + db: db, + } } func (r *usersRepo) Post(user entity.User) error { - err := r.db.Create(&user).Error - if err != nil { - return err - } - return nil + err := r.db.Create(&user).Error + if err != nil { + return err + } + return nil } func (r *usersRepo) GetRoleByDepartment(departmentName string) (entity.Role, error) { - var role entity.Role - err := r.db.Where("name = ?", departmentName).First(&role).Error - if err != nil { - return role, err - } - return role, nil + var role entity.Role + err := r.db.Where("name = ?", departmentName).First(&role).Error + if err != nil { + return role, err + } + return role, nil } func (r *usersRepo) GetUserByUsername(username string) (entity.User, error) { - var users entity.User - err := r.db.Where("username = ?", username).First(&users).Error - if err != nil { - return users, err - } - return users, nil + var users entity.User + err := r.db.Where("username = ?", username).Preload("Role").First(&users).Error + if err != nil { + return users, err + } + return users, nil } +// New methods for RBAC +func (r *usersRepo) GetUserByNomorInduk(nomorInduk string) (entity.User, error) { + var user entity.User + err := r.db.Where("nomor_induk = ?", nomorInduk).Preload("Role").First(&user).Error + return user, err +} + +func (r *usersRepo) CreateUserFromExternal(user entity.User) error { + return r.db.Create(&user).Error +} + +func (r *usersRepo) UpdateUserRole(nomorInduk string, roleID uuid.UUID) error { + return r.db.Model(&entity.User{}).Where("nomor_induk = ?", nomorInduk).Update("role_id", roleID).Error +} + +func (r *usersRepo) GetAllUsers() ([]entity.User, error) { + var users []entity.User + err := r.db.Preload("Role").Find(&users).Error + return users, err +} \ No newline at end of file diff --git a/usecase/activity_logs_usecase.go b/usecase/activity_logs_usecase.go new file mode 100644 index 0000000..fe66043 --- /dev/null +++ b/usecase/activity_logs_usecase.go @@ -0,0 +1,101 @@ +package usecase + +import ( + "encoding/json" + "log" + "users_management/m/model/entity" + "users_management/m/repository" + + "github.com/google/uuid" +) + +type ActivityLogUseCase interface { + LogActivity(userID uuid.UUID, action, resource string, resourceID *string, oldData, newData interface{}, ipAddress, userAgent, details string) error + GetUserLogs(userID uuid.UUID, page, limit int) ([]entity.ActivityLog, int64, error) + GetAllLogs(page, limit int) ([]entity.ActivityLog, int64, error) + GetTeknisinLogs(page, limit int) ([]entity.ActivityLog, int64, error) +} + +type activityLogUseCase struct { + activityLogRepo repository.ActivityLogRepo +} + +func NewActivityLogUseCase(activityLogRepo repository.ActivityLogRepo) ActivityLogUseCase { + return &activityLogUseCase{ + activityLogRepo: activityLogRepo, + } +} + +func (uc *activityLogUseCase) LogActivity(userID uuid.UUID, action, resource string, resourceID *string, oldData, newData interface{}, ipAddress, userAgent, details string) error { + var oldDataJSON, newDataJSON *string + + if oldData != nil { + oldBytes, err := json.Marshal(oldData) + if err == nil { + oldStr := string(oldBytes) + oldDataJSON = &oldStr + } + } + + if newData != nil { + newBytes, err := json.Marshal(newData) + if err == nil { + newStr := string(newBytes) + newDataJSON = &newStr + } + } + + var detailsPtr *string + if details != "" { + detailsPtr = &details + } + + log := entity.ActivityLog{ + ID: uuid.New(), + UserID: userID, + Action: action, + Resource: resource, + ResourceID: resourceID, + OldData: oldDataJSON, + NewData: newDataJSON, + IPAddress: ipAddress, + UserAgent: userAgent, + Details: detailsPtr, + } + + return uc.activityLogRepo.Create(log) +} + +func (uc *activityLogUseCase) GetUserLogs(userID uuid.UUID, page, limit int) ([]entity.ActivityLog, int64, error) { + offset := (page - 1) * limit + log.Printf("Fetching logs for user %s, page: %d, limit: %d, offset: %d", userID, page, limit, offset) + logs, err := uc.activityLogRepo.GetByUserID(userID, limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := uc.activityLogRepo.CountByUserID(userID) + return logs, total, err +} + +func (uc *activityLogUseCase) GetAllLogs(page, limit int) ([]entity.ActivityLog, int64, error) { + offset := (page - 1) * limit + logs, err := uc.activityLogRepo.GetAllForAdmins(limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := uc.activityLogRepo.CountAll() + return logs, total, err +} + +func (uc *activityLogUseCase) GetTeknisinLogs(page, limit int) ([]entity.ActivityLog, int64, error) { + offset := (page - 1) * limit + logs, err := uc.activityLogRepo.GetByUserRole("Teknisi", limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := uc.activityLogRepo.CountByUserRole("Teknisi") + return logs, total, err +} \ No newline at end of file diff --git a/usecase/auth_usecase.go b/usecase/auth_usecase.go index 2d84e7e..9a50afb 100644 --- a/usecase/auth_usecase.go +++ b/usecase/auth_usecase.go @@ -2,6 +2,7 @@ package usecase import ( "bytes" + "crypto/tls" "encoding/json" "errors" "io/ioutil" @@ -28,13 +29,32 @@ type authUsecase struct { userRepo repository.UsersRepo validate *validator.Validate cfg *config.Config + client *http.Client } func NewAuthUsecase(userRepo repository.UsersRepo, cfg *config.Config) AuthUsecase { + var client *http.Client + + if cfg.LoginConfig.SkipSSLVerification { + // Create HTTP client with SSL verification disabled + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client = &http.Client{ + Transport: tr, + Timeout: 30 * time.Second, + } + } else { + // Use standard HTTP client with SSL verification enabled + client = &http.Client{ + Timeout: 30 * time.Second, + } + } return &authUsecase{ userRepo: userRepo, validate: validator.New(), cfg: cfg, + client: client, } } @@ -54,14 +74,16 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err return "","","", err } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - client := &http.Client{} - resp, err := client.Do(req) + // client := &http.Client{} + resp, err := u.client.Do(req) if err != nil { return "","","", err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -88,7 +110,8 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err meReq.Header.Set("Authorization", "Bearer "+token) meReq.Header.Set("Accept", "application/json") - meResp, err := client.Do(meReq) + meResp, err := u.client.Do(meReq) + if err != nil { return "","","", err } @@ -109,15 +132,25 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err return "","","", err } + departemen := meResponse.Data.Departemen - if departemen != "Teknisi" { + if departemen != "TECHNICAL PRORGAMMER" { return "","","", errors.New("user is not a technician") } - role , err := u.userRepo.GetRoleByDepartment(departemen) - - if err != nil { - return "","","", err + isUserExist, err := u.userRepo.GetUserByUsername(login.Username) + if isUserExist.ID != uuid.Nil { + // Validate the password + if !utils.CheckPasswordHash(login.Password, isUserExist.Password) { + return "","","", errors.New("incorrect password") + } + if err != nil { + return "","","", err + } + + return token, isUserExist.Role.Name, isUserExist.Name, nil + }else if err != nil && err != gorm.ErrRecordNotFound { + return "ERROR WHILE SEARCHING USERNAME","", "", err } password, err := utils.HashPassword(login.Password) @@ -125,19 +158,11 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err return "error while hasing password: ","", "", err } - isUserExist, err := u.userRepo.GetUserByUsername(login.Username) - - if isUserExist.ID != uuid.Nil { - // Validate the password - if !utils.CheckPasswordHash(login.Password, isUserExist.Password) { - return "","","", errors.New("incorrect password") - } - - return token, role.Name, isUserExist.Name, nil - }else if err != nil && err != gorm.ErrRecordNotFound { - return "ERROR WHILE SEARCHING USERNAME","", "", err - } - + default_role := "Teknisi" + role, err := u.userRepo.GetRoleByDepartment(default_role) + if err != nil { + return "","","", err + } user := entity.User{ ID: uuid.New(), @@ -148,7 +173,6 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err CreatedAt: time.Now(), UpdatedAt: time.Now(), } - err = u.userRepo.Post(user) if err != nil { return "","","", err @@ -166,8 +190,8 @@ func (u *authUsecase) Logout(token string) error { req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") - client := &http.Client{} - resp, err := client.Do(req) + // client := &http.Client{} + resp, err := u.client.Do(req) if err != nil { return err } diff --git a/usecase/count_assets_usecase.go b/usecase/count_assets_usecase.go new file mode 100644 index 0000000..6f8882f --- /dev/null +++ b/usecase/count_assets_usecase.go @@ -0,0 +1,31 @@ +package usecase + +import ( + "users_management/m/model/entity" + "users_management/m/repository" +) + +type CountAssetsUseCase interface { + GetCounts() (entity.CountAssets, error) +} + +type countAssetsUseCase struct { + countAssetsRepo repository.CountAssetsRepo +} + +func NewCountAssetsUseCase(countAssetsRepo repository.CountAssetsRepo) CountAssetsUseCase { + return &countAssetsUseCase{ + countAssetsRepo: countAssetsRepo, + } +} + +func (uc *countAssetsUseCase) GetCounts() (entity.CountAssets, error) { + // Update counts before returning + err := uc.countAssetsRepo.UpdateCounts() + if err != nil { + return entity.CountAssets{}, err + } + + // Get the updated counts + return uc.countAssetsRepo.Get() +} \ No newline at end of file diff --git a/usecase/device_usecase.go b/usecase/device_usecase.go index 4420c10..73f2af8 100644 --- a/usecase/device_usecase.go +++ b/usecase/device_usecase.go @@ -51,6 +51,10 @@ func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error { Latitude: device.Latitude, PortAmount: device.PortAmount, Status: entity.DeviceStatus(device.Status), + Region: device.Region, + Province: device.Province, + City: device.City, + District: device.District, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -106,6 +110,21 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e if device.PortAmount != nil { updates["PortAmount"] = *device.PortAmount } + if device.Status != nil { + updates["Status"] = *device.Status + } + if device.Region != nil { + updates["Region"] = *device.Region + } + if device.Province != nil { + updates["Province"] = *device.Province + } + if device.City != nil { + updates["City"] = *device.City + } + if device.District != nil { + updates["District"] = *device.District + } if len(updates) == 0 { return fmt.Errorf("no update data") diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index de4e146..e9ba47d 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -1,97 +1,188 @@ package usecase import ( - "errors" - "time" - "users_management/m/model/dto/req" - "users_management/m/model/entity" - "users_management/m/repository" + "errors" + "time" + "users_management/m/model/dto/req" + "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/repository" + "users_management/m/utils/helper" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" ) type FishboneUseCase interface { - CreateFishbone(fishbone req.FishboneDTO) error - GetAllFishbone() ([]entity.Fishbone, error) - GetByID(id uuid.UUID) (entity.Fishbone, error) - - UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error + CreateFishbone(fishbone req.FishboneDTO) error + GetAllFishbone() ([]res.FishboneResponse, error) + GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) + UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error + DeleteFishbone(id uuid.UUID) error + GetFishboneStats() (map[string]interface{}, error) } type fishboneUsecase struct { - fishboneRepo repository.FishboneRepo - validate *validator.Validate + fishboneRepo repository.FishboneRepo + validate *validator.Validate } func NewFishboneUseCase(fishboneRepo repository.FishboneRepo) FishboneUseCase { - return &fishboneUsecase{ - fishboneRepo: fishboneRepo, - validate: validator.New(), - } + return &fishboneUsecase{ + fishboneRepo: fishboneRepo, + validate: validator.New(), + } } func (u *fishboneUsecase) CreateFishbone(fishbone req.FishboneDTO) error { - err := u.validate.Struct(fishbone) - if err != nil { - return err - } + err := u.validate.Struct(fishbone) + if err != nil { + return err + } - newFishbone := entity.Fishbone{ - ID: uuid.New(), - BackboneID: fishbone.BackboneID, - DeviceStartID: fishbone.DeviceStartID, - DeviceEndID: fishbone.DeviceEndID, - CoreAmount: fishbone.CoreAmount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + // Check if backbone exists + backboneExists, err := u.fishboneRepo.CheckBackboneExists(fishbone.BackboneID) + if err != nil { + return err + } + if !backboneExists { + return errors.New("backbone not found") + } - return u.fishboneRepo.Post(newFishbone) + // Check if devices exist + deviceStartExists, err := u.fishboneRepo.CheckDeviceExists(fishbone.DeviceStartID) + if err != nil { + return err + } + if !deviceStartExists { + return errors.New("start device not found") + } + + deviceEndExists, err := u.fishboneRepo.CheckDeviceExists(fishbone.DeviceEndID) + if err != nil { + return err + } + if !deviceEndExists { + return errors.New("end device not found") + } + + newFishbone := entity.Fishbone{ + ID: uuid.New(), + FishboneCode: fishbone.FishboneCode, + BackboneID: fishbone.BackboneID, + DeviceStartID: fishbone.DeviceStartID, + DeviceEndID: fishbone.DeviceEndID, + CoreAmount: fishbone.CoreAmount, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.fishboneRepo.Post(newFishbone) } -func (u *fishboneUsecase) GetAllFishbone() ([]entity.Fishbone, error) { - fishbones, err := u.fishboneRepo.GetAll() - if err != nil { - return fishbones, err - } - return fishbones, nil +func (u *fishboneUsecase) GetAllFishbone() ([]res.FishboneResponse, error) { + fishbones, err := u.fishboneRepo.GetAll() + if err != nil { + return nil, err + } + + return helper.ConvertToSimpleFishboneResponses(fishbones), nil } -func (u *fishboneUsecase) GetByID(id uuid.UUID) (entity.Fishbone, error) { - fishbone, err := u.fishboneRepo.GetByID(id) - if err != nil { - return fishbone, err - } - return fishbone, nil +func (u *fishboneUsecase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) { + fishbone, err := u.fishboneRepo.GetByIDWithRelations(id) + if err != nil { + return res.FishboneDetailResponse{}, err + } + + // Convert to detailed response using helper + return helper.ConvertToFishboneDetailResponse(fishbone), nil } func (u *fishboneUsecase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error { - err := u.validate.Struct(fishbone) - if err != nil { - return err - } + err := u.validate.Struct(fishbone) + if err != nil { + return err + } - updates := make(map[string]interface{}) + // Check if fishbone exists + exists, err := u.fishboneRepo.CheckFishboneExists(id) + if err != nil { + return err + } + if !exists { + return errors.New("fishbone not found") + } - if fishbone.BackboneID != nil { - updates["BackboneID"] = *fishbone.BackboneID - } - if fishbone.DeviceStartID != nil { - updates["DeviceStartID"] = *fishbone.DeviceStartID - } - if fishbone.DeviceEndID != nil { - updates["DeviceEndID"] = *fishbone.DeviceEndID - } - if fishbone.CoreAmount != nil { - updates["CoreAmount"] = *fishbone.CoreAmount - } - if len(updates) == 0 { - return errors.New("no fields to update") - } - - updates["UpdatedAt"] = time.Now() + updates := make(map[string]interface{}) - return u.fishboneRepo.Update(id, updates) + if fishbone.FishboneCode != nil { + updates["fishbone_code"] = *fishbone.FishboneCode + } + if fishbone.BackboneID != nil { + // Validate backbone exists + backboneExists, err := u.fishboneRepo.CheckBackboneExists(*fishbone.BackboneID) + if err != nil { + return err + } + if !backboneExists { + return errors.New("backbone not found") + } + updates["bb_id"] = *fishbone.BackboneID + } + if fishbone.DeviceStartID != nil { + // Validate device exists + deviceExists, err := u.fishboneRepo.CheckDeviceExists(*fishbone.DeviceStartID) + if err != nil { + return err + } + if !deviceExists { + return errors.New("start device not found") + } + updates["dev_start_id"] = *fishbone.DeviceStartID + } + if fishbone.DeviceEndID != nil { + // Validate device exists + deviceExists, err := u.fishboneRepo.CheckDeviceExists(*fishbone.DeviceEndID) + if err != nil { + return err + } + if !deviceExists { + return errors.New("end device not found") + } + updates["dev_end_id"] = *fishbone.DeviceEndID + } + if fishbone.CoreAmount != nil { + updates["core_amount"] = *fishbone.CoreAmount + } + + if len(updates) == 0 { + return errors.New("no fields to update") + } + + updates["updated_at"] = time.Now() + + return u.fishboneRepo.Update(id, updates) } +func (u *fishboneUsecase) DeleteFishbone(id uuid.UUID) error { + // Check if fishbone exists + exists, err := u.fishboneRepo.CheckFishboneExists(id) + if err != nil { + return err + } + if !exists { + return errors.New("fishbone not found") + } + + return u.fishboneRepo.Delete(id) +} + +func (u *fishboneUsecase) GetFishboneStats() (map[string]interface{}, error) { + fishbones, err := u.fishboneRepo.GetAll() + if err != nil { + return nil, err + } + + return helper.GetFishboneStats(fishbones), nil +} \ No newline at end of file diff --git a/usecase/tower_usecase.go b/usecase/tower_usecase.go index 86456b5..dcf5dd0 100644 --- a/usecase/tower_usecase.go +++ b/usecase/tower_usecase.go @@ -2,6 +2,7 @@ package usecase import ( "errors" + "mime/multipart" "time" "users_management/m/model/dto/req" "users_management/m/model/dto/res" @@ -15,10 +16,10 @@ import ( ) type TowerUseCase interface { - Post(tower req.TowerDTO) error + Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error GetAll() ([]res.TowerResponse, error) GetByID(id uuid.UUID) (res.TowerResponse, error) - UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error + UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error } type towerUsecase struct { @@ -35,18 +36,27 @@ func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingS } } -func (u *towerUsecase) Post(tower req.TowerDTO) error { +func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error { err := u.validate.Struct(tower) if err != nil { return err } + var imageURL string + if imageFile != nil { + imageURL, err = helper.SaveTowerImage(imageFile) + if err != nil { + return err + } + } + newTower := entity.Tower{ ID: uuid.New(), DeviceID: tower.DeviceID, TowerCode: tower.TowerCode, Longitude: tower.Longitude, Latitude: tower.Latitude, + ImageURL: imageURL, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -80,7 +90,7 @@ func (u *towerUsecase) GetByID(id uuid.UUID) (res.TowerResponse, error) { return towerResp, nil } -func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error { +func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error { err := u.validate.Struct(tower) if err != nil { return err // Return validation error @@ -100,6 +110,23 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error updates["Latitude"] = *tower.Latitude } + // Handle image upload + if imageFile != nil { + // Get current tower to delete old image + currentTower, err := u.towerRepo.GetByID(id) + if err == nil && currentTower.ImageURL != "" { + // Delete old image + helper.DeleteTowerImage(currentTower.ImageURL) + } + + // Save new image + imageURL, err := helper.SaveTowerImage(imageFile) + if err != nil { + return err + } + updates["ImageURL"] = imageURL + } + // If no fields are updated, return an error if len(updates) == 0 { return errors.New("no fields to update") @@ -108,4 +135,4 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error updates["UpdatedAt"] = time.Now() return u.towerRepo.Update(id, updates) -} \ No newline at end of file +} diff --git a/usecase/users_usecase.go b/usecase/users_usecase.go index 19ec93f..608406b 100644 --- a/usecase/users_usecase.go +++ b/usecase/users_usecase.go @@ -1,30 +1,33 @@ package usecase import ( - "users_management/m/model/entity" - "users_management/m/repository" + "time" + "users_management/m/model/entity" + "users_management/m/repository" - "github.com/google/uuid" + "github.com/google/uuid" ) type UsersUsecase interface { - GetRoleByDepartment(departmentName string) (uuid.UUID, error) - GetUserByUsername(username string) (entity.User, error) + GetRoleByDepartment(departmentName string) (uuid.UUID, error) + GetUserByUsername(username string) (entity.User, error) + GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this + CreateUserFromExternal(nomorInduk, name, roleName string) error // Add this + UpdateUserRole(nomorInduk, roleName string) error // Add this + GetAllUsers() ([]entity.User, error) // Add this } type usersUsecase struct { - userRepo repository.UsersRepo + userRepo repository.UsersRepo } func NewUsersUsecase(userRepo repository.UsersRepo) UsersUsecase { - return &usersUsecase{ - userRepo: userRepo, - } + return &usersUsecase{ + userRepo: userRepo, + } } - func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, error) { - role, err := u.userRepo.GetRoleByDepartment(departmentName) if err != nil { return uuid.Nil, err @@ -33,10 +36,49 @@ func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, er } func (u *usersUsecase) GetUserByUsername(username string) (entity.User, error) { - users, err := u.userRepo.GetUserByUsername(username) - if err != nil { - return users, err - } - return users, nil + users, err := u.userRepo.GetUserByUsername(username) + if err != nil { + return users, err + } + return users, nil } +// New methods for RBAC +func (u *usersUsecase) GetUserByNomorInduk(nomorInduk string) (entity.User, error) { + return u.userRepo.GetUserByNomorInduk(nomorInduk) +} + +func (u *usersUsecase) CreateUserFromExternal(nomorInduk, name, roleName string) error { + // Get role ID + roleID, err := u.GetRoleByDepartment(roleName) + if err != nil { + return err + } + + // Create user + user := entity.User{ + ID: uuid.New(), + NomorInduk: &nomorInduk, + RoleID: roleID, + Name: name, + Username: nomorInduk, // Use nomor_induk as username + Password: "", // External auth, no local password needed + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.userRepo.CreateUserFromExternal(user) +} + +func (u *usersUsecase) UpdateUserRole(nomorInduk, roleName string) error { + roleID, err := u.GetRoleByDepartment(roleName) + if err != nil { + return err + } + + return u.userRepo.UpdateUserRole(nomorInduk, roleID) +} + +func (u *usersUsecase) GetAllUsers() ([]entity.User, error) { + return u.userRepo.GetAllUsers() +} \ No newline at end of file diff --git a/utils/helper/activity_logs_helper.go b/utils/helper/activity_logs_helper.go new file mode 100644 index 0000000..66f0d50 --- /dev/null +++ b/utils/helper/activity_logs_helper.go @@ -0,0 +1,84 @@ +package helper + +import ( + "users_management/m/model/dto/res" + "users_management/m/model/entity" +) + +// ConvertToActivityLogResponses converts activity log entities to simple response format for users +func ConvertToActivityLogResponses(logs []entity.ActivityLog) []res.ActivityLogResponse { + var responses []res.ActivityLogResponse + + for _, log := range logs { + response := res.ActivityLogResponse{ + ID: log.ID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } + responses = append(responses, response) + } + + return responses +} + +// ConvertToActivityLogDetailResponses converts activity log entities to detailed response format for admins +func ConvertToActivityLogDetailResponses(logs []entity.ActivityLog) []res.ActivityLogDetailResponse { + var responses []res.ActivityLogDetailResponse + + for _, log := range logs { + response := res.ActivityLogDetailResponse{ + ID: log.ID, + UserID: log.UserID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + ResourceID: log.ResourceID, + OldData: log.OldData, + NewData: log.NewData, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } + responses = append(responses, response) + } + + return responses +} + +// ConvertToSingleActivityLogResponse converts single activity log entity to simple response +func ConvertToSingleActivityLogResponse(log entity.ActivityLog) res.ActivityLogResponse { + return res.ActivityLogResponse{ + ID: log.ID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } +} + +// ConvertToSingleActivityLogDetailResponse converts single activity log entity to detailed response +func ConvertToSingleActivityLogDetailResponse(log entity.ActivityLog) res.ActivityLogDetailResponse { + return res.ActivityLogDetailResponse{ + ID: log.ID, + UserID: log.UserID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + ResourceID: log.ResourceID, + OldData: log.OldData, + NewData: log.NewData, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } +} \ No newline at end of file diff --git a/utils/helper/fishboneHelperRes.go b/utils/helper/fishboneHelperRes.go new file mode 100644 index 0000000..721850d --- /dev/null +++ b/utils/helper/fishboneHelperRes.go @@ -0,0 +1,90 @@ +package helper + +import ( + "users_management/m/model/dto/res" + "users_management/m/model/entity" + + "github.com/google/uuid" +) + +// ConvertToFishboneResponses converts fishbone entities to response DTOs +func ConvertToFishboneResponses(fishbones []entity.Fishbone, totalFishbone map[uuid.UUID]int) ([]res.FishboneResponse, error) { + var responses []res.FishboneResponse + + for _, fishbone := range fishbones { + count := 0 + if fishboneCount, exist := totalFishbone[fishbone.ID]; exist { + count = fishboneCount + } + + fishboneResp := res.FishboneResponse{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + BackboneCode: fishbone.Backbone.BackboneCode, + DeviceStart: fishbone.DeviceStart.DeviceCode, + DeviceEnd: fishbone.DeviceEnd.DeviceCode, + CoreAmount: fishbone.CoreAmount, + TotalFishbone: count, + CreatedAt: fishbone.CreatedAt, + UpdatedAt: fishbone.UpdatedAt, + } + responses = append(responses, fishboneResp) + } + + return responses, nil +} + +// ConvertToFishboneDetailResponse converts a single fishbone entity to detailed response +func ConvertToFishboneDetailResponse(fishbone entity.Fishbone) res.FishboneDetailResponse { + return res.FishboneDetailResponse{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + Backbone: res.BackboneInfo{ + BackboneCode: fishbone.Backbone.BackboneCode,// Fixed missing field + }, + CoreAmount: fishbone.CoreAmount, + CreatedAt: fishbone.CreatedAt, + UpdatedAt: fishbone.UpdatedAt, + } +} + +// ConvertToSimpleFishboneResponses converts fishbone entities to simple response format +func ConvertToSimpleFishboneResponses(fishbones []entity.Fishbone) []res.FishboneResponse { + var responses []res.FishboneResponse + + for _, fishbone := range fishbones { + fishboneResp := res.FishboneResponse{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + BackboneCode: fishbone.Backbone.BackboneCode, + DeviceStart: fishbone.DeviceStart.DeviceCode, + DeviceEnd: fishbone.DeviceEnd.DeviceCode, + CoreAmount: fishbone.CoreAmount, + CreatedAt: fishbone.CreatedAt, + UpdatedAt: fishbone.UpdatedAt, + } + responses = append(responses, fishboneResp) + } + + return responses +} + +// GetFishboneStats calculates statistics for fishbones +func GetFishboneStats(fishbones []entity.Fishbone) map[string]interface{} { + totalFishbones := len(fishbones) + totalCores := 0 + backboneCount := make(map[uuid.UUID]int) + + for _, fishbone := range fishbones { + totalCores += fishbone.CoreAmount + backboneCount[fishbone.BackboneID]++ + } + + return map[string]interface{}{ + "total_fishbones": totalFishbones, + "total_cores": totalCores, + "average_cores": float64(totalCores) / float64(totalFishbones), + "unique_backbones": len(backboneCount), + "backbone_usage": backboneCount, + } +} \ No newline at end of file diff --git a/utils/helper/image_uploader.go b/utils/helper/image_uploader.go new file mode 100644 index 0000000..3c83c79 --- /dev/null +++ b/utils/helper/image_uploader.go @@ -0,0 +1,90 @@ +package helper + +import ( + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" +) + +const ( + MaxFileSize = 5 << 20 // 5MB + UploadDir = "./uploads/towers" +) + +// SaveTowerImage saves uploaded image and returns the URL +func SaveTowerImage(file *multipart.FileHeader) (string, error) { + // Validate file size + if file.Size > MaxFileSize { + return "", fmt.Errorf("file size exceeds 5MB limit") + } + + // Validate file type + allowedTypes := []string{".jpg", ".jpeg", ".png", ".webp"} + ext := strings.ToLower(filepath.Ext(file.Filename)) + isAllowed := false + for _, allowedType := range allowedTypes { + if ext == allowedType { + isAllowed = true + break + } + } + if !isAllowed { + return "", fmt.Errorf("file type not allowed. Only jpg, jpeg, png, webp are allowed") + } + + // Create upload directory if it doesn't exist + if err := os.MkdirAll(UploadDir, 0755); err != nil { + return "", fmt.Errorf("failed to create upload directory: %v", err) + } + + // Generate unique filename + filename := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext) + filePath := filepath.Join(UploadDir, filename) + + // Open uploaded file + src, err := file.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + // Create destination file + dst, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("failed to create destination file: %v", err) + } + defer dst.Close() + + // Copy file + if _, err := io.Copy(dst, src); err != nil { + return "", fmt.Errorf("failed to copy file: %v", err) + } + + // Return URL (adjust based on your server setup) + return fmt.Sprintf("/uploads/towers/%s", filename), nil +} + +// DeleteTowerImage deletes an image file +func DeleteTowerImage(imageURL string) error { + if imageURL == "" { + return nil + } + + // Extract filename from URL + filename := filepath.Base(imageURL) + filePath := filepath.Join(UploadDir, filename) + + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil // File doesn't exist, nothing to delete + } + + // Delete file + return os.Remove(filePath) +} \ No newline at end of file diff --git a/utils/helper/towerHelperRes.go b/utils/helper/towerHelperRes.go index 2533c1a..763cc42 100644 --- a/utils/helper/towerHelperRes.go +++ b/utils/helper/towerHelperRes.go @@ -33,6 +33,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe Longitude: tower.Longitude, Latitude: tower.Latitude, Address: address, + ImageURL: tower.ImageURL, CreatedAt: tower.CreatedAt, } responses = append(responses, towerResp) @@ -62,6 +63,7 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer Longitude: tower.Longitude, Latitude: tower.Latitude, Address: address, + ImageURL: tower.ImageURL, CreatedAt: tower.CreatedAt, } return towerResp, nil diff --git a/utils/insecure_http.go b/utils/insecure_http.go new file mode 100644 index 0000000..65858b2 --- /dev/null +++ b/utils/insecure_http.go @@ -0,0 +1,34 @@ +package utils + +import ( + "crypto/tls" + "net/http" + "time" +) + +// CreateInsecureHTTPClient creates an HTTP client that skips SSL verification +func CreateInsecureHTTPClient() *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + return &http.Client{ + Transport: tr, + Timeout: 30 * time.Second, + } +} + +// CreateSecureHTTPClient creates a standard HTTP client with SSL verification +func CreateSecureHTTPClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + } +} + +// GetHTTPClient returns appropriate client based on environment +func GetHTTPClient(skipSSL bool) *http.Client { + if skipSSL { + return CreateInsecureHTTPClient() + } + return CreateSecureHTTPClient() +} \ No newline at end of file diff --git a/utils/service/geo_service.go b/utils/service/geo_service.go index 14a92e2..2ac7952 100644 --- a/utils/service/geo_service.go +++ b/utils/service/geo_service.go @@ -17,7 +17,7 @@ type nominatimGeocoder struct { client *http.Client } -// NewGeocodingService creates a new geocoding service using Nominatim (OpenStreetMap) + func NewGeocodingService() GeocodingService { client := &http.Client{ Timeout: 10 * time.Second,