package service import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "time" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" "golang.org/x/crypto/bcrypt" ) var ( ErrInvalidCredentials = errors.New("invalid username or password") ErrUserNotFound = errors.New("user not found") ) type AuthService struct { userRepo *repository.UserRepository } func NewAuthService() *AuthService { return &AuthService{ userRepo: repository.NewUserRepository(), } } // SimpleToken represents a simple JWT-like token type SimpleToken struct { UserID int64 `json:"user_id"` Username string `json:"username"` Roles []string `json:"roles"` Exp int64 `json:"exp"` } // GenerateToken creates a simple token for the user func (s *AuthService) GenerateToken(user *model.User) (string, error) { exp := time.Now().Add(24 * time.Hour).Unix() roleCodes := make([]string, 0, len(user.Roles)) for _, role := range user.Roles { if role.Enabled { roleCodes = append(roleCodes, role.Code) } } token := SimpleToken{ UserID: user.ID, Username: user.Username, Roles: roleCodes, Exp: exp, } // Create header header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) // Create payload payloadBytes, err := json.Marshal(token) if err != nil { return "", err } payload := base64.RawURLEncoding.EncodeToString(payloadBytes) // Create signature using HMAC-SHA256 (cryptographically secure) signatureInput := header + "." + payload mac := hmac.New(sha256.New, []byte("warehouse-mms-secret-key")) mac.Write([]byte(signatureInput)) signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) return header + "." + payload + "." + signature, nil } // ValidateToken validates a token and returns the user info func (s *AuthService) ValidateToken(tokenString string) (*SimpleToken, error) { parts := splitToken(tokenString) if len(parts) != 3 { return nil, errors.New("invalid token format") } header, payload, signature := parts[0], parts[1], parts[2] // Verify signature using HMAC-SHA256 (cryptographically secure) signatureInput := header + "." + payload mac := hmac.New(sha256.New, []byte("warehouse-mms-secret-key")) mac.Write([]byte(signatureInput)) expectedSig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(signature), []byte(expectedSig)) { return nil, errors.New("invalid token signature") } // Decode payload payloadBytes, err := base64.RawURLEncoding.DecodeString(payload) if err != nil { return nil, err } var token SimpleToken if err := json.Unmarshal(payloadBytes, &token); err != nil { return nil, err } // Check expiration if time.Now().Unix() > token.Exp { return nil, errors.New("token expired") } return &token, nil } func splitToken(token string) []string { var parts []string start := 0 for i := 0; i < len(token); i++ { if token[i] == '.' { parts = append(parts, token[start:i]) start = i + 1 } } parts = append(parts, token[start:]) return parts } // Login authenticates a user and returns a token func (s *AuthService) Login(username, password string) (*model.LoginResponse, error) { user, err := s.userRepo.GetByUsername(username) if err != nil { return nil, ErrInvalidCredentials } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { return nil, ErrInvalidCredentials } // Generate token token, err := s.GenerateToken(user) if err != nil { return nil, err } return &model.LoginResponse{ Token: token, User: *user, }, nil } // GetUserByID returns a user by ID func (s *AuthService) GetUserByID(id int64) (*model.User, error) { return s.userRepo.GetByID(id) } // ListUsers returns all users with pagination func (s *AuthService) ListUsers(search string, limit, offset int) ([]model.User, int64, error) { return s.userRepo.List(search, limit, offset) } // CreateUser creates a new user func (s *AuthService) CreateUser(username, password, description string, roleIDs []int64) (*model.User, error) { // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err } user := &model.User{ Username: username, Password: string(hashedPassword), Description: description, } if err := s.userRepo.Create(user, roleIDs); err != nil { return nil, err } return s.userRepo.GetByID(user.ID) } // UpdateUser updates a user (admin function) func (s *AuthService) UpdateUser(id int64, description string, roleIDs []int64, newPassword string) error { user, err := s.userRepo.GetByID(id) if err != nil { return err } user.Description = description if newPassword != "" { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { return err } user.Password = string(hashedPassword) return s.userRepo.UpdateWithPassword(user, roleIDs) } return s.userRepo.Update(user, roleIDs) } // DeleteUser deletes a user func (s *AuthService) DeleteUser(id int64) error { return s.userRepo.Delete(id) } // HashPassword hashes a password using bcrypt func HashPassword(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil }