package services import ( "errors" "lost-and-found/internal/config" "lost-and-found/internal/models" "lost-and-found/internal/repositories" "lost-and-found/internal/utils" "go.uber.org/zap" "gorm.io/gorm" ) // --- 1. Definisi Interface (Contract) --- // Interface ini mendefinisikan method apa saja yang dibutuhkan oleh Service dari Repository. // Dengan ini, kita bisa menukar Repository asli dengan Mock Repository saat testing. type IUserRepository interface { FindByEmail(email string) (*models.User, error) FindByNRP(nrp string) (*models.User, error) FindByID(id uint) (*models.User, error) Create(user *models.User) error } type IRoleRepository interface { FindByName(name string) (*models.Role, error) } type IAuditLogRepository interface { Log(userID *uint, action, entityType string, entityID *uint, details, ipAddress, userAgent string) error } // --- 2. Struct AuthService dengan Dependency Injection --- type AuthService struct { userRepo IUserRepository // Menggunakan Interface, bukan struct konkret *repositories.UserRepository roleRepo IRoleRepository // Menggunakan Interface auditLogRepo IAuditLogRepository // Menggunakan Interface logger *zap.Logger } // NewAuthService menginisialisasi service dengan repository asli (untuk Production) func NewAuthService(db *gorm.DB, logger *zap.Logger) *AuthService { // ✅ Inisialisasi Enkripsi (Bonus Keamanan) if err := utils.InitEncryption(); err != nil { logger.Fatal("Failed to initialize encryption", zap.Error(err)) } return &AuthService{ // Struct repository asli secara otomatis memenuhi interface (duck typing) userRepo: repositories.NewUserRepository(db), roleRepo: repositories.NewRoleRepository(db), auditLogRepo: repositories.NewAuditLogRepository(db), logger: logger, } } // --- 3. Struct Request & Response --- type RegisterRequest struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` NRP string `json:"nrp"` Phone string `json:"phone"` } type LoginRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } type AuthResponse struct { Token string `json:"token"` User models.UserResponse `json:"user"` } // --- 4. Implementasi Method Service --- // Register menangani pendaftaran user baru func (s *AuthService) Register(req RegisterRequest, ipAddress, userAgent string) (*AuthResponse, error) { s.logger.Info("Registration attempt", zap.String("email", req.Email), zap.String("name", req.Name), zap.String("ip_address", ipAddress), ) // Cek apakah email sudah terdaftar existingUser, _ := s.userRepo.FindByEmail(req.Email) if existingUser != nil { s.logger.Warn("Registration failed: email already exists", zap.String("email", req.Email), zap.String("ip_address", ipAddress), ) return nil, errors.New("email already registered") } // Cek apakah NRP sudah terdaftar (jika ada) if req.NRP != "" { existingNRP, _ := s.userRepo.FindByNRP(req.NRP) if existingNRP != nil { s.logger.Warn("Registration failed: NRP already exists", zap.String("email", req.Email), ) return nil, errors.New("NRP already registered") } } // Hash password hashedPassword, err := utils.HashPassword(req.Password) if err != nil { s.logger.Error("Failed to hash password", zap.String("email", req.Email), zap.Error(err), ) return nil, errors.New("failed to hash password") } // Ambil role default "user" userRole, err := s.roleRepo.FindByName(models.RoleUser) if err != nil { s.logger.Error("Failed to get user role", zap.Error(err)) return nil, errors.New("failed to get user role") } // Buat objek User user := &models.User{ Name: req.Name, Email: req.Email, Password: hashedPassword, NRP: req.NRP, // Disimpan plain text (atau terenkripsi jika model mendukung hook) Phone: req.Phone, // Disimpan plain text RoleID: userRole.ID, Status: "active", } // Simpan ke database if err := s.userRepo.Create(user); err != nil { s.logger.Error("Failed to create user", zap.String("email", req.Email), zap.Error(err), ) return nil, errors.New("failed to create user") } // Muat ulang user untuk mendapatkan relasi Role yang lengkap user, err = s.userRepo.FindByID(user.ID) if err != nil { s.logger.Error("Failed to load user", zap.Uint("user_id", user.ID), zap.Error(err), ) return nil, err } // Generate JWT Token token, err := config.GenerateToken(user.ID, user.Email, user.Role.Name) if err != nil { s.logger.Error("Failed to generate token", zap.Uint("user_id", user.ID), zap.Error(err), ) return nil, errors.New("failed to generate token") } // Log Audit s.auditLogRepo.Log(&user.ID, models.ActionCreate, models.EntityUser, &user.ID, "User registered", ipAddress, userAgent) s.logger.Info("Registration successful", zap.Uint("user_id", user.ID), zap.String("email", user.Email), zap.String("ip_address", ipAddress), ) return &AuthResponse{ Token: token, User: user.ToResponse(), }, nil } // Login menangani autentikasi user func (s *AuthService) Login(req LoginRequest, ipAddress, userAgent string) (*AuthResponse, error) { s.logger.Info("Login attempt", zap.String("email", req.Email), zap.String("ip_address", ipAddress), ) // Cari user berdasarkan email user, err := s.userRepo.FindByEmail(req.Email) if err != nil { s.logger.Warn("Login failed: user not found", zap.String("email", req.Email), zap.String("ip_address", ipAddress), ) return nil, errors.New("invalid email or password") } // Cek apakah akun diblokir if user.IsBlocked() { s.logger.Warn("Login failed: account blocked", zap.String("email", user.Email), zap.Uint("user_id", user.ID), zap.String("ip_address", ipAddress), ) return nil, errors.New("account is blocked") } // Verifikasi password passwordMatch := utils.CheckPasswordHash(req.Password, user.Password) if !passwordMatch { s.logger.Warn("Login failed: incorrect password", zap.String("email", user.Email), zap.Uint("user_id", user.ID), zap.String("ip_address", ipAddress), ) return nil, errors.New("invalid email or password") } // Pastikan Role ter-load (Reload jika perlu) if user.Role.ID == 0 { user, err = s.userRepo.FindByID(user.ID) if err != nil { return nil, errors.New("failed to load user data") } } // Generate JWT Token token, err := config.GenerateToken(user.ID, user.Email, user.Role.Name) if err != nil { s.logger.Error("Failed to generate token", zap.Uint("user_id", user.ID), zap.Error(err), ) return nil, errors.New("failed to generate token") } // Log Audit s.auditLogRepo.Log(&user.ID, models.ActionLogin, models.EntityUser, &user.ID, "User logged in", ipAddress, userAgent) s.logger.Info("Login successful", zap.Uint("user_id", user.ID), zap.String("email", user.Email), zap.String("ip_address", ipAddress), ) return &AuthResponse{ Token: token, User: user.ToResponse(), }, nil } // ValidateToken memvalidasi token JWT dan mengembalikan user terkait func (s *AuthService) ValidateToken(tokenString string) (*models.User, error) { claims, err := config.ValidateToken(tokenString) if err != nil { s.logger.Warn("Token validation failed", zap.Error(err)) return nil, errors.New("invalid token") } user, err := s.userRepo.FindByID(claims.UserID) if err != nil { return nil, errors.New("user not found") } if user.IsBlocked() { return nil, errors.New("account is blocked") } return user, nil } // RefreshToken memperbarui token JWT yang sudah ada func (s *AuthService) RefreshToken(oldToken string) (string, error) { s.logger.Info("Token refresh attempt") newToken, err := config.RefreshToken(oldToken) if err != nil { s.logger.Error("Token refresh failed", zap.Error(err)) return "", err } s.logger.Info("Token refreshed successfully") return newToken, nil }