From de96e0dcef1312aaed0d253574734a9e027722ca Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Fri, 25 Jun 2021 18:42:47 +0800 Subject: [PATCH] feat(api): store authentication activity closes #160 --- ...0210625155626__authentication_activity.sql | 11 +++ .../domain/model/AuthenticationActivity.kt | 13 +++ .../AuthenticationActivityRepository.kt | 17 ++++ .../domain/persistence/KomgaUserRepository.kt | 2 +- .../domain/service/KomgaUserLifecycle.kt | 11 ++- .../jooq/AuthenticationActivityDao.kt | 96 +++++++++++++++++++ .../komga/infrastructure/jooq/KomgaUserDao.kt | 2 +- .../infrastructure/security/LoginListener.kt | 67 +++++++++++++ .../security/SecurityConfiguration.kt | 15 ++- .../UserAgentWebAuthenticationDetails.kt | 8 ++ ...UserAgentWebAuthenticationDetailsSource.kt | 9 ++ .../komga/interfaces/rest/UserController.kt | 55 +++++++++++ .../rest/dto/AuthenticationActivityDto.kt | 27 ++++++ ...AuthenticationActivityCleanupController.kt | 24 +++++ .../komga/domain/service/BookImporterTest.kt | 1 + .../komga/domain/service/BookLifecycleTest.kt | 1 + .../infrastructure/jooq/KomgaUserDaoTest.kt | 4 +- 17 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 komga/src/flyway/resources/db/migration/sqlite/V20210625155626__authentication_activity.sql create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/AuthenticationActivity.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/persistence/AuthenticationActivityRepository.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/AuthenticationActivityDao.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/security/LoginListener.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetails.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/AuthenticationActivityDto.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/scheduler/AuthenticationActivityCleanupController.kt diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20210625155626__authentication_activity.sql b/komga/src/flyway/resources/db/migration/sqlite/V20210625155626__authentication_activity.sql new file mode 100644 index 000000000..3428555cf --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20210625155626__authentication_activity.sql @@ -0,0 +1,11 @@ +create table AUTHENTICATION_ACTIVITY +( + USER_ID varchar NULL DEFAULT NULL, + EMAIL varchar NULL DEFAULT NULL, + IP varchar NULL DEFAULT NULL, + USER_AGENT varchar NULL DEFAULT NULL, + SUCCESS boolean NOT NULL, + ERROR varchar NULL DEFAULT NULL, + DATE_TIME datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (USER_ID) references USER (ID) +); diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/AuthenticationActivity.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/AuthenticationActivity.kt new file mode 100644 index 000000000..15a1f6fcb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/AuthenticationActivity.kt @@ -0,0 +1,13 @@ +package org.gotson.komga.domain.model + +import java.time.LocalDateTime + +data class AuthenticationActivity( + val userId: String? = null, + val email: String? = null, + val ip: String? = null, + val userAgent: String? = null, + val success: Boolean, + val error: String? = null, + val dateTime: LocalDateTime = LocalDateTime.now(), +) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/AuthenticationActivityRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/AuthenticationActivityRepository.kt new file mode 100644 index 000000000..3280a02ff --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/AuthenticationActivityRepository.kt @@ -0,0 +1,17 @@ +package org.gotson.komga.domain.persistence + +import org.gotson.komga.domain.model.AuthenticationActivity +import org.gotson.komga.domain.model.KomgaUser +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import java.time.LocalDateTime + +interface AuthenticationActivityRepository { + fun findAll(pageable: Pageable): Page + fun findAllByUser(user: KomgaUser, pageable: Pageable): Page + + fun insert(activity: AuthenticationActivity) + + fun deleteByUser(user: KomgaUser) + fun deleteOlderThan(dateTime: LocalDateTime) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/KomgaUserRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/KomgaUserRepository.kt index 3afac7ab3..5ad0b40ea 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/KomgaUserRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/KomgaUserRepository.kt @@ -6,7 +6,7 @@ interface KomgaUserRepository { fun count(): Long fun findByIdOrNull(id: String): KomgaUser? - fun findByEmailIgnoreCase(email: String): KomgaUser? + fun findByEmailIgnoreCaseOrNull(email: String): KomgaUser? fun findAll(): Collection diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt index e5c2ed5dd..5b9ec9e82 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt @@ -3,6 +3,7 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import org.gotson.komga.domain.model.KomgaUser import org.gotson.komga.domain.model.UserEmailAlreadyExistsException +import org.gotson.komga.domain.persistence.AuthenticationActivityRepository import org.gotson.komga.domain.persistence.KomgaUserRepository import org.gotson.komga.domain.persistence.ReadProgressRepository import org.gotson.komga.infrastructure.security.KomgaPrincipal @@ -20,18 +21,19 @@ private val logger = KotlinLogging.logger {} class KomgaUserLifecycle( private val userRepository: KomgaUserRepository, private val readProgressRepository: ReadProgressRepository, + private val authenticationActivityRepository: AuthenticationActivityRepository, private val passwordEncoder: PasswordEncoder, - private val sessionRegistry: SessionRegistry + private val sessionRegistry: SessionRegistry, ) : UserDetailsService { override fun loadUserByUsername(username: String): UserDetails = - userRepository.findByEmailIgnoreCase(username)?.let { + userRepository.findByEmailIgnoreCaseOrNull(username)?.let { KomgaPrincipal(it) } ?: throw UsernameNotFoundException(username) fun updatePassword(user: UserDetails, newPassword: String, expireSessions: Boolean): UserDetails { - userRepository.findByEmailIgnoreCase(user.username)?.let { komgaUser -> + userRepository.findByEmailIgnoreCaseOrNull(user.username)?.let { komgaUser -> logger.info { "Changing password for user ${user.username}" } val updatedUser = komgaUser.copy(password = passwordEncoder.encode(newPassword)) userRepository.update(updatedUser) @@ -58,8 +60,11 @@ class KomgaUserLifecycle( @Transactional fun deleteUser(user: KomgaUser) { logger.info { "Deleting user: $user" } + readProgressRepository.deleteByUserId(user.id) + authenticationActivityRepository.deleteByUser(user) userRepository.delete(user.id) + expireSessions(user) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/AuthenticationActivityDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/AuthenticationActivityDao.kt new file mode 100644 index 000000000..c2fc674ad --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/AuthenticationActivityDao.kt @@ -0,0 +1,96 @@ +package org.gotson.komga.infrastructure.jooq + +import org.gotson.komga.domain.model.AuthenticationActivity +import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.persistence.AuthenticationActivityRepository +import org.gotson.komga.jooq.Tables +import org.gotson.komga.jooq.tables.records.AuthenticationActivityRecord +import org.jooq.Condition +import org.jooq.DSLContext +import org.jooq.impl.DSL +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class AuthenticationActivityDao( + private val dsl: DSLContext +) : AuthenticationActivityRepository { + + private val aa = Tables.AUTHENTICATION_ACTIVITY + + private val sorts = mapOf( + "dateTime" to aa.DATE_TIME, + "email" to aa.EMAIL, + "success" to aa.SUCCESS, + "ip" to aa.IP, + "error" to aa.ERROR, + "userId" to aa.USER_ID, + "userAgent" to aa.USER_AGENT, + ) + + override fun findAll(pageable: Pageable): Page { + val conditions: Condition = DSL.trueCondition() + return findAll(conditions, pageable) + } + + override fun findAllByUser(user: KomgaUser, pageable: Pageable): Page { + val conditions = aa.USER_ID.eq(user.id).or(aa.EMAIL.eq(user.email)) + return findAll(conditions, pageable) + } + + private fun findAll(conditions: Condition, pageable: Pageable): PageImpl { + val count = dsl.fetchCount(aa) + + val orderBy = pageable.sort.toOrderBy(sorts) + + val items = dsl.selectFrom(aa) + .where(conditions) + .orderBy(orderBy) + .apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } + .fetchInto(aa) + .map { it.toDomain() } + + val pageSort = if (orderBy.size > 1) pageable.sort else Sort.unsorted() + return PageImpl( + items, + if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort) + else PageRequest.of(0, maxOf(count, 20), pageSort), + count.toLong(), + ) + } + + override fun insert(activity: AuthenticationActivity) { + dsl.insertInto(aa, aa.USER_ID, aa.EMAIL, aa.IP, aa.USER_AGENT, aa.SUCCESS, aa.ERROR) + .values(activity.userId, activity.email, activity.ip, activity.userAgent, activity.success, activity.error) + .execute() + } + + override fun deleteByUser(user: KomgaUser) { + dsl.deleteFrom(aa) + .where(aa.USER_ID.eq(user.id)) + .or(aa.EMAIL.eq(user.email)) + .execute() + } + + override fun deleteOlderThan(dateTime: LocalDateTime) { + dsl.deleteFrom(aa) + .where(aa.DATE_TIME.lt(dateTime)) + .execute() + } + + private fun AuthenticationActivityRecord.toDomain() = + AuthenticationActivity( + userId = userId, + email = email, + ip = ip, + userAgent = userAgent, + success = success, + error = error, + dateTime = dateTime.toCurrentTimeZone(), + ) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDao.kt index cfcc657ea..daba28022 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDao.kt @@ -118,7 +118,7 @@ class KomgaUserDao( .where(u.EMAIL.equalIgnoreCase(email)) ) - override fun findByEmailIgnoreCase(email: String): KomgaUser? = + override fun findByEmailIgnoreCaseOrNull(email: String): KomgaUser? = selectBase() .where(u.EMAIL.equalIgnoreCase(email)) .fetchAndMap() diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/LoginListener.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/LoginListener.kt new file mode 100644 index 000000000..13e1646be --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/LoginListener.kt @@ -0,0 +1,67 @@ +package org.gotson.komga.infrastructure.security + +import mu.KotlinLogging +import org.gotson.komga.domain.model.AuthenticationActivity +import org.gotson.komga.domain.persistence.AuthenticationActivityRepository +import org.gotson.komga.domain.persistence.KomgaUserRepository +import org.springframework.context.event.EventListener +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent +import org.springframework.security.authentication.event.AuthenticationSuccessEvent +import org.springframework.security.web.authentication.WebAuthenticationDetails +import org.springframework.stereotype.Component +import java.util.EventObject + +private val logger = KotlinLogging.logger {} + +@Component +class LoginListener( + private val authenticationActivityRepository: AuthenticationActivityRepository, + private val userRepository: KomgaUserRepository, +) { + + @EventListener + fun onSuccess(event: AuthenticationSuccessEvent) { + val user = (event.authentication.principal as KomgaPrincipal).user + val activity = AuthenticationActivity( + userId = user.id, + email = user.email, + ip = event.getIp(), + userAgent = event.getUserAgent(), + success = true, + ) + + logger.info { activity } + authenticationActivityRepository.insert(activity) + } + + @EventListener + fun onFailure(event: AbstractAuthenticationFailureEvent) { + val user = event.authentication.principal.toString() + val activity = AuthenticationActivity( + userId = userRepository.findByEmailIgnoreCaseOrNull(user)?.id, + email = user, + ip = event.getIp(), + userAgent = event.getUserAgent(), + success = false, + error = event.exception.message, + ) + + logger.info { activity } + authenticationActivityRepository.insert(activity) + } + + private fun EventObject.getIp(): String? = + when (source) { + is WebAuthenticationDetails -> (source as WebAuthenticationDetails).remoteAddress + is AbstractAuthenticationToken -> ((source as AbstractAuthenticationToken).details as WebAuthenticationDetails).remoteAddress + else -> null + } + + private fun EventObject.getUserAgent(): String? = + when (source) { + is UserAgentWebAuthenticationDetails -> (source as UserAgentWebAuthenticationDetails).userAgent + is AbstractAuthenticationToken -> ((source as AbstractAuthenticationToken).details as UserAgentWebAuthenticationDetails).userAgent + else -> null + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt index ecce5295a..bf92581b9 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt @@ -12,6 +12,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.core.session.SessionRegistry import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices private val logger = KotlinLogging.logger {} @@ -25,6 +26,7 @@ class SecurityConfiguration( override fun configure(http: HttpSecurity) { // @formatter:off + val userAgentWebAuthenticationDetailsSource = UserAgentWebAuthenticationDetailsSource() http .cors() @@ -51,11 +53,13 @@ class SecurityConfiguration( } .httpBasic() + .authenticationDetailsSource(userAgentWebAuthenticationDetailsSource) .and() .logout() .logoutUrl("/api/v1/users/logout") .deleteCookies("JSESSIONID") + .invalidateHttpSession(true) .and() .sessionManagement() @@ -67,10 +71,13 @@ class SecurityConfiguration( http .rememberMe() - .key(komgaProperties.rememberMe.key) - .tokenValiditySeconds(komgaProperties.rememberMe.validity) - .alwaysRemember(true) - .userDetailsService(komgaUserDetailsLifecycle) + .rememberMeServices( + TokenBasedRememberMeServices(komgaProperties.rememberMe.key, komgaUserDetailsLifecycle).apply { + setTokenValiditySeconds(komgaProperties.rememberMe.validity) + setAlwaysRemember(true) + setAuthenticationDetailsSource(userAgentWebAuthenticationDetailsSource) + } + ) } // @formatter:on } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetails.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetails.kt new file mode 100644 index 000000000..b9dbcad93 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetails.kt @@ -0,0 +1,8 @@ +package org.gotson.komga.infrastructure.security + +import org.springframework.security.web.authentication.WebAuthenticationDetails +import javax.servlet.http.HttpServletRequest + +class UserAgentWebAuthenticationDetails(request: HttpServletRequest) : WebAuthenticationDetails(request) { + val userAgent: String = request.getHeader("User-Agent") +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt new file mode 100644 index 000000000..e5aa7048f --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt @@ -0,0 +1,9 @@ +package org.gotson.komga.infrastructure.security + +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import javax.servlet.http.HttpServletRequest + +class UserAgentWebAuthenticationDetailsSource : WebAuthenticationDetailsSource() { + override fun buildDetails(context: HttpServletRequest): UserAgentWebAuthenticationDetails = + UserAgentWebAuthenticationDetails(context) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/UserController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/UserController.kt index 06177304b..5b680c430 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/UserController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/UserController.kt @@ -1,14 +1,18 @@ package org.gotson.komga.interfaces.rest +import io.swagger.v3.oas.annotations.Parameter import mu.KotlinLogging import org.gotson.komga.domain.model.ROLE_ADMIN import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING import org.gotson.komga.domain.model.UserEmailAlreadyExistsException +import org.gotson.komga.domain.persistence.AuthenticationActivityRepository import org.gotson.komga.domain.persistence.KomgaUserRepository import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.service.KomgaUserLifecycle +import org.gotson.komga.infrastructure.jooq.UnpagedSorted import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.interfaces.rest.dto.AuthenticationActivityDto import org.gotson.komga.interfaces.rest.dto.PasswordUpdateDto import org.gotson.komga.interfaces.rest.dto.RolesUpdateDto import org.gotson.komga.interfaces.rest.dto.SharedLibrariesUpdateDto @@ -17,7 +21,12 @@ import org.gotson.komga.interfaces.rest.dto.UserDto import org.gotson.komga.interfaces.rest.dto.UserWithSharedLibrariesDto import org.gotson.komga.interfaces.rest.dto.toDto import org.gotson.komga.interfaces.rest.dto.toWithSharedLibrariesDto +import org.springdoc.core.converters.models.PageableAsQueryParam import org.springframework.core.env.Environment +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -29,6 +38,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException @@ -42,6 +52,7 @@ class UserController( private val userLifecycle: KomgaUserLifecycle, private val userRepository: KomgaUserRepository, private val libraryRepository: LibraryRepository, + private val authenticationActivityRepository: AuthenticationActivityRepository, env: Environment ) { @@ -126,4 +137,48 @@ class UserController( logger.info { "Updated user shared libraries: $updatedUser" } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + + @GetMapping("me/authentication-activity") + @PageableAsQueryParam + fun getMyAuthenticationActivity( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val sort = + if (page.sort.isSorted) page.sort + else Sort.by(Sort.Order.desc("dateTime")) + + val pageRequest = + if (unpaged) UnpagedSorted(sort) + else PageRequest.of( + page.pageNumber, + page.pageSize, + sort + ) + + return authenticationActivityRepository.findAllByUser(principal.user, pageRequest).map { it.toDto() } + } + + @GetMapping("authentication-activity") + @PageableAsQueryParam + @PreAuthorize("hasRole('$ROLE_ADMIN')") + fun getAuthenticationActivity( + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val sort = + if (page.sort.isSorted) page.sort + else Sort.by(Sort.Order.desc("dateTime")) + + val pageRequest = + if (unpaged) UnpagedSorted(sort) + else PageRequest.of( + page.pageNumber, + page.pageSize, + sort + ) + + return authenticationActivityRepository.findAll(pageRequest).map { it.toDto() } + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/AuthenticationActivityDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/AuthenticationActivityDto.kt new file mode 100644 index 000000000..79fa09d56 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/AuthenticationActivityDto.kt @@ -0,0 +1,27 @@ +package org.gotson.komga.interfaces.rest.dto + +import com.fasterxml.jackson.annotation.JsonFormat +import org.gotson.komga.domain.model.AuthenticationActivity +import java.time.LocalDateTime + +data class AuthenticationActivityDto( + val userId: String?, + val email: String?, + val ip: String?, + val userAgent: String?, + val success: Boolean, + val error: String?, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + val dateTime: LocalDateTime, +) + +fun AuthenticationActivity.toDto() = + AuthenticationActivityDto( + userId = userId, + email = email, + ip = ip, + userAgent = userAgent, + success = success, + error = error, + dateTime = dateTime, + ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/scheduler/AuthenticationActivityCleanupController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/scheduler/AuthenticationActivityCleanupController.kt new file mode 100644 index 000000000..c89382920 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/scheduler/AuthenticationActivityCleanupController.kt @@ -0,0 +1,24 @@ +package org.gotson.komga.interfaces.scheduler + +import mu.KotlinLogging +import org.gotson.komga.domain.persistence.AuthenticationActivityRepository +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.time.ZoneId + +private val logger = KotlinLogging.logger {} + +@Component +class AuthenticationActivityCleanupController( + private val authenticationActivityRepository: AuthenticationActivityRepository, +) { + + // Run every day + @Scheduled(fixedRate = 86_400_000) + fun cleanup() { + val olderThan = LocalDateTime.now(ZoneId.of("Z")).minusMonths(1) + logger.info { "Remove authentication activity older than $olderThan (UTC)" } + authenticationActivityRepository.deleteOlderThan(olderThan) + } +} diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt index 0683a4882..410cf9bc2 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt @@ -70,6 +70,7 @@ class BookImporterTest( @AfterAll fun teardown() { libraryRepository.deleteAll() + readProgressRepository.deleteAll() userRepository.deleteAll() } diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt index 5e2f60884..12ff961e4 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt @@ -56,6 +56,7 @@ class BookLifecycleTest( @AfterAll fun teardown() { libraryRepository.deleteAll() + readProgressRepository.deleteAll() userRepository.deleteAll() } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDaoTest.kt index 54e618999..5cc7c204c 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/KomgaUserDaoTest.kt @@ -173,8 +173,8 @@ class KomgaUserDaoTest( KomgaUser("user1@example.org", "p", false) ) - val found = komgaUserDao.findByEmailIgnoreCase("USER1@EXAMPLE.ORG") - val notFound = komgaUserDao.findByEmailIgnoreCase("USER2@EXAMPLE.ORG") + val found = komgaUserDao.findByEmailIgnoreCaseOrNull("USER1@EXAMPLE.ORG") + val notFound = komgaUserDao.findByEmailIgnoreCaseOrNull("USER2@EXAMPLE.ORG") assertThat(found).isNotNull assertThat(found?.email).isEqualTo("user1@example.org")