diff --git a/src/main/kotlin/com/zufar/urlshortener/UrlShortenerApplication.kt b/src/main/kotlin/com/zufar/urlshortener/UrlShortenerApplication.kt index 0a7bfb6..3da3f8c 100644 --- a/src/main/kotlin/com/zufar/urlshortener/UrlShortenerApplication.kt +++ b/src/main/kotlin/com/zufar/urlshortener/UrlShortenerApplication.kt @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition import io.swagger.v3.oas.annotations.info.Info import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling @OpenAPIDefinition( info = Info( @@ -13,6 +14,7 @@ import org.springframework.boot.runApplication ) ) @SpringBootApplication +@EnableScheduling class UrlShortenerApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/zufar/urlshortener/auth/service/CustomUserDetailsService.kt b/src/main/kotlin/com/zufar/urlshortener/auth/service/CustomUserDetailsService.kt index 13e549f..af4e482 100644 --- a/src/main/kotlin/com/zufar/urlshortener/auth/service/CustomUserDetailsService.kt +++ b/src/main/kotlin/com/zufar/urlshortener/auth/service/CustomUserDetailsService.kt @@ -19,4 +19,10 @@ class CustomUserDetailsService(private val userRepository: UserRepository) : Use .authorities(emptyList()) .build() } + + fun getEmailByUserId(userId: String): String { + val user = userRepository.findById(userId) + .orElseThrow { UsernameNotFoundException("User with ID='$userId' is not found") } + return user.email + } } diff --git a/src/main/kotlin/com/zufar/urlshortener/shorten/service/EmailNotificationSender.kt b/src/main/kotlin/com/zufar/urlshortener/shorten/service/EmailNotificationSender.kt new file mode 100644 index 0000000..83b2d42 --- /dev/null +++ b/src/main/kotlin/com/zufar/urlshortener/shorten/service/EmailNotificationSender.kt @@ -0,0 +1,32 @@ +package com.zufar.urlshortener.shorten.service + +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.MimeMessageHelper +import org.springframework.stereotype.Service + +@Service +class EmailNotificationService( + private val mailSender: JavaMailSender +) { + fun sendExpirationNotification(email: String, shortUrl: String, expirationDate: String) { + val message = mailSender.createMimeMessage() + val helper = MimeMessageHelper(message, true) + + helper.setTo(email) + helper.setSubject("Your Short URL is Expiring Soon") + helper.setText( + """ + Hello, + + This is a reminder that your short URL ($shortUrl) will expire on $expirationDate. + Please consider extending its validity if necessary. + + Best regards, + URL Shortener Team + """.trimIndent(), + false + ) + + mailSender.send(message) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/zufar/urlshortener/shorten/service/UrlExpirationNotificationScheduler.kt b/src/main/kotlin/com/zufar/urlshortener/shorten/service/UrlExpirationNotificationScheduler.kt new file mode 100644 index 0000000..5f798fc --- /dev/null +++ b/src/main/kotlin/com/zufar/urlshortener/shorten/service/UrlExpirationNotificationScheduler.kt @@ -0,0 +1,44 @@ +package com.zufar.urlshortener.shorten.service + +import com.zufar.urlshortener.auth.service.CustomUserDetailsService +import com.zufar.urlshortener.shorten.repository.UrlRepository +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class UrlExpirationNotificationScheduler( + private val urlRepository: UrlRepository, + private val emailNotificationService: EmailNotificationService, + private val userDetailsService: CustomUserDetailsService +) { + private val log = LoggerFactory.getLogger(UrlExpirationNotificationScheduler::class.java) + + @Scheduled(cron = "0 0 0 * * *") + fun notifyExpiringUrls() { + log.info("Starting expiration notification task") + val thresholdDate = LocalDateTime.now().plusDays(7) + + val expiringUrls = urlRepository.findAll().filter { it.expirationDate.isBefore(thresholdDate) } + + if (expiringUrls.isEmpty()) { + log.info("No URLs found nearing expiration") + return + } + + expiringUrls.forEach { url -> + val email = userDetailsService.getEmailByUserId(url.userId ?: "") + try { + emailNotificationService.sendExpirationNotification( + email = email, + shortUrl = url.shortUrl, + expirationDate = url.expirationDate.toString() + ) + log.info("Sent expiration notification for URL: ${url.shortUrl} to user: $email") + } catch (e: Exception) { + log.error("Failed to send notification for URL: ${url.shortUrl} to user: $email", e) + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 909fa7f..70b32f8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,37 +1,45 @@ -# Application Properties -spring.application.name=URL-Shortener -app.base-url=${SERVER_BASE_URL} -server.port=8080 + # Application Properties + spring.application.name=URL-Shortener + app.base-url=${SERVER_BASE_URL} + server.port=8080 -# Springdoc Configuration -springdoc.swagger-ui.path=/api/v1/swagger-ui -springdoc.api-docs.path=/api/v1/api-docs -springdoc.api-docs.enabled=true -springdoc.show-actuator=true -springdoc.packages-to-scan=com.zufar.urlshortener + # Springdoc Configuration + springdoc.swagger-ui.path=/api/v1/swagger-ui + springdoc.api-docs.path=/api/v1/api-docs + springdoc.api-docs.enabled=true + springdoc.show-actuator=true + springdoc.packages-to-scan=com.zufar.urlshortener -# JWT Configuration -jwt.secret=${JWT_SECRET} -jwt.accessTokenExpiration=3600000 -jwt.refreshTokenExpiration=604800000 + # JWT Configuration + jwt.secret=${JWT_SECRET} + jwt.accessTokenExpiration=3600000 + jwt.refreshTokenExpiration=604800000 -# MongoDB Configuration -spring.data.mongodb.uri=${MONGODB_URI} -spring.data.mongodb.database=${MONGODB_DATABASE} + # MongoDB Configuration + spring.data.mongodb.uri=${MONGODB_URI} + spring.data.mongodb.database=${MONGODB_DATABASE} -# Actuator -management.endpoints.web.exposure.include=* -management.endpoints.web.base-path=/actuator -management.endpoint.health.show-details=always + # Actuator + management.endpoints.web.exposure.include=* + management.endpoints.web.base-path=/actuator + management.endpoint.health.show-details=always -# Logging -logging.level.root=WARN -logging.level.org.mongodb.driver=WARN -logging.level.org.springframework.data.mongodb=WARN -logging.level.org.springframework.web=WARN -logging.level.com.zufar.urlshortener=INFO -logging.file.name=logs/application.log -logging.logback.rollingpolicy.max-file-size=10MB -logging.logback.rollingpolicy.max-history=30 -logging.pattern.console="%d{yyyy-MM-dd HH:mm:ss.SSS} - correlationId=%X{correlationId} [%thread] %-5level %logger{36} - %msg%n" \ No newline at end of file + # Logging + logging.level.root=WARN + logging.level.org.mongodb.driver=WARN + logging.level.org.springframework.data.mongodb=WARN + logging.level.org.springframework.web=WARN + logging.level.com.zufar.urlshortener=INFO + logging.file.name=logs/application.log + logging.logback.rollingpolicy.max-file-size=10MB + logging.logback.rollingpolicy.max-history=30 + logging.pattern.console="%d{yyyy-MM-dd HH:mm:ss.SSS} - correlationId=%X{correlationId} [%thread] %-5level %logger{36} - %msg%n" + + # Mail + spring.mail.host=smtp.gmail.com + spring.mail.port=587 + spring.mail.username=${MAIL_USERNAME} + spring.mail.password=${MAIL_PASSWORD} + spring.mail.properties.mail.smtp.auth=true + spring.mail.properties.mail.smtp.starttls.enable=true