Initialize base
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tm/internal/domain"
|
||||
"tm/internal/infrastructure"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// UserAuthServiceImpl implements the UserAuthService interface
|
||||
type UserAuthServiceImpl struct {
|
||||
userRepo domain.UserRepository
|
||||
config *infrastructure.AuthConfig
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserAuthService creates a new panel user authentication service
|
||||
func NewUserAuthService(
|
||||
userRepo domain.UserRepository,
|
||||
config *infrastructure.AuthConfig,
|
||||
logger logger.Logger,
|
||||
) domain.UserAuthService {
|
||||
return &UserAuthServiceImpl{
|
||||
userRepo: userRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a panel user and returns tokens
|
||||
func (s *UserAuthServiceImpl) Login(ctx context.Context, req domain.LoginRequest) (*domain.UserAuthResponse, error) {
|
||||
s.logger.Info("Panel user login attempt", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
|
||||
// Get user by email
|
||||
user, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
s.logger.Warn("Panel user login failed - user not found", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if !user.IsActive {
|
||||
s.logger.Warn("Panel user login failed - account inactive", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !s.verifyPassword(req.Password, user.Password) {
|
||||
s.logger.Warn("Panel user login failed - invalid password", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
user.LastLoginAt = &time.Time{}
|
||||
*user.LastLoginAt = time.Now()
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
s.logger.Warn("Failed to update user last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
// Don't fail login for this
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate access token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateRefreshToken(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user logged in successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"department": user.Department,
|
||||
})
|
||||
|
||||
return &domain.UserAuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken generates new tokens using refresh token
|
||||
func (s *UserAuthServiceImpl) RefreshToken(ctx context.Context, refreshToken string) (*domain.UserAuthResponse, error) {
|
||||
// Parse and validate refresh token
|
||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(s.config.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify token type
|
||||
tokenType, ok := claims["type"].(string)
|
||||
if !ok || tokenType != "refresh" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// Verify user type
|
||||
userType, ok := claims["user_type"].(string)
|
||||
if !ok || userType != "user" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
userIDStr, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid user ID format")
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
accessToken, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate access token")
|
||||
}
|
||||
|
||||
newRefreshToken, err := s.generateRefreshToken(user)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to generate refresh token")
|
||||
}
|
||||
|
||||
return &domain.UserAuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the user
|
||||
func (s *UserAuthServiceImpl) ValidateToken(ctx context.Context, tokenString string) (*domain.User, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return []byte(s.config.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify token type
|
||||
tokenType, ok := claims["type"].(string)
|
||||
if !ok || tokenType != "access" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// Verify user type
|
||||
userType, ok := claims["user_type"].(string)
|
||||
if !ok || userType != "user" {
|
||||
return nil, errors.New("invalid user type")
|
||||
}
|
||||
|
||||
userIDStr, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid user ID format")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("account is inactive")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword changes panel user password
|
||||
func (s *UserAuthServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
if !s.verifyPassword(oldPassword, user.Password) {
|
||||
return errors.New("invalid old password")
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := s.hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("failed to process new password")
|
||||
}
|
||||
|
||||
// Update password
|
||||
user.Password = hashedPassword
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
})
|
||||
return errors.New("failed to update password")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user password changed successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePanelUser creates a new panel user (admin only)
|
||||
func (s *UserAuthServiceImpl) CreatePanelUser(ctx context.Context, req domain.CreateUserRequest, createdBy uuid.UUID) (*domain.User, error) {
|
||||
s.logger.Info("Creating new panel user", map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"role": req.Role,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
|
||||
// Check if user already exists
|
||||
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, errors.New("user already exists")
|
||||
}
|
||||
|
||||
// Validate role
|
||||
if !s.isValidRole(req.Role) {
|
||||
return nil, errors.New("invalid role")
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := s.hashPassword(req.Password)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to process password")
|
||||
}
|
||||
|
||||
// Create user
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Role: req.Role,
|
||||
Department: req.Department,
|
||||
IsActive: true,
|
||||
Permissions: s.getDefaultPermissions(req.Role),
|
||||
CreatedBy: &createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Add custom permissions if provided
|
||||
if len(req.Permissions) > 0 {
|
||||
user.Permissions = append(user.Permissions, req.Permissions...)
|
||||
user.Permissions = s.removeDuplicatePermissions(user.Permissions)
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to create panel user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": req.Email,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
return nil, errors.New("failed to create user")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"created_by": createdBy.String(),
|
||||
})
|
||||
|
||||
// Remove password from response
|
||||
user.Password = ""
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUserPermissions updates panel user permissions (admin only)
|
||||
func (s *UserAuthServiceImpl) UpdateUserPermissions(ctx context.Context, userID uuid.UUID, permissions []string, updatedBy uuid.UUID) error {
|
||||
// Check if user exists
|
||||
_, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
for _, permission := range permissions {
|
||||
if !s.isValidPermission(permission) {
|
||||
return errors.New("invalid permission: " + permission)
|
||||
}
|
||||
}
|
||||
|
||||
// Update permissions
|
||||
if err := s.userRepo.UpdatePermissions(ctx, userID, permissions); err != nil {
|
||||
s.logger.Error("Failed to update user permissions", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
return errors.New("failed to update permissions")
|
||||
}
|
||||
|
||||
s.logger.Info("Panel user permissions updated successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"permissions": permissions,
|
||||
"updated_by": updatedBy.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
func (s *UserAuthServiceImpl) hashPassword(password string) (string, error) {
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) verifyPassword(password, hashedPassword string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) generateAccessToken(user *domain.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"department": user.Department,
|
||||
"permissions": user.Permissions,
|
||||
"user_type": "user",
|
||||
"exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "access",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) generateRefreshToken(user *domain.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID.String(),
|
||||
"user_type": "user",
|
||||
"exp": time.Now().Add(s.config.JWT.RefreshTokenDuration).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "refresh",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) isValidRole(role domain.UserRole) bool {
|
||||
validRoles := []domain.UserRole{
|
||||
domain.UserRoleAdmin,
|
||||
domain.UserRoleModerator,
|
||||
domain.UserRoleSupport,
|
||||
domain.UserRoleAnalyst,
|
||||
}
|
||||
|
||||
for _, validRole := range validRoles {
|
||||
if role == validRole {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) getDefaultPermissions(role domain.UserRole) []string {
|
||||
switch role {
|
||||
case domain.UserRoleAdmin:
|
||||
return []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
}
|
||||
case domain.UserRoleModerator:
|
||||
return []string{
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"view_customers",
|
||||
"view_reports",
|
||||
"manage_sources",
|
||||
}
|
||||
case domain.UserRoleSupport:
|
||||
return []string{
|
||||
"view_customers",
|
||||
"view_tenders",
|
||||
"view_companies",
|
||||
"create_tickets",
|
||||
}
|
||||
case domain.UserRoleAnalyst:
|
||||
return []string{
|
||||
"view_reports",
|
||||
"view_tenders",
|
||||
"view_customers",
|
||||
"view_companies",
|
||||
"export_data",
|
||||
}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) isValidPermission(permission string) bool {
|
||||
validPermissions := []string{
|
||||
"manage_users",
|
||||
"manage_tenders",
|
||||
"manage_companies",
|
||||
"manage_customers",
|
||||
"view_customers",
|
||||
"view_tenders",
|
||||
"view_companies",
|
||||
"view_reports",
|
||||
"manage_system",
|
||||
"manage_sources",
|
||||
"create_tickets",
|
||||
"export_data",
|
||||
}
|
||||
|
||||
for _, validPermission := range validPermissions {
|
||||
if permission == validPermission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UserAuthServiceImpl) removeDuplicatePermissions(permissions []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
result := []string{}
|
||||
|
||||
for _, permission := range permissions {
|
||||
if !seen[permission] {
|
||||
seen[permission] = true
|
||||
result = append(result, permission)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user