diff --git a/delivery/controller/auth_controller.go b/delivery/controller/auth_controller.go new file mode 100644 index 0000000..fd7ee0b --- /dev/null +++ b/delivery/controller/auth_controller.go @@ -0,0 +1,104 @@ +package controller + +import ( + "net/http" + "users_management/m/model/dto" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +type AuthController struct { + authUC usecase.AuthUsecase + rg *gin.RouterGroup +} + +func NewAuthController(authUC usecase.AuthUsecase, rg *gin.RouterGroup) *AuthController { + return &AuthController{ + authUC: authUC, + rg: rg, + } +} + +func (c *AuthController) Route() { + auth := c.rg.Group("/auth") + { + auth.POST("/login", c.login) + auth.POST("/logout", c.logout) + auth.POST("/validate", c.validateToken) + } +} + +func (c *AuthController) login(ctx *gin.Context) { + var loginDTO dto.UserLoginDTO + if err := ctx.ShouldBindJSON(&loginDTO); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + token, role, name, err := c.authUC.Login(loginDTO) + if err != nil { + common.ErrorResponses(ctx, http.StatusUnauthorized, err.Error()) + return + } + + response := gin.H{ + "token": token, + "user": gin.H{ + "name": name, + "username": loginDTO.Username, + "role": role, + }, + "expires_in": "24h", + } + + common.SingleResponses(ctx, "Login successful", response) +} + +func (c *AuthController) logout(ctx *gin.Context) { + token := ctx.GetHeader("Authorization") + if token == "" { + common.ErrorResponses(ctx, http.StatusBadRequest, "Authorization token required") + return + } + + // Remove "Bearer " prefix + if len(token) > 7 && token[:7] == "Bearer " { + token = token[7:] + } + + err := c.authUC.Logout(token) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "Logout successful", nil) +} + +func (c *AuthController) validateToken(ctx *gin.Context) { + token := ctx.GetHeader("Authorization") + if token == "" { + common.ErrorResponses(ctx, http.StatusBadRequest, "Authorization token required") + return + } + + // Remove "Bearer " prefix + if len(token) > 7 && token[:7] == "Bearer " { + token = token[7:] + } + + username, err := c.authUC.ValidateToken(token) + if err != nil { + common.ErrorResponses(ctx, http.StatusUnauthorized, err.Error()) + return + } + + response := gin.H{ + "valid": true, + "username": username, + } + + common.SingleResponses(ctx, "Token is valid", response) +} \ No newline at end of file diff --git a/delivery/controller/backbone_controller.go b/delivery/controller/backbone_controller.go index 4920076..8ce5c26 100644 --- a/delivery/controller/backbone_controller.go +++ b/delivery/controller/backbone_controller.go @@ -24,8 +24,8 @@ func (bc *BackboneController) Route() { rg.GET("", bc.GetBackbone()) rg.GET("/:uuid", bc.GetBackboneByID()) - rg.POST("", middleware.ConditionalRequireAnyRole(bc.cfg, "Teknisi", "Admin", "Superadmin"), bc.CreateBackbone()) - rg.PUT("/:uuid", middleware.ConditionalRequireAnyRole(bc.cfg, "Teknisi", "Admin", "Superadmin"), bc.UpdateBackbone()) + rg.POST("", middleware.ConditionalRequireAnyRole(bc.cfg, "Teknisi", "Admin", "Super Admin"), bc.CreateBackbone()) + rg.PUT("/:uuid", middleware.ConditionalRequireAnyRole(bc.cfg, "Teknisi", "Admin", "Super Admin"), bc.UpdateBackbone()) } } diff --git a/delivery/controller/creatin_admin.go b/delivery/controller/creatin_admin.go new file mode 100644 index 0000000..2e52d50 --- /dev/null +++ b/delivery/controller/creatin_admin.go @@ -0,0 +1,140 @@ +package controller + +import ( + "net/http" + "time" + "users_management/m/model/entity" + "users_management/m/usecase" + "users_management/m/utils" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type BypassController struct { + userUC usecase.UsersUsecase + rg *gin.RouterGroup +} + +func NewBypassController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *BypassController { + return &BypassController{ + userUC: userUC, + rg: rg, + } +} + +func (c *BypassController) Route() { + bypass := c.rg.Group("/bypass") + { + // Dangerous endpoint - should be secured or removed in production + bypass.POST("/create-super-admin", c.createSuperAdmin) + bypass.GET("/roles", c.getRoles) // Helper to get role IDs + } +} + +type CreateSuperAdminRequest struct { + Name string `json:"name" binding:"required"` + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required,min=6"` + NomorInduk string `json:"nomor_induk" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` // Extra security +} + +func (c *BypassController) createSuperAdmin(ctx *gin.Context) { + var req CreateSuperAdminRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + // Simple secret key check (change this in production) + if req.SecretKey != "create-super-admin-secret-2024" { + common.ErrorResponses(ctx, http.StatusForbidden, "Invalid secret key") + return + } + + // Check if username already exists + existingUser, err := c.userUC.GetUserByUsernameWithStatus(req.Username) + if err == nil && existingUser.ID != uuid.Nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Username already exists") + return + } + + // Check if nomor_induk already exists + if req.NomorInduk != "" { + existingUser, err := c.userUC.GetUserByNomorInduk(req.NomorInduk) + if err == nil && existingUser.ID != uuid.Nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Nomor induk already exists") + return + } + } + + // Get or create Super Admin role + superAdminRoleID, err := c.userUC.GetRoleByDepartment("Super Admin") + if err != nil { + // If Super Admin role doesn't exist, try Admin + superAdminRoleID, err = c.userUC.GetRoleByDepartment("Admin") + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, "No admin role found in system") + return + } + } + + // Hash password + hashedPassword, err := utils.HashPassword(req.Password) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to hash password") + return + } + + // Create super admin user directly (bypass normal registration flow) + user := entity.User{ + ID: uuid.New(), + Name: req.Name, + Username: req.Username, + Password: hashedPassword, + NomorInduk: &req.NomorInduk, + RoleID: superAdminRoleID, + Status: entity.UserStatusApproved, // Directly approved + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err = c.userUC.CreateSuperAdminBypass(user) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to create super admin: "+err.Error()) + return + } + + response := gin.H{ + "message": "Super admin created successfully", + "user": gin.H{ + "id": user.ID, + "name": user.Name, + "username": user.Username, + "nomor_induk": user.NomorInduk, + "status": string(user.Status), + }, + "warning": "This bypass endpoint should be disabled in production", + } + + common.SingleResponses(ctx, "Super admin created", response) +} + +func (c *BypassController) getRoles(ctx *gin.Context) { + // Helper endpoint to see available roles + roles := []gin.H{ + {"name": "Super Admin", "description": "Full system access"}, + {"name": "Admin", "description": "Administrative access"}, + {"name": "Teknisi", "description": "Technical user access"}, + {"name": "Manager", "description": "Management access"}, + } + + response := gin.H{ + "roles": roles, + "note": "Use the role name to get role ID via GetRoleByDepartment", + } + + common.SingleResponses(ctx, "Available roles", response) +} \ No newline at end of file diff --git a/delivery/controller/device_details.go b/delivery/controller/device_details.go index 23e0d16..060fe76 100644 --- a/delivery/controller/device_details.go +++ b/delivery/controller/device_details.go @@ -36,7 +36,7 @@ func NewDeviceDetailsController(deviceDetailsUC usecase.DeviceDetailsUseCase, rg func (c *DeviceDetailsController) Route() { deviceDetails := c.rg.Group("/device-details") - deviceDetails.Use(middleware.ConditionalRequireAnyRole(c.cfg, "Teknisi", "Admin", "Superadmin")) + deviceDetails.Use(middleware.ConditionalRequireAnyRole(c.cfg, "Teknisi", "Admin", "Super Admin")) { deviceDetails.GET("", c.getAllDeviceDetails) deviceDetails.POST("", c.createDeviceDetails) diff --git a/delivery/controller/devices_controller.go b/delivery/controller/devices_controller.go index 5081efb..616a24c 100644 --- a/delivery/controller/devices_controller.go +++ b/delivery/controller/devices_controller.go @@ -33,7 +33,7 @@ func NewDeviceController(du usecase.DeviceUseCase, rg *gin.RouterGroup, cfg *con func (dc *DeviceController) Route() { rg := dc.rg.Group("/devices") - rg.Use(middleware.ConditionalRequireAnyRole(dc.cfg,"Teknisi", "Admin", "Superadmin")) + rg.Use(middleware.ConditionalRequireAnyRole(dc.cfg,"Teknisi", "Admin", "Super Admin")) { rg.POST("", dc.CreateDevice()) rg.GET("", dc.GetAllDevices()) diff --git a/delivery/controller/fishbone_controller.go b/delivery/controller/fishbone_controller.go index d1c5fa6..7e320dc 100644 --- a/delivery/controller/fishbone_controller.go +++ b/delivery/controller/fishbone_controller.go @@ -1,7 +1,6 @@ package controller import ( - "log" "net/http" "users_management/m/config" "users_management/m/middleware" @@ -29,7 +28,7 @@ func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup, cfg func (fc *FishboneController) Route() { rg := fc.rg.Group("/fishbone") - rg.Use(middleware.ConditionalRequireAnyRole(fc.cfg,"Teknisi", "Admin", "Superadmin")) + rg.Use(middleware.ConditionalRequireAnyRole(fc.cfg,"Teknisi", "Admin", "Super Admin")) // Apply middleware to all routes { rg.GET("", fc.GetFishbone()) @@ -67,7 +66,6 @@ func (fc *FishboneController) GetFishboneByBackboneID() gin.HandlerFunc { func (fc *FishboneController) GetFishbone() gin.HandlerFunc { return func(c *gin.Context) { - log.Println("Fetching all fishbones") fishbones, err := fc.fu.GetAllFishbone() if err != nil { diff --git a/delivery/controller/nearest_device_controller.go b/delivery/controller/nearest_device_controller.go index 59e33cd..08986c0 100644 --- a/delivery/controller/nearest_device_controller.go +++ b/delivery/controller/nearest_device_controller.go @@ -29,7 +29,7 @@ func NewNearestDeviceController(nearestDeviceUC usecase.NearestDeviceUseCase, rg func (c *NearestDeviceController) Route() { nearestDevices := c.rg.Group("/nearest-devices") - nearestDevices.Use(middleware.ConditionalRequireAnyRole(c.cfg,"Teknisi", "Admin", "Superadmin")) + nearestDevices.Use(middleware.ConditionalRequireAnyRole(c.cfg,"Teknisi", "Admin", "Super Admin")) { nearestDevices.POST("/search", c.getNearestDevices) nearestDevices.GET("/:id", c.getNearestDeviceByID) diff --git a/delivery/controller/tower_controller.go b/delivery/controller/tower_controller.go index cd597bc..e9cddb3 100644 --- a/delivery/controller/tower_controller.go +++ b/delivery/controller/tower_controller.go @@ -25,7 +25,7 @@ type TowerController struct { func (tc *TowerController) Route() { rg := tc.rg.Group("/tower") - rg.Use(middleware.ConditionalRequireAnyRole(tc.cfg,"Teknisi", "Admin", "Superadmin")) // Apply middleware to all routes + rg.Use(middleware.ConditionalRequireAnyRole(tc.cfg,"Teknisi", "Admin","Super Admin")) // Apply middleware to all routes { rg.GET("", tc.GetTower()) rg.POST("", tc.CreateTower()) diff --git a/delivery/controller/user_registration.go b/delivery/controller/user_registration.go new file mode 100644 index 0000000..2bfbf33 --- /dev/null +++ b/delivery/controller/user_registration.go @@ -0,0 +1,128 @@ +package controller + +import ( + "net/http" + "users_management/m/model/dto" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type UserRegistrationController struct { + userUC usecase.UsersUsecase + rg *gin.RouterGroup +} + +func NewUserRegistrationController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *UserRegistrationController { + return &UserRegistrationController{ + userUC: userUC, + rg: rg, + } +} + +func (c *UserRegistrationController) Route() { + registration := c.rg.Group("/user-registration") + { + // Public registration endpoint + registration.POST("/register", c.registerUser) + } + + adminRegistration := c.rg.Group("/user-registration") + adminRegistration.GET("/pending", c.getPendingUsers) + adminRegistration.PUT("/approve/:id", c.approveUser) + adminRegistration.PUT("/reject/:id", c.rejectUser) +} + +func (c *UserRegistrationController) RegisterUser(ctx *gin.Context) { + c.registerUser(ctx) +} + +func (c *UserRegistrationController) GetPendingUsers(ctx *gin.Context) { + c.getPendingUsers(ctx) +} + +func (c *UserRegistrationController) ApproveUser(ctx *gin.Context) { + c.approveUser(ctx) +} + +func (c *UserRegistrationController) RejectUser(ctx *gin.Context) { + c.rejectUser(ctx) +} + +func (c *UserRegistrationController) registerUser(ctx *gin.Context) { + var registerDTO dto.UserRegisterDTO + if err := ctx.ShouldBindJSON(®isterDTO); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err := c.userUC.RegisterUser(registerDTO) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + response := gin.H{ + "message": "Registration successful. Your account is pending approval from an administrator.", + "status": "pending", + "next_steps": "Please wait for an administrator to approve your account before you can log in.", + } + + common.SingleResponses(ctx, "User registered successfully", response) +} + +func (c *UserRegistrationController) getPendingUsers(ctx *gin.Context) { + + users, err := c.userUC.GetPendingUsers() + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + response := gin.H{ + "users": users, + "total": len(users), + } + + common.SingleResponses(ctx, "Pending users retrieved successfully", response) +} + +func (c *UserRegistrationController) approveUser(ctx *gin.Context) { + userID, err := uuid.Parse(ctx.Param("id")) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + err = c.userUC.ApproveUser(userID) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "User approved successfully", gin.H{ + "user_id": userID, + "status": "approved", + }) +} + +func (c *UserRegistrationController) rejectUser(ctx *gin.Context) { + userID, err := uuid.Parse(ctx.Param("id")) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + err = c.userUC.RejectUser(userID) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "User rejected successfully", gin.H{ + "user_id": userID, + "status": "rejected", + }) +} \ No newline at end of file diff --git a/delivery/server.go b/delivery/server.go index 95dea2d..021874a 100644 --- a/delivery/server.go +++ b/delivery/server.go @@ -34,7 +34,6 @@ func NewServer() *Server { engine := gin.Default() engine.Use(middleware.CORSMiddleware()) - engine.Use(middleware.ConditionalAuthMiddleware(ucManager.NewUserUsecase(), cfg)) engine.Use(middleware.ConditionalActivityLoggingMiddleware(ucManager.NewActivityLogUsecase(), cfg)) engine.Use(common.LoggingToFile()) @@ -50,20 +49,49 @@ func NewServer() *Server { func (s *Server) setupController() { - s.engine.Static("/uploads", "./uploads") + s.engine.Static("/uploads", "./uploads") + + rg := s.engine.Group("/api/v1") + + // Add bypass controller FIRST - before any auth middleware + controller.NewBypassController(s.ucManager.NewUserUsecase(), rg).Route() + + // Add auth controller (public endpoints) + controller.NewAuthController(s.ucManager.NewAuthUsecase(), rg).Route() + + publicRegistration := rg.Group("/user-registration") + { + publicRegistration.POST("/register", controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).RegisterUser) + } + - rg := s.engine.Group("/api/v1") - controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route() - controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route() - controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg,s.cfg).Route() - controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), rg,s.cfg).Route() - controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg,s.cfg).Route() - controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg,s.cfg).Route() - controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route() - controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg,s.cfg).Route() - controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), rg).Route() - controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), rg,s.cfg).Route() - controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), rg,s.cfg).Route() + + // Add user registration controller (public endpoints) + // controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).Route() + + // Now apply auth middleware to protected routes + protected := rg.Use(middleware.ConditionalAuthMiddleware(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), s.cfg)).(*gin.RouterGroup) + { + protectedRegistration := protected.Group("/user-registration") + { + protectedRegistration.GET("/pending", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).GetPendingUsers) + protectedRegistration.PUT("/approve/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).ApproveUser) + protectedRegistration.PUT("/reject/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).RejectUser) + } + controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), protected).Route() + controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), protected).Route() + controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), protected, s.cfg).Route() + controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), protected, s.cfg).Route() + controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), protected, s.cfg).Route() + controller.NewTowerController(s.ucManager.NewTowerUsecase(), protected, s.cfg).Route() + controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), protected).Route() + controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), protected, s.cfg).Route() + controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route() + controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route() + controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route() + + } + } diff --git a/go.mod b/go.mod index 319d1d7..c8cc8ad 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.1 require ( github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.24.0 + github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 golang.org/x/crypto v0.33.0 diff --git a/go.sum b/go.sum index ceb4f59..4671253 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index 3a9c66d..6ee78f0 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,17 +1,14 @@ package middleware import ( - "crypto/tls" - "encoding/json" "log" "net/http" "strings" - "time" "users_management/m/config" - "users_management/m/model/dto/res" "users_management/m/model/entity" "users_management/m/usecase" + "users_management/m/utils" "users_management/m/utils/common" "github.com/gin-gonic/gin" @@ -19,7 +16,7 @@ import ( ) // ConditionalAuthMiddleware checks config to decide whether to apply auth -func ConditionalAuthMiddleware(userUC usecase.UsersUsecase, cfg *config.Config) gin.HandlerFunc { +func ConditionalAuthMiddleware(userUC usecase.UsersUsecase, authUC usecase.AuthUsecase, cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { // If auth is disabled, skip authentication but set default values if !cfg.AuthConfig.UserAuthEnabled { @@ -35,85 +32,62 @@ func ConditionalAuthMiddleware(userUC usecase.UsersUsecase, cfg *config.Config) return } - // If auth is enabled, run the normal auth middleware - AuthMiddleware(userUC)(c) + // If auth is enabled, run the local auth middleware + LocalAuthMiddleware(userUC, authUC)(c) } } - -func AuthMiddleware(userUC usecase.UsersUsecase) gin.HandlerFunc { +// LocalAuthMiddleware handles local JWT authentication +func LocalAuthMiddleware(userUC usecase.UsersUsecase, authUC usecase.AuthUsecase) gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" { - common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required") + common.ErrorResponses(c, http.StatusUnauthorized, "Authorization token required") c.Abort() return } token = strings.TrimPrefix(token, "Bearer ") - c.Set("token", token) - req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil) - + // Validate JWT token locally + _, username, err := utils.ValidateJWT(token) if err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + log.Printf("JWT validation error: %v", err) + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid or expired token") c.Abort() return } - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - - - 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) + // Get user from local database + user, err := userUC.GetUserByUsername(username) if err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + log.Printf("User not found: %s", username) + common.ErrorResponses(c, http.StatusUnauthorized, "User not found") c.Abort() return } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized") + // Check if user is approved + switch user.Status { + case entity.UserStatusPending: + common.ErrorResponses(c, http.StatusForbidden, "Your account is pending approval from an administrator. Please wait for approval before accessing the system.") c.Abort() return - } - - var authResponse res.AuthMeResponse - if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + case entity.UserStatusRejected: + common.ErrorResponses(c, http.StatusForbidden, "Your account has been rejected. Please contact an administrator for more information.") 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 + case entity.UserStatusApproved: + // User is approved, continue with normal flow + c.Set("userID", user.ID) + c.Set("userName", user.Username) c.Set("userRole", user.Role.Name) + default: + common.ErrorResponses(c, http.StatusForbidden, "Your account status is invalid. Please contact an administrator.") + c.Abort() + return } c.Next() diff --git a/middleware/rbac.go b/middleware/rbac.go index aa94e68..0d03651 100644 --- a/middleware/rbac.go +++ b/middleware/rbac.go @@ -72,12 +72,12 @@ func RequireAnyRole(roles ...string) gin.HandlerFunc { // RequireAdminRole middleware for admin-only access func RequireAdminRole() gin.HandlerFunc { - return RequireRole("Admin", "Superadmin") + return RequireRole("Admin", "Super Admin") } // RequireSuperAdminRole middleware for superadmin-only access func RequireSuperAdminRole() gin.HandlerFunc { - return RequireRole("Superadmin") + return RequireRole("Super Admin") } // RequireTeknisiRole middleware for teknisi access diff --git a/model/dto/user_dto.go b/model/dto/user_dto.go index 1bd6858..947e335 100644 --- a/model/dto/user_dto.go +++ b/model/dto/user_dto.go @@ -1,13 +1,39 @@ package dto +import ( + "time" + + "github.com/google/uuid" +) + +// UserStatus defines the possible status values for a user +type UserStatus string + +const ( + UserStatusApproved UserStatus = "approved" + UserStatusRejected UserStatus = "rejected" + UserStatusPending UserStatus = "pending" +) + type UserRegisterDTO struct { - Name string `json:"name" validate:"required"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` - RoleID string `json:"role_id" validate:"required,uuid"` + Name string `json:"name" validate:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required,min=6"` + NomorInduk string `json:"nomor_induk" validate:"required"` } type UserLoginDTO struct { - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + +} +type PendingUserResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + NomorInduk *string `json:"nomor_induk"` + RoleName string `json:"role_name"` + Status UserStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } \ No newline at end of file diff --git a/model/entity/users.go b/model/entity/users.go index 29db6f3..39c45ca 100644 --- a/model/entity/users.go +++ b/model/entity/users.go @@ -1,8 +1,17 @@ package entity import ( - "time" - "github.com/google/uuid" + "time" + + "github.com/google/uuid" +) + +type UserStatus string + +const ( + UserStatusPending UserStatus = "pending" + UserStatusApproved UserStatus = "approved" + UserStatusRejected UserStatus = "rejected" ) type User struct { @@ -13,10 +22,12 @@ type User struct { Name string `json:"name"` Username string `json:"username" gorm:"unique"` Password string `json:"password"` + Status UserStatus `json:"status" gorm:"type:varchar(20);default:'pending'"` // Add status field 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/fishbone_repo.go b/repository/fishbone_repo.go index 27566d6..dd0305b 100644 --- a/repository/fishbone_repo.go +++ b/repository/fishbone_repo.go @@ -1,7 +1,6 @@ package repository import ( - "log" "users_management/m/model/entity" "github.com/google/uuid" @@ -56,14 +55,12 @@ func (r *fishboneRepo) GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, } 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").Find(&fishbones).Error if err != nil { return fishbones, err } - log.Print(fishbones) return fishbones, nil } diff --git a/repository/users_repo.go b/repository/users_repo.go index 055f3bd..9e72816 100644 --- a/repository/users_repo.go +++ b/repository/users_repo.go @@ -1,10 +1,10 @@ package repository import ( - "users_management/m/model/entity" + "users_management/m/model/entity" - "github.com/google/uuid" - "gorm.io/gorm" + "github.com/google/uuid" + "gorm.io/gorm" ) type UsersRepo interface { @@ -15,6 +15,12 @@ type UsersRepo interface { CreateUserFromExternal(user entity.User) error // Add this UpdateUserRole(nomorInduk string, roleID uuid.UUID) error // Add this GetAllUsers() ([]entity.User, error) // Add this + RegisterUser(user entity.User) error + GetPendingUsers() ([]entity.User, error) + UpdateUserStatus(userID uuid.UUID, status entity.UserStatus) error + GetUserByID(userID uuid.UUID) (entity.User, error) + GetUserByUsernameWithStatus(username string) (entity.User, error) + CreateSuperAdminBypass(user entity.User) error } type usersRepo struct { @@ -27,6 +33,36 @@ func NewUsersRepo(db *gorm.DB) UsersRepo { } } +func (r *usersRepo) CreateSuperAdminBypass(user entity.User) error { + return r.db.Create(&user).Error +} + +func (r *usersRepo) RegisterUser(user entity.User) error { + return r.db.Create(&user).Error +} + +func (r *usersRepo) GetPendingUsers() ([]entity.User, error) { + var users []entity.User + err := r.db.Where("status = ?", entity.UserStatusPending).Preload("Role").Find(&users).Error + return users, err +} + +func (r *usersRepo) UpdateUserStatus(userID uuid.UUID, status entity.UserStatus) error { + return r.db.Model(&entity.User{}).Where("id = ?", userID).Update("status", status).Error +} + +func (r *usersRepo) GetUserByID(userID uuid.UUID) (entity.User, error) { + var user entity.User + err := r.db.Where("id = ?", userID).Preload("Role").First(&user).Error + return user, err +} + +func (r *usersRepo) GetUserByUsernameWithStatus(username string) (entity.User, error) { + var user entity.User + err := r.db.Where("username = ?", username).Preload("Role").First(&user).Error + return user, err +} + func (r *usersRepo) Post(user entity.User) error { err := r.db.Create(&user).Error if err != nil { diff --git a/usecase/activity_logs_usecase.go b/usecase/activity_logs_usecase.go index fe66043..6761e7a 100644 --- a/usecase/activity_logs_usecase.go +++ b/usecase/activity_logs_usecase.go @@ -2,7 +2,6 @@ package usecase import ( "encoding/json" - "log" "users_management/m/model/entity" "users_management/m/repository" @@ -68,7 +67,6 @@ func (uc *activityLogUseCase) LogActivity(userID uuid.UUID, action, resource str 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 diff --git a/usecase/auth_usecase.go b/usecase/auth_usecase.go index 7e9480b..5d4da80 100644 --- a/usecase/auth_usecase.go +++ b/usecase/auth_usecase.go @@ -1,205 +1,95 @@ package usecase import ( - "bytes" - "crypto/tls" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "time" - "users_management/m/config" - "users_management/m/model/dto" - "users_management/m/model/dto/res" - "users_management/m/model/entity" - "users_management/m/repository" - "users_management/m/utils" + "errors" + "users_management/m/config" + "users_management/m/model/dto" + "users_management/m/model/entity" + "users_management/m/repository" + "users_management/m/utils" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" - "gorm.io/gorm" + "github.com/go-playground/validator/v10" ) type AuthUsecase interface { - Login(login dto.UserLoginDTO) (string,string, string ,error) - Logout(token string) error + Login(login dto.UserLoginDTO) (string, string, string, error) + Logout(token string) error + ValidateToken(token string) (string, error) } type authUsecase struct { userRepo repository.UsersRepo - validate *validator.Validate cfg *config.Config - client *http.Client + validate *validator.Validate } 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, + validate: validator.New(), } } func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, error) { - err := u.validate.Struct(login) + // Validate input + if err := u.validate.Struct(login); err != nil { + return "", "", "", err + } + + // Get user from database (only approved users can login) + user, err := u.userRepo.GetUserByUsername(login.Username) if err != nil { - return "","","", err + return "", "", "", errors.New("invalid username or password") } - payload, err := json.Marshal(login) - if err != nil { - return "","","", err - } - - req, err := http.NewRequest("POST", u.cfg.LoginAPI, bytes.NewBuffer(payload)) - if err != nil { - return "","","", err - } - - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - // client := &http.Client{} - resp, err := u.client.Do(req) - if err != nil { - return "","","", err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "","","", errors.New("wrong password or username") - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "","","", err - } - - var authResponse res.AuthResponses - err = json.Unmarshal(body, &authResponse) - if err != nil { - return "","","", err - } - - token := authResponse.Token - - meReq, err := http.NewRequest("POST", u.cfg.AuthMeAPI, nil) - if err != nil { - return "","","", err - } - meReq.Header.Set("Authorization", "Bearer "+token) - meReq.Header.Set("Accept", "application/json") - - meResp, err := u.client.Do(meReq) - - if err != nil { - return "","","", err - } - defer meResp.Body.Close() - - if meResp.StatusCode != http.StatusOK { - return "","","", errors.New("failed to validate token: " + meResp.Status) - } - - meBody, err := ioutil.ReadAll(meResp.Body) - if err != nil { - return "","","", err - } - - var meResponse res.AuthMeResponse - err = json.Unmarshal(meBody, &meResponse) - if err != nil { - return "","","", err - } - - - // departemen := meResponse.Data.Departemen - // if departemen != "TECHNICAL PRORGAMMER" { - // return "","","", errors.New("user is not a technician") - // } - - 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") + // Check user status + if user.Status != entity.UserStatusApproved { + switch user.Status { + case entity.UserStatusPending: + return "", "", "", errors.New("your account is pending approval from an administrator") + case entity.UserStatusRejected: + return "", "", "", errors.New("your account has been rejected. Please contact an administrator") + default: + return "", "", "", errors.New("your account is not active") } - 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) - if err != nil { - return "error while hasing password: ","", "", err - } - - default_role := "Teknisi" - role, err := u.userRepo.GetRoleByDepartment(default_role) - if err != nil { - return "","","", err } - user := entity.User{ - ID: uuid.New(), - Name: meResponse.Data.Nama, - Username: login.Username, - Password: password, - RoleID: role.Id, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - err = u.userRepo.Post(user) - if err != nil { - return "","","", err - } + // Verify password + if !utils.CheckPasswordHash(login.Password, user.Password) { + return "", "", "", errors.New("invalid username or password") + } + + // Generate JWT token + token, err := utils.GenerateJWT(user.ID.String(), user.Username, user.Role.Name) + if err != nil { + return "", "", "", err + } return token, user.Role.Name, user.Name, nil } func (u *authUsecase) Logout(token string) error { - req, err := http.NewRequest("POST", u.cfg.LogoutAPI, nil) - if err != nil { - return err - } + // For JWT, we could implement a blacklist mechanism here + // For now, we'll just return success since JWT tokens expire naturally + return nil +} - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - - // client := &http.Client{} - resp, err := u.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return errors.New("failed to logout: " + resp.Status) - } - - return nil +func (u *authUsecase) ValidateToken(token string) (string, error) { + _, username, err := utils.ValidateJWT(token) + if err != nil { + return "", err + } + + // Verify user still exists and is approved + user, err := u.userRepo.GetUserByUsername(username) + if err != nil { + return "", errors.New("user not found") + } + + if user.Status != entity.UserStatusApproved { + return "", errors.New("user account is not active") + } + + return username, nil } \ No newline at end of file diff --git a/usecase/users_usecase.go b/usecase/users_usecase.go index 608406b..d49de6f 100644 --- a/usecase/users_usecase.go +++ b/usecase/users_usecase.go @@ -1,11 +1,15 @@ package usecase import ( - "time" - "users_management/m/model/entity" - "users_management/m/repository" + "errors" + "time" + "users_management/m/model/dto" + "users_management/m/model/entity" + "users_management/m/repository" + "users_management/m/utils" - "github.com/google/uuid" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" ) type UsersUsecase interface { @@ -15,18 +19,140 @@ type UsersUsecase interface { CreateUserFromExternal(nomorInduk, name, roleName string) error // Add this UpdateUserRole(nomorInduk, roleName string) error // Add this GetAllUsers() ([]entity.User, error) // Add this + RegisterUser(registerDTO dto.UserRegisterDTO) error + GetPendingUsers() ([]dto.PendingUserResponse, error) + ApproveUser(userID uuid.UUID) error + RejectUser(userID uuid.UUID) error + GetUserByUsernameWithStatus(username string) (entity.User, error) + CreateSuperAdminBypass(user entity.User) error + } type usersUsecase struct { userRepo repository.UsersRepo + validate *validator.Validate } func NewUsersUsecase(userRepo repository.UsersRepo) UsersUsecase { return &usersUsecase{ userRepo: userRepo, + validate: validator.New(), } } + +func (u *usersUsecase) CreateSuperAdminBypass(user entity.User) error { + return u.userRepo.CreateSuperAdminBypass(user) +} + +func (u *usersUsecase) RegisterUser(registerDTO dto.UserRegisterDTO) error { + // Validate input + if err := u.validate.Struct(registerDTO); err != nil { + return err + } + + // Check if username already exists + existingUser, err := u.userRepo.GetUserByUsernameWithStatus(registerDTO.Username) + if err == nil && existingUser.ID != uuid.Nil { + return errors.New("username already exists") + } + + // Check if nomor_induk already exists + if registerDTO.NomorInduk != "" { + existingUser, err := u.userRepo.GetUserByNomorInduk(registerDTO.NomorInduk) + if err == nil && existingUser.ID != uuid.Nil { + return errors.New("nomor induk already exists") + } + } + + // Hash password + hashedPassword, err := utils.HashPassword(registerDTO.Password) + if err != nil { + return err + } + + teknisiRoleID, err := u.GetRoleByDepartment("Teknisi") + if err != nil { + return errors.New("failed to get Teknisi role: " + err.Error()) + } + + + + // Create user with pending status + user := entity.User{ + ID: uuid.New(), + Name: registerDTO.Name, + Username: registerDTO.Username, + Password: hashedPassword, + NomorInduk: ®isterDTO.NomorInduk, + RoleID: teknisiRoleID, + Status: entity.UserStatusPending, // Set status to pending + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.userRepo.RegisterUser(user) +} + +func (u *usersUsecase) GetPendingUsers() ([]dto.PendingUserResponse, error) { + users, err := u.userRepo.GetPendingUsers() + if err != nil { + return nil, err + } + + + var responses []dto.PendingUserResponse + for _, user := range users { + response := dto.PendingUserResponse{ + ID: user.ID, + Name: user.Name, + Username: user.Username, + NomorInduk: user.NomorInduk, + RoleName: user.Role.Name, + Status: dto.UserStatus(user.Status), + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } + responses = append(responses, response) + } + + + + return responses, nil +} + +func (u *usersUsecase) ApproveUser(userID uuid.UUID) error { + // Check if user exists and is pending + user, err := u.userRepo.GetUserByID(userID) + if err != nil { + return errors.New("user not found") + } + + if user.Status != entity.UserStatusPending { + return errors.New("user is not in pending status") + } + + return u.userRepo.UpdateUserStatus(userID, entity.UserStatusApproved) +} + +func (u *usersUsecase) RejectUser(userID uuid.UUID) error { + // Check if user exists and is pending + user, err := u.userRepo.GetUserByID(userID) + if err != nil { + return errors.New("user not found") + } + + if user.Status != entity.UserStatusPending { + return errors.New("user is not in pending status") + } + + return u.userRepo.UpdateUserStatus(userID, entity.UserStatusRejected) +} + +func (u *usersUsecase) GetUserByUsernameWithStatus(username string) (entity.User, error) { + return u.userRepo.GetUserByUsernameWithStatus(username) +} + func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, error) { role, err := u.userRepo.GetRoleByDepartment(departmentName) if err != nil { diff --git a/utils/service/geoCache_service.go b/utils/service/geoCache_service.go index ade1dbe..ae103f2 100644 --- a/utils/service/geoCache_service.go +++ b/utils/service/geoCache_service.go @@ -41,7 +41,6 @@ func (g *CachedGeocoder) GetAddressFromCoordinates(latitude, longitude float64) } g.mutex.RUnlock() - log.Printf("CACHE MISS: Coordinates (%.6f,%.6f) not in cache, fetching from service", latitude, longitude) atomic.AddInt64(&g.misses, 1) // Not in cache, call the underlying service diff --git a/utils/utils.go b/utils/utils.go index b20dca9..100ac9e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,12 @@ package utils -import "golang.org/x/crypto/bcrypt" +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" + "golang.org/x/crypto/bcrypt" +) func HashPassword(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -14,4 +20,47 @@ func HashPassword(password string) (string, error) { func CheckPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil +} + +var jwtSecret = []byte("your-secret-key-change-this-in-production") // Change this in production + +type Claims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func GenerateJWT(userID, username, role string) (string, error) { + claims := Claims{ + UserID: userID, + Username: username, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Token expires in 24 hours + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +func ValidateJWT(tokenString string) (string, string, error) { + claims := &Claims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if err != nil { + return "", "", err + } + + if !token.Valid { + return "", "", errors.New("invalid token") + } + + return claims.UserID, claims.Username, nil } \ No newline at end of file