package service import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "time" "zee/internal/resource/role" "zee/internal/resource/user" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" // Added gorm import ) // AuthService xử lý các tác vụ liên quan đến xác thực type AuthService interface { Register(ctx context.Context, req RegisterRequest) (*user.User, error) Login(ctx context.Context, username, password string) (string, string, error) RefreshToken(refreshToken string) (string, string, error) ValidateToken(tokenString string) (*Claims, error) } type authService struct { userRepo user.Repository roleRepo role.Repository jwtSecret string jwtExpiration time.Duration refreshExpires int } // Claims định nghĩa các thông tin trong JWT token type Claims struct { UserID string `json:"user_id"` Username string `json:"username"` Roles []string `json:"roles"` jwt.RegisteredClaims } // NewAuthService tạo mới một AuthService func NewAuthService( userRepo user.Repository, roleRepo role.Repository, jwtSecret string, jwtExpiration time.Duration, ) AuthService { return &authService{ userRepo: userRepo, roleRepo: roleRepo, jwtSecret: jwtSecret, jwtExpiration: jwtExpiration, refreshExpires: 7 * 24 * 60, // 7 days in minutes } } // Register đăng ký người dùng mới func (s *authService) Register(ctx context.Context, req RegisterRequest) (*user.User, error) { // Kiểm tra username đã tồn tại chưa existingUser, err := s.userRepo.GetByUsername(ctx, req.Username) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // Chỉ coi là lỗi nếu không phải RecordNotFound return nil, fmt.Errorf("error checking username: %w", err) } if existingUser != nil { // Nếu existingUser không nil, nghĩa là user đã tồn tại return nil, errors.New("username already exists") } // Kiểm tra email đã tồn tại chưa existingEmail, err := s.userRepo.GetByEmail(ctx, req.Email) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // Chỉ coi là lỗi nếu không phải RecordNotFound return nil, fmt.Errorf("error checking email: %v", err) } if existingEmail != nil { return nil, errors.New("email already exists") } // Mã hóa mật khẩu hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("error hashing password: %v", err) } // Tạo user mới newUser := &user.User{ Username: req.Username, Email: req.Email, PasswordHash: string(hashedPassword), FullName: req.FullName, IsActive: true, } // Lưu user vào database if err := s.userRepo.Create(ctx, newUser); err != nil { return nil, fmt.Errorf("error creating user: %v", err) } // Thêm role mặc định là 'user' cho người dùng mới userRole, err := s.roleRepo.GetByName(ctx, role.User) if err != nil { return nil, fmt.Errorf("error getting user role: %v", err) } if userRole == nil { return nil, errors.New("default user role not found") } if err := s.userRepo.AddRole(ctx, newUser.ID, userRole.ID); err != nil { return nil, fmt.Errorf("error adding role to user: %v", err) } // Lấy lại thông tin user với đầy đủ roles createdUser, err := s.userRepo.GetByID(ctx, newUser.ID) if err != nil { return nil, fmt.Errorf("error getting created user: %v", err) } return createdUser, nil } // Login xác thực đăng nhập và trả về token func (s *authService) Login(ctx context.Context, username, password string) (string, string, error) { // Lấy thông tin user user, err := s.userRepo.GetByUsername(ctx, username) if err != nil { return "", "", errors.New("invalid credentials") } if user == nil || !user.IsActive { return "", "", errors.New("invalid credentials") } // Kiểm tra mật khẩu if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { return "", "", errors.New("invalid credentials") } // Tạo access token accessToken, err := s.generateToken(user) if err != nil { return "", "", fmt.Errorf("error generating token: %v", err) } // Tạo refresh token tokenBytes := make([]byte, 32) if _, err := rand.Read(tokenBytes); err != nil { return "", "", fmt.Errorf("error generating refresh token: %v", err) } // Lưu refresh token vào database (trong thực tế nên lưu vào Redis hoặc database) // Ở đây chỉ minh họa, nên implement thật kỹ hơn h := sha256.New() h.Write(tokenBytes) tokenID := base64.URLEncoding.EncodeToString(h.Sum(nil)) // TODO: Lưu refresh token vào database với userID và tokenID _ = tokenID // Cập nhật thời gian đăng nhập cuối cùng if err := s.userRepo.UpdateLastLogin(ctx, user.ID); err != nil { // Log lỗi nhưng không ảnh hưởng đến quá trình đăng nhập fmt.Printf("Error updating last login: %v\n", err) } return accessToken, string(tokenBytes), nil } // RefreshToken làm mới access token func (s *authService) RefreshToken(refreshToken string) (string, string, error) { // TODO: Kiểm tra refresh token trong database // Nếu hợp lệ, tạo access token mới và trả về return "", "", errors.New("not implemented") } // ValidateToken xác thực và trả về thông tin từ token func (s *authService) ValidateToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { // Kiểm tra signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.jwtSecret), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token") } // generateToken tạo JWT token cho user func (s *authService) generateToken(user *user.User) (string, error) { // Lấy danh sách roles roles := make([]string, len(user.Roles)) for i, r := range user.Roles { roles[i] = r.Name } // Tạo claims expirationTime := time.Now().Add(s.jwtExpiration) claims := &Claims{ UserID: user.ID, Username: user.Username, Roles: roles, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "ulflow-zee", }, } // Tạo token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Ký token và trả về tokenString, err := token.SignedString([]byte(s.jwtSecret)) if err != nil { return "", err } return tokenString, nil } // RegisterRequest định dạng dữ liệu đăng ký type RegisterRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` FullName string `json:"full_name"` }