diff --git a/cmd/main.go b/cmd/main.go index d4ee0e9..024c357 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,27 +13,33 @@ func main() { app := fiber.New() app.Use(cors.New(cors.Config{ - AllowOrigins: "http://localhost:3000", - AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", - AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", - AllowCredentials: true, + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,PATCH,DELETE", + AllowHeaders: "Content-Type,x-api-key", })) - app.Use(func(c *fiber.Ctx) error { - c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - c.Set("Access-Control-Allow-Credentials", "true") - return c.Next() - }) + // app.Use(cors.New(cors.Config{ + // AllowOrigins: "http://localhost:3000", + // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + // AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", + // AllowCredentials: true, + // })) - app.Options("*", func(c *fiber.Ctx) error { - c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - c.Set("Access-Control-Allow-Credentials", "true") - return c.SendStatus(fiber.StatusNoContent) - }) + // app.Use(func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.Next() + // }) + + // app.Options("*", func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.SendStatus(fiber.StatusNoContent) + // }) router.SetupRoutes(app) diff --git a/config/database.go b/config/database.go index 52ac433..34451f3 100644 --- a/config/database.go +++ b/config/database.go @@ -45,6 +45,8 @@ func ConnectDatabase() { &model.Role{}, &model.UserPin{}, &model.Address{}, + &model.IdentityCard{}, + &model.CompanyProfile{}, // =>user preparation<= // =>store preparation<= diff --git a/config/redis.go b/config/redis.go index 1d93165..61801f0 100644 --- a/config/redis.go +++ b/config/redis.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "strconv" "github.com/go-redis/redis/v8" ) @@ -13,13 +14,21 @@ var RedisClient *redis.Client var Ctx = context.Background() func ConnectRedis() { + + redisDBStr := os.Getenv("REDIS_DB") + + redisDB, err := strconv.Atoi(redisDBStr) + if err != nil { + log.Fatalf("Error converting REDIS_DB to integer: %v", err) + } + RedisClient = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")), Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, + DB: redisDB, }) - _, err := RedisClient.Ping(Ctx).Result() + _, err = RedisClient.Ping(Ctx).Result() if err != nil { log.Fatalf("Error connecting to Redis: %v", err) } diff --git a/config/server.go b/config/server.go index 10caf70..9f5cf5e 100644 --- a/config/server.go +++ b/config/server.go @@ -18,8 +18,8 @@ func StartServer(app *fiber.App) { address := fmt.Sprintf("%s:%s", host, port) - log.Printf("Server is running on http://%s", address) + log.Printf("server berjalan di http://%s", address) if err := app.Listen(address); err != nil { - log.Fatalf("Error starting server: %v", err) + log.Fatalf("gagal saat menjalankan server: %v", err) } } diff --git a/dto/auth/auth_admin_dto.go b/dto/auth/auth_admin_dto.go new file mode 100644 index 0000000..b15a248 --- /dev/null +++ b/dto/auth/auth_admin_dto.go @@ -0,0 +1,130 @@ +package dto + +import ( + "regexp" + "strings" +) + +type LoginAdminRequest struct { + Deviceid string `json:"device_id"` + Email string `json:"email"` + Password string `json:"password"` +} + +type LoginResponse struct { + UserID string `json:"user_id"` + Role string `json:"role"` + Token string `json:"token"` +} + +type RegisterAdminRequest struct { + Name string `json:"name"` + Gender string `json:"gender"` + Dateofbirth string `json:"dateofbirth"` + Placeofbirth string `json:"placeofbirth"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password"` + PasswordConfirm string `json:"password_confirm"` +} + +type UserAdminDataResponse struct { + UserID string `json:"user_id"` + Name string `json:"name"` + Gender string `json:"gender"` + Dateofbirth string `json:"dateofbirth"` + Placeofbirth string `json:"placeofbirth"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password"` + Role string `json:"role"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func (r *RegisterAdminRequest) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "Name is required") + } + + if strings.TrimSpace(r.Gender) == "" { + errors["gender"] = append(errors["gender"], "Gender is required") + } else if r.Gender != "male" && r.Gender != "female" { + errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'") + } + + if strings.TrimSpace(r.Dateofbirth) == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") + } + + if strings.TrimSpace(r.Placeofbirth) == "" { + errors["placeofbirth"] = append(errors["placeofbirth"], "Place of birth is required") + } + + if strings.TrimSpace(r.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone is required") + } else if !IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") + } + + if strings.TrimSpace(r.Email) == "" { + errors["email"] = append(errors["email"], "Email is required") + } else if !IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + + if len(r.Password) < 6 { + errors["password"] = append(errors["password"], "Password must be at least 6 characters") + } else if !IsValidPassword(r.Password) { + errors["password"] = append(errors["password"], "Password must contain at least one uppercase letter, one number, and one special character") + } + + if r.Password != r.PasswordConfirm { + errors["password_confirm"] = append(errors["password_confirm"], "Password and confirmation do not match") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func IsValidPhoneNumber(phone string) bool { + re := regexp.MustCompile(`^62\d{9,13}$`) + return re.MatchString(phone) +} + +func IsValidEmail(email string) bool { + re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) + return re.MatchString(email) +} + +func IsValidPassword(password string) bool { + + if len(password) < 6 { + return false + } + + hasUpper := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + if char >= 'A' && char <= 'Z' { + hasUpper = true + } else if char >= '0' && char <= '9' { + hasDigit = true + } else if isSpecialCharacter(char) { + hasSpecial = true + } + } + + return hasUpper && hasDigit && hasSpecial +} + +func isSpecialCharacter(char rune) bool { + specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" + return strings.ContainsRune(specialChars, char) +} diff --git a/dto/auth/auth_masyarakat_dto.go b/dto/auth/auth_masyarakat_dto.go new file mode 100644 index 0000000..cbd8f99 --- /dev/null +++ b/dto/auth/auth_masyarakat_dto.go @@ -0,0 +1 @@ +package dto \ No newline at end of file diff --git a/dto/auth/auth_pengelola_dto.go b/dto/auth/auth_pengelola_dto.go new file mode 100644 index 0000000..58c916c --- /dev/null +++ b/dto/auth/auth_pengelola_dto.go @@ -0,0 +1,133 @@ +package dto + +import ( + "regexp" + "rijig/utils" +) + +type LoginPengelolaRequest struct { + Phone string `json:"phone"` +} + +func (r *LoginPengelolaRequest) ValidateLogin() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Phone == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } else if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Phone number is not valid") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type VerifLoginPengelolaRequest struct { + Phone string `json:"phone"` + Otp string `json:"verif_otp"` +} + +func (r *VerifLoginPengelolaRequest) ValidateVerifLogin() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Phone == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } else if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Phone number is not valid") + } + + if r.Otp == "" { + errors["otp"] = append(errors["otp"], "OTP is required") + } else if len(r.Otp) != 6 { + errors["otp"] = append(errors["otp"], "OTP must be 6 digits") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type LoginPengelolaResponse struct { + UserID string `json:"user_id"` + Role string `json:"role"` + Token string `json:"token"` +} + +type PengelolaIdentityCard struct { + Cardphoto string `json:"cardphoto"` + Identificationumber string `json:"identificationumber"` + Placeofbirth string `json:"placeofbirth"` + Dateofbirth string `json:"dateofbirth"` + Gender string `json:"gender"` + BloodType string `json:"bloodtype"` + District string `json:"district"` + Village string `json:"village"` + Neighbourhood string `json:"neighbourhood"` + Religion string `json:"religion"` + Maritalstatus string `json:"maritalstatus"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"validuntil"` +} + +func (r *PengelolaIdentityCard) ValidateIDcard() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Cardphoto == "" { + errors["cardphoto"] = append(errors["cardphoto"], "Card photo is required") + } + + if r.Identificationumber == "" { + errors["identificationumber"] = append(errors["identificationumber"], "Identification number is required") + } + + if r.Dateofbirth == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") + } else if !isValidDate(r.Dateofbirth) { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth must be in DD-MM-YYYY format") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type PengelolaCompanyProfile struct { + CompanyName string `json:"company_name"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` +} + +func (r *PengelolaCompanyProfile) ValidateCompany() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.CompanyName == "" { + errors["company_name"] = append(errors["company_name"], "Company name is required") + } + + if r.CompanyPhone == "" { + errors["company_phone"] = append(errors["company_phone"], "Company phone is required") + } else if !utils.IsValidPhoneNumber(r.CompanyPhone) { + errors["company_phone"] = append(errors["company_phone"], "Invalid phone number format") + } + + if r.CompanyEmail == "" { + errors["company_email"] = append(errors["company_email"], "Company email is required") + } else if !utils.IsValidEmail(r.CompanyEmail) { + errors["company_email"] = append(errors["company_email"], "Invalid email format") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func isValidDate(date string) bool { + re := regexp.MustCompile(`^\d{2}-\d{2}-\d{4}$`) + return re.MatchString(date) +} diff --git a/dto/auth/auth_pengepul_dto.go b/dto/auth/auth_pengepul_dto.go new file mode 100644 index 0000000..cbd8f99 --- /dev/null +++ b/dto/auth/auth_pengepul_dto.go @@ -0,0 +1 @@ +package dto \ No newline at end of file diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 367e0e9..5782667 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -6,14 +6,13 @@ import ( ) type RegisterRequest struct { - RoleID string `json:"role_id"` - Phone string `json:"phone"` + Phone string `json:"phone"` } type VerifyOTPRequest struct { - RoleID string `json:"role_id"` - Phone string `json:"phone"` + Phone string `json:"phone"` OTP string `json:"otp"` + DeviceID string `json:"device_id"` } type UserDataResponse struct { @@ -25,10 +24,6 @@ type UserDataResponse struct { func (r *RegisterRequest) Validate() (map[string][]string, bool) { errors := make(map[string][]string) - if strings.TrimSpace(r.RoleID) == "" { - errors["role_id"] = append(errors["role_id"], "Role ID is required") - } - if strings.TrimSpace(r.Phone) == "" { errors["phone"] = append(errors["phone"], "Phone is required") } else if !IsValidPhoneNumber(r.Phone) { diff --git a/dto/user_dto.go b/dto/user_dto.go index b527605..0d74533 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -37,12 +37,6 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } - // if strings.TrimSpace(r.Email) == "" { - // errors["email"] = append(errors["email"], "Email is required") - // } else if !IsValidEmail(r.Email) { - // errors["email"] = append(errors["email"], "Invalid email format") - // } - if len(errors) > 0 { return errors, false } diff --git a/internal/handler/auth/auth_admin_handler.go b/internal/handler/auth/auth_admin_handler.go new file mode 100644 index 0000000..98341b4 --- /dev/null +++ b/internal/handler/auth/auth_admin_handler.go @@ -0,0 +1,80 @@ +package handler + +import ( + "log" + dto "rijig/dto/auth" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthAdminHandler struct { + UserService services.AuthAdminService +} + +func NewAuthAdminHandler(userService services.AuthAdminService) *AuthAdminHandler { + return &AuthAdminHandler{UserService: userService} +} + +func (h *AuthAdminHandler) RegisterAdmin(c *fiber.Ctx) error { + var request dto.RegisterAdminRequest + + if err := c.BodyParser(&request); err != nil { + return utils.InternalServerErrorResponse(c, "Failed to parse request body") + } + + errors, valid := request.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + user, err := h.UserService.RegisterAdmin(&request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) + } + + return utils.SuccessResponse(c, user, "Admin registered successfully") +} + +func (h *AuthAdminHandler) LoginAdmin(c *fiber.Ctx) error { + var request dto.LoginAdminRequest + + if err := c.BodyParser(&request); err != nil { + return utils.InternalServerErrorResponse(c, "Failed to parse request body") + } + + loginResponse, err := h.UserService.LoginAdmin(&request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error()) + } + + return utils.SuccessResponse(c, loginResponse, "Login successful") +} + +func (h *AuthAdminHandler) LogoutAdmin(c *fiber.Ctx) error { + // Ambil userID dari c.Locals + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + log.Println("Error: UserID is nil or empty") + return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") + } + + // Ambil deviceID dari header atau c.Locals + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + log.Printf("UserID: %s, DeviceID: %s", userID, deviceID) + + err := h.UserService.LogoutAdmin(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Successfully logged out") +} + diff --git a/internal/handler/auth/auth_masyarakat_handler.go b/internal/handler/auth/auth_masyarakat_handler.go new file mode 100644 index 0000000..426e689 --- /dev/null +++ b/internal/handler/auth/auth_masyarakat_handler.go @@ -0,0 +1,81 @@ +package handler + +import ( + "log" + "rijig/dto" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthMasyarakatHandler struct { + authMasyarakatService services.AuthMasyarakatService +} + +func NewAuthMasyarakatHandler(authMasyarakatService services.AuthMasyarakatService) *AuthMasyarakatHandler { + return &AuthMasyarakatHandler{authMasyarakatService} +} + +func (h *AuthMasyarakatHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { + var req dto.RegisterRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.Phone == "" { + return utils.ErrorResponse(c, "Phone number is required") + } + + if err := h.authMasyarakatService.RegisterOrLogin(&req); err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "OTP sent successfully") +} + +func (h *AuthMasyarakatHandler) VerifyOTPHandler(c *fiber.Ctx) error { + var req dto.VerifyOTPRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + + if req.DeviceID == "" { + return utils.ErrorResponse(c, "DeviceID is required") + } + + response, err := h.authMasyarakatService.VerifyOTP(&req) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthMasyarakatHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + err := h.authMasyarakatService.Logout(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/internal/handler/auth/auth_pengepul_handler.go b/internal/handler/auth/auth_pengepul_handler.go new file mode 100644 index 0000000..50da9ab --- /dev/null +++ b/internal/handler/auth/auth_pengepul_handler.go @@ -0,0 +1,81 @@ +package handler + +import ( + "log" + "rijig/dto" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthPengepulHandler struct { + authPengepulService services.AuthMasyarakatService +} + +func NewAuthPengepulHandler(authPengepulService services.AuthMasyarakatService) *AuthPengepulHandler { + return &AuthPengepulHandler{authPengepulService} +} + +func (h *AuthPengepulHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { + var req dto.RegisterRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.Phone == "" { + return utils.ErrorResponse(c, "Phone number is required") + } + + if err := h.authPengepulService.RegisterOrLogin(&req); err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "OTP sent successfully") +} + +func (h *AuthPengepulHandler) VerifyOTPHandler(c *fiber.Ctx) error { + var req dto.VerifyOTPRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + + if req.DeviceID == "" { + return utils.ErrorResponse(c, "DeviceID is required") + } + + response, err := h.authPengepulService.VerifyOTP(&req) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthPengepulHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + err := h.authPengepulService.Logout(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/internal/handler/auth/auth_pnegelola_handler.go b/internal/handler/auth/auth_pnegelola_handler.go new file mode 100644 index 0000000..1491f74 --- /dev/null +++ b/internal/handler/auth/auth_pnegelola_handler.go @@ -0,0 +1,81 @@ +package handler + +import ( + "log" + "rijig/dto" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthPengelolaHandler struct { + authPengelolaService services.AuthMasyarakatService +} + +func NewAuthPengelolaHandler(authPengelolaService services.AuthMasyarakatService) *AuthPengelolaHandler { + return &AuthPengelolaHandler{authPengelolaService} +} + +func (h *AuthPengelolaHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { + var req dto.RegisterRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.Phone == "" { + return utils.ErrorResponse(c, "Phone number is required") + } + + if err := h.authPengelolaService.RegisterOrLogin(&req); err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "OTP sent successfully") +} + +func (h *AuthPengelolaHandler) VerifyOTPHandler(c *fiber.Ctx) error { + var req dto.VerifyOTPRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + + if req.DeviceID == "" { + return utils.ErrorResponse(c, "DeviceID is required") + } + + response, err := h.authPengelolaService.VerifyOTP(&req) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthPengelolaHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + err := h.authPengelolaService.Logout(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index fe3fa6f..0782d02 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,80 +1,80 @@ package handler -import ( - "log" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" +// import ( +// "log" +// "rijig/dto" +// "rijig/internal/services" +// "rijig/utils" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -type AuthHandler struct { - authService services.AuthService -} +// type AuthHandler struct { +// authService services.AuthService +// } -func NewAuthHandler(authService services.AuthService) *AuthHandler { - return &AuthHandler{authService} -} +// func NewAuthHandler(authService services.AuthService) *AuthHandler { +// return &AuthHandler{authService} +// } -func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { - var req dto.RegisterRequest +// func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { +// var req dto.RegisterRequest - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } +// if err := c.BodyParser(&req); err != nil { +// return utils.ErrorResponse(c, "Invalid request body") +// } - if req.Phone == "" || req.RoleID == "" { - return utils.ErrorResponse(c, "Phone number and role ID are required") - } +// if req.Phone == "" || req.RoleID == "" { +// return utils.ErrorResponse(c, "Phone number and role ID are required") +// } - if err := h.authService.RegisterOrLogin(&req); err != nil { - return utils.ErrorResponse(c, err.Error()) - } +// if err := h.authService.RegisterOrLogin(&req); err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, nil, "OTP sent successfully") -} +// return utils.SuccessResponse(c, nil, "OTP sent successfully") +// } -func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error { - var req dto.VerifyOTPRequest +// func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error { +// var req dto.VerifyOTPRequest - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } +// if err := c.BodyParser(&req); err != nil { +// return utils.ErrorResponse(c, "Invalid request body") +// } - if req.OTP == "" { - return utils.ErrorResponse(c, "OTP is required") - } +// if req.OTP == "" { +// return utils.ErrorResponse(c, "OTP is required") +// } - response, err := h.authService.VerifyOTP(&req) - if err != nil { - return utils.ErrorResponse(c, err.Error()) - } +// response, err := h.authService.VerifyOTP(&req) +// if err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, response, "Registration/Login successful") -} +// return utils.SuccessResponse(c, response, "Registration/Login successful") +// } -func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error { +// func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "User is not logged in or invalid session") - } +// userID, ok := c.Locals("userID").(string) +// if !ok || userID == "" { +// return utils.ErrorResponse(c, "User is not logged in or invalid session") +// } - phoneKey := "user_phone:" + userID - phone, err := utils.GetStringData(phoneKey) - if err != nil || phone == "" { +// phoneKey := "user_phone:" + userID +// phone, err := utils.GetStringData(phoneKey) +// if err != nil || phone == "" { - log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err) - return utils.ErrorResponse(c, "Phone number is missing or invalid session data") - } +// log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err) +// return utils.ErrorResponse(c, "Phone number is missing or invalid session data") +// } - err = h.authService.Logout(userID, phone) - if err != nil { +// err = h.authService.Logout(userID, phone) +// if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } +// log.Printf("Error during logout process for user %s: %v", userID, err) +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, nil, "Logged out successfully") -} +// return utils.SuccessResponse(c, nil, "Logged out successfully") +// } diff --git a/internal/repositories/auth/auth_admin_repo.go b/internal/repositories/auth/auth_admin_repo.go new file mode 100644 index 0000000..4652e1d --- /dev/null +++ b/internal/repositories/auth/auth_admin_repo.go @@ -0,0 +1,93 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthAdminRepository interface { + FindByEmail(email string) (*model.User, error) + FindAdminByEmailandRoleid(email, roleId string) (*model.User, error) + FindAdminByPhoneandRoleid(phone, roleId string) (*model.User, error) + FindByPhone(phone string) (*model.User, error) + FindByEmailOrPhone(identifier string) (*model.User, error) + FindRoleByName(roleName string) (*model.Role, error) + CreateUser(user *model.User) (*model.User, error) +} + +type authAdminRepository struct { + DB *gorm.DB +} + +func NewAuthAdminRepository(db *gorm.DB) AuthAdminRepository { + return &authAdminRepository{DB: db} +} + +func (r *authAdminRepository) FindByEmail(email string) (*model.User, error) { + var user model.User + err := r.DB.Preload("Role").Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindAdminByEmailandRoleid(email, roleId string) (*model.User, error) { + var user model.User + err := r.DB.Where("email = ? AND role_id = ?", email, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindAdminByPhoneandRoleid(phone, roleId string) (*model.User, error) { + var user model.User + err := r.DB.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindByPhone(phone string) (*model.User, error) { + var user model.User + err := r.DB.Where("phone = ?", phone).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindByEmailOrPhone(identifier string) (*model.User, error) { + var user model.User + err := r.DB.Where("email = ? OR phone = ?", identifier, identifier).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) CreateUser(user *model.User) (*model.User, error) { + err := r.DB.Create(user).Error + if err != nil { + return nil, err + } + return user, nil +} + +func (r *authAdminRepository) FindRoleByName(roleName string) (*model.Role, error) { + var role model.Role + err := r.DB.Where("role_name = ?", roleName).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} diff --git a/internal/repositories/auth/auth_masyarakat_repo.go b/internal/repositories/auth/auth_masyarakat_repo.go new file mode 100644 index 0000000..f49bfd0 --- /dev/null +++ b/internal/repositories/auth/auth_masyarakat_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthMasyarakatRepository interface { + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) +} + +type authMasyarakatRepository struct { + db *gorm.DB +} + +func NewAuthMasyarakatRepositories(db *gorm.DB) AuthMasyarakatRepository { + return &authMasyarakatRepository{db} +} + +func (r *authMasyarakatRepository) CreateUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *authMasyarakatRepository) GetUserByPhone(phone string) (*model.User, error) { + var user model.User + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authMasyarakatRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/repositories/auth/auth_pengelola_repo.go b/internal/repositories/auth/auth_pengelola_repo.go new file mode 100644 index 0000000..f7b561a --- /dev/null +++ b/internal/repositories/auth/auth_pengelola_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthPengelolaRepository interface { + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) +} + +type authPengelolaRepository struct { + db *gorm.DB +} + +func NewAuthPengelolaRepositories(db *gorm.DB) AuthPengelolaRepository { + return &authPengelolaRepository{db} +} + +func (r *authPengelolaRepository) CreateUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *authPengelolaRepository) GetUserByPhone(phone string) (*model.User, error) { + var user model.User + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authPengelolaRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/repositories/auth/auth_pengepul_repo.go b/internal/repositories/auth/auth_pengepul_repo.go new file mode 100644 index 0000000..5253ee1 --- /dev/null +++ b/internal/repositories/auth/auth_pengepul_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthPengepulRepository interface { + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) +} + +type authPengepulRepository struct { + db *gorm.DB +} + +func NewAuthPengepulRepositories(db *gorm.DB) AuthPengepulRepository { + return &authPengepulRepository{db} +} + +func (r *authPengepulRepository) CreateUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *authPengepulRepository) GetUserByPhone(phone string) (*model.User, error) { + var user model.User + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authPengepulRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/repositories/role_repo.go b/internal/repositories/role_repo.go index df4299b..27698f3 100644 --- a/internal/repositories/role_repo.go +++ b/internal/repositories/role_repo.go @@ -8,6 +8,7 @@ import ( type RoleRepository interface { FindByID(id string) (*model.Role, error) + FindRoleByName(roleName string) (*model.Role, error) FindAll() ([]model.Role, error) } @@ -36,3 +37,12 @@ func (r *roleRepository) FindAll() ([]model.Role, error) { } return roles, nil } + +func (r *roleRepository) FindRoleByName(roleName string) (*model.Role, error) { + var role model.Role + err := r.DB.Where("role_name = ?", roleName).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} diff --git a/internal/services/auth/auth_admin_service.go b/internal/services/auth/auth_admin_service.go new file mode 100644 index 0000000..89ac8aa --- /dev/null +++ b/internal/services/auth/auth_admin_service.go @@ -0,0 +1,191 @@ +package service + +import ( + "errors" + "fmt" + "log" + "rijig/config" + dto "rijig/dto/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +const ( + ErrEmailTaken = "email is already used" + ErrPhoneTaken = "phone number is already used" + ErrInvalidPassword = "password does not match" + ErrRoleNotFound = "role not found" + ErrFailedToGenerateToken = "failed to generate token" + ErrFailedToHashPassword = "failed to hash password" + ErrFailedToCreateUser = "failed to create user" + ErrIncorrectPassword = "incorrect password" + ErrAccountNotFound = "account not found" +) + +type AuthAdminService interface { + RegisterAdmin(request *dto.RegisterAdminRequest) (*model.User, error) + + LoginAdmin(req *dto.LoginAdminRequest) (*dto.LoginResponse, error) + LogoutAdmin(userID, deviceID string) error +} + +type authAdminService struct { + UserRepo repository.AuthAdminRepository + RoleRepo repositories.RoleRepository + SecretKey string +} + +func NewAuthAdminService(userRepo repository.AuthAdminRepository, roleRepo repositories.RoleRepository, secretKey string) AuthAdminService { + return &authAdminService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey} +} + +func (s *authAdminService) RegisterAdmin(request *dto.RegisterAdminRequest) (*model.User, error) { + + if existingUser, _ := s.UserRepo.FindByEmail(request.Email); existingUser != nil { + return nil, errors.New(ErrEmailTaken) + } + + if existingUser, _ := s.UserRepo.FindByPhone(request.Phone); existingUser != nil { + return nil, errors.New(ErrPhoneTaken) + } + + role, err := s.UserRepo.FindRoleByName("administrator") + if err != nil { + return nil, errors.New(ErrRoleNotFound) + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) + if err != nil { + log.Println("Error hashing password:", err) + return nil, errors.New(ErrFailedToHashPassword) + } + + user := &model.User{ + Name: request.Name, + Email: request.Email, + Phone: request.Phone, + Password: string(hashedPassword), + RoleID: role.ID, + Role: role, + Dateofbirth: request.Dateofbirth, + Placeofbirth: request.Placeofbirth, + Gender: request.Gender, + RegistrationStatus: "completed", + } + + createdUser, err := s.UserRepo.CreateUser(user) + if err != nil { + log.Println("Error creating user:", err) + return nil, fmt.Errorf("%s: %v", ErrFailedToCreateUser, err) + } + + return createdUser, nil +} + +func (s *authAdminService) LoginAdmin(req *dto.LoginAdminRequest) (*dto.LoginResponse, error) { + + user, err := s.UserRepo.FindByEmail(req.Email) + if err != nil { + log.Println("User not found:", err) + return nil, errors.New(ErrAccountNotFound) + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + log.Println("Incorrect password:", err) + return nil, errors.New(ErrIncorrectPassword) + } + + existingUser, err := s.UserRepo.FindAdminByEmailandRoleid(req.Email, "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var adminUser *model.User + if existingUser != nil { + adminUser = existingUser + } else { + + adminUser = &model.User{ + Email: req.Email, + RoleID: "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7", + } + createdUser, err := s.UserRepo.CreateUser(adminUser) + if err != nil { + return nil, err + } + adminUser = createdUser + } + + token, err := s.generateJWTToken(adminUser.ID, req.Deviceid) + if err != nil { + return nil, err + } + + role, err := s.RoleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.Deviceid + if err := s.saveSessionAdminData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.LoginResponse{ + UserID: user.ID, + Role: user.Role.RoleName, + Token: token, + }, nil +} + +func (s *authAdminService) saveSessionAdminData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authAdminService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authAdminService) LogoutAdmin(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/auth_masyarakat_service.go b/internal/services/auth/auth_masyarakat_service.go new file mode 100644 index 0000000..407ba9b --- /dev/null +++ b/internal/services/auth/auth_masyarakat_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "errors" + "fmt" + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type AuthMasyarakatService interface { + RegisterOrLogin(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, deviceID string) error +} + +type authMasyarakatService struct { + userRepo repository.AuthPengelolaRepository + roleRepo repositories.RoleRepository +} + +func NewAuthMasyarakatService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthMasyarakatService { + return &authMasyarakatService{userRepo, roleRepo} +} + +func (s *authMasyarakatService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authMasyarakatService) RegisterOrLogin(req *dto.RegisterRequest) error { + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err + } + return s.sendOTP(req.Phone) +} + +func (s *authMasyarakatService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil + } + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") + } + return nil +} + +func (s *authMasyarakatService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + +func (s *authMasyarakatService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + storedOTP, err := utils.GetStringData("otp:" + req.Phone) + if err != nil || storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) + } + if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) + } + + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "0e5684e4-b214-4bd0-972f-3be80c4649a0") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: "0e5684e4-b214-4bd0-972f-3be80c4649a0", + PhoneVerified: true, + RegistrationStatus: "completed", + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser + } + + token, err := s.generateJWTToken(user.ID, req.DeviceID) + if err != nil { + return nil, err + } + + role, err := s.roleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.DeviceID + if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.UserDataResponse{ + UserID: user.ID, + UserRole: role.RoleName, + Token: token, + }, nil +} + +func (s *authMasyarakatService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authMasyarakatService) Logout(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/auth_pengelola_service.go b/internal/services/auth/auth_pengelola_service.go new file mode 100644 index 0000000..840f22c --- /dev/null +++ b/internal/services/auth/auth_pengelola_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "errors" + "fmt" + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type AuthPengelolaService interface { + RegisterOrLogin(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, deviceID string) error +} + +type authPengelolaService struct { + userRepo repository.AuthPengelolaRepository + roleRepo repositories.RoleRepository +} + +func NewAuthPengelolaService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthPengelolaService { + return &authPengelolaService{userRepo, roleRepo} +} + +func (s *authPengelolaService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authPengelolaService) RegisterOrLogin(req *dto.RegisterRequest) error { + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err + } + return s.sendOTP(req.Phone) +} + +func (s *authPengelolaService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil + } + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") + } + return nil +} + +func (s *authPengelolaService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + +func (s *authPengelolaService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + storedOTP, err := utils.GetStringData("otp:" + req.Phone) + if err != nil || storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) + } + if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) + } + + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "0bf86966-7042-410a-a88c-d01f70832348") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: "0bf86966-7042-410a-a88c-d01f70832348", + PhoneVerified: true, + RegistrationStatus: "uncompleted", + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser + } + + token, err := s.generateJWTToken(user.ID, req.DeviceID) + if err != nil { + return nil, err + } + + role, err := s.roleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.DeviceID + if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.UserDataResponse{ + UserID: user.ID, + UserRole: role.RoleName, + Token: token, + }, nil +} + +func (s *authPengelolaService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authPengelolaService) Logout(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/auth_pengepul_service.go b/internal/services/auth/auth_pengepul_service.go new file mode 100644 index 0000000..0fb0f3c --- /dev/null +++ b/internal/services/auth/auth_pengepul_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "errors" + "fmt" + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type AuthPengepulService interface { + RegisterOrLogin(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, deviceID string) error +} + +type authPengepulService struct { + userRepo repository.AuthPengelolaRepository + roleRepo repositories.RoleRepository +} + +func NewAuthPengepulService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthPengepulService { + return &authPengepulService{userRepo, roleRepo} +} + +func (s *authPengepulService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authPengepulService) RegisterOrLogin(req *dto.RegisterRequest) error { + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err + } + return s.sendOTP(req.Phone) +} + +func (s *authPengepulService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil + } + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") + } + return nil +} + +func (s *authPengepulService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + +func (s *authPengepulService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + storedOTP, err := utils.GetStringData("otp:" + req.Phone) + if err != nil || storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) + } + if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) + } + + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "d7245535-0e9e-4d35-ab39-baece5c10b3c") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: "d7245535-0e9e-4d35-ab39-baece5c10b3c", + PhoneVerified: true, + RegistrationStatus: "uncompleted", + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser + } + + token, err := s.generateJWTToken(user.ID, req.DeviceID) + if err != nil { + return nil, err + } + + role, err := s.roleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.DeviceID + if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.UserDataResponse{ + UserID: user.ID, + UserRole: role.RoleName, + Token: token, + }, nil +} + +func (s *authPengepulService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authPengepulService) Logout(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/otp.go b/internal/services/auth/otp.go new file mode 100644 index 0000000..d96c534 --- /dev/null +++ b/internal/services/auth/otp.go @@ -0,0 +1,14 @@ +package service + +import ( + "fmt" + "math/rand" + "time" +) + +func generateOTP() string { + randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) + return fmt.Sprintf("%04d", randGenerator.Intn(10000)) +} + +const otpCooldown = 50 * time.Second diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 92da706..47a733d 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,213 +1,213 @@ package services -import ( - "errors" - "fmt" - "math/rand" - "rijig/config" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - "time" +// import ( +// "errors" +// "fmt" +// "math/rand" +// "rijig/config" +// "rijig/dto" +// "rijig/internal/repositories" +// "rijig/model" +// "rijig/utils" +// "time" - "github.com/golang-jwt/jwt/v5" -) +// "github.com/golang-jwt/jwt/v5" +// ) -const otpCooldown = 30 * time.Second +// const otpCooldown = 30 * time.Second -type AuthService interface { - RegisterOrLogin(req *dto.RegisterRequest) error - VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) - Logout(userID, phone string) error -} +// type AuthService interface { +// RegisterOrLogin(req *dto.RegisterRequest) error +// VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) +// Logout(userID, phone string) error +// } -type authService struct { - userRepo repositories.UserRepository - roleRepo repositories.RoleRepository -} +// type authService struct { +// userRepo repositories.UserRepository +// roleRepo repositories.RoleRepository +// } -func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { - return &authService{userRepo, roleRepo} -} +// func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { +// return &authService{userRepo, roleRepo} +// } -func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error { +// func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error { - if err := s.checkOTPRequestCooldown(req.Phone); err != nil { - return err - } +// if err := s.checkOTPRequestCooldown(req.Phone); err != nil { +// return err +// } - user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) - if err != nil { - return fmt.Errorf("failed to check existing user: %w", err) - } +// user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) +// if err != nil { +// return fmt.Errorf("failed to check existing user: %w", err) +// } - if user != nil { - return s.sendOTP(req.Phone) - } +// if user != nil { +// return s.sendOTP(req.Phone) +// } - user = &model.User{ - Phone: req.Phone, - RoleID: req.RoleID, - } +// user = &model.User{ +// Phone: req.Phone, +// RoleID: req.RoleID, +// } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return fmt.Errorf("failed to create new user: %w", err) - } +// createdUser, err := s.userRepo.CreateUser(user) +// if err != nil { +// return fmt.Errorf("failed to create new user: %w", err) +// } - if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil { - return err - } +// if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil { +// return err +// } - return s.sendOTP(req.Phone) -} +// return s.sendOTP(req.Phone) +// } -func (s *authService) checkOTPRequestCooldown(phone string) error { - otpSentTime, err := utils.GetStringData("otp_sent:" + phone) - if err != nil || otpSentTime == "" { - return nil - } - lastSent, _ := time.Parse(time.RFC3339, otpSentTime) - if time.Since(lastSent) < otpCooldown { - return errors.New("please wait before requesting a new OTP") - } - return nil -} +// func (s *authService) checkOTPRequestCooldown(phone string) error { +// otpSentTime, err := utils.GetStringData("otp_sent:" + phone) +// if err != nil || otpSentTime == "" { +// return nil +// } +// lastSent, _ := time.Parse(time.RFC3339, otpSentTime) +// if time.Since(lastSent) < otpCooldown { +// return errors.New("please wait before requesting a new OTP") +// } +// return nil +// } -func (s *authService) sendOTP(phone string) error { - otp := generateOTP() - if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { - return err - } +// func (s *authService) sendOTP(phone string) error { +// otp := generateOTP() +// if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { +// return err +// } - if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { - return err - } - return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) -} +// if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { +// return err +// } +// return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +// } -func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { +// func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - storedOTP, err := utils.GetStringData("otp:" + req.Phone) - if err != nil || storedOTP == "" { - return nil, errors.New("OTP expired or not found") - } +// storedOTP, err := utils.GetStringData("otp:" + req.Phone) +// if err != nil || storedOTP == "" { +// return nil, errors.New("OTP expired or not found") +// } - if storedOTP != req.OTP { - return nil, errors.New("invalid OTP") - } +// if storedOTP != req.OTP { +// return nil, errors.New("invalid OTP") +// } - if err := utils.DeleteData("otp:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) - } +// if err := utils.DeleteData("otp:" + req.Phone); err != nil { +// return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) +// } - existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to check existing user: %w", err) - } +// existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) +// if err != nil { +// return nil, fmt.Errorf("failed to check existing user: %w", err) +// } - var user *model.User - if existingUser != nil { - user = existingUser - } else { +// var user *model.User +// if existingUser != nil { +// user = existingUser +// } else { - user = &model.User{ - Phone: req.Phone, - RoleID: req.RoleID, - } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return nil, err - } - user = createdUser - } +// user = &model.User{ +// Phone: req.Phone, +// RoleID: req.RoleID, +// } +// createdUser, err := s.userRepo.CreateUser(user) +// if err != nil { +// return nil, err +// } +// user = createdUser +// } - token, err := s.generateJWTToken(user.ID) - if err != nil { - return nil, err - } +// token, err := s.generateJWTToken(user.ID) +// if err != nil { +// return nil, err +// } - role, err := s.roleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to get role: %w", err) - } +// role, err := s.roleRepo.FindByID(user.RoleID) +// if err != nil { +// return nil, fmt.Errorf("failed to get role: %w", err) +// } - if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil { - return nil, err - } +// if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil { +// return nil, err +// } - return &dto.UserDataResponse{ - UserID: user.ID, - UserRole: role.RoleName, - Token: token, - }, nil -} +// return &dto.UserDataResponse{ +// UserID: user.ID, +// UserRole: role.RoleName, +// Token: token, +// }, nil +// } -func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error { - if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil { - return fmt.Errorf("failed to store user data in Redis: %w", err) - } +// func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error { +// if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil { +// return fmt.Errorf("failed to store user data in Redis: %w", err) +// } - if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil { - return fmt.Errorf("failed to store user phone in Redis: %w", err) - } +// if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil { +// return fmt.Errorf("failed to store user phone in Redis: %w", err) +// } - return nil -} +// return nil +// } -func (s *authService) generateJWTToken(userID string) (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) - claims := &jwt.RegisteredClaims{ - Subject: userID, - ExpiresAt: jwt.NewNumericDate(expirationTime), - } +// func (s *authService) generateJWTToken(userID string) (string, error) { +// expirationTime := time.Now().Add(24 * time.Hour) +// claims := &jwt.RegisteredClaims{ +// Subject: userID, +// ExpiresAt: jwt.NewNumericDate(expirationTime), +// } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - secretKey := config.GetSecretKey() +// token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) +// secretKey := config.GetSecretKey() - return token.SignedString([]byte(secretKey)) -} +// return token.SignedString([]byte(secretKey)) +// } -func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error { - sessionKey := fmt.Sprintf("session:%s", userID) - sessionData := map[string]interface{}{ - "userID": userID, - "roleID": roleID, - "roleName": roleName, - } +// func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error { +// sessionKey := fmt.Sprintf("session:%s", userID) +// sessionData := map[string]interface{}{ +// "userID": userID, +// "roleID": roleID, +// "roleName": roleName, +// } - if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session data: %w", err) - } +// if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { +// return fmt.Errorf("failed to set session data: %w", err) +// } - if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session token: %w", err) - } +// if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil { +// return fmt.Errorf("failed to set session token: %w", err) +// } - return nil -} +// return nil +// } -func (s *authService) Logout(userID, phone string) error { - keys := []string{ - "session:" + userID, - "session_token:" + userID, - "user_logged_in:" + userID, - "user:" + userID, - "user_phone:" + userID, - "otp_sent:" + phone, - } +// func (s *authService) Logout(userID, phone string) error { +// keys := []string{ +// "session:" + userID, +// "session_token:" + userID, +// "user_logged_in:" + userID, +// "user:" + userID, +// "user_phone:" + userID, +// "otp_sent:" + phone, +// } - for _, key := range keys { - if err := utils.DeleteData(key); err != nil { - return fmt.Errorf("failed to delete key %s from Redis: %w", key, err) - } - } +// for _, key := range keys { +// if err := utils.DeleteData(key); err != nil { +// return fmt.Errorf("failed to delete key %s from Redis: %w", key, err) +// } +// } - return nil -} +// return nil +// } -func generateOTP() string { - randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) - return fmt.Sprintf("%04d", randGenerator.Intn(10000)) -} +// func generateOTP() string { +// randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) +// return fmt.Sprintf("%04d", randGenerator.Intn(10000)) +// } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index f1ab25a..72a5994 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -45,12 +45,12 @@ func (s *userProfileService) prepareUserResponse(user *model.User) *dto.UserResp return &dto.UserResponseDTO{ ID: user.ID, - Username: user.Username, + // Username: user.Username, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, Email: user.Email, - EmailVerified: user.EmailVerified, + // EmailVerified: user.EmailVerified, RoleName: user.Role.RoleName, CreatedAt: createdAt, UpdatedAt: updatedAt, @@ -100,12 +100,12 @@ func (s *userProfileService) GetAllUsers() ([]dto.UserResponseDTO, error) { for _, user := range users { response = append(response, dto.UserResponseDTO{ ID: user.ID, - Username: user.Username, + // Username: user.Username, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, + // Email: user.Email, + // EmailVerified: user.EmailVerified, RoleName: user.Role.RoleName, CreatedAt: user.CreatedAt.Format(time.RFC3339), UpdatedAt: user.UpdatedAt.Format(time.RFC3339), @@ -125,12 +125,12 @@ func (s *userProfileService) GetUsersByRoleID(roleID string) ([]dto.UserResponse for _, user := range users { response = append(response, dto.UserResponseDTO{ ID: user.ID, - Username: user.Username, + // Username: user.Username, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, + // Email: user.Email, + // EmailVerified: user.EmailVerified, RoleName: user.Role.RoleName, CreatedAt: user.CreatedAt.Format(time.RFC3339), UpdatedAt: user.UpdatedAt.Format(time.RFC3339), diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index c1b868d..400798b 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,8 +1,9 @@ package middleware import ( + "fmt" + "log" "os" - "rijig/utils" "github.com/gofiber/fiber/v2" @@ -22,35 +23,39 @@ func AuthMiddleware(c *fiber.Ctx) error { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("SECRET_KEY")), nil }) + if err != nil || !token.Valid { + log.Printf("Error parsing token: %v", err) return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token") } claims, ok := token.Claims.(jwt.MapClaims) - if !ok || claims["sub"] == nil { + if !ok || claims["sub"] == nil || claims["device_id"] == nil { + log.Println("Invalid token claims") return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") } userID := claims["sub"].(string) - if userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid user session") - } + deviceID := claims["device_id"].(string) - sessionKey := "session:" + userID + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) sessionData, err := utils.GetJSONData(sessionKey) if err != nil || sessionData == nil { + log.Printf("Session expired or invalid for userID: %s, deviceID: %s", userID, deviceID) return utils.GenericResponse(c, fiber.StatusUnauthorized, "Session expired or invalid") } roleID, roleOK := sessionData["roleID"].(string) roleName, roleNameOK := sessionData["roleName"].(string) if !roleOK || !roleNameOK { + log.Println("Invalid session data for userID:", userID) return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid session data") } c.Locals("userID", userID) c.Locals("roleID", roleID) c.Locals("roleName", roleName) + c.Locals("device_id", deviceID) return c.Next() } diff --git a/model/company_profile_model.go b/model/company_profile_model.go new file mode 100644 index 0000000..521185a --- /dev/null +++ b/model/company_profile_model.go @@ -0,0 +1,24 @@ +package model + +import ( + "time" +) + +type CompanyProfile struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + CompanyName string `gorm:"not null" json:"company_name"` + CompanyAddress string `gorm:"not null" json:"company_address"` + CompanyPhone string `gorm:"not null" json:"company_phone"` + CompanyEmail string `gorm:"not null" json:"company_email"` + CompanyLogo string `gorm:"not null" json:"company_logo"` + CompanyWebsite string `json:"company_website"` + TaxID string `json:"tax_id"` + FoundedDate time.Time `json:"founded_date"` + CompanyType string `gorm:"not null" json:"company_type"` + CompanyDescription string `gorm:"type:text" json:"company_description"` + CompanyStatus string `gorm:"not null" json:"company_status"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` +} diff --git a/model/identitycard_model.go b/model/identitycard_model.go new file mode 100644 index 0000000..d7f6f31 --- /dev/null +++ b/model/identitycard_model.go @@ -0,0 +1,25 @@ +package model + +import "time" + +type IdentityCard struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + Identificationumber string `gorm:"not null" json:"identificationumber"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Gender string `gorm:"not null" json:"gender"` + BloodType string `gorm:"not null" json:"bloodtype"` + District string `gorm:"not null" json:"district"` + Village string `gorm:"not null" json:"village"` + Neighbourhood string `gorm:"not null" json:"neighbourhood"` + Religion string `gorm:"not null" json:"religion"` + Maritalstatus string `gorm:"not null" json:"maritalstatus"` + Job string `gorm:"not null" json:"job"` + Citizenship string `gorm:"not null" json:"citizenship"` + Validuntil string `gorm:"not null" json:"validuntil"` + Cardphoto string `gorm:"not null" json:"cardphoto"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/model/role_model.go b/model/role_model.go index 5f595e0..72c910e 100644 --- a/model/role_model.go +++ b/model/role_model.go @@ -5,7 +5,7 @@ import "time" type Role struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` RoleName string `gorm:"unique;not null" json:"roleName"` - Users []User `gorm:"foreignKey:RoleID" json:"users"` + // Users []User `gorm:"foreignKey:RoleID" json:"users"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/user_model.go b/model/user_model.go index 8f26826..c6fecf7 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -3,16 +3,19 @@ package model import "time" type User struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - Avatar *string `json:"avatar,omitempty"` - Username string `gorm:"not null" json:"username"` - Name string `gorm:"not null" json:"name"` - Phone string `gorm:"not null" json:"phone"` - Email string `gorm:"not null" json:"email"` - EmailVerified bool `gorm:"default:false" json:"emailVerified"` - Password string `gorm:"not null" json:"password"` - RoleID string `gorm:"not null" json:"roleId"` - Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Name string `gorm:"not null" json:"name"` + Gender string `gorm:"not null" json:"gender"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Phone string `gorm:"not null" json:"phone"` + Email string `json:"email,omitempty"` + PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` + Password string `json:"password,omitempty"` + RoleID string `gorm:"not null" json:"roleId"` + Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` + RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/presentation/auth/auth_admin_route.go b/presentation/auth/auth_admin_route.go new file mode 100644 index 0000000..07f3324 --- /dev/null +++ b/presentation/auth/auth_admin_route.go @@ -0,0 +1,35 @@ +package presentation + +import ( + "log" + "os" + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AdminAuthRouter(api fiber.Router) { + secretKey := os.Getenv("SECRET_KEY") + if secretKey == "" { + log.Fatal("SECRET_KEY is not set in the environment variables") + os.Exit(1) + } + + adminAuthRepo := repository.NewAuthAdminRepository(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + + adminAuthService := services.NewAuthAdminService(adminAuthRepo, roleRepo, secretKey) + + adminAuthHandler := handler.NewAuthAdminHandler(adminAuthService) + + adminAuthAPI := api.Group("/admin-auth") + + adminAuthAPI.Post("/register", adminAuthHandler.RegisterAdmin) + adminAuthAPI.Post("/login", adminAuthHandler.LoginAdmin) + adminAuthAPI.Post("/logout", middleware.AuthMiddleware, adminAuthHandler.LogoutAdmin) +} diff --git a/presentation/auth/auth_masyarakat_route.go b/presentation/auth/auth_masyarakat_route.go new file mode 100644 index 0000000..dbf4e7a --- /dev/null +++ b/presentation/auth/auth_masyarakat_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthMasyarakatRouter(api fiber.Router) { + authMasyarakatRepo := repository.NewAuthMasyarakatRepositories(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + authMasyarakatService := services.NewAuthMasyarakatService(authMasyarakatRepo, roleRepo) + + authHandler := handler.NewAuthMasyarakatHandler(authMasyarakatService) + + authMasyarakat := api.Group("/authmasyarakat") + + authMasyarakat.Post("/auth", authHandler.RegisterOrLoginHandler) + authMasyarakat.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + authMasyarakat.Post("/verify-otp", authHandler.VerifyOTPHandler) +} diff --git a/presentation/auth/auth_pengelola_route.go b/presentation/auth/auth_pengelola_route.go new file mode 100644 index 0000000..358b244 --- /dev/null +++ b/presentation/auth/auth_pengelola_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthPengelolaRouter(api fiber.Router) { + authPengelolaRepo := repository.NewAuthPengelolaRepositories(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + authPengelolaService := services.NewAuthPengelolaService(authPengelolaRepo, roleRepo) + + authHandler := handler.NewAuthPengelolaHandler(authPengelolaService) + + authPengelola := api.Group("/authpengelola") + + authPengelola.Post("/auth", authHandler.RegisterOrLoginHandler) + authPengelola.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + authPengelola.Post("/verify-otp", authHandler.VerifyOTPHandler) +} diff --git a/presentation/auth/auth_pengepul_route.go b/presentation/auth/auth_pengepul_route.go new file mode 100644 index 0000000..1f60f2d --- /dev/null +++ b/presentation/auth/auth_pengepul_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthPengepulRouter(api fiber.Router) { + authPengepulRepo := repository.NewAuthPengepulRepositories(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + authPengepulService := services.NewAuthPengepulService(authPengepulRepo, roleRepo) + + authHandler := handler.NewAuthPengepulHandler(authPengepulService) + + authPengepul := api.Group("/authpengepul") + + authPengepul.Post("/auth", authHandler.RegisterOrLoginHandler) + authPengepul.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + authPengepul.Post("/verify-otp", authHandler.VerifyOTPHandler) +} diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 1ae2d9a..ee733c1 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -1,23 +1,23 @@ package presentation -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" +// import ( +// "rijig/config" +// "rijig/internal/handler" +// "rijig/internal/repositories" +// "rijig/internal/services" +// "rijig/middleware" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -func AuthRouter(api fiber.Router) { - userRepo := repositories.NewUserRepository(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - authService := services.NewAuthService(userRepo, roleRepo) +// func AuthRouter(api fiber.Router) { +// userRepo := repositories.NewUserRepository(config.DB) +// roleRepo := repositories.NewRoleRepository(config.DB) +// authService := services.NewAuthService(userRepo, roleRepo) - authHandler := handler.NewAuthHandler(authService) +// authHandler := handler.NewAuthHandler(authService) - api.Post("/auth", authHandler.RegisterOrLoginHandler) - api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) - api.Post("/verify-otp", authHandler.VerifyOTPHandler) -} +// api.Post("/auth", authHandler.RegisterOrLoginHandler) +// api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) +// api.Post("/verify-otp", authHandler.VerifyOTPHandler) +// } diff --git a/presentation/userpin_route.go b/presentation/userpin_route.go index 15dea4d..c145907 100644 --- a/presentation/userpin_route.go +++ b/presentation/userpin_route.go @@ -11,15 +11,14 @@ import ( ) func UserPinRouter(api fiber.Router) { - userPinRepo := repositories.NewUserPinRepository(config.DB) userPinService := services.NewUserPinService(userPinRepo) userPinHandler := handler.NewUserPinHandler(userPinService) - api.Post("/user/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) - api.Post("/user/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) - api.Get("/user/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) - api.Patch("/user/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) + api.Post("/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) + api.Post("/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) + api.Get("/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) + api.Patch("/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index ef85491..feae5f6 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -5,6 +5,7 @@ import ( "rijig/middleware" "rijig/presentation" + presentationn "rijig/presentation/auth" "github.com/gofiber/fiber/v2" ) @@ -15,7 +16,13 @@ func SetupRoutes(app *fiber.App) { api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) - presentation.AuthRouter(api) + // || auth router || // + // presentation.AuthRouter(api) + presentationn.AdminAuthRouter(api) + presentationn.AuthPengelolaRouter(api) + presentationn.AuthPengepulRouter(api) + presentationn.AuthMasyarakatRouter(api) + // || auth router || // presentation.UserProfileRouter(api) presentation.UserPinRouter(api) presentation.RoleRouter(api) diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 7b7bc18..99083c7 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -17,7 +17,6 @@ var ctx = context.Background() const defaultExpiration = 1 * time.Hour func SetData[T any](key string, value T, expiration time.Duration) error { - if expiration == 0 { expiration = defaultExpiration } @@ -36,6 +35,27 @@ func SetData[T any](key string, value T, expiration time.Duration) error { return nil } +func SaveSessionTokenToRedis(userID string, deviceID string, token string) error { + + sessionKey := "session:" + userID + ":" + deviceID + + err := config.RedisClient.Set(ctx, sessionKey, token, 24*time.Hour).Err() + if err != nil { + return err + } + log.Printf("Session token saved to Redis with key: %s", sessionKey) + return nil +} + +func GetSessionTokenFromRedis(userID string, deviceID string) (string, error) { + sessionKey := "session:" + userID + ":" + deviceID + token, err := config.RedisClient.Get(ctx, sessionKey).Result() + if err != nil { + return "", err + } + return token, nil +} + func GetData(key string) (string, error) { val, err := config.RedisClient.Get(ctx, key).Result() if err == redis.Nil { @@ -89,9 +109,23 @@ func GetJSONData(key string) (map[string]interface{}, error) { return data, nil } -func DeleteSessionData(userID string) error { - sessionKey := "session:" + userID - return DeleteData(sessionKey) +func DeleteSessionData(userID string, deviceID string) error { + sessionKey := "session:" + userID + ":" + deviceID + sessionTokenKey := "session_token:" + userID + ":" + deviceID + + log.Printf("Attempting to delete session data with keys: %s, %s", sessionKey, sessionTokenKey) + + err := DeleteData(sessionKey) + if err != nil { + return fmt.Errorf("failed to delete session data: %w", err) + } + err = DeleteData(sessionTokenKey) + if err != nil { + return fmt.Errorf("failed to delete session token: %w", err) + } + + log.Printf("Successfully deleted session data for userID: %s, deviceID: %s", userID, deviceID) + return nil } func logAndReturnError(message string, err error) error { @@ -124,43 +158,11 @@ func GetStringData(key string) (string, error) { return val, nil } -// func SetStringData(key, value string, expiration time.Duration) error { -// if expiration == 0 { -// expiration = defaultExpiration -// } - -// err := config.RedisClient.Set(ctx, key, value, expiration).Err() -// if err != nil { -// return fmt.Errorf("Error setting string data in Redis with key: %s: %v", key, err) -// } - -// log.Printf("String data stored in Redis with key: %s", key) -// return nil -// } - -// GetStringData retrieves a string value from Redis by key -// func GetStringData(key string) (string, error) { -// val, err := config.RedisClient.Get(ctx, key).Result() -// if err == redis.Nil { -// return "", nil -// } else if err != nil { -// return "", fmt.Errorf("Error retrieving string data from Redis with key: %s: %v", key, err) -// } - -// return val, nil -// } -// func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { -// err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() -// if err != nil { -// return fmt.Errorf("failed to store OTP in Redis: %v", err) -// } -// return nil -// } - -// func GetOTPFromRedis(phone string) (string, error) { -// otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() -// if err != nil { -// return "", fmt.Errorf("failed to get OTP from Redis: %v", err) -// } -// return otpCode, nil -// } +func CheckSessionExists(userID string, deviceID string) (bool, error) { + sessionKey := "session:" + userID + ":" + deviceID + val, err := config.RedisClient.Exists(ctx, sessionKey).Result() + if err != nil { + return false, err + } + return val > 0, nil +} diff --git a/utils/regexp_formatter.go b/utils/regexp_formatter.go new file mode 100644 index 0000000..616675f --- /dev/null +++ b/utils/regexp_formatter.go @@ -0,0 +1,44 @@ +package utils + +import ( + "regexp" + "strings" +) + +func IsValidPhoneNumber(phone string) bool { + re := regexp.MustCompile(`^62\d{9,13}$`) + return re.MatchString(phone) +} + +func IsValidEmail(email string) bool { + re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) + return re.MatchString(email) +} + +func IsValidPassword(password string) bool { + + if len(password) < 6 { + return false + } + + hasUpper := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + if char >= 'A' && char <= 'Z' { + hasUpper = true + } else if char >= '0' && char <= '9' { + hasDigit = true + } else if isSpecialCharacter(char) { + hasSpecial = true + } + } + + return hasUpper && hasDigit && hasSpecial +} + +func isSpecialCharacter(char rune) bool { + specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" + return strings.ContainsRune(specialChars, char) +} diff --git a/utils/role_permission.go b/utils/role_permission.go index e892bdd..6de0e72 100644 --- a/utils/role_permission.go +++ b/utils/role_permission.go @@ -1,8 +1,8 @@ package utils const ( - RoleAdministrator = "46f75bb9-7f64-44b7-b378-091a67b3e229" - RoleMasyarakat = "6cfa867b-536c-448d-ba11-fe060b5af971" - RolePengepul = "8171883c-ea9e-4d17-9f28-a7896d88380f" - RolePengelola = "84d72ddb-68a8-430c-9b79-5d71f90cb1be" + RoleAdministrator = "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7" + RolePengelola = "0bf86966-7042-410a-a88c-d01f70832348" + RolePengepul = "d7245535-0e9e-4d35-ab39-baece5c10b3c" + RoleMasyarakat = "60e5684e4-b214-4bd0-972f-3be80c4649a0" )