All checks were successful
CI Pipeline / Security Scan (push) Successful in 2m52s
CI Pipeline / Lint (push) Successful in 4m57s
CI Pipeline / Test (push) Successful in 4m35s
CI Pipeline / Build (push) Successful in 1m19s
CI Pipeline / Notification (push) Successful in 2s
CI Pipeline / Lint (pull_request) Successful in 2m38s
CI Pipeline / Security Scan (pull_request) Successful in 4m47s
CI Pipeline / Test (pull_request) Successful in 2m38s
CI Pipeline / Build (pull_request) Successful in 1m17s
CI Pipeline / Notification (pull_request) Successful in 1s
237 lines
6.9 KiB
Go
237 lines
6.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm" // Added gorm import
|
|
"starter-kit/internal/domain/role"
|
|
"starter-kit/internal/domain/user"
|
|
)
|
|
|
|
// 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-starter-kit",
|
|
},
|
|
}
|
|
|
|
// 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"`
|
|
}
|