Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ dependencies {
implementation(libs.krontab)
implementation(libs.caffeine)
implementation(libs.tika)
implementation(libs.apachePoi)
implementation(libs.apachePoiOoxml)
}

tasks.withType<Test> {
Expand Down
11 changes: 7 additions & 4 deletions libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[versions]
ktor="2.3.7"
kotlin="1.9.10"
logback="1.4.14"
koin="3.5.0"
ktor = "2.3.7"
kotlin = "1.9.10"
logback = "1.4.14"
koin = "3.5.0"
apachePoi = "5.4.0"

[libraries]
ktorCore = { group = "io.ktor", name ="ktor-server-core-jvm", version.ref="ktor" }
Expand All @@ -28,6 +29,8 @@ kotlinxDatetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime",
krontab = { group = "dev.inmo", name = "krontab", version = "2.3.0" }
caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version = "3.1.8" }
tika = { group = "org.apache.tika", name = "tika-core", version = "2.9.1" }
apachePoi = { group = "org.apache.poi", name = "poi", version.ref = "apachePoi" }
apachePoiOoxml = { group = "org.apache.poi", name = "poi-ooxml", version.ref = "apachePoi" }

[bundles]
koin = ["koinKtor", "koinLogger"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ fun applicationModules(
single<AlertLogic> { AlertLogicImpl(get(), get(), logger) }
single<ProfilePictureLogic> { ProfilePictureLogicImpl(get()) }
single<AuthenticationLogic> { AuthenticationLogicImpl(get(), get(), get(), get()) }
single<BoxLogic> { BoxLogicImpl(get(), get()) }
single<MaterialLogic> { MaterialLogicImpl(get(), get(), get()) }
single<BoxLogic> { BoxLogicImpl(get(), get(), get()) }
single<BoxDefinitionLogic> { BoxDefinitionLogicImpl(get()) }
single<MaterialLogic> { MaterialLogicImpl(get()) }
single<ProcessLogic> { ProcessLogicImpl(get(), get(), get(), get()) }
single<ReportLogic> { ReportLogicImpl(get(), get(), get(), logger) }
single<RoleLogic> { RoleLogicImpl(get()) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package org.cdb.homunculus.controller

import io.ktor.http.ContentDisposition
import io.ktor.http.HttpHeaders
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.flow.toList
import org.cdb.homunculus.exceptions.UnauthorizedException
import org.cdb.homunculus.logic.AuthenticationLogic
import org.cdb.homunculus.logic.MaterialLogic
import org.cdb.homunculus.models.Material
import org.cdb.homunculus.models.filters.Filter
Expand All @@ -15,10 +19,13 @@ import org.cdb.homunculus.requests.authenticatedGet
import org.cdb.homunculus.requests.authenticatedPost
import org.cdb.homunculus.requests.authenticatedPut
import org.koin.ktor.ext.inject
import java.time.LocalDate
import java.time.format.DateTimeFormatter

fun Routing.materialController() =
route("/material") {
val materialLogic by inject<MaterialLogic>()
val authLogic by inject<AuthenticationLogic>()

authenticatedGet("") {
call.respond(materialLogic.getAll().toList())
Expand Down Expand Up @@ -82,4 +89,28 @@ fun Routing.materialController() =
val filter = call.receive<Filter>()
call.respond(materialLogic.filter(filter))
}

authenticatedPost("/report") {
materialLogic.createMaterialsReport()
call.respond(authLogic.generateOTT("GET", "/material/report"))
}

get("/report") {
requireNotNull(call.request.queryParameters["token"]) { "Token must not be null." }.also {
if (!authLogic.consumeOTT(it, "GET", "/material/report")) {
throw UnauthorizedException("Invalid OTT")
}
}
val report = materialLogic.createMaterialsReport()
val currentDate = LocalDate.now()
val formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
val formattedDate = currentDate.format(formatter)
val fileName = "report_$formattedDate.xlsx"
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, fileName).toString(),
)
call.response.header(HttpHeaders.ContentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
call.respondBytes(report)
}
}
1 change: 0 additions & 1 deletion src/main/kotlin/org/cdb/homunculus/dao/MaterialDao.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.cdb.homunculus.dao

import com.mongodb.kotlin.client.coroutine.FindFlow
import com.mongodb.kotlin.client.coroutine.MongoCollection
import kotlinx.coroutines.flow.Flow
import org.bson.conversions.Bson
Expand Down
12 changes: 8 additions & 4 deletions src/main/kotlin/org/cdb/homunculus/dao/impl/MaterialDaoImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ class MaterialDaoImpl(client: DBClient) : MaterialDao(client) {
),
).skip(skip).limit(limit)

override fun getSorted(sort: Bson?) = collection.find().let {
if (sort == null) it
else it.sort(sort)
}
override fun getSorted(sort: Bson?) =
collection.find().let {
if (sort == null) {
it
} else {
it.sort(sort)
}
}

override fun getLastCreated(limit: Int): Flow<Material> =
collection.find(
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/org/cdb/homunculus/logic/AuthenticationLogic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,26 @@ interface AuthenticationLogic {
* @throws UnauthorizedException if it is not possible to refresh the token for the user.
*/
suspend fun refresh(userId: EntityId): AuthResponse

/**
* @param method the HTTP method the token is valid for.
* @param path the path the token is valid for.
* @return a single-use token valid for a single operation for the following 5 minutes.
*/
fun generateOTT(
method: String,
path: String,
): String

/**
* Checks the validity of [token]. If it is valid, it will be invalidated.
* @param method the HTTP method the token is valid for.
* @param path the path the token is valid for.
* @return true if the token is valid, false otherwise.
*/
fun consumeOTT(
token: String,
method: String,
path: String,
): Boolean
}
14 changes: 14 additions & 0 deletions src/main/kotlin/org/cdb/homunculus/logic/MaterialLogic.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cdb.homunculus.logic

import kotlinx.coroutines.flow.Flow
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.cdb.homunculus.exceptions.NotFoundException
import org.cdb.homunculus.models.Material
import org.cdb.homunculus.models.filters.Filter
Expand Down Expand Up @@ -128,4 +129,17 @@ interface MaterialLogic {
* @return a [Flow] of [Material]s.
*/
fun filter(filter: Filter): Flow<Material>

/**
* Creates an Excel report for each [Material]. Each row corresponds to a material and contains [Material.name], [Material.brand],
* [Material.referenceCode] and the total quantity.
* The report is cached and if more users call this method at the same time, they will wait for the same completable future.
* @return an [XSSFWorkbook] as [ByteArray]
*/
suspend fun createMaterialsReport(): ByteArray

/**
* Invalidates the current cached material report.
*/
fun invalidateReport()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.cdb.homunculus.logic.impl

import com.github.benmanes.caffeine.cache.Caffeine
import org.cdb.homunculus.components.JWTManager
import org.cdb.homunculus.components.PasswordEncoder
import org.cdb.homunculus.dao.RoleDao
Expand All @@ -14,13 +15,17 @@ import org.cdb.homunculus.models.security.JWTClaims
import org.cdb.homunculus.models.security.JWTRefreshClaims
import org.cdb.homunculus.utils.DynamicBitArray
import java.util.Date
import java.util.UUID
import java.util.concurrent.TimeUnit

class AuthenticationLogicImpl(
private val userDao: UserDao,
private val roleDao: RoleDao,
private val passwordEncoder: PasswordEncoder,
private val jwtManager: JWTManager,
) : AuthenticationLogic {
private val tokenCache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build<String, String>()

private suspend fun User.permissionsAsBitArray(): DynamicBitArray =
role?.let {
roleDao.getById(it)
Expand Down Expand Up @@ -65,4 +70,21 @@ class AuthenticationLogicImpl(
refreshJwt = null,
)
} ?: throw UnauthorizedException("It is not possible to refresh the token for the user $userId")

override fun generateOTT(
method: String,
path: String,
): String =
UUID.randomUUID().toString().substring(0, 6).also {
tokenCache.put(it, "$method:$path")
}

override fun consumeOTT(
token: String,
method: String,
path: String,
): Boolean =
tokenCache.getIfPresent(token)?.also {
tokenCache.invalidate(token)
} == "$method:$path"
}
12 changes: 11 additions & 1 deletion src/main/kotlin/org/cdb/homunculus/logic/impl/BoxLogicImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package org.cdb.homunculus.logic.impl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.toList
import org.cdb.homunculus.components.NotificationManager
import org.cdb.homunculus.dao.BoxDao
import org.cdb.homunculus.exceptions.NotFoundException
import org.cdb.homunculus.logic.BoxLogic
import org.cdb.homunculus.logic.MaterialLogic
import org.cdb.homunculus.models.Box
import org.cdb.homunculus.models.embed.Operation
import org.cdb.homunculus.models.embed.UsageLog
Expand All @@ -20,6 +22,7 @@ import java.util.Date
class BoxLogicImpl(
private val boxDao: BoxDao,
private val notificationManager: NotificationManager,
private val materialLogic: MaterialLogic,
) : BoxLogic {
override suspend fun create(
box: Box,
Expand All @@ -41,6 +44,7 @@ class BoxLogicImpl(
return checkNotNull(boxDao.save(boxWithLog)) {
"Error during box creation"
}.also {
materialLogic.invalidateReport()
notificationManager.checkMaterial(box.material)
}
}
Expand All @@ -63,6 +67,8 @@ class BoxLogicImpl(
deletionDate = Date(),
),
)
}.onCompletion {
materialLogic.invalidateReport()
}

override fun getByPosition(shelfId: HierarchicalId): Flow<Box> = boxDao.getByPosition(shelfId, false)
Expand All @@ -76,6 +82,7 @@ class BoxLogicImpl(
deletionDate = Date(),
),
)?.id?.also {
materialLogic.invalidateReport()
notificationManager.checkMaterial(box.material)
} ?: throw IllegalStateException("Cannot delete the box with id $id")
}
Expand Down Expand Up @@ -104,6 +111,7 @@ class BoxLogicImpl(
},
),
)?.id?.also {
materialLogic.invalidateReport()
notificationManager.checkMaterial(box.material)
} ?: throw IllegalStateException("Cannot update the quantity for box $boxId")
}
Expand All @@ -120,6 +128,8 @@ class BoxLogicImpl(
description = box.description,
),
)?.id,
) { "An error occurred while updating box ${box.id}" }
) { "An error occurred while updating box ${box.id}" }.also {
materialLogic.invalidateReport()
}
}
}
Loading
Loading