Newer
Older
teacher-diary / src / main / java / ru / mcs / diary / auth / AuthService.java
package ru.mcs.diary.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.mcs.diary.auth.dto.ParentRegisterRequest;
import ru.mcs.diary.auth.dto.RegisterRequest;
import ru.mcs.diary.auth.dto.ResetPasswordRequest;
import ru.mcs.diary.common.exception.ResourceNotFoundException;
import ru.mcs.diary.common.service.MailService;
import ru.mcs.diary.parent.Parent;
import ru.mcs.diary.parent.ParentRepository;
import ru.mcs.diary.teacher.Teacher;
import ru.mcs.diary.teacher.TeacherRepository;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {

    private final TeacherRepository teacherRepository;
    private final ParentRepository parentRepository;
    private final PasswordResetTokenRepository passwordResetTokenRepository;
    private final ParentInviteTokenRepository parentInviteTokenRepository;
    private final PasswordEncoder passwordEncoder;
    private final MailService mailService;

    @Value("${app.security.password-reset-token-expiry-hours}")
    private int passwordResetTokenExpiryHours;

    @Value("${app.security.parent-invite-token-expiry-days}")
    private int parentInviteTokenExpiryDays;

    // ==================== Регистрация преподавателя ====================

    @Transactional
    public Teacher registerTeacher(RegisterRequest request) {
        log.info("Registering new teacher: {}", request.getEmail());

        if (teacherRepository.existsByEmail(request.getEmail())) {
            throw new IllegalArgumentException("Пользователь с таким email уже существует");
        }

        if (!request.getPassword().equals(request.getConfirmPassword())) {
            throw new IllegalArgumentException("Пароли не совпадают");
        }

        Teacher teacher = Teacher.builder()
                .email(request.getEmail().toLowerCase().trim())
                .password(passwordEncoder.encode(request.getPassword()))
                .firstName(request.getFirstName().trim())
                .lastName(request.getLastName().trim())
                .patronymic(request.getPatronymic() != null ? request.getPatronymic().trim() : null)
                .phone(request.getPhone())
                .enabled(true)
                .build();

        return teacherRepository.save(teacher);
    }

    // ==================== Восстановление пароля ====================

    @Transactional
    public void initiatePasswordReset(String email) {
        log.info("Initiating password reset for: {}", email);

        String normalizedEmail = email.toLowerCase().trim();

        // Ищем среди преподавателей
        Optional<Teacher> teacher = teacherRepository.findByEmail(normalizedEmail);
        if (teacher.isPresent()) {
            createAndSendPasswordResetToken(normalizedEmail, teacher.get().getFullName(), "TEACHER");
            return;
        }

        // Ищем среди активных родителей
        Optional<Parent> parent = parentRepository.findByEmailAndEnabledTrue(normalizedEmail);
        if (parent.isPresent()) {
            createAndSendPasswordResetToken(normalizedEmail, parent.get().getFullName(), "PARENT");
            return;
        }

        // Не раскрываем, существует ли пользователь (безопасность)
        log.warn("Password reset requested for non-existent user: {}", email);
    }

    private void createAndSendPasswordResetToken(String email, String userName, String userType) {
        // Инвалидируем старые токены
        passwordResetTokenRepository.invalidateAllTokensForUser(email);

        String token = UUID.randomUUID().toString();

        PasswordResetToken resetToken = PasswordResetToken.builder()
                .token(token)
                .userEmail(email)
                .userType(userType)
                .expiresAt(LocalDateTime.now().plusHours(passwordResetTokenExpiryHours))
                .used(false)
                .build();

        passwordResetTokenRepository.save(resetToken);
        mailService.sendPasswordResetEmail(email, token, userName);
    }

    @Transactional
    public void resetPassword(ResetPasswordRequest request) {
        log.info("Resetting password with token");

        if (!request.getPassword().equals(request.getConfirmPassword())) {
            throw new IllegalArgumentException("Пароли не совпадают");
        }

        PasswordResetToken token = passwordResetTokenRepository
                .findByTokenAndUsedFalse(request.getToken())
                .orElseThrow(() -> new IllegalArgumentException("Недействительная ссылка для сброса пароля"));

        if (token.isExpired()) {
            throw new IllegalArgumentException("Срок действия ссылки истёк");
        }

        String encodedPassword = passwordEncoder.encode(request.getPassword());

        if ("TEACHER".equals(token.getUserType())) {
            Teacher teacher = teacherRepository.findByEmail(token.getUserEmail())
                    .orElseThrow(() -> new ResourceNotFoundException("Пользователь не найден"));
            teacher.setPassword(encodedPassword);
            teacherRepository.save(teacher);
        } else {
            Parent parent = parentRepository.findByEmailAndEnabledTrue(token.getUserEmail())
                    .orElseThrow(() -> new ResourceNotFoundException("Пользователь не найден"));
            parent.setPassword(encodedPassword);
            parentRepository.save(parent);
        }

        token.setUsed(true);
        passwordResetTokenRepository.save(token);

        log.info("Password reset successful for: {}", token.getUserEmail());
    }

    public Optional<PasswordResetToken> validatePasswordResetToken(String token) {
        return passwordResetTokenRepository.findByTokenAndUsedFalse(token)
                .filter(t -> !t.isExpired());
    }

    // ==================== Приглашение родителя ====================

    @Transactional
    public String createParentInviteToken(Parent parent) {
        log.info("Creating invite token for parent: {}", parent.getEmail());

        // Проверяем, есть ли уже активный токен
        Optional<ParentInviteToken> existingToken = parentInviteTokenRepository.findByParentAndUsedFalse(parent);
        if (existingToken.isPresent()) {
            return existingToken.get().getToken();
        }

        String token = UUID.randomUUID().toString();

        ParentInviteToken inviteToken = ParentInviteToken.builder()
                .token(token)
                .parent(parent)
                .expiresAt(LocalDateTime.now().plusDays(parentInviteTokenExpiryDays))
                .used(false)
                .build();

        parentInviteTokenRepository.save(inviteToken);

        return token;
    }

    @Transactional
    public void sendParentInvite(Parent parent, String teacherName) {
        String token = createParentInviteToken(parent);
        mailService.sendParentInviteEmail(parent.getEmail(), token, parent.getFullName(), teacherName);
        log.info("Invite sent to parent: {}", parent.getEmail());
    }

    public Optional<ParentInviteToken> validateParentInviteToken(String token) {
        return parentInviteTokenRepository.findByTokenAndUsedFalse(token)
                .filter(t -> t.getExpiresAt() == null || LocalDateTime.now().isBefore(t.getExpiresAt()));
    }

    @Transactional
    public void activateParent(ParentRegisterRequest request) {
        log.info("Activating parent with token");

        if (!request.getPassword().equals(request.getConfirmPassword())) {
            throw new IllegalArgumentException("Пароли не совпадают");
        }

        ParentInviteToken token = parentInviteTokenRepository
                .findByTokenAndUsedFalse(request.getToken())
                .orElseThrow(() -> new IllegalArgumentException("Недействительная ссылка для регистрации"));

        if (token.getExpiresAt() != null && LocalDateTime.now().isAfter(token.getExpiresAt())) {
            throw new IllegalArgumentException("Срок действия приглашения истёк");
        }

        Parent parent = token.getParent();
        parent.setPassword(passwordEncoder.encode(request.getPassword()));
        parent.setEnabled(true);
        parentRepository.save(parent);

        token.setUsed(true);
        parentInviteTokenRepository.save(token);

        log.info("Parent activated: {}", parent.getEmail());
    }

    // ==================== Утилиты ====================

    @Transactional
    public void cleanupExpiredTokens() {
        passwordResetTokenRepository.deleteExpiredTokens(LocalDateTime.now());
        log.info("Expired tokens cleaned up");
    }
}