Skip to content

Commit

Permalink
Merge pull request #3 from LotuxPunk/feat/jwt-support
Browse files Browse the repository at this point in the history
feat: Added JWT support
  • Loading branch information
LotuxPunk authored Aug 30, 2024
2 parents d03eabe + 19d19bb commit 794d7ec
Show file tree
Hide file tree
Showing 16 changed files with 769 additions and 175 deletions.
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ dependencies {
implementation("io.ktor:ktor-server-host-common-jvm")
implementation("io.ktor:ktor-server-cio-jvm")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-config-yaml:2.3.12")
implementation("io.ktor:ktor-server-config-yaml")

// Ktor features plugins
implementation("io.ktor:ktor-server-partial-content:$ktor_version")
implementation("io.ktor:ktor-server-auto-head-response:$ktor_version")
implementation("io.ktor:ktor-server-status-pages:$ktor_version")
implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation("io.ktor:ktor-server-auth-jwt:$ktor_version")

// Cache4k
implementation("io.github.reactivecircus.cache4k:cache4k:0.13.0")
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/be/vandeas/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ fun Application.module() {
configureSerialization()
configureMonitoring()
configureHTTP()
configureRouting()
configureKoin()
configureStatus()
configureSecurity()
configureRouting()
}
168 changes: 168 additions & 0 deletions src/main/kotlin/be/vandeas/controller/v1/FileController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package be.vandeas.controller.v1

import be.vandeas.domain.*
import be.vandeas.dto.*
import be.vandeas.dto.ReadFileBytesResult.Companion.mapToReadFileBytesDto
import be.vandeas.exception.AuthorizationException
import be.vandeas.service.v1.FileService
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject

fun Route.fileControllerV1() = route("/file") {
val fileService by inject<FileService>()

get {
val path = call.request.queryParameters["path"] ?: ""
val fileName = call.request.queryParameters["fileName"] ?: ""
val authorization = call.request.authorization() ?: throw AuthorizationException("Authorization header is required")
val accept = call.request.accept()?.let { ContentType.parse(it) } ?: ContentType.Application.Json

if (path.isBlank() || fileName.isBlank()) {
call.respond(HttpStatusCode.BadRequest, mapOf("path" to path, "fileName" to fileName))
return@get
}

val options = FileReadOptions(
path = path,
fileName = fileName
)

when (val result = fileService.readFile(authorization, options)) {
is FileBytesReadResult.Failure -> call.respond(HttpStatusCode.InternalServerError, result.message)
is FileBytesReadResult.NotFound -> call.respond(HttpStatusCode.NotFound, FileNameWithPath(path = options.path, fileName = options.fileName))
is FileBytesReadResult.Success -> when(accept) {
ContentType.Application.Json -> call.respond(HttpStatusCode.OK, result.mapToReadFileBytesDto())
ContentType.Application.OctetStream -> call.respondBytes(result.data)
else -> call.respond(HttpStatusCode.NotAcceptable, "Accept header must be application/json or application/octet-stream")
}
}
}

get("/embed") {
val path = call.request.queryParameters["path"] ?: ""
val fileName = call.request.queryParameters["fileName"] ?: ""
val downloadFileName = call.request.queryParameters["download"] ?: ""
val authorization = call.request.queryParameters["token"] ?: call.request.authorization() ?: throw IllegalArgumentException("Authorization header is required")

if (path.isBlank() || fileName.isBlank()) {
call.respond(HttpStatusCode.BadRequest, mapOf("path" to path, "fileName" to fileName))
return@get
}

when (val result = fileService.getFile(authorization, FileReadOptions(path = path, fileName = fileName))) {
is FileReadResult.Failure -> call.respond(HttpStatusCode.InternalServerError, result.message)
is FileReadResult.NotFound -> call.respond(HttpStatusCode.NotFound, FileNameWithPath(path = path, fileName = fileName))
is FileReadResult.Success -> {
if (downloadFileName.isNotBlank()) {
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, downloadFileName)
.toString()
)
} else {
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, fileName)
.toString()
)
}
call.respondFile(result.file)
}
}
}

