diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 161e7bdd..02bd9baa 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,7 +12,7 @@ jobs: - name: Install Dependencies run: cd ./next-app && yarn install --frozen-lockfile - name: Cache node_modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: './next-app/node_modules' key: ${{ runner.os }}-next-${{ hashFiles('./next-app/yarn.lock') }} @@ -24,7 +24,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v2 - name: Cache node_modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: './next-app/node_modules' key: ${{ runner.os }}-next-${{ hashFiles('./next-app/yarn.lock') }} @@ -38,7 +38,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v2 - name: Cache node_modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: './next-app/node_modules' key: ${{ runner.os }}-next-${{ hashFiles('./next-app/yarn.lock') }} @@ -47,7 +47,7 @@ jobs: - name: Package Jar run: cd next-app && ln -s out static && zip -0 -r next-app.jar static - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: next-app.jar path: next-app/next-app.jar @@ -94,7 +94,7 @@ jobs: java-version: 17 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: next-app.jar path: next-app @@ -103,7 +103,7 @@ jobs: run: ./gradlew bootjar - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: up.jar path: spring-app/build/libs/up.jar @@ -116,7 +116,7 @@ jobs: - uses: actions/checkout@v2 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: up.jar path: spring-app/build/libs diff --git a/.gitignore b/.gitignore index adca7a2e..4686e3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,10 @@ gatsby-app/.DS_Store #up / project files uploads/* + +# locale configuration +*.env +.env +.env.* +docker-compose.yml +docker-compose.yaml diff --git a/spring-app/build.gradle.kts b/spring-app/build.gradle.kts index efdf363c..5b706824 100644 --- a/spring-app/build.gradle.kts +++ b/spring-app/build.gradle.kts @@ -1,10 +1,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - id("org.springframework.boot") version "3.0.1" + id("org.springframework.boot") version "3.1.6" id("io.spring.dependency-management") version "1.1.0" id("org.asciidoctor.jvm.convert") version "3.2.0" - id("org.flywaydb.flyway") version "7.5.2" id("org.jlleitschuh.gradle.ktlint") version "12.1.2" kotlin("jvm") version "2.1.0" @@ -27,23 +26,24 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("commons-fileupload:commons-fileupload:1.5") - implementation("io.jsonwebtoken:jjwt-impl:0.11.5") - implementation("io.jsonwebtoken:jjwt-gson:0.11.5") - implementation("org.flywaydb:flyway-core:9.10.2") - implementation("org.flywaydb:flyway-mysql:9.10.2") - implementation("ch.vorburger.mariaDB4j:mariaDB4j:2.4.0") + implementation("org.flywaydb:flyway-core:11.3.1") + implementation("org.flywaydb:flyway-database-postgresql:11.3.1") implementation("com.ibm.icu:icu4j:72.1") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2") implementation("com.github.therapi:therapi-runtime-javadoc:0.15.0") kapt("com.github.therapi:therapi-runtime-javadoc-scribe:0.15.0") - runtimeOnly("com.h2database:h2:1.4.200") - runtimeOnly("mysql:mysql-connector-java") - runtimeOnly("org.mariadb.jdbc:mariadb-java-client") runtimeOnly(files("../next-app/next-app.jar")) + implementation("org.ktorm:ktorm-support-postgresql:4.1.1") + implementation("org.ktorm:ktorm-core:4.1.1") + runtimeOnly("org.postgresql:postgresql:42.7.5") + + testImplementation("org.postgresql:postgresql:42.7.5") + testImplementation("org.testcontainers:postgresql:1.20.4") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } @@ -67,7 +67,6 @@ ext { ktlint { version.set("1.4.1") ignoreFailures.set(true) - disabledRules.set(setOf("no-wildcard-imports")) } tasks { diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/DatabaseConfiguration.kt b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/DatabaseConfiguration.kt new file mode 100644 index 00000000..fe243e1b --- /dev/null +++ b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/DatabaseConfiguration.kt @@ -0,0 +1,22 @@ +package pl.starchasers.up.configuration + +import org.ktorm.database.Database +import org.ktorm.logging.Slf4jLoggerAdapter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.sql.DataSource +import kotlin.reflect.jvm.jvmName + +@Configuration +class DatabaseConfiguration( + private val dataSource: DataSource +) { + + @Bean + fun database(): Database = Database.connect( + dataSource = dataSource, + alwaysQuoteIdentifiers = true, + logger = Slf4jLoggerAdapter(this::class.jvmName) + ) + +} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/EmbeddedMariadbConfiguration.kt b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/EmbeddedMariadbConfiguration.kt deleted file mode 100644 index 7c1c0701..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/EmbeddedMariadbConfiguration.kt +++ /dev/null @@ -1,34 +0,0 @@ -package pl.starchasers.up.configuration - -import ch.vorburger.mariadb4j.springframework.MariaDB4jSpringService -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.jdbc.DataSourceBuilder -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile -import javax.sql.DataSource - -@Configuration -@Profile("test", "localdb") -class EmbeddedMariadbConfiguration { - - @Bean - fun mariaDB4jSpringService() = MariaDB4jSpringService() - - @Bean - fun dataSource( - mariaDB4jSpringService: MariaDB4jSpringService, - @Value("\${app.mariaDB4j.databaseName}") databaseName: String, - @Value("\${spring.datasource.driver-class-name}") datasourceDriver: String - ): DataSource { - mariaDB4jSpringService.db.createDB(databaseName) - - val config = mariaDB4jSpringService.configuration - - return DataSourceBuilder - .create() - .url(config.getURL(databaseName)) - .driverClassName(datasourceDriver) - .build() - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/ObjectMapperConfiguration.kt b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/ObjectMapperConfiguration.kt index 4e765378..34626d9c 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/ObjectMapperConfiguration.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/ObjectMapperConfiguration.kt @@ -1,11 +1,17 @@ package pl.starchasers.up.configuration import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class ObjectMapperConfiguration { @Bean - fun getObjectMapper(): ObjectMapper = ObjectMapper().findAndRegisterModules() + fun getObjectMapper(): ObjectMapper = + jacksonObjectMapper() + .registerModule(JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/WebSecurityConfiguration.kt b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/WebSecurityConfiguration.kt index 59d9709f..72784ce1 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/configuration/WebSecurityConfiguration.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/configuration/WebSecurityConfiguration.kt @@ -8,18 +8,13 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.web.cors.CorsConfiguration import org.springframework.web.cors.CorsConfigurationSource import org.springframework.web.cors.UrlBasedCorsConfigurationSource -import pl.starchasers.up.security.JwtTokenFilter -import pl.starchasers.up.service.JwtTokenService @Configuration @EnableWebSecurity -class WebSecurityConfiguration( - private val jwtTokenService: JwtTokenService -) { +class WebSecurityConfiguration() { private val logger = LoggerFactory.getLogger(this::class.java) @@ -30,13 +25,7 @@ class WebSecurityConfiguration( fun filterChain(http: HttpSecurity): SecurityFilterChain = http .csrf { it.disable() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } - .addFilterBefore(JwtTokenFilter(jwtTokenService), UsernamePasswordAuthenticationFilter::class.java) - .also { - if (devCors) { - logger.info("Development environment set. Enabling CORS for all origins.") - } - it.cors() - } + .cors {} .build() @Bean @@ -44,7 +33,7 @@ class WebSecurityConfiguration( val configuration = CorsConfiguration() configuration.allowedOrigins = listOf("*") configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH") -// configuration.allowCredentials = true + configuration.allowCredentials = false configuration.applyPermitDefaultValues() val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", configuration) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/controller/AuthenticationController.kt b/spring-app/src/main/kotlin/pl/starchasers/up/controller/AuthenticationController.kt deleted file mode 100644 index 0ac00cbd..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/controller/AuthenticationController.kt +++ /dev/null @@ -1,49 +0,0 @@ -package pl.starchasers.up.controller - -import org.springframework.validation.annotation.Validated -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.RestController -import pl.starchasers.up.data.dto.authentication.LoginDTO -import pl.starchasers.up.data.dto.authentication.TokenDTO -import pl.starchasers.up.data.value.RawPassword -import pl.starchasers.up.data.value.Username -import pl.starchasers.up.security.IsUser -import pl.starchasers.up.service.JwtTokenService -import pl.starchasers.up.service.UserService -import java.security.Principal - -@RestController -@RequestMapping("/api/auth") -class AuthenticationController( - private val userService: UserService, - private val jwtTokenService: JwtTokenService -) { - - @PostMapping("/login") - fun login(@RequestBody @Validated loginDTO: LoginDTO): TokenDTO { - return TokenDTO( - jwtTokenService.issueRefreshToken( - userService.getUserFromCredentials( - Username(loginDTO.username), - RawPassword(loginDTO.password) - ) - ) - ) - } - - @IsUser - @PostMapping("/logout") - fun logout(principal: Principal) { - jwtTokenService.invalidateUser(userService.getUser(principal.name.toLong())) - } - - @PostMapping("/getAccessToken") - fun getAccessToken(@Validated @RequestBody tokenDTO: TokenDTO): TokenDTO = - TokenDTO(jwtTokenService.issueAccessToken(tokenDTO.token)) - - @PostMapping("/refreshToken") - fun refreshToken(@Validated @RequestBody tokenDTO: TokenDTO): TokenDTO = - TokenDTO(jwtTokenService.refreshRefreshToken(tokenDTO.token)) -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/controller/ConfigurationController.kt b/spring-app/src/main/kotlin/pl/starchasers/up/controller/ConfigurationController.kt index 134dea64..896b8feb 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/controller/ConfigurationController.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/controller/ConfigurationController.kt @@ -4,29 +4,24 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import pl.starchasers.up.data.dto.configuration.UserConfigurationDTO import pl.starchasers.up.service.ConfigurationService -import pl.starchasers.up.service.UserService -import java.security.Principal @RestController class ConfigurationController( - private val configurationService: ConfigurationService, - private val userService: UserService + private val configurationService: ConfigurationService ) { /** * Returns configuration for anonymous or logged in user, if Authorization header is provided */ @GetMapping("/api/configuration") - fun getConfiguration(principal: Principal?): UserConfigurationDTO { - val user = userService.fromPrincipal(principal) - + fun getConfiguration(): UserConfigurationDTO { return UserConfigurationDTO( - user?.maxTemporaryFileSize?.value ?: configurationService.getAnonymousMaxFileSize().value, - user?.maxFileLifetime?.value ?: configurationService.getAnonymousMaxFileLifetime().value, - user?.defaultFileLifetime?.value ?: configurationService.getAnonymousDefaultFileLifetime().value, - if (user != null) user.maxFileLifetime.value == 0L else configurationService.getAnonymousDefaultFileLifetime().value == 0L, - user?.maxPermanentFileSize?.value ?: if (configurationService.getAnonymousMaxFileLifetime().value == 0L) { - configurationService.getAnonymousMaxFileSize().value + configurationService.getAnonymousMaxFileSize(), + configurationService.getAnonymousMaxFileLifetime(), + configurationService.getAnonymousDefaultFileLifetime(), + configurationService.getAnonymousDefaultFileLifetime() == 0L, + if (configurationService.getAnonymousMaxFileLifetime() == 0L) { + configurationService.getAnonymousMaxFileSize() } else 0 ) } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/controller/UploadController.kt b/spring-app/src/main/kotlin/pl/starchasers/up/controller/UploadController.kt index 861d6b6a..573d69e7 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/controller/UploadController.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/controller/UploadController.kt @@ -13,25 +13,21 @@ import org.springframework.web.multipart.MultipartFile import pl.starchasers.up.data.dto.upload.AuthorizedOperationDTO import pl.starchasers.up.data.dto.upload.FileDetailsDTO import pl.starchasers.up.data.dto.upload.UploadCompleteResponseDTO -import pl.starchasers.up.data.value.* import pl.starchasers.up.exception.AccessDeniedException import pl.starchasers.up.exception.NotFoundException import pl.starchasers.up.service.FileService import pl.starchasers.up.service.FileStorageService -import pl.starchasers.up.service.UserService import pl.starchasers.up.util.BasicResponseDTO import pl.starchasers.up.util.RequestRangeParser import java.io.BufferedInputStream import java.io.IOException import java.nio.charset.Charset -import java.security.Principal @RestController class UploadController( private val fileStorageService: FileStorageService, private val fileService: FileService, - private val requestRangeParser: RequestRangeParser, - private val userService: UserService + private val requestRangeParser: RequestRangeParser ) { private val logger = LoggerFactory.getLogger(this::class.java) @@ -41,19 +37,16 @@ class UploadController( * @param file Uploaded file content */ @PostMapping("/api/upload") - fun anonymousUpload(@RequestParam file: MultipartFile, principal: Principal?): UploadCompleteResponseDTO { - val user = userService.fromPrincipal(principal) - val contentType = ContentType( + fun anonymousUpload(@RequestParam file: MultipartFile): UploadCompleteResponseDTO { + val contentType = if (file.contentType == null || file.contentType!!.isBlank()) "application/octet-stream" else file.contentType!! - ) return fileService.createFile( BufferedInputStream(file.inputStream), - Filename(file.originalFilename ?: "file"), + file.originalFilename ?: "file", contentType, - FileSize(file.size), - user + file.size ) } @@ -63,8 +56,8 @@ class UploadController( */ @GetMapping("/u/{fileKey}") fun getAnonymousUpload(@PathVariable fileKey: String, request: HttpServletRequest, response: HttpServletResponse) { - val (fileEntry, stream) = fileStorageService.getStoredFileRaw(FileKey(fileKey)) - response.contentType = fileEntry.contentType.value + val (fileEntry, stream) = fileStorageService.getStoredFileRaw(fileKey) + response.contentType = fileEntry.contentType response.addHeader( HttpHeaders.ACCEPT_RANGES, @@ -74,20 +67,20 @@ class UploadController( HttpHeaders.CONTENT_DISPOSITION, ContentDisposition .builder("inline") - .filename(fileEntry.filename.value.ifBlank { "file" }, Charset.forName("UTF-8")) + .filename(fileEntry.filename.ifBlank { "file" }, Charset.forName("UTF-8")) .build() .toString() ) try { - val range = requestRangeParser(request.getHeader("Range"), fileEntry.size.value) + val range = requestRangeParser(request.getHeader("Range"), fileEntry.size) if (range.partial) { - response.addHeader(HttpHeaders.CONTENT_RANGE, "bytes ${range.from}-${range.to}/${fileEntry.size.value}") + response.addHeader(HttpHeaders.CONTENT_RANGE, "bytes ${range.from}-${range.to}/${fileEntry.size}") response.addHeader(HttpHeaders.CONTENT_LENGTH, range.responseSize.toString()) response.status = HttpStatus.PARTIAL_CONTENT.value() IOUtils.copyLarge(stream, response.outputStream, range.from, range.responseSize) } else { - response.addHeader(HttpHeaders.CONTENT_LENGTH, fileEntry.size.value.toString()) + response.addHeader(HttpHeaders.CONTENT_LENGTH, fileEntry.size.toString()) IOUtils.copyLarge(stream, response.outputStream) } response.outputStream.flush() @@ -105,13 +98,11 @@ class UploadController( fun verifyFileAccess( @PathVariable fileKey: String, @Validated @RequestBody - operationDto: AuthorizedOperationDTO?, - principal: Principal? + operationDto: AuthorizedOperationDTO? ): BasicResponseDTO { - val fileEntry = fileService.findFileEntry(FileKey(fileKey)) ?: throw NotFoundException() + val fileEntry = fileService.findFileEntry(fileKey) ?: throw NotFoundException() - val user = userService.fromPrincipal(principal) - if (!fileService.verifyFileAccess(fileEntry, operationDto?.accessToken?.let { FileAccessToken(it) }, user)) { + if (!fileService.verifyFileAccess(fileEntry, operationDto?.accessToken)) { throw AccessDeniedException() } return BasicResponseDTO() @@ -121,13 +112,11 @@ class UploadController( fun deleteFile( @PathVariable fileKey: String, @Validated @RequestBody - operationDto: AuthorizedOperationDTO?, - principal: Principal? + operationDto: AuthorizedOperationDTO? ) { - val fileEntry = fileService.findFileEntry(FileKey(fileKey)) ?: throw NotFoundException() - val user = userService.fromPrincipal(principal) + val fileEntry = fileService.findFileEntry(fileKey) ?: throw NotFoundException() - if (!fileService.verifyFileAccess(fileEntry, operationDto?.accessToken?.let { FileAccessToken(it) }, user)) { + if (!fileService.verifyFileAccess(fileEntry, operationDto?.accessToken)) { throw AccessDeniedException() } @@ -138,5 +127,5 @@ class UploadController( * @return Uploaded file metadata */ @GetMapping("/api/u/{fileKey}/details") - fun getFileDetails(@PathVariable fileKey: String): FileDetailsDTO = fileService.getFileDetails(FileKey(fileKey)) + fun getFileDetails(@PathVariable fileKey: String): FileDetailsDTO = fileService.getFileDetails(fileKey) } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/controller/UserController.kt b/spring-app/src/main/kotlin/pl/starchasers/up/controller/UserController.kt deleted file mode 100644 index c331756f..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/controller/UserController.kt +++ /dev/null @@ -1,40 +0,0 @@ -package pl.starchasers.up.controller - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import pl.starchasers.up.data.dto.upload.UploadHistoryEntryDTO -import pl.starchasers.up.exception.AccessDeniedException -import pl.starchasers.up.security.IsUser -import pl.starchasers.up.service.FileService -import pl.starchasers.up.service.UserService -import java.security.Principal - -@RestController -@RequestMapping("/api/user") -class UserController( - val fileService: FileService, - val userService: UserService -) { - - @GetMapping("/history") - @IsUser - fun listUserUploadHistory(principal: Principal, pageable: Pageable): Page { - return fileService.getUploadHistory( - userService.fromPrincipal(principal) ?: throw AccessDeniedException(), - pageable - ).map { - UploadHistoryEntryDTO( - it.filename.value, - it.createdDate, - it.permanent, - it.toDeleteDate, - it.size.value, - it.contentType.value, - it.key.value - ) - } - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/controller/admin/UserAdminController.kt b/spring-app/src/main/kotlin/pl/starchasers/up/controller/admin/UserAdminController.kt deleted file mode 100644 index 97f285c9..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/controller/admin/UserAdminController.kt +++ /dev/null @@ -1,69 +0,0 @@ -package pl.starchasers.up.controller.admin - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.* -import pl.starchasers.up.data.dto.users.CreateUserDTO -import pl.starchasers.up.data.dto.users.UpdateUserDTO -import pl.starchasers.up.data.dto.users.UserDTO -import pl.starchasers.up.data.dto.users.toUserDTO -import pl.starchasers.up.data.value.* -import pl.starchasers.up.exception.AccessDeniedException -import pl.starchasers.up.exception.NotFoundException -import pl.starchasers.up.security.IsAdmin -import pl.starchasers.up.service.UserService -import java.security.Principal - -@RestController -@RequestMapping("/api/admin/users") -class UserAdminController( - private val userService: UserService -) { - - @IsAdmin - @GetMapping("/{userId}") - fun getOne(@PathVariable userId: Long): UserDTO { - return userService.findUser(userId)?.toUserDTO() - ?: throw NotFoundException() - } - - @IsAdmin - @GetMapping("") - fun list(pageable: Pageable): Page { - return userService.listUsers(pageable).map { it.toUserDTO() } - } - - @IsAdmin - @PostMapping("") - fun create(@Validated @RequestBody createUserDTO: CreateUserDTO): UserDTO { - return userService.createUser( - Username(createUserDTO.username), - RawPassword(createUserDTO.password), - if (!createUserDTO.email.isNullOrBlank()) Email(createUserDTO.email) else null, - createUserDTO.role - ).toUserDTO() - } - - @IsAdmin - @PatchMapping("/{userId}") - fun update(@PathVariable userId: Long, @RequestBody userDTO: UpdateUserDTO) { - userService.updateUser( - userId, - userDTO.username?.toUsername(), - if (userDTO.email.isNullOrBlank()) null else Email(userDTO.email), - if (userDTO.password.isNullOrBlank()) null else RawPassword(userDTO.password), - userDTO.role, - userDTO.maxTemporaryFileSize.toFileSize(), - userDTO.maxPermanentFileSize.toFileSize(), - userDTO.defaultFileLifetime.toMilliseconds(), - userDTO.maxFileLifetime.toMilliseconds() - ) - } - - @IsAdmin - @DeleteMapping("/{userId}") - fun delete(@PathVariable userId: Long, principal: Principal) { - userService.deleteUser(userId, principal.name.toLongOrNull() ?: throw AccessDeniedException()) - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/FileDetailsDTO.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/FileDetailsDTO.kt index 3998c034..8312e284 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/FileDetailsDTO.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/FileDetailsDTO.kt @@ -1,6 +1,6 @@ package pl.starchasers.up.data.dto.upload -import java.sql.Timestamp +import java.time.Instant data class FileDetailsDTO( @@ -19,7 +19,7 @@ data class FileDetailsDTO( /** * File will expire at this date and download link will no longer work. Can be null, if file is permanent */ - val expirationDate: Timestamp?, + val expirationDate: Instant?, /** * File size in bytes */ diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadCompleteResponseDTO.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadCompleteResponseDTO.kt index 231d4171..f617e78c 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadCompleteResponseDTO.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadCompleteResponseDTO.kt @@ -1,6 +1,6 @@ package pl.starchasers.up.data.dto.upload -import java.sql.Timestamp +import java.time.Instant data class UploadCompleteResponseDTO( /** @@ -16,5 +16,5 @@ data class UploadCompleteResponseDTO( /** * Date and time after which file will be deleted */ - val toDelete: Timestamp + val toDelete: Instant ) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadHistoryEntryDTO.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadHistoryEntryDTO.kt index a94bb1df..bb94f962 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadHistoryEntryDTO.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/upload/UploadHistoryEntryDTO.kt @@ -1,6 +1,6 @@ package pl.starchasers.up.data.dto.upload -import java.sql.Timestamp +import java.time.Instant data class UploadHistoryEntryDTO( /** @@ -10,7 +10,7 @@ data class UploadHistoryEntryDTO( /** * When was this file uploaded */ - val uploadDate: Timestamp, + val uploadDate: Instant, /** * Whether this file will be automatically deleted */ @@ -18,7 +18,7 @@ data class UploadHistoryEntryDTO( /** * When will this file be automatically deleted. Null if temporary == false */ - val deleteDate: Timestamp?, + val deleteDate: Instant?, /** * File size in bytes */ diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/users/UserDTO.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/users/UserDTO.kt deleted file mode 100644 index 5dfc5150..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/dto/users/UserDTO.kt +++ /dev/null @@ -1,26 +0,0 @@ -package pl.starchasers.up.data.dto.users - -import pl.starchasers.up.data.model.User -import pl.starchasers.up.security.Role - -data class UserDTO( - val id: Long, - val username: String, - val email: String?, - val role: Role, - val maxTemporaryFileSize: Long, - val maxPermanentFileSize: Long, - val defaultFileLifetime: Long, - val maxFileLifetime: Long -) - -fun User.toUserDTO() = UserDTO( - id, - username.value, - email?.value ?: "", - role, - maxTemporaryFileSize.value, - maxPermanentFileSize.value, - defaultFileLifetime.value, - maxFileLifetime.value -) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/ConfigurationEntry.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/ConfigurationEntry.kt index c48ca0b6..ce1fd370 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/ConfigurationEntry.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/ConfigurationEntry.kt @@ -1,25 +1,30 @@ package pl.starchasers.up.data.model -import jakarta.persistence.* - -@Entity -class ConfigurationEntry( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long, - - @Column(name = "configuration_key", unique = true, nullable = false) - @Enumerated(EnumType.STRING) - val key: ConfigurationKey, - - @Column(name = "configuration_value", nullable = false) +import org.ktorm.entity.Entity +import org.ktorm.schema.Table +import org.ktorm.schema.enum +import org.ktorm.schema.long +import org.ktorm.schema.text +import java.time.Duration +import java.time.temporal.ChronoUnit + +interface ConfigurationEntry : Entity { + companion object : Entity.Factory() + val id: Long + var key: ConfigurationKey var value: String -) +} + +object ConfigurationEntries : Table("configuration_entry") { + val id = long("id").primaryKey().bindTo { it.id } + val key = enum("configuration_key").bindTo { it.key } + val value = text("configuration_value").bindTo { it.value } +} enum class ConfigurationKey(val defaultValue: String) { /** * Maximum allowed file size in bytes for anonymous uploads. - * Default value: 1GB + * Default value: 1GiB */ ANONYMOUS_MAX_FILE_SIZE("${1L * 1024 * 1024 * 1024}"), @@ -27,39 +32,11 @@ enum class ConfigurationKey(val defaultValue: String) { * Default time in milliseconds, after which uploaded anonymous file will be deleted * Default value: 1 day */ - ANONYMOUS_DEFAULT_FILE_LIFETIME("${1L * 24 * 60 * 60 * 1000}"), + ANONYMOUS_DEFAULT_FILE_LIFETIME(Duration.of(1, ChronoUnit.DAYS).toMillis().toString()), /** * Maximum configurable by user time in milliseconds, after which uploaded anonymous file will be deleted. * Default value: 1 day */ - ANONYMOUS_MAX_FILE_LIFETIME("${1L * 24 * 60 * 60 * 1000}"), - - /** - * Maximum allowed temporary file size in bytes for logged in users. - * New users will be initialized with this configuration. - * Default value: 5GB - */ - DEFAULT_USER_MAX_TEMPORARY_FILE_SIZE("${5L * 1024 * 1024 * 1024}"), - - /** - * Maximum allowed file size in bytes for permanent uploads for logged in users. - * new users will be initialized with this configuration - * Default value: 512MB - */ - DEFAULT_USER_MAX_PERMANENT_FILE_SIZE("${512L * 1024 * 1024}"), - - /** - * Default time in milliseconds, after which file uploaded by a logged in user will be deleted. - * New users will be initialized with this configuration. - * Default value: 1 day - */ - DEFAULT_USER_DEFAULT_FILE_LIFETIME("${1L * 24 * 60 * 60 * 1000}"), - - /** - * Maximum configurable by user time in milliseconds, after which uploaded file will be deleted. - * New users will be initialized with this configuration - * Default value: 0 (no limit) - */ - DEFAULT_USER_MAX_FILE_LIFETIME("0") + ANONYMOUS_MAX_FILE_LIFETIME(Duration.of(1, ChronoUnit.DAYS).toMillis().toString()), } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileContent.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileContent.kt index 95d3db7a..71f5d3c2 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileContent.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileContent.kt @@ -1,9 +1,8 @@ package pl.starchasers.up.data.model -import pl.starchasers.up.data.value.FileKey import java.io.InputStream class FileContent( - val key: FileKey, + val key: String, val data: InputStream ) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileEntry.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileEntry.kt index 9963c714..712885b6 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileEntry.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/FileEntry.kt @@ -1,45 +1,32 @@ package pl.starchasers.up.data.model -import jakarta.persistence.* -import pl.starchasers.up.data.value.* -import java.sql.Timestamp - -@Entity -class FileEntry( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long, - - @Embedded - val key: FileKey, - - @Embedded - val filename: Filename, - - @Embedded - val contentType: ContentType, - - @Embedded - val password: FilePassword?, - - @Column(nullable = false, unique = false) - val encrypted: Boolean, - - @Column(nullable = false, unique = false) - val createdDate: Timestamp, - - @Column(nullable = true, unique = false) - val toDeleteDate: Timestamp?, - - @Column(nullable = false, unique = false) - val permanent: Boolean, - - @Embedded - val accessToken: FileAccessToken, - - @Embedded - val size: FileSize, - - @ManyToOne(fetch = FetchType.LAZY) - val owner: User? -) +import org.ktorm.entity.Entity +import org.ktorm.schema.* +import java.time.Instant + +interface FileEntry : Entity { + companion object : Entity.Factory() + val id: Long + var accessToken: String? + var contentType: String + var createdAt: Instant + var encrypted: Boolean + var filename: String + var key: String + var password: String? + var size: Long + var toDeleteAt: Instant? +} + +object FileEntries : Table("file_entry") { + val id = long("id").primaryKey().bindTo { it.id } + val accessToken = text("file_access_token").bindTo { it.accessToken } + val contentType = text("content_type").bindTo { it.contentType } + val createdAt = timestamp("created_at").bindTo { it.createdAt } + val encrypted = boolean("encrypted").bindTo { it.encrypted } + val filename = text("filename").bindTo { it.filename } + val key = text("file_key").bindTo { it.key } + val password = text("file_password").bindTo { it.password } + val size = long("file_size").bindTo { it.size } + val deleteAt = timestamp("to_delete_at").bindTo { it.toDeleteAt } +} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/RefreshToken.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/RefreshToken.kt deleted file mode 100644 index 794638b9..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/RefreshToken.kt +++ /dev/null @@ -1,26 +0,0 @@ -package pl.starchasers.up.data.model - -import jakarta.persistence.* -import jakarta.validation.constraints.NotNull -import pl.starchasers.up.data.value.RefreshTokenId -import java.sql.Timestamp - -@Entity -class RefreshToken( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long, - - @NotNull - @ManyToOne(targetEntity = User::class, fetch = FetchType.LAZY) - val user: User, - - @Embedded - val token: RefreshTokenId, - - @Column(nullable = false, updatable = false) - val creationDate: Timestamp, - - @Column(nullable = false, updatable = false) - val expirationDate: Timestamp -) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/User.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/model/User.kt deleted file mode 100644 index 38411976..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/model/User.kt +++ /dev/null @@ -1,43 +0,0 @@ -package pl.starchasers.up.data.model - -import jakarta.persistence.* -import pl.starchasers.up.data.value.* -import pl.starchasers.up.security.Role - -@Entity -class User( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long, - - @Embedded - var username: Username, - - @Embedded - var password: UserPassword, - - @Embedded - var email: Email?, - - @Column(nullable = false, unique = false) - var role: Role, - - @Embedded - @AttributeOverride(name = "value", column = Column(name = "maxTemporaryFileSize")) - var maxTemporaryFileSize: FileSize = FileSize(0), - - @Embedded - @AttributeOverride(name = "value", column = Column(name = "maxPermanentFileSize")) - var maxPermanentFileSize: FileSize = FileSize(0), - - @Embedded - @AttributeOverride(name = "value", column = Column(name = "defaultFileLifetime")) - var defaultFileLifetime: Milliseconds = Milliseconds(0), - - @Embedded - @AttributeOverride(name = "value", column = Column(name = "maxFileLifetime")) - var maxFileLifetime: Milliseconds = Milliseconds(0), - - @OneToMany(fetch = FetchType.LAZY, mappedBy = "owner") - val files: MutableSet = mutableSetOf() -) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Email.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Email.kt deleted file mode 100644 index 6fd7e206..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Email.kt +++ /dev/null @@ -1,20 +0,0 @@ -package pl.starchasers.up.data.value - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable -import pl.starchasers.up.util.validate - -@Embeddable -data class Email( - @Column(name = "email", length = 64) - val value: String -) { - init { - validate(this, Email::value) { - check { it.isNotBlank() } - check { it.length <= 64 } - } - } -} - -fun String?.toEmail(): Email? = this?.let { Email(it) } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/FileKey.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/FileKey.kt deleted file mode 100644 index 4d3632f4..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/FileKey.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pl.starchasers.up.data.value - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable -import pl.starchasers.up.util.validate - -@Embeddable -data class FileKey( - @Column(name = "fileKey", nullable = false, length = 32) - val value: String -) { - init { - validate(this, FileKey::value) { - check { it.isNotEmpty() } - check { it.all { character -> character.isLetterOrDigit() } } - check { it.length <= 32 } - } - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/FileSize.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/FileSize.kt deleted file mode 100644 index 3298601f..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/FileSize.kt +++ /dev/null @@ -1,23 +0,0 @@ -package pl.starchasers.up.data.value - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable -import pl.starchasers.up.util.validate - -@Embeddable -data class FileSize( - @Column(name = "fileSize") - val value: Long -) { - init { - validate(this, FileSize::value) { - check { it >= 0 } - } - } - - operator fun compareTo(fileSize: FileSize): Int { - return (this.value - fileSize.value).toInt() - } -} - -fun Long?.toFileSize(): FileSize? = this?.let { FileSize(it) } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Filename.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Filename.kt deleted file mode 100644 index 8851012f..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Filename.kt +++ /dev/null @@ -1,18 +0,0 @@ -package pl.starchasers.up.data.value - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable -import pl.starchasers.up.util.validate - -@Embeddable -data class Filename( - @Column(name = "filename", nullable = false, length = 1024) - val value: String -) { - init { - validate(this, Filename::value) { - check { it.length <= 1024 } - check { it.isNotBlank() } - } - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/RawPassword.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/RawPassword.kt deleted file mode 100644 index db935ac7..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/RawPassword.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pl.starchasers.up.data.value - -import pl.starchasers.up.util.validate - -data class RawPassword( - val value: String -) { - init { - validate(this, RawPassword::value) { - check { it.isNotEmpty() } - } - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/UserPassword.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/UserPassword.kt deleted file mode 100644 index 067be546..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/UserPassword.kt +++ /dev/null @@ -1,18 +0,0 @@ -package pl.starchasers.up.data.value - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable -import pl.starchasers.up.util.validate - -@Embeddable -data class UserPassword( - @Column(name = "password", length = 160, nullable = false) - val value: String -) { - init { - validate(this, UserPassword::value) { - check { it.isNotBlank() } - check { it.length < 160 } - } - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Username.kt b/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Username.kt deleted file mode 100644 index d8638e11..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/data/value/Username.kt +++ /dev/null @@ -1,21 +0,0 @@ -package pl.starchasers.up.data.value - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable -import pl.starchasers.up.util.validate - -@Embeddable -data class Username( - @Column(name = "username", length = 32, unique = true, updatable = true, nullable = false) - val value: String -) { - init { - validate(this, Username::value) { - check { it.isNotBlank() } - check { it.length < 32 } - check { it.all { character -> character.isLetterOrDigit() } } - } - } -} - -fun String.toUsername(): Username = Username(this) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/ConfigurationRepository.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/ConfigurationRepository.kt index 20e9d8b6..eebda1e1 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/repository/ConfigurationRepository.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/repository/ConfigurationRepository.kt @@ -1,12 +1,25 @@ package pl.starchasers.up.repository -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository +import org.ktorm.database.Database +import org.ktorm.dsl.* +import org.springframework.stereotype.Service +import pl.starchasers.up.data.model.ConfigurationEntries import pl.starchasers.up.data.model.ConfigurationEntry import pl.starchasers.up.data.model.ConfigurationKey -@Repository -interface ConfigurationRepository : JpaRepository { +@Service +class ConfigurationRepository( + database: Database +) : StandardRepository(ConfigurationEntries, database) { - fun findFirstByKey(key: ConfigurationKey): ConfigurationEntry? + fun findFirstByKey(key: ConfigurationKey): ConfigurationEntry? { + return database + .from(table) + .select() + .where(table.key eq key) + .limit(1) + .map { row -> table.createEntity(row) } + .firstOrNull() + } } + diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/FileEntryRepository.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/FileEntryRepository.kt index 6638778b..bded308f 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/repository/FileEntryRepository.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/repository/FileEntryRepository.kt @@ -1,30 +1,29 @@ package pl.starchasers.up.repository -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query +import org.ktorm.database.Database +import org.ktorm.dsl.* +import org.springframework.stereotype.Service +import pl.starchasers.up.data.model.FileEntries import pl.starchasers.up.data.model.FileEntry -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.FileKey +import java.time.Instant -interface FileEntryRepository : JpaRepository { +@Service +class FileEntryRepository( + database: Database +) : StandardRepository(FileEntries, database) { - @Query( - """ - from FileEntry f where f.key=:key - """ - ) - fun findExistingFileByKey(key: FileKey): FileEntry? + fun findExistingFileByKey(key: String): FileEntry? = + database.from(table) + .select() + .where { table.key eq key } + .map { table.createEntity(it) } + .firstOrNull() - @Query( - """ - from FileEntry f - where f.permanent = false - and f.toDeleteDate < current_timestamp - """ - ) - fun findExpiredFiles(): Set + fun findExpiredFiles(): Set = + database.from(table) + .select() + .where { table.deleteAt.isNotNull() and table.deleteAt.less(Instant.now()) } + .map { table.createEntity(it) } + .toSet() - fun findAllByOwner(owner: User, pageable: Pageable): Page } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/QueryResultContainer.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/QueryResultContainer.kt new file mode 100644 index 00000000..532ad3df --- /dev/null +++ b/spring-app/src/main/kotlin/pl/starchasers/up/repository/QueryResultContainer.kt @@ -0,0 +1,16 @@ +package pl.starchasers.up.repository + +import org.ktorm.dsl.Query +import org.ktorm.dsl.QueryRowSet +import org.ktorm.dsl.asIterable + +data class QueryResultContainer( + val result: List, + val totalRecords: Int +) + +fun Query.transform(how: Iterable.() -> List) = + QueryResultContainer( + how.invoke(this.asIterable()), + this.totalRecordsInAllPages + ) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/RefreshTokenRepository.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/RefreshTokenRepository.kt deleted file mode 100644 index 573c8c9f..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/repository/RefreshTokenRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package pl.starchasers.up.repository - -import org.springframework.data.jpa.repository.JpaRepository -import pl.starchasers.up.data.model.RefreshToken -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.RefreshTokenId - -interface RefreshTokenRepository : JpaRepository { - fun findFirstByTokenAndUser(token: RefreshTokenId, user: User): RefreshToken? - - fun findAllByUser(user: User): List - - fun deleteAllByUser(user: User) - - fun deleteAllByToken(token: RefreshTokenId) -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/StandardRepository.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/StandardRepository.kt new file mode 100644 index 00000000..2597904b --- /dev/null +++ b/spring-app/src/main/kotlin/pl/starchasers/up/repository/StandardRepository.kt @@ -0,0 +1,90 @@ +package pl.starchasers.up.repository + +import jakarta.persistence.EntityNotFoundException +import org.ktorm.database.Database +import org.ktorm.dsl.* +import org.ktorm.entity.* +import org.ktorm.schema.ColumnDeclaring +import org.ktorm.schema.Table +import org.ktorm.support.postgresql.BulkInsertStatementBuilder +import org.ktorm.support.postgresql.bulkInsert +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable + +open class StandardRepository, T : Table>( + internal open val table: T, + open val database: Database +) { + + private val logger: Logger = LoggerFactory.getLogger(this::class.java) + + fun update(record: E): E = + database + .sequenceOf(table) + .update(record) + .let { + if (it == 0) throw EntityNotFoundException() + record + } + + fun insert(record: E) = + database + .sequenceOf(table) + .add(record) + .let { + // Not safe, WILL CRASH if entity doesn't have column "id" of type Long + record["id"] as Long + } + + /** + * Slower version, see [bulkInsert] for faster version + */ + fun saveAll(records: Iterable) = + database + .sequenceOf(table) + .let { sequence -> + records.map { record -> + sequence.add(record) + } + } + + fun bulkInsert(records: BulkInsertStatementBuilder.(T) -> Unit) = + database.bulkInsert(table, records) + + fun deleteAll(): Int { + logger.warn("Deleting everything from ${table.tableName}") + return database.deleteAll(table) + } + + fun findByPredicate(predicate: () -> ColumnDeclaring): List = + findByPredicate(predicate.invoke()) + + fun deleteByPredicate(predicate: (T) -> ColumnDeclaring): Int = + database.delete(table, predicate).let { + if (it == 0) throw EntityNotFoundException() + logger.warn("Deleted $it rows") + return it + } + + fun findAll() = database.from(table).select().map { table.createEntity(it) } + + fun findByPredicate(predicate: ColumnDeclaring): List = + database.from(table).select().where(predicate).map { table.createEntity(it) } + + fun findAll(pageable: Pageable): Page = + database.from(table) + .select() + .limit(pageable.pageSize) + .offset(pageable.offset.toInt()) + .map { table.createEntity(it) } + .let { + PageImpl(it, pageable, it.size.toLong()) + } + + fun count(): Int = database.sequenceOf(table).count() + +} + diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/UploadRepository.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/UploadRepository.kt index 6a9c3eb2..1b94448a 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/repository/UploadRepository.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/repository/UploadRepository.kt @@ -5,7 +5,6 @@ import org.apache.commons.io.IOUtils import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository import pl.starchasers.up.data.model.FileContent -import pl.starchasers.up.data.value.FileKey import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -14,11 +13,11 @@ import java.nio.file.Paths interface UploadRepository { fun save(fileContent: FileContent) - fun find(key: FileKey): FileContent? + fun find(key: String): FileContent? - fun delete(key: FileKey) + fun delete(key: String) - fun exists(key: FileKey): Boolean + fun exists(key: String): Boolean } @Repository @@ -57,8 +56,8 @@ class UploadRepositoryImpl() : UploadRepository { IOUtils.closeQuietly(outputStream) } - override fun find(key: FileKey): FileContent? { - if (key.value.length < 4) throw IllegalArgumentException("Malformed file key") + override fun find(key: String): FileContent? { + if (key.length < 4) throw IllegalArgumentException("Malformed file key") val file = getFileFromKey(key) @@ -69,7 +68,7 @@ class UploadRepositoryImpl() : UploadRepository { return FileContent(key, FileInputStream(file)) } - override fun delete(key: FileKey) { + override fun delete(key: String) { val file = getFileFromKey(key) if (!file.exists()) return @@ -81,7 +80,7 @@ class UploadRepositoryImpl() : UploadRepository { deleteDirIfEmpty(file.parentFile.parentFile) } - override fun exists(key: FileKey): Boolean { + override fun exists(key: String): Boolean { val file = getFileFromKey(key) if (file.isDirectory || !file.isFile) throwExceptionDataStoreCorrupted(key) @@ -89,15 +88,15 @@ class UploadRepositoryImpl() : UploadRepository { return file.exists() } - private fun getFileFromKey(key: FileKey): File = Paths.get( + private fun getFileFromKey(key: String): File = Paths.get( dataStorePath, - key.value.substring(0, 2), - key.value.substring(2, 4), - key.value + key.substring(0, 2), + key.substring(2, 4), + key ).toFile() - private fun throwExceptionDataStoreCorrupted(key: FileKey): Nothing = - throw IllegalStateException("Requested file ${key.value} is not a regular file - datastore corrupted!") + private fun throwExceptionDataStoreCorrupted(key: String): Nothing = + throw IllegalStateException("Requested file $key is not a regular file - datastore corrupted!") private fun deleteDirIfEmpty(file: File) { if (file.listFiles()?.isEmpty() ?: throw IllegalArgumentException("Not a directory!")) { diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/repository/UserRepository.kt b/spring-app/src/main/kotlin/pl/starchasers/up/repository/UserRepository.kt deleted file mode 100644 index 8efa072f..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/repository/UserRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pl.starchasers.up.repository - -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.Username - -@Repository -interface UserRepository : JpaRepository { - fun findFirstByUsername(username: Username): User? - - fun findFirstById(id: Long): User? -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtConfiguration.kt b/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtConfiguration.kt deleted file mode 100644 index 26cb9aea..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtConfiguration.kt +++ /dev/null @@ -1,32 +0,0 @@ -package pl.starchasers.up.security - -import io.jsonwebtoken.SignatureAlgorithm -import org.slf4j.LoggerFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import java.security.SecureRandom -import java.util.* -import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec - -@Configuration -class JwtConfiguration { - - private val logger = LoggerFactory.getLogger(this::class.java) - - @Bean - fun jwtSigningKey(jwtProperties: JwtProperties): JwtSigningKey { - val secretBytes = if (jwtProperties.base64Secret.isNotBlank()) { - Base64.getDecoder().decode(jwtProperties.base64Secret) - } else { - logger.info("JWT secret not defined. Generating random secret...") - ByteArray(256) - .apply { SecureRandom().nextBytes(this) } - } - return JwtSigningKey(SecretKeySpec(secretBytes, SignatureAlgorithm.HS256.jcaName)) - } -} - -data class JwtSigningKey( - val key: SecretKey -) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtProperties.kt b/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtProperties.kt deleted file mode 100644 index 2d728160..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtProperties.kt +++ /dev/null @@ -1,10 +0,0 @@ -package pl.starchasers.up.security - -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.validation.annotation.Validated - -@ConfigurationProperties("up.jwt") -@Validated -data class JwtProperties constructor( - val base64Secret: String = "" -) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtTokenFilter.kt b/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtTokenFilter.kt deleted file mode 100644 index c48f4402..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/security/JwtTokenFilter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package pl.starchasers.up.security - -import jakarta.servlet.FilterChain -import jakarta.servlet.ServletRequest -import jakarta.servlet.ServletResponse -import jakarta.servlet.http.HttpServletRequest -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.web.filter.GenericFilterBean -import pl.starchasers.up.service.JwtTokenService -import pl.starchasers.up.service.ROLE_KEY - -class JwtTokenFilter( - private val tokenService: JwtTokenService -) : GenericFilterBean() { - - override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain) { - val token = (request as HttpServletRequest).getHeader("Authorization") - - try { - val claims = tokenService.parseToken(token.removePrefix("Bearer ")) - - val authorities = mutableListOf() - authorities.add(SimpleGrantedAuthority(Role.USER.roleString())) - if (Role.valueOf(claims[ROLE_KEY] as String) == Role.ADMIN) authorities.add(SimpleGrantedAuthority(Role.ADMIN.roleString())) - - SecurityContextHolder.getContext().authentication = - UsernamePasswordAuthenticationToken(claims.subject, null, authorities) - } catch (e: Exception) { - SecurityContextHolder.clearContext() - } - - chain.doFilter(request, response) - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/service/AutomaticCleanupService.kt b/spring-app/src/main/kotlin/pl/starchasers/up/service/AutomaticCleanupService.kt index af8c8b98..e64d64f7 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/service/AutomaticCleanupService.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/service/AutomaticCleanupService.kt @@ -29,7 +29,7 @@ class AutomaticCleanupService( if (uploadRepository.exists(it.key)) { uploadRepository.delete(it.key) } - fileEntryRepository.delete(it) + it.delete() } catch (e: IOException) { val sw = StringWriter() val pw = PrintWriter(sw) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/service/ConfigurationService.kt b/spring-app/src/main/kotlin/pl/starchasers/up/service/ConfigurationService.kt index f8cf00f1..4d5d231b 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/service/ConfigurationService.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/service/ConfigurationService.kt @@ -2,21 +2,12 @@ package pl.starchasers.up.service import jakarta.annotation.PostConstruct import org.springframework.stereotype.Service -import pl.starchasers.up.data.dto.configuration.UpdateUserConfigurationDTO import pl.starchasers.up.data.model.ConfigurationEntry import pl.starchasers.up.data.model.ConfigurationKey -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.FileSize -import pl.starchasers.up.data.value.Milliseconds import pl.starchasers.up.exception.BadRequestException import pl.starchasers.up.repository.ConfigurationRepository -import pl.starchasers.up.repository.UserRepository interface ConfigurationService { - fun applyDefaultConfiguration(user: User) - - fun updateUserConfiguration(user: User, configuration: UpdateUserConfigurationDTO) - fun setConfigurationOption(key: ConfigurationKey, value: String) fun getConfigurationOption(key: ConfigurationKey): String @@ -25,59 +16,22 @@ interface ConfigurationService { fun updateGlobalConfiguration(configuration: Map) - fun getAnonymousMaxFileSize(): FileSize + fun getAnonymousMaxFileSize(): Long - fun getAnonymousDefaultFileLifetime(): Milliseconds + fun getAnonymousDefaultFileLifetime(): Long - fun getAnonymousMaxFileLifetime(): Milliseconds + fun getAnonymousMaxFileLifetime(): Long } @Service class ConfigurationServiceImpl( - private val configurationRepository: ConfigurationRepository, - private val userRepository: UserRepository + private val configurationRepository: ConfigurationRepository ) : ConfigurationService { - override fun applyDefaultConfiguration(user: User) { - user.apply { - maxTemporaryFileSize = FileSize( - getConfigurationOption( - ConfigurationKey.DEFAULT_USER_MAX_TEMPORARY_FILE_SIZE - ).toLong() - ) - maxPermanentFileSize = FileSize( - getConfigurationOption( - ConfigurationKey.DEFAULT_USER_MAX_PERMANENT_FILE_SIZE - ).toLong() - ) - defaultFileLifetime = Milliseconds( - getConfigurationOption( - ConfigurationKey.DEFAULT_USER_DEFAULT_FILE_LIFETIME - ).toLong() - ) - maxFileLifetime = Milliseconds( - getConfigurationOption( - ConfigurationKey.DEFAULT_USER_MAX_FILE_LIFETIME - ).toLong() - ) - } - } - - override fun updateUserConfiguration(user: User, configuration: UpdateUserConfigurationDTO) { - user.apply { - maxTemporaryFileSize = FileSize(configuration.maxTemporaryFileSize) - maxPermanentFileSize = FileSize(configuration.maxPermanentFileSize) - defaultFileLifetime = Milliseconds(configuration.defaultFileLifetime) - maxFileLifetime = Milliseconds(configuration.maxFileLifetime) - } - userRepository.save(user) - } - override fun setConfigurationOption(key: ConfigurationKey, value: String) { if (value.toLongOrNull() == null) throw BadRequestException("Value must be of type Long.") // TODO change if more data types are required - val entry = configurationRepository.findFirstByKey(key) ?: ConfigurationEntry(0, key, value) - entry.value = value - configurationRepository.save(entry) + configurationRepository.findFirstByKey(key)?.apply { this.value = value }?.flushChanges() + ?: configurationRepository.insert(ConfigurationEntry { this.key = key; this.value = value }) } override fun getConfigurationOption(key: ConfigurationKey): String { @@ -86,7 +40,7 @@ class ConfigurationServiceImpl( override fun getGlobalConfiguration(): Map = mapOf( - *ConfigurationKey.values().map { + *ConfigurationKey.entries.map { Pair(it, configurationRepository.findFirstByKey(it)?.value ?: it.defaultValue) }.toTypedArray() ) @@ -97,23 +51,23 @@ class ConfigurationServiceImpl( configuration.forEach { setConfigurationOption(it.key, it.value) } } - override fun getAnonymousMaxFileSize(): FileSize = - FileSize(getConfigurationOption(ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE).toLong()) + override fun getAnonymousMaxFileSize(): Long = + getConfigurationOption(ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE).toLong() - override fun getAnonymousDefaultFileLifetime(): Milliseconds = - Milliseconds(getConfigurationOption(ConfigurationKey.ANONYMOUS_DEFAULT_FILE_LIFETIME).toLong()) + override fun getAnonymousDefaultFileLifetime(): Long = + getConfigurationOption(ConfigurationKey.ANONYMOUS_DEFAULT_FILE_LIFETIME).toLong() - override fun getAnonymousMaxFileLifetime(): Milliseconds = - Milliseconds(getConfigurationOption(ConfigurationKey.ANONYMOUS_MAX_FILE_LIFETIME).toLong()) + override fun getAnonymousMaxFileLifetime(): Long = + getConfigurationOption(ConfigurationKey.ANONYMOUS_MAX_FILE_LIFETIME).toLong() @PostConstruct private fun initialize() { - ConfigurationKey.values().forEach { key -> + ConfigurationKey.entries.forEach { key -> val entry = configurationRepository.findFirstByKey(key) if (entry == null) { - val defaultEntry = ConfigurationEntry(0, key, key.defaultValue) - configurationRepository.save(defaultEntry) + val defaultEntry = ConfigurationEntry { this.key = key; this.value = key.defaultValue } + configurationRepository.insert(defaultEntry) } } } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/service/FileService.kt b/spring-app/src/main/kotlin/pl/starchasers/up/service/FileService.kt index 261bda3a..a12dd43e 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/service/FileService.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/service/FileService.kt @@ -8,37 +8,34 @@ import pl.starchasers.up.data.dto.upload.FileDetailsDTO import pl.starchasers.up.data.dto.upload.UploadCompleteResponseDTO import pl.starchasers.up.data.model.ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE import pl.starchasers.up.data.model.FileEntry -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.* import pl.starchasers.up.exception.FileTooLargeException import pl.starchasers.up.exception.NotFoundException import pl.starchasers.up.repository.FileEntryRepository import pl.starchasers.up.util.Util import java.io.InputStream -import java.sql.Timestamp -import java.time.LocalDateTime +import java.time.Instant +import java.time.temporal.ChronoUnit interface FileService { fun createFile( tmpFile: InputStream, - filename: Filename, - contentType: ContentType, - size: FileSize, - user: User? + filename: String, + contentType: String, + size: Long ): UploadCompleteResponseDTO - fun verifyFileAccess(fileEntry: FileEntry, accessToken: FileAccessToken?, user: User?): Boolean + fun verifyFileAccess(fileEntry: FileEntry, accessToken: String?): Boolean - fun verifyFileAccess(fileKey: FileKey, accessToken: FileAccessToken?, user: User?): Boolean + fun verifyFileAccess(fileKey: String, accessToken: String?): Boolean - fun findFileEntry(fileKey: FileKey): FileEntry? + fun findFileEntry(fileKey: String): FileEntry? - fun getFileDetails(fileKey: FileKey): FileDetailsDTO + fun getFileDetails(fileKey: String): FileDetailsDTO fun deleteFile(fileEntry: FileEntry) - fun getUploadHistory(user: User, pageable: Pageable): Page + fun getUploadHistory(pageable: Pageable): Page } @Service @@ -54,71 +51,61 @@ class FileServiceImpl( @Transactional override fun createFile( tmpFile: InputStream, - filename: Filename, - contentType: ContentType, - size: FileSize, - user: User? + filename: String, + contentType: String, + size: Long ): UploadCompleteResponseDTO { val actualContentType = when { - contentType.value.isBlank() -> ContentType("application/octet-stream") - contentType.value == "text/plain" -> ContentType( - "text/plain; charset=" + charsetDetectionService.detect( - tmpFile - ) - ) + contentType.isBlank() -> "application/octet-stream" + contentType == "text/plain" -> "text/plain; charset=" + charsetDetectionService.detect(tmpFile) else -> contentType } - val personalLimit: FileSize = user?.maxTemporaryFileSize - ?: FileSize(configurationService.getConfigurationOption(ANONYMOUS_MAX_FILE_SIZE).toLong()) + val personalLimit: Long = configurationService.getConfigurationOption(ANONYMOUS_MAX_FILE_SIZE).toLong() if (size > personalLimit) throw FileTooLargeException() val key = fileStorageService.storeNonPermanentFile(tmpFile, filename) // TODO check key already used val accessToken = generateFileAccessToken() - val toDeleteDate = Timestamp.valueOf(LocalDateTime.now().plusDays(1)) - val fileEntry = FileEntry( - 0, - key, - filename, - actualContentType, - null, - false, - Timestamp.valueOf(LocalDateTime.now()), - toDeleteDate, - false, - accessToken, - size, - user - ) - - fileEntryRepository.save(fileEntry) - - return UploadCompleteResponseDTO(key.value, accessToken.value, toDeleteDate) + val toDeleteAt = Instant.now().plus(1, ChronoUnit.DAYS) + + val fileEntry = FileEntry { + this.accessToken = accessToken + this.contentType = actualContentType + this.createdAt = Instant.now() + this.encrypted = false + this.filename = filename + this.key = key + this.password = null + this.size = size + this.toDeleteAt = toDeleteAt + } + + fileEntryRepository.insert(fileEntry) + + return UploadCompleteResponseDTO(key, accessToken, toDeleteAt) } - override fun verifyFileAccess(fileEntry: FileEntry, accessToken: FileAccessToken?, user: User?): Boolean { - return (user != null && fileEntry.owner == user) || fileEntry.accessToken == accessToken + override fun verifyFileAccess(fileEntry: FileEntry, accessToken: String?): Boolean { + return (fileEntry.accessToken != null) && fileEntry.accessToken == accessToken } - override fun verifyFileAccess(fileKey: FileKey, accessToken: FileAccessToken?, user: User?): Boolean = + override fun verifyFileAccess(fileKey: String, accessToken: String?): Boolean = fileEntryRepository .findExistingFileByKey(fileKey) - ?.let { verifyFileAccess(it, accessToken, user) } ?: false + ?.let { verifyFileAccess(it, accessToken) } == true - override fun findFileEntry(fileKey: FileKey): FileEntry? = fileEntryRepository.findExistingFileByKey(fileKey) + override fun findFileEntry(fileKey: String): FileEntry? = fileEntryRepository.findExistingFileByKey(fileKey) - override fun getFileDetails(fileKey: FileKey): FileDetailsDTO = + override fun getFileDetails(fileKey: String): FileDetailsDTO = fileEntryRepository.findExistingFileByKey(fileKey)?.let { FileDetailsDTO( - it.key.value, - it.filename.value, - it.permanent, - if (!it.permanent) it.toDeleteDate - ?: throw IllegalStateException("Temporary file without delete date! FileKey: ${it.key}") - else null, - it.size.value, - it.contentType.value + it.key, + it.filename, + it.toDeleteAt == null, + it.toDeleteAt, + it.size, + it.contentType ) } ?: throw NotFoundException() @@ -126,9 +113,9 @@ class FileServiceImpl( fileStorageService.deleteFile(fileEntry) } - override fun getUploadHistory(user: User, pageable: Pageable): Page { - return fileEntryRepository.findAllByOwner(user, pageable) + override fun getUploadHistory(pageable: Pageable): Page { + return fileEntryRepository.findAll(pageable) } - private fun generateFileAccessToken(): FileAccessToken = FileAccessToken(util.secureAlphanumericRandomString(128)) + private fun generateFileAccessToken(): String = util.secureAlphanumericRandomString(128) } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/service/FileStorageService.kt b/spring-app/src/main/kotlin/pl/starchasers/up/service/FileStorageService.kt index 2ffdb2be..245fc087 100644 --- a/spring-app/src/main/kotlin/pl/starchasers/up/service/FileStorageService.kt +++ b/spring-app/src/main/kotlin/pl/starchasers/up/service/FileStorageService.kt @@ -4,8 +4,6 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import pl.starchasers.up.data.model.FileContent import pl.starchasers.up.data.model.FileEntry -import pl.starchasers.up.data.value.FileKey -import pl.starchasers.up.data.value.Filename import pl.starchasers.up.exception.NotFoundException import pl.starchasers.up.repository.FileEntryRepository import pl.starchasers.up.repository.UploadRepository @@ -13,9 +11,9 @@ import pl.starchasers.up.util.Util import java.io.InputStream interface FileStorageService { - fun storeNonPermanentFile(tmpFile: InputStream, filename: Filename): FileKey + fun storeNonPermanentFile(tmpFile: InputStream, filename: String): String - fun getStoredFileRaw(key: FileKey): Pair + fun getStoredFileRaw(key: String): Pair fun deleteFile(fileEntry: FileEntry) } @@ -33,15 +31,15 @@ class FileStorageServiceImpl( private val util = Util() @Transactional - override fun storeNonPermanentFile(tmpFile: InputStream, filename: Filename): FileKey { - val key = FileKey(util.secureReadableRandomString(NON_PERMANENT_FILE_KEY_LENGTH)) + override fun storeNonPermanentFile(tmpFile: InputStream, filename: String): String { + val key = util.secureReadableRandomString(NON_PERMANENT_FILE_KEY_LENGTH) val fileContent = FileContent(key, tmpFile) uploadRepository.save(fileContent) return key } - override fun getStoredFileRaw(key: FileKey): Pair { + override fun getStoredFileRaw(key: String): Pair { val fileEntry = fileEntryRepository.findExistingFileByKey(key) ?: throw NotFoundException() val upload = uploadRepository.find(key) ?: throw NotFoundException() // TODO handle possible data inconsistency @@ -51,6 +49,6 @@ class FileStorageServiceImpl( override fun deleteFile(fileEntry: FileEntry) { uploadRepository.delete(fileEntry.key) - fileEntryRepository.delete(fileEntry) + fileEntry.delete() } } diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/service/JwtTokenService.kt b/spring-app/src/main/kotlin/pl/starchasers/up/service/JwtTokenService.kt deleted file mode 100644 index 09a2bff7..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/service/JwtTokenService.kt +++ /dev/null @@ -1,133 +0,0 @@ -package pl.starchasers.up.service - -import io.jsonwebtoken.Claims -import io.jsonwebtoken.Jwts -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import pl.starchasers.up.data.model.RefreshToken -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.RefreshTokenId -import pl.starchasers.up.exception.JwtTokenException -import pl.starchasers.up.repository.RefreshTokenRepository -import pl.starchasers.up.security.JwtSigningKey -import java.sql.Timestamp -import java.time.LocalDateTime -import java.util.* - -const val TOKEN_ID_KEY = "tokenId" -const val ROLE_KEY = "role" - -interface JwtTokenService { - fun issueRefreshToken(user: User): String - - fun refreshRefreshToken(oldRefreshToken: String): String - - fun issueAccessToken(refreshToken: String): String - - fun verifyRefreshToken(token: RefreshTokenId, user: User) - - fun parseToken(token: String): Claims - - fun invalidateUser(user: User) - - fun invalidateRefreshToken(refreshToken: String) -} - -@Service -class JwtTokenServiceImpl( - private val refreshTokenRepository: RefreshTokenRepository, - private val userService: UserService, - private val jwtSigningKey: JwtSigningKey -) : JwtTokenService { - - private val refreshTokenValidTime: Long = 7 * 24 * 60 * 60 * 1000 - private val accessTokenValidTime: Long = 15 * 60 * 1000 - private val logger = LoggerFactory.getLogger(this::class.java) - - override fun issueRefreshToken(user: User): String { - val claims = Jwts.claims().setSubject(user.id.toString()) - val now = Date() - val tokenId = RefreshTokenId(UUID.randomUUID().toString()) - - claims[TOKEN_ID_KEY] = tokenId.value - val refreshToken = RefreshToken( - 0, - user, - tokenId, - Timestamp.valueOf(LocalDateTime.now()), - Timestamp.valueOf(LocalDateTime.now().plusNanos(refreshTokenValidTime * 1000)) - ) - - refreshTokenRepository.save(refreshToken) - - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(Date(now.time + refreshTokenValidTime)) - .signWith(jwtSigningKey.key) - .compact() - } - - override fun refreshRefreshToken(oldRefreshToken: String): String { - val oldClaims = parseToken(oldRefreshToken) - val user = userService.getUser(oldClaims.subject.toLong()) - - verifyRefreshToken(oldClaims.getTokenId(), user) - - return issueRefreshToken(user) - } - - override fun issueAccessToken(refreshToken: String): String { - val refreshTokenClaims = parseToken(refreshToken) - val user = userService.getUser(refreshTokenClaims.subject.toLong()) - - val claims = Jwts.claims().setSubject(user.id.toString()) - claims[ROLE_KEY] = user.role - - verifyRefreshToken(RefreshTokenId(refreshTokenClaims[TOKEN_ID_KEY] as String), user) - - val now = Date() - - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(Date(now.time + accessTokenValidTime)) - .signWith(jwtSigningKey.key) - .compact() - } - - override fun verifyRefreshToken(token: RefreshTokenId, user: User) { - refreshTokenRepository.findFirstByTokenAndUser(token, user) ?: throw JwtTokenException("Invalid refresh token.") - } - - override fun parseToken(token: String): Claims { - try { - return Jwts.parserBuilder() - .setSigningKey(jwtSigningKey.key) - .build() - .parseClaimsJws(token) - .body - } catch (e: Exception) { - logger.warn("error parsing jwt token", e) - throw JwtTokenException("Invalid or corrupted token.") - } - } - - @Transactional - override fun invalidateUser(user: User) { - refreshTokenRepository.deleteAllByUser(user) - } - - @Transactional - override fun invalidateRefreshToken(refreshToken: String) { - val claims = parseToken(refreshToken) - refreshTokenRepository.deleteAllByToken(claims.getTokenId()) - } -} - -fun Claims.getTokenId(): RefreshTokenId { - val idString = this[TOKEN_ID_KEY] - if (idString !is String) throw JwtTokenException("Invalid refresh token") - return RefreshTokenId(idString) -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/service/UserService.kt b/spring-app/src/main/kotlin/pl/starchasers/up/service/UserService.kt deleted file mode 100644 index 09cb83b9..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/service/UserService.kt +++ /dev/null @@ -1,136 +0,0 @@ -package pl.starchasers.up.service - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.stereotype.Service -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.* -import pl.starchasers.up.exception.AccessDeniedException -import pl.starchasers.up.exception.BadRequestException -import pl.starchasers.up.exception.UserException -import pl.starchasers.up.repository.UserRepository -import pl.starchasers.up.security.Role -import pl.starchasers.up.util.encode -import pl.starchasers.up.util.matches -import java.security.Principal - -const val ROOT_USER_NAME = "root" - -interface UserService { - fun getUser(id: Long): User - - fun getUser(username: Username): User - - fun findUser(id: Long): User? - - fun findUser(username: Username): User? - - fun fromPrincipal(principal: Principal?): User? - - fun createUser(username: Username, rawPassword: RawPassword, email: Email?, role: Role): User - - fun getUserFromCredentials(username: Username, password: RawPassword): User - - fun listUsers(pageable: Pageable): Page - - fun updateUser( - userId: Long, - username: Username?, - email: Email?, - password: RawPassword?, - role: Role?, - maxTemporaryFileSize: FileSize?, - maxPermanentFileSize: FileSize?, - defaultFileLifetime: Milliseconds?, - maxFileLifetime: Milliseconds? - ) - - fun deleteUser(user: User) - - fun deleteUser(userId: Long, thisUserId: Long) -} - -@Service -class UserServiceImpl( - private val userRepository: UserRepository, - private val passwordEncoder: PasswordEncoder, - private val configurationService: ConfigurationService -) : UserService { - override fun getUser(id: Long): User = userRepository.findFirstById(id) - ?: throw UserException("User with ID `$id` doesn't exist") - - override fun getUser(username: Username): User = userRepository.findFirstByUsername(username) - ?: throw UserException("User with username '$username' doesn't exist") - - override fun findUser(id: Long): User? = userRepository.findFirstById(id) - - override fun findUser(username: Username): User? = userRepository.findFirstByUsername(username) - - override fun fromPrincipal(principal: Principal?): User? { - if (principal == null) return null - return findUser(principal.name.toLong()) - } - - override fun createUser(username: Username, rawPassword: RawPassword, email: Email?, role: Role): User { - val oldUser = findUser(username) - if (oldUser != null) throw BadRequestException("Username already taken.") - val user = User( - 0, - username, - passwordEncoder.encode(rawPassword), - email, - role - ) - configurationService.applyDefaultConfiguration(user) - userRepository.save(user) - return user - } - - override fun getUserFromCredentials(username: Username, password: RawPassword): User = - findUser(username)?.takeIf { passwordEncoder.matches(password, it.password) } - ?: throw AccessDeniedException("Incorrect username or password") - - override fun listUsers(pageable: Pageable): Page = userRepository.findAll(pageable) - - override fun updateUser( - userId: Long, - username: Username?, - email: Email?, - password: RawPassword?, - role: Role?, - maxTemporaryFileSize: FileSize?, - maxPermanentFileSize: FileSize?, - defaultFileLifetime: Milliseconds?, - maxFileLifetime: Milliseconds? - ) { - val user = findUser(userId) ?: throw BadRequestException("User does not exist.") - - if (username != null && user.username != username) { - if (findUser(username) != null) throw BadRequestException("Username already taken") - user.username = username - } - - user.email = email - password?.let { user.password = passwordEncoder.encode(it) } - role?.let { user.role = it } - maxTemporaryFileSize?.let { user.maxTemporaryFileSize = it } - maxPermanentFileSize?.let { user.maxPermanentFileSize = it } - defaultFileLifetime?.let { user.defaultFileLifetime = it } - maxFileLifetime?.let { user.maxFileLifetime = it } - - userRepository.save(user) - } - - override fun deleteUser(user: User) { - userRepository.delete(user) - } - - override fun deleteUser(userId: Long, thisUserId: Long) { - if (userId == thisUserId) throw BadRequestException("Cannot delete current user.") - if (findUser(userId)?.username?.value == ROOT_USER_NAME) throw AccessDeniedException() - - val toDelete = findUser(userId) ?: throw BadRequestException("User does not exist") - userRepository.delete(toDelete) - } -} diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/util/PasswordEncoderExtensions.kt b/spring-app/src/main/kotlin/pl/starchasers/up/util/PasswordEncoderExtensions.kt deleted file mode 100644 index d080a7db..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/util/PasswordEncoderExtensions.kt +++ /dev/null @@ -1,10 +0,0 @@ -package pl.starchasers.up.util - -import org.springframework.security.crypto.password.PasswordEncoder -import pl.starchasers.up.data.value.RawPassword -import pl.starchasers.up.data.value.UserPassword - -fun PasswordEncoder.encode(password: RawPassword): UserPassword = UserPassword(this.encode(password.value)) - -fun PasswordEncoder.matches(password: RawPassword, actualPassword: UserPassword) = - this.matches(password.value, actualPassword.value) diff --git a/spring-app/src/main/kotlin/pl/starchasers/up/util/initializer/AccountInitializer.kt b/spring-app/src/main/kotlin/pl/starchasers/up/util/initializer/AccountInitializer.kt deleted file mode 100644 index 86b64396..00000000 --- a/spring-app/src/main/kotlin/pl/starchasers/up/util/initializer/AccountInitializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package pl.starchasers.up.util.initializer - -import jakarta.annotation.PostConstruct -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import pl.starchasers.up.data.value.RawPassword -import pl.starchasers.up.data.value.Username -import pl.starchasers.up.security.Role -import pl.starchasers.up.service.UserService -import pl.starchasers.up.util.Util - -@Component -class AccountInitializer( - private val userService: UserService -) : Initializer() { - - private val util = Util() - private val logger = LoggerFactory.getLogger(AccountInitializer::class.java) - - @PostConstruct - override fun initialize() { - ensureRootAccount() - } - - private fun ensureRootAccount() { - val user = userService.findUser(Username("root")) - - if (user == null) { - val password = util.secureAlphanumericRandomString(16) - userService.createUser(Username("root"), RawPassword(password), null, Role.ADMIN) - logger.info("Root account not found. Creating new one.\n\nUsername: root Password: $password\n") - } - } -} diff --git a/spring-app/src/main/resources/application-h2.properties b/spring-app/src/main/resources/application-h2.properties deleted file mode 100644 index 17ba9066..00000000 --- a/spring-app/src/main/resources/application-h2.properties +++ /dev/null @@ -1,6 +0,0 @@ -spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 -spring.datasource.username=sa -spring.datasource.password=sa -spring.datasource.driver-class-name=org.h2.Driver -spring.flyway.enabled=false -spring.jpa.hibernate.ddl-auto=create diff --git a/spring-app/src/main/resources/application-junit.yaml b/spring-app/src/main/resources/application-junit.yaml deleted file mode 100644 index 270c82c2..00000000 --- a/spring-app/src/main/resources/application-junit.yaml +++ /dev/null @@ -1,3 +0,0 @@ -up: - jwt: - base64-secret: "YTNrWl5idUwqNSFSb2FDZ2lkTmJzdFFMckB4TUdERmU=" diff --git a/spring-app/src/main/resources/application-localdb.properties b/spring-app/src/main/resources/application-localdb.properties deleted file mode 100644 index 6ecd3abe..00000000 --- a/spring-app/src/main/resources/application-localdb.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.datasource.driver-class-name=org.mariadb.jdbc.Driver -app.mariaDB4j.databaseName=up_dev -mariaDB4j.port=3308 -mariaDB4j.dataDir=./database diff --git a/spring-app/src/main/resources/application.properties b/spring-app/src/main/resources/application.properties deleted file mode 100644 index bfac2742..00000000 --- a/spring-app/src/main/resources/application.properties +++ /dev/null @@ -1,27 +0,0 @@ -spring.jpa.hibernate.ddl-auto=${UP_DDL_AUTO:validate} -spring.datasource.url=${UP_JDBC_STRING} -spring.datasource.username=${UP_DB_USER} -spring.datasource.password=${UP_DB_PASS} -spring.datasource.driver-class-name=${UP_JDBC_DRIVER} -server.port=${UP_PORT:8080} -spring.servlet.multipart.enabled=true -spring.servlet.multipart.resolve-lazily=true -spring.servlet.multipart.max-file-size=${UP_MAX_FILE_SIZE:10GB} -spring.servlet.multipart.max-request-size=${UP_MAX_REQUEST_SIZE:10GB} - -springdoc.api-docs.enabled=${UP_SWAGGER_ENABLED:false} -springdoc.swagger-ui.enabled=${UP_SWAGGER_ENABLED:false} - -up.datastore=${UP_DATASTORE_PATH:uploads} - -#in bytes. Default 4 * 1024 * 1024 (4MB) -up.chunk-size=${UP_CHUNK_SIZE:4194304} - -#in milliseconds. Default 1h -up.cleanup-interval=${UP_CLEANUP_INTERVAL:3600000} - -#Secret will be randomly generated at runtime if UP_JWT_BASE64_SECRET variable is not set. -#Warning: sessions will be invalidated during application restart -up.jwt.base64-secret=${UP_JWT_BASE64_SECRET:} -up.domain=${UP_DOMAIN:http://localhost:8080} -up.dev.cors=${UP_DEV_CORS:false} diff --git a/spring-app/src/main/resources/application.yaml b/spring-app/src/main/resources/application.yaml new file mode 100644 index 00000000..906a2a49 --- /dev/null +++ b/spring-app/src/main/resources/application.yaml @@ -0,0 +1,40 @@ +spring: + jpa: + hibernate: + ddl-auto: validate + datasource: + url: jdbc:postgresql://${UP_DB_HOST:localhost}:${UP_DB_PORT:5432}/${UP_DB_NAME}?currentSchema=${UP_DB_SCHEMA}&encoding=UTF-8 + username: ${UP_DB_USER} + password: ${UP_DB_PASS} + driver-class-name: org.postgresql.Driver + servlet: + multipart: + enabled: true + resolve-lazily: true + max-file-size: ${UP_MAX_FILE_SIZE:10GB} + max-request-size: ${UP_MAX_REQUEST_SIZE:10GB} + flyway: + enabled: true + +server: + port: ${UP_PORT:8080} + +springdoc: + api-docs: + enabled: ${UP_SWAGGER_ENABLED:false} + swagger-ui: + enabled: ${UP_SWAGGER_ENABLED:false} + +up: + datastore: ${UP_DATASTORE_PATH:uploads} + #in bytes. Default 4 * 1024 * 1024 (4MB) + chunk-size: ${UP_CHUNK_SIZE:4194304} + #in milliseconds. Default 1h + cleanup-interval: ${UP_CLEANUP_INTERVAL:3600000} + domain: ${UP_DOMAIN:http://localhost:8080} + dev: + cors: ${UP_DEV_CORS:false} + #Secret will be randomly generated at runtime if UP_JWT_BASE64_SECRET variable is not set. + #Warning: sessions will be invalidated during application restart + jwt: + base64-secret: ${UP_JWT_BASE64_SECRET:} diff --git a/spring-app/src/main/resources/db/migration/V1__Init.sql b/spring-app/src/main/resources/db/migration/V1__Init.sql index 0c86dfe6..7322a249 100644 --- a/spring-app/src/main/resources/db/migration/V1__Init.sql +++ b/spring-app/src/main/resources/db/migration/V1__Init.sql @@ -1,84 +1,22 @@ -START TRANSACTION; - -CREATE TABLE `configuration_entry` -( - `id` bigint(20) NOT NULL, - `configuration_key` varchar(255) NOT NULL, - `configuration_value` varchar(255) NOT NULL -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4; - -CREATE TABLE `file_entry` -( - `id` bigint(20) NOT NULL, - `file_access_token` varchar(128) DEFAULT NULL, - `content_type` varchar(256) NOT NULL, - `created_date` datetime(6) NOT NULL, - `encrypted` bit(1) NOT NULL, - `filename` varchar(1024) NOT NULL, - `file_key` varchar(32) NOT NULL, - `file_password` varchar(255) DEFAULT NULL, - `permanent` bit(1) NOT NULL, - `file_size` bigint(20) DEFAULT NULL, - `to_delete_date` datetime(6) DEFAULT NULL -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4; - -CREATE TABLE `refresh_token` +CREATE TABLE configuration_entry ( - `id` bigint(20) NOT NULL, - `creation_date` datetime(6) NOT NULL, - `expiration_date` datetime(6) NOT NULL, - `refresh_token` varchar(255) NOT NULL, - `user_id` bigint(20) DEFAULT NULL -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4; + id bigserial NOT NULL, + configuration_key text NOT NULL, + configuration_value text NOT NULL, + CONSTRAINT pk_configuration_entry PRIMARY KEY (id) +); -CREATE TABLE `user` +CREATE TABLE file_entry ( - `id` bigint(20) NOT NULL, - `default_file_lifetime` bigint(20) DEFAULT NULL, - `email` varchar(64) DEFAULT NULL, - `max_file_lifetime` bigint(20) DEFAULT NULL, - `max_permanent_file_size` bigint(20) DEFAULT NULL, - `max_temporary_file_size` bigint(20) DEFAULT NULL, - `password` varchar(160) NOT NULL, - `role` int(11) NOT NULL, - `username` varchar(32) NOT NULL -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4; - -ALTER TABLE `configuration_entry` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `uq_configuration_entry__configuration_key` (`configuration_key`); - -ALTER TABLE `file_entry` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `refresh_token` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `uq_refresh_token__refresh_token` (`refresh_token`), - ADD KEY `ix_refresh_token__user_id` (`user_id`); - -ALTER TABLE `user` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `uq_user__username` (`username`); - -ALTER TABLE `configuration_entry` - MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, - AUTO_INCREMENT = 8; - -ALTER TABLE `file_entry` - MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `refresh_token` - MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `user` - MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, - AUTO_INCREMENT = 2; - -ALTER TABLE `refresh_token` - ADD CONSTRAINT `fk_user__refresh_token` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`); -COMMIT; - + id bigserial NOT NULL, + file_access_token text DEFAULT NULL, + content_type text NOT NULL, + created_at timestamp NOT NULL DEFAULT now(), + encrypted bool NOT NULL, + filename text NOT NULL, + file_key text NOT NULL, + file_password text DEFAULT NULL, + file_size bigint NOT NULL, + to_delete_at timestamp DEFAULT NULL, + CONSTRAINT pk_file_entry PRIMARY KEY (id) +); diff --git a/spring-app/src/main/resources/db/migration/V2__file_owners.sql b/spring-app/src/main/resources/db/migration/V2__file_owners.sql deleted file mode 100644 index 5904916b..00000000 --- a/spring-app/src/main/resources/db/migration/V2__file_owners.sql +++ /dev/null @@ -1,8 +0,0 @@ -start transaction; - -alter table file_entry - add column owner_id bigint(20) default null, - add key `ix_file_entry__owner_id` (owner_id), - add constraint `fk_user__file_entry` foreign key (owner_id) references `user` (`id`); - -commit; diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/DatabaseCleaner.kt b/spring-app/src/test/kotlin/pl/starchasers/up/DatabaseCleaner.kt index 59d3e03c..222f0f38 100644 --- a/spring-app/src/test/kotlin/pl/starchasers/up/DatabaseCleaner.kt +++ b/spring-app/src/test/kotlin/pl/starchasers/up/DatabaseCleaner.kt @@ -2,16 +2,16 @@ package pl.starchasers.up import org.junit.jupiter.api.extension.BeforeTestExecutionCallback import org.junit.jupiter.api.extension.ExtensionContext -import org.springframework.data.jpa.repository.JpaRepository +import org.ktorm.database.Database import org.springframework.stereotype.Component +import pl.starchasers.up.repository.StandardRepository import pl.starchasers.up.util.initializer.Initializer -import javax.sql.DataSource @Component class DatabaseCleaner( - private val repositories: List>, - private val initializers: List, - private val dataSource: DataSource + private val database: Database, + private val repositories: List>, + private val initializers: List ) : BeforeTestExecutionCallback { fun reset() { @@ -20,14 +20,11 @@ class DatabaseCleaner( } private fun clean() { - val connection = dataSource.connection - - connection.prepareStatement("set foreign_key_checks = 0").execute() - repositories.forEach { - it.deleteAllInBatch() + database.useTransaction { + repositories.forEach { + it.deleteAll() + } } - connection.prepareStatement("set foreign_key_checks = 1").execute() - connection.close() } private fun initialize() { diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/JpaTestBase.kt b/spring-app/src/test/kotlin/pl/starchasers/up/JpaTestBase.kt index 769649da..60982e08 100644 --- a/spring-app/src/test/kotlin/pl/starchasers/up/JpaTestBase.kt +++ b/spring-app/src/test/kotlin/pl/starchasers/up/JpaTestBase.kt @@ -4,10 +4,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.TestInstance import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.springframework.http.HttpHeaders -import org.springframework.test.web.servlet.MockHttpServletRequestDsl -import pl.starchasers.up.data.model.User -import pl.starchasers.up.service.JwtTokenService @SpringBootTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -16,20 +12,9 @@ class JpaTestBase { @Autowired private lateinit var databaseCleaner: DatabaseCleaner - @Autowired - private lateinit var jwtTokenService: JwtTokenService - @AfterEach fun cleanup() { databaseCleaner.reset() } - private fun getUserAccessToken(user: User): String { - val refreshToken = jwtTokenService.issueRefreshToken(user) - return jwtTokenService.issueAccessToken(refreshToken) - } - - fun MockHttpServletRequestDsl.authorizeAsUser(user: User) { - header(HttpHeaders.AUTHORIZATION, "Bearer ${getUserAccessToken(user)}") - } } diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/MockMvcTestBase.kt b/spring-app/src/test/kotlin/pl/starchasers/up/MockMvcTestBase.kt index 94f156d7..985b3c1f 100644 --- a/spring-app/src/test/kotlin/pl/starchasers/up/MockMvcTestBase.kt +++ b/spring-app/src/test/kotlin/pl/starchasers/up/MockMvcTestBase.kt @@ -1,38 +1,16 @@ package pl.starchasers.up -import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.http.HttpHeaders import org.springframework.test.context.web.WebAppConfiguration -import org.springframework.test.web.servlet.MockHttpServletRequestDsl import org.springframework.test.web.servlet.MockMvc -import pl.starchasers.up.data.value.Username -import pl.starchasers.up.service.JwtTokenService -import pl.starchasers.up.service.UserService @SpringBootTest @WebAppConfiguration @AutoConfigureMockMvc abstract class MockMvcTestBase { - @Autowired - protected lateinit var mapper: ObjectMapper - @Autowired protected lateinit var mockMvc: MockMvc - @Autowired - private lateinit var jwtTokenService: JwtTokenService - - @Autowired - private lateinit var userService: UserService - - final fun getAdminAccessToken(): String { - return jwtTokenService.issueAccessToken(jwtTokenService.issueRefreshToken(userService.getUser(Username("root")))) - } - - fun MockHttpServletRequestDsl.authorizeAsAdmin() { - header(HttpHeaders.AUTHORIZATION, "Bearer ${getAdminAccessToken()}") - } } diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/PostgresContainerConfiguration.kt b/spring-app/src/test/kotlin/pl/starchasers/up/PostgresContainerConfiguration.kt new file mode 100644 index 00000000..6671009b --- /dev/null +++ b/spring-app/src/test/kotlin/pl/starchasers/up/PostgresContainerConfiguration.kt @@ -0,0 +1,40 @@ +package pl.starchasers.up + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.DriverManagerDataSource +import org.testcontainers.containers.PostgreSQLContainer +import javax.sql.DataSource + +@Configuration +class PostgresContainerConfiguration { + + @Bean + fun dataSource(): DataSource { + val ds = DriverManagerDataSource() + MyPostgresContainer.instance.start() + ds.setDriverClassName(MyPostgresContainer.instance.driverClassName) + ds.url = MyPostgresContainer.instance.jdbcUrl + ds.username = MyPostgresContainer.instance.username + ds.password = MyPostgresContainer.instance.password + return ds + } + +} + +@Configuration +class MyPostgresContainer : PostgreSQLContainer(IMAGE) { + + companion object { + private const val IMAGE = "postgres:17" + val instance by lazy { MyPostgresContainer() } + } + + override fun start() { + super.start() + System.setProperty("DATASOURCE_URL", this.jdbcUrl) + System.setProperty("DATASOURCE_USERNAME", this.username) + System.setProperty("DATASOURCE_PASSWORD", this.password) + } + +} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/TestUtil.kt b/spring-app/src/test/kotlin/pl/starchasers/up/TestUtil.kt index 648c49d9..0bf90754 100644 --- a/spring-app/src/test/kotlin/pl/starchasers/up/TestUtil.kt +++ b/spring-app/src/test/kotlin/pl/starchasers/up/TestUtil.kt @@ -1,13 +1,14 @@ package pl.starchasers.up import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import org.springframework.http.MediaType import org.springframework.test.web.servlet.* object DefaultObjectMapper { - val objectMapper: ObjectMapper = jacksonObjectMapper() + val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()) } var MockHttpServletRequestDsl.jsonContent: Any? diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/controller/AuthenticationControllerTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/controller/AuthenticationControllerTest.kt deleted file mode 100644 index 16febf85..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/controller/AuthenticationControllerTest.kt +++ /dev/null @@ -1,166 +0,0 @@ -package pl.starchasers.up.controller - -import io.kotest.matchers.string.shouldNotBeEmpty -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpHeaders -import org.springframework.test.web.servlet.post -import pl.starchasers.up.* -import pl.starchasers.up.data.dto.authentication.LoginDTO -import pl.starchasers.up.data.dto.authentication.TokenDTO -import pl.starchasers.up.data.model.User -import pl.starchasers.up.repository.RefreshTokenRepository -import pl.starchasers.up.service.JwtTokenService -import pl.starchasers.up.testdata.UserTestData - -internal class AuthenticationControllerTest( - @Autowired private val jwtTokenService: JwtTokenService, - @Autowired private val userTestData: UserTestData -) : JpaTestBase() { - - private lateinit var testUser: User - - @BeforeEach - fun createTestUser() { - testUser = userTestData.createTestUser() - } - - @Nested - inner class LogIn : MockMvcTestBase() { - private val requestPath = "/api/auth/login" - - @Test - fun `Given valid data, should return refresh token`() { - val response: TokenDTO = mockMvc.postJson(requestPath) { - jsonContent = LoginDTO(UserTestData.DEFAULT_USERNAME, UserTestData.DEFAULT_PASSWORD) - }.andExpect { - status { isOk() } - }.andReturn().parse() - - response.token.shouldNotBeEmpty() - } - - @Test - fun `Given incorrect password, should return 403`() { - mockMvc.postJson(requestPath) { - jsonContent = LoginDTO(UserTestData.DEFAULT_USERNAME, UserTestData.DEFAULT_PASSWORD + "qwe") - }.andExpect { - status { isForbidden() } - } - } - - @Test - fun `Given incorrect username, should return 403`() { - mockMvc.postJson(requestPath) { - jsonContent = LoginDTO(UserTestData.DEFAULT_USERNAME + "qwe", UserTestData.DEFAULT_PASSWORD) - }.andExpect { - status { isForbidden() } - } - } - } - - @Nested - inner class LogOut( - @Autowired private val refreshTokenRepository: RefreshTokenRepository - ) : MockMvcTestBase() { - - private lateinit var refreshToken: String - private lateinit var accessToken: String - - private val requestPath = "/api/auth/logout" - - @BeforeEach - fun createSessions() { - refreshToken = jwtTokenService.issueRefreshToken(testUser) - jwtTokenService.issueRefreshToken(testUser) - jwtTokenService.issueRefreshToken(testUser) - accessToken = jwtTokenService.issueAccessToken(refreshToken) - } - - @Test - fun `Given correct access token, should invalidate all refresh tokens`() { - mockMvc.post(requestPath) { - header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken") - }.andExpect { - status { isOk() } - } - - assertTrue(refreshTokenRepository.findAllByUser(testUser).isEmpty()) - } - - @Test - fun `Given incorrect access token or logged out user, should return 403`() { - mockMvc.post(requestPath) - .andExpect { - status { isForbidden() } - } - - assertEquals(3, refreshTokenRepository.findAllByUser(testUser).size) - } - } - - @Nested - inner class GetAccessToken : MockMvcTestBase() { - private val requestPath = "/api/auth/getAccessToken" - - @Test - fun `Given valid refresh token, should return access token`() { - val refreshToken = jwtTokenService.issueRefreshToken(testUser) - - val response: TokenDTO = mockMvc.postJson(requestPath) { - jsonContent = TokenDTO(refreshToken) - }.andExpect { - status { isOk() } - }.andReturn().parse() - - response.token.shouldNotBeEmpty() - } - - @Test - fun `Given invalid refresh token, should return 403`() { - val refreshToken = jwtTokenService.issueRefreshToken(testUser) - jwtTokenService.invalidateRefreshToken(refreshToken) - - mockMvc.postJson(requestPath) { - jsonContent = TokenDTO(refreshToken) - }.andExpect { - status { isForbidden() } - } - } - } - - @Nested - inner class RefreshRefreshToken : MockMvcTestBase() { - - private val requestPath = "/api/auth/refreshToken" - - @Test - fun `Given valid refresh token, should return new refresh token`() { - val refreshToken = jwtTokenService.issueRefreshToken(testUser) - - val response: TokenDTO = mockMvc.postJson(requestPath) { - jsonContent = TokenDTO(refreshToken) - }.andExpect { - status { isOk() } - }.andReturn().parse() - - response.token.shouldNotBeEmpty() - } - - @Test - fun `Given invalid refresh token, should return 403`() { - val refreshToken = jwtTokenService.issueRefreshToken(testUser) - jwtTokenService.invalidateRefreshToken(refreshToken) - - mockMvc.postJson(requestPath) { - jsonContent = TokenDTO(refreshToken) - }.andExpect { - status { isForbidden() } - } - } - } -} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/controller/ConfigurationControllerTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/controller/ConfigurationControllerTest.kt deleted file mode 100644 index d608ac2f..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/controller/ConfigurationControllerTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package pl.starchasers.up.controller - -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.test.web.servlet.get -import pl.starchasers.up.* -import pl.starchasers.up.data.dto.configuration.UserConfigurationDTO -import pl.starchasers.up.data.model.ConfigurationKey -import pl.starchasers.up.data.value.Username -import pl.starchasers.up.service.UserService - -internal class ConfigurationControllerTest( - @Autowired private val userService: UserService -) : JpaTestBase() { - - @Nested - inner class GetConfiguration : MockMvcTestBase() { - - private val requestPath = "/api/configuration" - - @Test - fun `Given unauthorized request, should return anonymous configuration`() { - val response: UserConfigurationDTO = mockMvc.get(requestPath) - .andExpect { - status { isOk() } - }.andReturn().parse() - - with(response) { - maxTemporaryFileSize shouldBe ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE.defaultValue.toLong() - maxFileLifetime shouldBe ConfigurationKey.ANONYMOUS_MAX_FILE_LIFETIME.defaultValue.toLong() - defaultFileLifetime shouldBe ConfigurationKey.ANONYMOUS_DEFAULT_FILE_LIFETIME.defaultValue.toLong() - permanentAllowed shouldBe false - maxPermanentFileSize shouldBe 0 - } - } - - @Test - fun `Given authorized request, should return user-specific details`() { - val user = requireNotNull(userService.findUser(Username("root"))) - - val response: UserConfigurationDTO = mockMvc.get(requestPath) { - authorizeAsUser(user) - }.andExpect { - status { isOk() } - }.andReturn().parse() - - with(response) { - maxTemporaryFileSize shouldBe ConfigurationKey.DEFAULT_USER_MAX_TEMPORARY_FILE_SIZE.defaultValue.toLong() - maxFileLifetime shouldBe ConfigurationKey.DEFAULT_USER_MAX_FILE_LIFETIME.defaultValue.toLong() - defaultFileLifetime shouldBe ConfigurationKey.DEFAULT_USER_DEFAULT_FILE_LIFETIME.defaultValue.toLong() - permanentAllowed shouldBe true - maxPermanentFileSize shouldBe ConfigurationKey.DEFAULT_USER_MAX_PERMANENT_FILE_SIZE.defaultValue.toLong() - } - } - } -} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/controller/UploadControllerTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/controller/UploadControllerTest.kt index 81f59f76..36b5f35c 100644 --- a/spring-app/src/test/kotlin/pl/starchasers/up/controller/UploadControllerTest.kt +++ b/spring-app/src/test/kotlin/pl/starchasers/up/controller/UploadControllerTest.kt @@ -15,17 +15,14 @@ import org.springframework.mock.web.MockMultipartFile import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.multipart import pl.starchasers.up.* -import pl.starchasers.up.data.dto.configuration.UpdateUserConfigurationDTO import pl.starchasers.up.data.dto.upload.FileDetailsDTO import pl.starchasers.up.data.dto.upload.UploadCompleteResponseDTO -import pl.starchasers.up.data.value.* +import pl.starchasers.up.data.model.ConfigurationKey import pl.starchasers.up.repository.FileEntryRepository import pl.starchasers.up.repository.UploadRepository -import pl.starchasers.up.security.Role import pl.starchasers.up.service.ConfigurationService import pl.starchasers.up.service.FileService -import pl.starchasers.up.service.UserService -import java.time.LocalDateTime +import java.time.Instant internal class UploadControllerTest : JpaTestBase() { @@ -33,7 +30,6 @@ internal class UploadControllerTest : JpaTestBase() { inner class AnonymousUpload( @Autowired private val fileEntryRepository: FileEntryRepository, @Autowired private val uploadRepository: UploadRepository, - @Autowired private val userService: UserService, @Autowired private val configurationService: ConfigurationService ) : MockMvcTestBase() { @@ -56,21 +52,21 @@ internal class UploadControllerTest : JpaTestBase() { status { isOk() } }.andReturn().parse() + assertEquals(1, fileEntryRepository.count()) val fileEntry = fileEntryRepository.findAll()[0] with(response) { - key shouldBe fileEntry.key.value - accessToken shouldBe fileEntry.accessToken.value + key shouldBe fileEntry.key + accessToken shouldBe fileEntry.accessToken toDelete.shouldNotBeNull() } with(fileEntry) { - contentType.value shouldBe "text/plain; charset=UTF-8" + contentType shouldBe "text/plain; charset=UTF-8" encrypted shouldBe false - filename.value shouldBe "exampleTextFile.txt" + filename shouldBe "exampleTextFile.txt" password.shouldBeNull() - permanent shouldBe false - toDeleteDate.shouldNotBeNull() - fileEntry.toDeleteDate!!.toLocalDateTime().isAfter(LocalDateTime.now()) shouldBe true + toDeleteAt.shouldNotBeNull() + fileEntry.toDeleteAt!!.isAfter(Instant.now()) shouldBe true } uploadRepository.find(fileEntry.key)?.let { fileContent -> @@ -103,19 +99,18 @@ internal class UploadControllerTest : JpaTestBase() { val fileEntry = fileEntryRepository.findAll()[0] with(response) { - key shouldBe fileEntry.key.value - accessToken shouldBe fileEntry.accessToken.value + key shouldBe fileEntry.key + accessToken shouldBe fileEntry.accessToken toDelete.shouldNotBeNull() } with(fileEntry) { - fileEntry.contentType.value shouldBe "application/octet-stream" + fileEntry.contentType shouldBe "application/octet-stream" encrypted shouldBe false - filename.value shouldBe "exampleTextFile.txt" + filename shouldBe "exampleTextFile.txt" password.shouldBeNull() - permanent shouldBe false - toDeleteDate.shouldNotBeNull() - toDeleteDate!!.toLocalDateTime().isAfter(LocalDateTime.now()) shouldBe true + toDeleteAt.shouldNotBeNull() + toDeleteAt!!.isAfter(Instant.now()) shouldBe true } uploadRepository.find(fileEntry.key)?.let { fileContent -> @@ -125,23 +120,17 @@ internal class UploadControllerTest : JpaTestBase() { @Test fun `Given too large file, should return 413`() { - val testUser = userService.createUser(Username("testUser"), RawPassword("password"), null, Role.USER) - configurationService.updateUserConfiguration( - testUser, - UpdateUserConfigurationDTO( - 10, - testUser.maxFileLifetime.value, - testUser.defaultFileLifetime.value, - testUser.maxPermanentFileSize.value - ) + configurationService.updateGlobalConfiguration( + mapOf(ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE to "8") ) - mockMvc.multipart(requestPath) { - authorizeAsUser(testUser) file(getExampleTextFile()) }.andExpect { status { isPayloadTooLarge() } } + configurationService.updateGlobalConfiguration( + mapOf(ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE to "10485760") + ) } } @@ -155,10 +144,9 @@ internal class UploadControllerTest : JpaTestBase() { private fun createFile(contentType: String, fileContent: String = content): String = fileService.createFile( fileContent.byteInputStream(), - Filename("fileName.txt"), - ContentType(contentType), - FileSize(fileContent.byteInputStream().readAllBytes().size.toLong()), - null + "fileName.txt", + contentType, + fileContent.byteInputStream().readAllBytes().size.toLong() ).key @Test @@ -248,8 +236,7 @@ internal class UploadControllerTest : JpaTestBase() { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class VerifyFileAccess( @Autowired val fileService: FileService, - @Autowired val fileEntryRepository: FileEntryRepository, - @Autowired val userService: UserService + @Autowired val fileEntryRepository: FileEntryRepository ) : MockMvcTestBase() { private val requestPath = "/api/u/{key}/verify" private val content = "example content" @@ -261,13 +248,12 @@ internal class UploadControllerTest : JpaTestBase() { fun setup() { fileKey = fileService.createFile( content.byteInputStream(), - Filename("filename.txt"), - ContentType("text/plain"), - FileSize(content.byteInputStream().readAllBytes().size.toLong()), - userService.getUser(Username("root")) + "filename.txt", + "text/plain", + content.byteInputStream().readAllBytes().size.toLong() ).key - fileAccessToken = fileEntryRepository.findExistingFileByKey(FileKey(fileKey))?.accessToken?.value + fileAccessToken = fileEntryRepository.findExistingFileByKey(fileKey)?.accessToken ?: throw IllegalStateException() } @@ -282,15 +268,6 @@ internal class UploadControllerTest : JpaTestBase() { } } - @Test - fun `Given valid owner and no token, should return 200`() { - mockMvc.postJson(requestPath, fileKey) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - } - @Test fun `Given invalid owner and valid token, should return 200`() { mockMvc.postJson(requestPath, fileKey) { @@ -349,10 +326,9 @@ internal class UploadControllerTest : JpaTestBase() { fun setup() { fileKey = fileService.createFile( content.byteInputStream(), - Filename(filename), - ContentType(contentType), - FileSize(content.byteInputStream().readAllBytes().size.toLong()), - null + filename, + contentType, + content.byteInputStream().readAllBytes().size.toLong() ).key } @@ -386,8 +362,7 @@ internal class UploadControllerTest : JpaTestBase() { inner class DeleteFile( @Autowired val fileService: FileService, @Autowired val uploadRepository: UploadRepository, - @Autowired val fileEntryRepository: FileEntryRepository, - @Autowired val userService: UserService + @Autowired val fileEntryRepository: FileEntryRepository ) : MockMvcTestBase() { private val requestPath = "/api/u/{key}" @@ -396,10 +371,9 @@ internal class UploadControllerTest : JpaTestBase() { val fileContent = "fileContent" return fileService.createFile( fileContent.byteInputStream(), - Filename("file"), - ContentType("text/plain"), - FileSize(fileContent.length.toLong()), - userService.getUser(Username("root")) + "file", + "text/plain", + fileContent.length.toLong() ) } @@ -414,19 +388,8 @@ internal class UploadControllerTest : JpaTestBase() { status { isOk() } } - assertNull(uploadRepository.find(FileKey(response.key))) - assertNull(fileEntryRepository.findExistingFileByKey(FileKey(response.key))) - } - - @Test - fun `Given valid owner, should delete file`() { - val response = createTestFile() - - mockMvc.deleteJson(requestPath, response.key) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } + assertNull(uploadRepository.find(response.key)) + assertNull(fileEntryRepository.findExistingFileByKey(response.key)) } @Test @@ -441,8 +404,8 @@ internal class UploadControllerTest : JpaTestBase() { status { isForbidden() } } - assertNotNull(uploadRepository.find(FileKey(response.key))) - assertNotNull(fileEntryRepository.findExistingFileByKey(FileKey(response.key))) + assertNotNull(uploadRepository.find(response.key)) + assertNotNull(fileEntryRepository.findExistingFileByKey(response.key)) } @Test @@ -457,8 +420,8 @@ internal class UploadControllerTest : JpaTestBase() { status { isNotFound() } } - assertNotNull(uploadRepository.find(FileKey(response.key))) - assertNotNull(fileEntryRepository.findExistingFileByKey(FileKey(response.key))) + assertNotNull(uploadRepository.find(response.key)) + assertNotNull(fileEntryRepository.findExistingFileByKey(response.key)) } } } diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/controller/UserControllerTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/controller/UserControllerTest.kt deleted file mode 100644 index f1a5f3cf..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/controller/UserControllerTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package pl.starchasers.up.controller - -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.data.domain.Page -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.web.servlet.get -import org.springframework.transaction.annotation.Transactional -import pl.starchasers.up.* -import pl.starchasers.up.data.dto.upload.UploadHistoryEntryDTO -import pl.starchasers.up.data.value.* -import pl.starchasers.up.service.FileService -import pl.starchasers.up.service.UserService -import java.lang.IllegalStateException - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) -class UserControllerTest : MockMvcTestBase() { - - @Transactional - @Nested - inner class ListUserUploadHistory( - @Autowired private val userService: UserService, - @Autowired private val fileService: FileService - ) : MockMvcTestBase() { - - private val requestPath = "/api/user/history" - - // TODO enable test after migration to postgres #212 - // @Test - fun `Given valid request, should return upload history`() { - val file = fileService.createFile( - "content".byteInputStream(), - Filename("file"), - ContentType("text/html"), - FileSize(7), - userService.getUser(Username("root")) - ) - val fileEntry = fileService.findFileEntry(FileKey(file.key)) ?: throw IllegalStateException() - - val response: Page = mockMvc.get(requestPath) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - }.andReturn().parse() - - with(response.content[0]) { - filename shouldBe fileEntry.filename.value - permanent shouldBe fileEntry.permanent - size shouldBe fileEntry.size.value - mimeType shouldBe fileEntry.contentType.value - key shouldBe fileEntry.key.value - } - } - - @Test - fun `Given unauthenticated request, should return 403`() { - mockMvc.get(requestPath) - .andExpect { - status { isForbidden() } - } - } - } -} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/controller/admin/ConfigurationAdminControllerTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/controller/admin/ConfigurationAdminControllerTest.kt deleted file mode 100644 index c3ccbdbe..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/controller/admin/ConfigurationAdminControllerTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -package pl.starchasers.up.controller.admin - -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.test.web.servlet.get -import pl.starchasers.up.* -import pl.starchasers.up.data.dto.configuration.ConfigurationDTO -import pl.starchasers.up.data.model.ConfigurationKey -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.RawPassword -import pl.starchasers.up.data.value.Username -import pl.starchasers.up.security.Role -import pl.starchasers.up.service.ConfigurationService -import pl.starchasers.up.service.UserService - -internal class ConfigurationAdminControllerTest( - @Autowired private val userService: UserService, - @Autowired private val configurationService: ConfigurationService -) : JpaTestBase() { - - private lateinit var testUser: User - - @BeforeEach - fun createTestUser() { - testUser = userService.createUser( - Username("unauthorizedUser"), - RawPassword("password"), - null, - Role.USER - ) - } - - @Nested - inner class UpdateConfiguration : MockMvcTestBase() { - - private val requestPath = "/api/admin/config" - - private val key1 = ConfigurationKey.DEFAULT_USER_MAX_PERMANENT_FILE_SIZE - private val value1 = "123456789" - private val key2 = ConfigurationKey.DEFAULT_USER_MAX_FILE_LIFETIME - private val value2 = "987654321" - - @Test - fun `Given valid request, should update all configuration values`() { - mockMvc.patchJson(requestPath) { - jsonContent = ConfigurationDTO( - mapOf(Pair(key1, value1), Pair(key2, value2)) - ) - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - - configurationService.getConfigurationOption(key1) shouldBe value1 - configurationService.getConfigurationOption(key2) shouldBe value2 - } - - @Test - fun `Given incorrect key, should return 400 and update nothing`() { - val incorrectKey = "incorrectKey" - - mockMvc.patchJson(requestPath) { - jsonContent = object { - val options = mapOf(Pair(key1.toString(), value1), Pair(incorrectKey, value2)) - } - authorizeAsAdmin() - }.andExpect { - status { isBadRequest() } - } - - configurationService.getConfigurationOption(key1) shouldBe key1.defaultValue - } - - @Test - fun `Given empty map, should update nothing`() { - mockMvc.patchJson(requestPath) { - jsonContent = ConfigurationDTO(mapOf()) - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - } - - @Test - fun `Given incorrect data type, should return 400 and update nothing`() { - val incorrectValue = "qwe" - mockMvc.patchJson(requestPath) { - jsonContent = ConfigurationDTO( - mapOf(Pair(key1, value1), Pair(key2, incorrectValue)) - ) - authorizeAsAdmin() - }.andExpect { - status { isBadRequest() } - } - - configurationService.getConfigurationOption(key1) shouldBe key1.defaultValue - configurationService.getConfigurationOption(key2) shouldBe key2.defaultValue - } - - @Test - fun `Given unauthorized request, should return 403`() { - mockMvc.patchJson(requestPath) { - jsonContent = ConfigurationDTO( - mapOf(Pair(key1, value1), Pair(key2, value2)) - ) - }.andExpect { - status { isForbidden() } - } - } - } - - @Nested - inner class GetAppConfiguration : MockMvcTestBase() { - - private val requestPath = "/api/admin/config" - - @Test - fun `Given valid request, should return entire global configuration`() { - val response: ConfigurationDTO = mockMvc.get(requestPath) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - }.andReturn().parse() - - with(response) { - options[ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE] shouldBe ConfigurationKey.ANONYMOUS_MAX_FILE_SIZE.defaultValue - options[ConfigurationKey.ANONYMOUS_DEFAULT_FILE_LIFETIME] shouldBe ConfigurationKey.ANONYMOUS_DEFAULT_FILE_LIFETIME.defaultValue - options[ConfigurationKey.ANONYMOUS_MAX_FILE_LIFETIME] shouldBe ConfigurationKey.ANONYMOUS_MAX_FILE_LIFETIME.defaultValue - options[ConfigurationKey.DEFAULT_USER_MAX_TEMPORARY_FILE_SIZE] shouldBe ConfigurationKey.DEFAULT_USER_MAX_TEMPORARY_FILE_SIZE.defaultValue - options[ConfigurationKey.DEFAULT_USER_MAX_PERMANENT_FILE_SIZE] shouldBe ConfigurationKey.DEFAULT_USER_MAX_PERMANENT_FILE_SIZE.defaultValue - options[ConfigurationKey.DEFAULT_USER_DEFAULT_FILE_LIFETIME] shouldBe ConfigurationKey.DEFAULT_USER_DEFAULT_FILE_LIFETIME.defaultValue - options[ConfigurationKey.DEFAULT_USER_MAX_FILE_LIFETIME] shouldBe ConfigurationKey.DEFAULT_USER_MAX_FILE_LIFETIME.defaultValue - } - } - - @Test - fun `Given unauthorized request, should return 403`() { - mockMvc.get(requestPath) { - authorizeAsUser(testUser) - }.andExpect { - status { isForbidden() } - } - } - } -} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/controller/admin/UserAdminControllerTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/controller/admin/UserAdminControllerTest.kt deleted file mode 100644 index 853bd76e..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/controller/admin/UserAdminControllerTest.kt +++ /dev/null @@ -1,348 +0,0 @@ -package pl.starchasers.up.controller.admin - -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder -import org.springframework.test.web.servlet.delete -import org.springframework.test.web.servlet.get -import pl.starchasers.up.* -import pl.starchasers.up.data.dto.users.CreateUserDTO -import pl.starchasers.up.data.dto.users.UpdateUserDTO -import pl.starchasers.up.data.dto.users.UserDTO -import pl.starchasers.up.data.model.ConfigurationKey -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.Email -import pl.starchasers.up.data.value.RawPassword -import pl.starchasers.up.data.value.Username -import pl.starchasers.up.data.value.toUsername -import pl.starchasers.up.security.Role -import pl.starchasers.up.service.UserService -import pl.starchasers.up.testdata.UserTestData - -internal class UserAdminControllerTest : JpaTestBase() { - - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - @Nested - inner class UsersGetOne : MockMvcTestBase() { - @Autowired - private lateinit var userService: UserService - private lateinit var adminUser: User - private lateinit var user: User - - @BeforeEach - fun createUsers() { - adminUser = userService.createUser( - Username("username"), - RawPassword("password"), - Email("email@gmail.com"), - Role.ADMIN - ) - user = userService.createUser( - Username("username2"), - RawPassword("password2"), - Email("email2@gmail.com"), - Role.USER - ) - } - - private val requestPath = "/api/admin/users/{id}" - - @Test - fun `Given valid request, should return user details`() { - val result: UserDTO = mockMvc.get(requestPath, adminUser.id) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - }.andReturn().parse() - - with(result) { - id shouldBe adminUser.id - username shouldBe adminUser.username.value - email shouldBe adminUser.email?.value - role shouldBe adminUser.role - maxTemporaryFileSize shouldBe adminUser.maxTemporaryFileSize.value - maxPermanentFileSize shouldBe adminUser.maxPermanentFileSize.value - defaultFileLifetime shouldBe adminUser.defaultFileLifetime.value - maxFileLifetime shouldBe adminUser.maxFileLifetime.value - } - } - - @Test - fun `Given unauthorized user, should throw 403`() { - mockMvc.get(requestPath, adminUser.id) { - authorizeAsUser(user) - }.andExpect { - status { isForbidden() } - } - } - - @Test - fun `Given invalid id, should throw 404`() { - mockMvc.get(requestPath, adminUser.id + 123) { - authorizeAsAdmin() - }.andExpect { - status { isNotFound() } - } - } - } - - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - @Nested - inner class UsersList : MockMvcTestBase() { - @Autowired - private lateinit var userService: UserService - private lateinit var adminUser: User - private lateinit var user: User - - @BeforeEach - fun createUsers() { - adminUser = userService.createUser( - Username("username3"), - RawPassword("password"), - Email("email@gmail.com"), - Role.ADMIN - ) - user = userService.createUser( - Username("username4"), - RawPassword("password2"), - Email("email2@gmail.com"), - Role.USER - ) - } - - private val requestPath = "/api/admin/users" - - @Test - fun `Given valid request, should return page`() { - mockMvc.get(requestPath) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - } - - @Test - fun `Given unauthorized user, should throw 403`() { - mockMvc.get(requestPath) { - authorizeAsUser(user) - }.andExpect { - status { isForbidden() } - } - } - } - - @Nested - inner class UsersCreate : MockMvcTestBase() { - - private val requestPath = "/api/admin/users" - - @Autowired - private lateinit var userService: UserService - - @Autowired - private lateinit var userTestData: UserTestData - - @Test - fun `Given valid request, should create user`() { - val newUserUsername = "createdUser" - - val response: UserDTO = mockMvc.postJson(requestPath) { - jsonContent = CreateUserDTO(newUserUsername, "password", "mail@example.com", Role.USER) - authorizeAsAdmin() - }.andExpect { - status { isOk() } - }.andReturn().parse() - - with(userService.getUser(newUserUsername.toUsername())) { - response.id shouldBe id - response.username shouldBe username.value - response.email shouldBe email?.value - response.role shouldBe role - } - } - - @Test - fun `Given unauthorized user, should throw 403`() { - mockMvc.postJson(requestPath) { - jsonContent = CreateUserDTO("createdUser", "password", "mail@example.com", Role.USER) - }.andExpect { - status { isForbidden() } - } - } - - @Test - fun `Given duplicate username, should throw 400`() { - userService.createUser( - Username("duplicateUser"), - RawPassword("password"), - Email("mail@example.com"), - Role.USER - ) - mockMvc.postJson(requestPath) { - jsonContent = CreateUserDTO("duplicateUser", "password", "mail2@example.com", Role.ADMIN) - authorizeAsAdmin() - }.andExpect { - status { isBadRequest() } - } - } - } - - @Nested - inner class UsersUpdate : MockMvcTestBase() { - - private val requestPath = "/api/admin/users/{id}" - - @Autowired - private lateinit var userService: UserService - - @Autowired - private lateinit var userTestData: UserTestData - - @Autowired - private lateinit var passwordEncoder: Pbkdf2PasswordEncoder - - @Test - fun `Given valid request, should update user`() { - val oldUser = userTestData.createTestUser() - val updateUserDTO = getUpdateUserDTO() - - mockMvc.patchJson(requestPath, oldUser.id) { - jsonContent = updateUserDTO - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - - with(userService.getUser(oldUser.id)) { - email?.value shouldBe updateUserDTO.email - username.value shouldBe updateUserDTO.username - role shouldBe Role.ADMIN - passwordEncoder.matches(updateUserDTO.password, password.value) shouldBe true - } - } - - @Test - fun `Given unauthorized user, should throw 403`() { - val oldUser = userTestData.createTestUser() - - mockMvc.patchJson(requestPath, oldUser.id) { - jsonContent = getUpdateUserDTO() - authorizeAsUser(oldUser) - }.andExpect { - status { isForbidden() } - } - } - - @Test - fun `Given no password field, should not update password`() { - val oldUser = userTestData.createTestUser() - mockMvc.patchJson(requestPath, oldUser.id) { - jsonContent = getUpdateUserDTO(password = null) - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - - assertTrue(passwordEncoder.matches("password", userService.getUser(oldUser.id).password.value)) - } - - @Test - fun `Given wrong userId, should return 400`() { - val oldUser = userTestData.createTestUser() - - mockMvc.patchJson(requestPath, oldUser.id + 123) { - jsonContent = getUpdateUserDTO() - authorizeAsAdmin() - }.andExpect { - status { isBadRequest() } - } - } - - private fun getUpdateUserDTO(password: String? = "password2"): UpdateUserDTO = UpdateUserDTO( - username = "newExampleUser", - email = "mail2@example.com", - password = password, - role = Role.ADMIN, - maxTemporaryFileSize = ConfigurationKey.DEFAULT_USER_MAX_TEMPORARY_FILE_SIZE.defaultValue.toLong(), - maxPermanentFileSize = ConfigurationKey.DEFAULT_USER_MAX_PERMANENT_FILE_SIZE.defaultValue.toLong(), - defaultFileLifetime = ConfigurationKey.DEFAULT_USER_DEFAULT_FILE_LIFETIME.defaultValue.toLong(), - maxFileLifetime = ConfigurationKey.DEFAULT_USER_MAX_FILE_LIFETIME.defaultValue.toLong() - ) - } - - @Nested - inner class UsersDelete : MockMvcTestBase() { - - @Autowired - private lateinit var userService: UserService - - @Autowired - private lateinit var userTestData: UserTestData - - private val requestPath = "/api/admin/users/{id}" - - @Test - fun `Given valid request, should delete user`() { - val user = userTestData.createTestUser() - - mockMvc.delete(requestPath, user.id) { - authorizeAsAdmin() - }.andExpect { - status { isOk() } - } - - assertNull(userService.findUser(user.id)) - } - - @Test - fun `Given unauthorized user, should throw 403`() { - val user = userTestData.createTestUser() - - mockMvc.delete(requestPath, user.id) { - authorizeAsUser(user) - }.andExpect { - status { isForbidden() } - } - - assertNotNull(userService.findUser(user.id)) - } - - @Test - fun `Given wrong userId, should return 400`() { - val user = userTestData.createTestUser() - - mockMvc.delete(requestPath, user.id + 123) { - authorizeAsAdmin() - }.andExpect { - status { isBadRequest() } - } - } - - @Test - fun `Given root account, should return 403`() { - val user = userTestData.createTestUser(role = Role.ADMIN) - - mockMvc.delete(requestPath, userService.getUser(Username("root")).id) { - authorizeAsUser(user) - }.andExpect { - status { isForbidden() } - } - } - - @Test - fun `Given current account, should return 400`() { - val user = userTestData.createTestUser(role = Role.ADMIN) - - mockMvc.delete(requestPath, user.id) { - authorizeAsUser(user) - }.andExpect { - status { isBadRequest() } - } - } - } -} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/data/value/UsernameTest.kt b/spring-app/src/test/kotlin/pl/starchasers/up/data/value/UsernameTest.kt deleted file mode 100644 index 65a41b8b..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/data/value/UsernameTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package pl.starchasers.up.data.value - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import pl.starchasers.up.exception.ValidationException - -internal class UsernameTest { - - @Test - fun `should allow valid usernames`() { - val username = Username("exampleUsername123") - assertEquals("exampleUsername123", username.value) - } - - @Test - fun `given blank username, should throw ValidationException`() { - assertThrows { Username("") } - } - - @Test - fun `given username with whitespace, should throw ValidationException`() { - assertThrows { Username("qwe asd") } - } - - @Test - fun `given too long username, should throw ValidationException`() { - assertThrows { Username("a".repeat(33)) } - } -} diff --git a/spring-app/src/test/kotlin/pl/starchasers/up/testdata/UserTestData.kt b/spring-app/src/test/kotlin/pl/starchasers/up/testdata/UserTestData.kt deleted file mode 100644 index c46ef7b2..00000000 --- a/spring-app/src/test/kotlin/pl/starchasers/up/testdata/UserTestData.kt +++ /dev/null @@ -1,28 +0,0 @@ -package pl.starchasers.up.testdata - -import org.springframework.stereotype.Service -import pl.starchasers.up.data.model.User -import pl.starchasers.up.data.value.Email -import pl.starchasers.up.data.value.RawPassword -import pl.starchasers.up.data.value.toUsername -import pl.starchasers.up.security.Role -import pl.starchasers.up.service.UserService - -@Service -class UserTestData( - private val userService: UserService -) { - - companion object { - const val DEFAULT_USERNAME = "exampleUser" - const val DEFAULT_PASSWORD = "password" - } - - fun createTestUser(role: Role = Role.USER): User = - userService.createUser( - DEFAULT_USERNAME.toUsername(), - RawPassword(DEFAULT_PASSWORD), - Email("email@example.com"), - role - ) -} diff --git a/spring-app/src/test/resources/application.properties b/spring-app/src/test/resources/application.properties deleted file mode 100644 index f5a55c27..00000000 --- a/spring-app/src/test/resources/application.properties +++ /dev/null @@ -1,15 +0,0 @@ -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 -spring.datasource.username=sa -spring.datasource.password=sa -spring.jpa.hibernate.ddl-auto=create-drop -spring.servlet.multipart.enabled=false -spring.flyway.enabled=false - -up.datastore=${DATASTORE_PATH:uploads} -up.max-file-size=100 -up.cleanup-interval=${UP_CLEANUP_INTERVAL:3600000} -up.jwt-secret=verySecret -up.domain=${UP_DOMAIN:http://localhost:8080} -up.dev.cors=${UP_DEV_CORS:false} -up.chunk-size=4194304 diff --git a/spring-app/src/test/resources/application.yaml b/spring-app/src/test/resources/application.yaml new file mode 100644 index 00000000..3df855f5 --- /dev/null +++ b/spring-app/src/test/resources/application.yaml @@ -0,0 +1,16 @@ +spring: + jpa: + hibernate: + ddl-auto: validate + servlet: + multipart: + enabled: false + +up: + datastore: ${DATASTORE_PATH:uploads} + max-file-size: 100 + cleanup-interval: ${UP_CLEANUP_INTERVAL:3600000} + domain: ${UP_DOMAIN:http://localhost:8080} + chunk-size: 4194304 + dev: + cors: ${UP_DEV_CORS:false}