package service import ( "context" "crypto/rand" "encoding/base64" "errors" "tailly_back_v2/internal/domain" "tailly_back_v2/internal/repository" "time" ) type RecoveryService interface { InitiateRecovery(ctx context.Context, email string, newDevice *domain.Device) (string, error) VerifyRecovery(ctx context.Context, token string) (*domain.Session, error) AddRecoveryMethod(ctx context.Context, userID int, method *domain.RecoveryMethod) error GetRecoveryMethods(ctx context.Context, userID int) ([]*domain.RecoveryMethod, error) } type recoveryService struct { recoveryRepo repository.RecoveryRepository userRepo repository.UserRepository sessionRepo repository.SessionRepository deviceRepo repository.DeviceRepository mailer MailService } func (s *recoveryService) AddRecoveryMethod(ctx context.Context, userID int, method *domain.RecoveryMethod) error { //TODO implement me panic("implement me") } func (s *recoveryService) GetRecoveryMethods(ctx context.Context, userID int) ([]*domain.RecoveryMethod, error) { //TODO implement me panic("implement me") } func NewRecoveryService(recoveryRepo repository.RecoveryRepository, userRepo repository.UserRepository, sessionRepo repository.SessionRepository, deviceRepo repository.DeviceRepository, mailer MailService) RecoveryService { return &recoveryService{ recoveryRepo: recoveryRepo, userRepo: userRepo, sessionRepo: sessionRepo, deviceRepo: deviceRepo, mailer: mailer, } } func (s *recoveryService) InitiateRecovery(ctx context.Context, email string, newDevice *domain.Device) (string, error) { user, err := s.userRepo.GetByEmail(ctx, email) if err != nil { return "", errors.New("user not found") } // Проверяем, что у пользователя есть методы восстановления methods, err := s.recoveryRepo.GetMethods(ctx, user.ID) if err != nil || len(methods) == 0 { return "", errors.New("no recovery methods available") } // Генерируем токен восстановления token := make([]byte, 32) if _, err := rand.Read(token); err != nil { return "", err } tokenStr := base64.URLEncoding.EncodeToString(token) req := &domain.RecoveryRequest{ UserID: user.ID, Token: tokenStr, NewDevice: newDevice, Status: "pending", CreatedAt: time.Now(), ExpiresAt: time.Now().Add(1 * time.Hour), } if err := s.recoveryRepo.SaveRequest(ctx, req); err != nil { return "", err } // Отправляем письмо на все методы восстановления for _, method := range methods { switch method.MethodType { case "email": if err := s.mailer.SendRecoveryEmail( method.Value, tokenStr, newDevice.IPAddress, newDevice.UserAgent, ); err != nil { return "", nil } // case "phone": отправка SMS } } return tokenStr, nil } func (s *recoveryService) VerifyRecovery(ctx context.Context, token string) (*domain.Session, error) { req, err := s.recoveryRepo.GetRequestByToken(ctx, token) if err != nil { return nil, errors.New("invalid recovery token") } if time.Now().After(req.ExpiresAt) { return nil, errors.New("recovery token expired") } // Регистрируем новое устройство if err := s.deviceRepo.Save(ctx, req.NewDevice); err != nil { return nil, err } // Создаем сессию session := &domain.Session{ UserID: req.UserID, DeviceID: req.NewDevice.ID, StartedAt: time.Now(), } if err := s.sessionRepo.Save(ctx, session); err != nil { return nil, err } // Помечаем запрос как выполненный req.Status = "completed" if err := s.recoveryRepo.UpdateRequest(ctx, req); err != nil { return nil, err } return session, nil }