post {
val options: Base64FileCreationOptions = call.receive()
val authorization = call.request.authorization() ?: throw AuthorizationException("Authorization header is required")

when (val result = fileService.createFile(authorization, options)) {
is FileCreationResult.Duplicate -> call.respond(HttpStatusCode.Conflict, FileNameWithPath(path = options.path, fileName = options.fileName))
is FileCreationResult.Failure -> call.respond(HttpStatusCode.InternalServerError, result.message)
is FileCreationResult.NotFound -> call.respond(HttpStatusCode.NotFound, mapOf("path" to options.path))
is FileCreationResult.Success -> call.respond(HttpStatusCode.Created, FileNameWithPath(path = options.path, fileName = options.fileName))
}
}

post("/upload") {
val authorization = call.request.authorization() ?: throw AuthorizationException("Authorization header is required")
val multipart = call.receiveMultipart()

var fileName: String? = null
var path: String? = null
var data: ByteArray? = null

multipart.forEachPart { part ->
when (part) {
is PartData.FormItem -> {
when (part.name) {
"path" -> path = part.value
"fileName" -> fileName = part.value
}
}
is PartData.FileItem -> {
data = part.streamProvider().readBytes()
}
else -> throw IllegalArgumentException("Unsupported part type: ${part::class.simpleName}")
}
part.dispose()
}

requireNotNull(fileName) { "fileName is required" }
requireNotNull(path) { "path is required" }
requireNotNull(data) { "data is required" }

val options = BytesFileCreationOptions(
path = path!!,
fileName = fileName!!,
content = data!!
)

when (val result = fileService.createFile(authorization, options)) {
is FileCreationResult.Duplicate -> call.respond(HttpStatusCode.Conflict, FileNameWithPath(path = options.path, fileName = options.fileName))
is FileCreationResult.Failure -> call.respond(HttpStatusCode.InternalServerError, result.message)
is FileCreationResult.NotFound -> call.respond(HttpStatusCode.NotFound, mapOf("path" to options.path))
is FileCreationResult.Success -> call.respond(HttpStatusCode.Created, FileNameWithPath(path = options.path, fileName = options.fileName))
}

}

delete {
val path = call.request.queryParameters["path"] ?: throw IllegalArgumentException("path query parameter is required")
val fileName = call.request.queryParameters["fileName"]
val recursive = call.request.queryParameters["recursive"]?.toBoolean() ?: false

val authorization = call.request.authorization() ?: throw IllegalArgumentException("Authorization header is required")

if (fileName == null) {
val options = DirectoryDeleteOptions(
path = path,
recursive = recursive
)

when (val result = fileService.deleteDirectory(authorization, options)) {
is DirectoryDeleteResult.DirectoryHasChildren -> call.respond(HttpStatusCode.BadRequest, mapOf("path" to options.path, "hasChildren" to true))
is DirectoryDeleteResult.Failure -> call.respond(HttpStatusCode.InternalServerError, result.message)
is DirectoryDeleteResult.IsAFile -> call.respond(HttpStatusCode.BadRequest, mapOf("path" to options.path, "hasChildren" to false))
is DirectoryDeleteResult.NotFound -> call.respond(HttpStatusCode.NotFound, mapOf("path" to options.path))
is DirectoryDeleteResult.Success -> call.respond(HttpStatusCode.NoContent)
}
} else {
val options = FileDeleteOptions(
path = call.request.queryParameters["path"] ?: throw IllegalArgumentException("path query parameter is required"),
fileName = call.request.queryParameters["fileName"] ?: ""
)

when (val result = fileService.deleteFile(authorization, options)) {
is FileDeleteResult.Failure -> call.respond(HttpStatusCode.InternalServerError, result.message)
is FileDeleteResult.IsADirectory -> call.respond(HttpStatusCode.BadRequest, FileNameWithPath(path = options.path, fileName = options.fileName))
is FileDeleteResult.NotFound -> call.respond(HttpStatusCode.NotFound, FileNameWithPath(path = options.path, fileName = options.fileName))
is FileDeleteResult.Success -> call.respond(HttpStatusCode.NoContent)
}
}
}
}
Loading

0 comments on commit 794d7ec

Please sign in to comment.