package auth import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "senlinai-agent/backend/internal/domain" ) type Service struct { db *gorm.DB secret string } func NewService(database *gorm.DB, secret string) *Service { return &Service{db: database, secret: secret} } func (s *Service) CreateInvite(adminID uint, email string) (string, error) { normalized := strings.ToLower(strings.TrimSpace(email)) if normalized == "" { return "", errors.New("email is required") } return normalized + "." + s.sign(normalized), nil } func (s *Service) RegisterWithInvite(token, email, displayName, password string) (*domain.User, error) { separator := strings.LastIndex(token, ".") if separator < 1 || separator == len(token)-1 { return nil, errors.New("invalid invite token") } inviteEmail := token[:separator] signature := token[separator+1:] if !hmac.Equal([]byte(signature), []byte(s.sign(inviteEmail))) { return nil, errors.New("invalid invite signature") } if inviteEmail != strings.ToLower(strings.TrimSpace(email)) { return nil, errors.New("invite email mismatch") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err } user := &domain.User{ Email: inviteEmail, DisplayName: strings.TrimSpace(displayName), PasswordHash: string(hash), Role: "user", } return user, s.db.Create(user).Error } func (s *Service) Login(email, password string) (string, error) { var user domain.User if err := s.db.Where("email = ?", strings.ToLower(strings.TrimSpace(email))).First(&user).Error; err != nil { return "", errors.New("invalid credentials") } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { return "", errors.New("invalid credentials") } return fmt.Sprintf("user:%d", user.ID), nil } func (s *Service) sign(value string) string { mac := hmac.New(sha256.New, []byte(s.secret)) mac.Write([]byte(value)) return hex.EncodeToString(mac.Sum(nil)) }