286 lines
8.2 KiB
Go
286 lines
8.2 KiB
Go
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
|
|
} |