214 lines
5.3 KiB
Go
214 lines
5.3 KiB
Go
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
|
|
}
|