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.findByTokenWithParent(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");
}
}