WIP: Move API logic to shared services
This commit is contained in:
parent
2b66ea916b
commit
a460421497
20 changed files with 235 additions and 169 deletions
|
@ -7,7 +7,7 @@ plugins {
|
|||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
api(project(":core"))
|
||||
implementation(project(":storage"))
|
||||
implementation(project(":service"))
|
||||
api(libs.ktor.server.core)
|
||||
api(libs.ktor.serialization)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
package com.wbrawner.twigs
|
||||
|
||||
import com.wbrawner.twigs.model.Budget
|
||||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.model.Session
|
||||
import com.wbrawner.twigs.storage.BudgetRepository
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.util.pipeline.*
|
||||
|
||||
suspend inline fun PipelineContext<Unit, ApplicationCall>.requireBudgetWithPermission(
|
||||
permissionRepository: PermissionRepository,
|
||||
userId: String,
|
||||
budgetId: String?,
|
||||
permission: Permission,
|
||||
otherwise: () -> Unit
|
||||
) {
|
||||
if (budgetId.isNullOrBlank()) {
|
||||
errorResponse(HttpStatusCode.BadRequest, "budgetId is required")
|
||||
return
|
||||
}
|
||||
permissionRepository.findAll(
|
||||
userId = userId,
|
||||
budgetIds = listOf(budgetId)
|
||||
).firstOrNull {
|
||||
it.permission.isAtLeast(permission)
|
||||
} ?: run {
|
||||
errorResponse(HttpStatusCode.Forbidden, "Insufficient permissions on budget $budgetId")
|
||||
otherwise()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun PipelineContext<Unit, ApplicationCall>.budgetWithPermission(
|
||||
budgetRepository: BudgetRepository,
|
||||
permissionRepository: PermissionRepository,
|
||||
budgetId: String,
|
||||
permission: Permission,
|
||||
block: suspend (Budget) -> Unit
|
||||
) {
|
||||
val session = call.principal<Session>()!!
|
||||
val userPermission = permissionRepository.findAll(
|
||||
userId = session.userId,
|
||||
budgetIds = listOf(budgetId)
|
||||
).firstOrNull()
|
||||
if (userPermission?.permission?.isAtLeast(permission) != true) {
|
||||
errorResponse(HttpStatusCode.Forbidden)
|
||||
return
|
||||
}
|
||||
block(budgetRepository.findAll(ids = listOf(budgetId)).first())
|
||||
}
|
||||
|
||||
suspend inline fun PipelineContext<Unit, ApplicationCall>.errorResponse(
|
||||
httpStatusCode: HttpStatusCode = HttpStatusCode.NotFound,
|
||||
message: String? = null
|
||||
) {
|
||||
message?.let {
|
||||
call.respond(httpStatusCode, ErrorResponse(message))
|
||||
} ?: call.respond(httpStatusCode)
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
package com.wbrawner.twigs
|
||||
|
||||
import com.wbrawner.twigs.model.Budget
|
||||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.model.Session
|
||||
import com.wbrawner.twigs.model.UserPermission
|
||||
import com.wbrawner.twigs.storage.BudgetRepository
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import com.wbrawner.twigs.service.budget.BudgetRequest
|
||||
import com.wbrawner.twigs.service.budget.BudgetService
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
|
@ -13,113 +10,41 @@ import io.ktor.server.request.*
|
|||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Application.budgetRoutes(
|
||||
budgetRepository: BudgetRepository,
|
||||
permissionRepository: PermissionRepository
|
||||
) {
|
||||
fun Application.budgetRoutes(budgetService: BudgetService) {
|
||||
routing {
|
||||
route("/api/budgets") {
|
||||
authenticate(optional = false) {
|
||||
get {
|
||||
val session = call.principal<Session>()!!
|
||||
val budgetIds = permissionRepository.findAll(userId = session.userId).map { it.budgetId }
|
||||
if (budgetIds.isEmpty()) {
|
||||
call.respond(emptyList<BudgetResponse>())
|
||||
return@get
|
||||
}
|
||||
val budgets = budgetRepository.findAll(ids = budgetIds).map {
|
||||
BudgetResponse(it, permissionRepository.findAll(budgetIds = listOf(it.id)))
|
||||
}
|
||||
call.respond(budgets)
|
||||
val session = requireNotNull(call.principal<Session>()) { "session is required" }
|
||||
call.respond(budgetService.budgetsForUser(userId = session.userId))
|
||||
}
|
||||
|
||||
get("/{id}") {
|
||||
budgetWithPermission(
|
||||
budgetRepository,
|
||||
permissionRepository,
|
||||
call.parameters["id"]!!,
|
||||
Permission.READ
|
||||
) { budget ->
|
||||
val users = permissionRepository.findAll(budgetIds = listOf(budget.id))
|
||||
call.respond(BudgetResponse(budget, users))
|
||||
}
|
||||
val session = requireNotNull(call.principal<Session>()) { "session is required" }
|
||||
val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" }
|
||||
call.respond(budgetService.budget(budgetId = budgetId, userId = session.userId))
|
||||
}
|
||||
|
||||
post {
|
||||
val session = call.principal<Session>()!!
|
||||
val request = call.receive<BudgetRequest>()
|
||||
if (request.name.isNullOrBlank()) {
|
||||
errorResponse(HttpStatusCode.BadRequest, "Name cannot be empty or null")
|
||||
return@post
|
||||
}
|
||||
val budget = budgetRepository.save(
|
||||
Budget(
|
||||
name = request.name,
|
||||
description = request.description
|
||||
)
|
||||
)
|
||||
val users = request.users?.map {
|
||||
permissionRepository.save(
|
||||
UserPermission(
|
||||
budgetId = budget.id,
|
||||
userId = it.user,
|
||||
permission = it.permission
|
||||
)
|
||||
)
|
||||
}?.toMutableSet() ?: mutableSetOf()
|
||||
if (users.none { it.userId == session.userId }) {
|
||||
users.add(
|
||||
permissionRepository.save(
|
||||
UserPermission(
|
||||
budgetId = budget.id,
|
||||
userId = session.userId,
|
||||
permission = Permission.OWNER
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
call.respond(BudgetResponse(budget, users))
|
||||
call.respond(budgetService.save(request = request, userId = session.userId))
|
||||
}
|
||||
|
||||
put("/{id}") {
|
||||
budgetWithPermission(
|
||||
budgetRepository,
|
||||
permissionRepository,
|
||||
call.parameters["id"]!!,
|
||||
Permission.MANAGE
|
||||
) { budget ->
|
||||
val session = requireNotNull(call.principal<Session>()) { "session was null" }
|
||||
val request = call.receive<BudgetRequest>()
|
||||
val name = request.name ?: budget.name
|
||||
val description = request.description ?: budget.description
|
||||
val users = request.users?.map {
|
||||
permissionRepository.save(UserPermission(budget.id, it.user, it.permission))
|
||||
} ?: permissionRepository.findAll(budgetIds = listOf(budget.id))
|
||||
permissionRepository.findAll(budgetIds = listOf(budget.id)).forEach {
|
||||
if (it.permission != Permission.OWNER && users.none { userPermission -> userPermission.userId == it.userId }) {
|
||||
permissionRepository.delete(it)
|
||||
}
|
||||
}
|
||||
call.respond(
|
||||
BudgetResponse(
|
||||
budgetRepository.save(budget.copy(name = name, description = description)),
|
||||
users
|
||||
)
|
||||
)
|
||||
}
|
||||
val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" }
|
||||
call.respond(budgetService.save(request = request, userId = session.id, budgetId = budgetId))
|
||||
}
|
||||
|
||||
delete("/{id}") {
|
||||
budgetWithPermission(
|
||||
budgetRepository,
|
||||
permissionRepository,
|
||||
budgetId = call.parameters["id"]!!,
|
||||
Permission.OWNER
|
||||
) { budget ->
|
||||
budgetRepository.delete(budget)
|
||||
val session = requireNotNull(call.principal<Session>()) { "session was null" }
|
||||
val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" }
|
||||
budgetService.delete(budgetId = budgetId, userId = session.userId)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@ package com.wbrawner.twigs
|
|||
import com.wbrawner.twigs.model.Category
|
||||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.model.Session
|
||||
import com.wbrawner.twigs.service.category.CategoryRequest
|
||||
import com.wbrawner.twigs.service.category.CategoryResponse
|
||||
import com.wbrawner.twigs.service.errorResponse
|
||||
import com.wbrawner.twigs.service.requireBudgetWithPermission
|
||||
import com.wbrawner.twigs.storage.CategoryRepository
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import io.ktor.http.*
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package com.wbrawner.twigs
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponse(val message: String)
|
|
@ -3,6 +3,10 @@ package com.wbrawner.twigs
|
|||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.model.RecurringTransaction
|
||||
import com.wbrawner.twigs.model.Session
|
||||
import com.wbrawner.twigs.service.errorResponse
|
||||
import com.wbrawner.twigs.service.recurringtransaction.RecurringTransactionRequest
|
||||
import com.wbrawner.twigs.service.recurringtransaction.asResponse
|
||||
import com.wbrawner.twigs.service.requireBudgetWithPermission
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
||||
import io.ktor.http.*
|
||||
|
|
|
@ -3,6 +3,10 @@ package com.wbrawner.twigs
|
|||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.model.Session
|
||||
import com.wbrawner.twigs.model.Transaction
|
||||
import com.wbrawner.twigs.service.errorResponse
|
||||
import com.wbrawner.twigs.service.requireBudgetWithPermission
|
||||
import com.wbrawner.twigs.service.transaction.BalanceResponse
|
||||
import com.wbrawner.twigs.service.transaction.TransactionRequest
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import com.wbrawner.twigs.storage.TransactionRepository
|
||||
import io.ktor.http.*
|
||||
|
|
|
@ -3,6 +3,11 @@ package com.wbrawner.twigs
|
|||
import com.wbrawner.twigs.model.PasswordResetToken
|
||||
import com.wbrawner.twigs.model.Session
|
||||
import com.wbrawner.twigs.model.User
|
||||
import com.wbrawner.twigs.service.errorResponse
|
||||
import com.wbrawner.twigs.service.user.LoginRequest
|
||||
import com.wbrawner.twigs.service.user.PasswordResetRequest
|
||||
import com.wbrawner.twigs.service.user.ResetPasswordRequest
|
||||
import com.wbrawner.twigs.service.user.UserRequest
|
||||
import com.wbrawner.twigs.storage.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
|
|
|
@ -29,6 +29,7 @@ ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
|
|||
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
|
||||
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
|
||||
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
||||
ktor-server-html = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor" }
|
||||
ktor-server-sessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" }
|
||||
ktor-server-test = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||
|
|
19
service/build.gradle.kts
Normal file
19
service/build.gradle.kts
Normal file
|
@ -0,0 +1,19 @@
|
|||
plugins {
|
||||
`java-library`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
api(libs.ktor.server.core)
|
||||
implementation(project(":storage"))
|
||||
api(libs.ktor.serialization)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
testImplementation(libs.junit.jupiter.api)
|
||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||
}
|
||||
|
||||
tasks.getByName<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
21
service/src/main/java/com/wbrawner/twigs/service/ApiUtils.kt
Normal file
21
service/src/main/java/com/wbrawner/twigs/service/ApiUtils.kt
Normal file
|
@ -0,0 +1,21 @@
|
|||
package com.wbrawner.twigs.service
|
||||
|
||||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.service.budget.BudgetResponse
|
||||
import com.wbrawner.twigs.storage.BudgetRepository
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import io.ktor.http.*
|
||||
|
||||
suspend fun Pair<BudgetRepository, PermissionRepository>.budgetWithPermission(
|
||||
userId: String,
|
||||
budgetId: String,
|
||||
permission: Permission
|
||||
): BudgetResponse {
|
||||
val allPermissions = second.findAll(budgetIds = listOf(budgetId))
|
||||
val userPermission = allPermissions.firstOrNull { it.userId == userId }
|
||||
?: throw HttpException(HttpStatusCode.NotFound)
|
||||
if (!userPermission.permission.isAtLeast(permission)) {
|
||||
throw HttpException(HttpStatusCode.Forbidden)
|
||||
}
|
||||
return BudgetResponse(first.findAll(ids = listOf(budgetId)).first(), allPermissions)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.wbrawner.twigs.service
|
||||
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponse(val message: String)
|
||||
|
||||
class HttpException(
|
||||
val statusCode: HttpStatusCode,
|
||||
override val cause: Throwable? = null,
|
||||
override val message: String? = null
|
||||
) : Throwable() {
|
||||
constructor(statusCode: HttpStatusCode) : this(statusCode = statusCode, message = statusCode.description)
|
||||
|
||||
fun toResponse(): ErrorResponse =
|
||||
ErrorResponse(requireNotNull(message) { "Cannot send error to client without message" })
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package com.wbrawner.twigs
|
||||
package com.wbrawner.twigs.service.budget
|
||||
|
||||
import com.wbrawner.twigs.model.Budget
|
||||
import com.wbrawner.twigs.model.UserPermission
|
||||
import com.wbrawner.twigs.service.user.UserPermissionRequest
|
||||
import com.wbrawner.twigs.service.user.UserPermissionResponse
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class BudgetRequest(
|
||||
|
@ -20,7 +21,7 @@ data class BudgetResponse(
|
|||
val users: List<UserPermissionResponse>
|
||||
) {
|
||||
constructor(budget: Budget, users: Iterable<UserPermission>) : this(
|
||||
Objects.requireNonNull<String>(budget.id),
|
||||
requireNotNull(budget.id),
|
||||
budget.name,
|
||||
budget.description,
|
||||
users.map { userPermission: UserPermission ->
|
|
@ -0,0 +1,130 @@
|
|||
package com.wbrawner.twigs.service.budget
|
||||
|
||||
import com.wbrawner.twigs.model.Budget
|
||||
import com.wbrawner.twigs.model.Permission
|
||||
import com.wbrawner.twigs.model.UserPermission
|
||||
import com.wbrawner.twigs.service.HttpException
|
||||
import com.wbrawner.twigs.service.budgetWithPermission
|
||||
import com.wbrawner.twigs.service.user.UserPermissionRequest
|
||||
import com.wbrawner.twigs.storage.BudgetRepository
|
||||
import com.wbrawner.twigs.storage.PermissionRepository
|
||||
import io.ktor.http.*
|
||||
|
||||
interface BudgetService {
|
||||
suspend fun budgetsForUser(userId: String): List<BudgetResponse>
|
||||
|
||||
suspend fun budget(budgetId: String, userId: String): BudgetResponse
|
||||
|
||||
suspend fun save(request: BudgetRequest, userId: String, budgetId: String? = null): BudgetResponse
|
||||
|
||||
suspend fun delete(budgetId: String, userId: String)
|
||||
}
|
||||
|
||||
class DefaultBudgetService(
|
||||
private val budgetRepository: BudgetRepository,
|
||||
private val permissionRepository: PermissionRepository
|
||||
) : BudgetService {
|
||||
private val budgetPermissionRepository = budgetRepository to permissionRepository
|
||||
override suspend fun budgetsForUser(userId: String): List<BudgetResponse> {
|
||||
val budgetIds = permissionRepository.findAll(userId = userId).map { it.budgetId }
|
||||
if (budgetIds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
return budgetRepository.findAll(ids = budgetIds).map {
|
||||
BudgetResponse(it, permissionRepository.findAll(budgetIds = listOf(it.id)))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun budget(budgetId: String, userId: String): BudgetResponse =
|
||||
budgetPermissionRepository.budgetWithPermission(userId, budgetId, Permission.READ)
|
||||
|
||||
override suspend fun save(request: BudgetRequest, userId: String, budgetId: String?): BudgetResponse {
|
||||
val budget = budgetId?.let {
|
||||
budgetPermissionRepository.budgetWithPermission(
|
||||
budgetId = it,
|
||||
userId = userId,
|
||||
permission = Permission.MANAGE
|
||||
).run {
|
||||
Budget(
|
||||
id = it,
|
||||
name = request.name ?: name,
|
||||
description = request.description ?: description
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
if (request.name.isNullOrBlank()) {
|
||||
throw HttpException(HttpStatusCode.BadRequest, message = "Name cannot be empty or null")
|
||||
}
|
||||
Budget(
|
||||
name = request.name,
|
||||
description = request.description
|
||||
)
|
||||
}
|
||||
val users = budgetId?.let {
|
||||
// If user is owner, apply changes
|
||||
// If user is manager, make sure they're not changing ownership
|
||||
val oldUsers = permissionRepository.findAll(budgetIds = listOf(budgetId))
|
||||
val oldPermissions = oldUsers.associate { it.userId to it.permission }
|
||||
val currentUserPermission = oldPermissions[userId] ?: throw HttpException(HttpStatusCode.NotFound)
|
||||
val newUsers = request.users?.map { userPermission ->
|
||||
if (userPermission.permission == Permission.OWNER
|
||||
&& oldPermissions[userPermission.user] !== Permission.OWNER
|
||||
&& currentUserPermission != Permission.OWNER
|
||||
) {
|
||||
// The user is attempting to add a new owner
|
||||
throw HttpException(
|
||||
HttpStatusCode.Forbidden,
|
||||
message = "You must be an owner to be able to modify other users' ownership"
|
||||
)
|
||||
}
|
||||
userPermission
|
||||
} ?: return@let oldUsers.map { UserPermissionRequest(it.userId, it.permission) }
|
||||
oldPermissions.filterValues { it == Permission.OWNER }
|
||||
.forEach { (user, permission) ->
|
||||
if (newUsers.none { it.user == user && it.permission == permission }
|
||||
&& currentUserPermission != Permission.OWNER
|
||||
) {
|
||||
// The user is attempting to remove a previous owner
|
||||
throw HttpException(
|
||||
HttpStatusCode.Forbidden,
|
||||
message = "You must be an owner to be able to modify other users' ownership"
|
||||
)
|
||||
}
|
||||
}
|
||||
oldUsers.forEach { oldUserPermission ->
|
||||
if (newUsers.none { it.user == oldUserPermission.userId && it.permission == oldUserPermission.permission }) {
|
||||
permissionRepository.delete(oldUserPermission)
|
||||
}
|
||||
}
|
||||
newUsers
|
||||
} ?: run {
|
||||
val newUsers = request.users
|
||||
?.toMutableList()
|
||||
?: mutableListOf()
|
||||
val currentUserPermission = newUsers.firstOrNull { it.user == userId }
|
||||
if (currentUserPermission == null || currentUserPermission.permission != Permission.OWNER) {
|
||||
newUsers.removeIf { it.user == userId }
|
||||
newUsers.add(UserPermissionRequest(userId, Permission.OWNER))
|
||||
}
|
||||
newUsers
|
||||
}
|
||||
val savedBudget = budgetRepository.save(budget)
|
||||
return BudgetResponse(
|
||||
savedBudget,
|
||||
users.map {
|
||||
permissionRepository.save(
|
||||
UserPermission(
|
||||
budgetId = savedBudget.id,
|
||||
userId = it.user,
|
||||
permission = it.permission
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun delete(budgetId: String, userId: String) {
|
||||
val budgetResponse = budgetPermissionRepository.budgetWithPermission(userId, budgetId, Permission.OWNER)
|
||||
budgetRepository.delete(Budget(budgetResponse.id, budgetResponse.name, budgetResponse.description))
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.twigs
|
||||
package com.wbrawner.twigs.service.category
|
||||
|
||||
import com.wbrawner.twigs.model.Category
|
||||
import kotlinx.serialization.Serializable
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.twigs
|
||||
package com.wbrawner.twigs.service.recurringtransaction
|
||||
|
||||
import com.wbrawner.twigs.model.RecurringTransaction
|
||||
import kotlinx.serialization.Serializable
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.twigs
|
||||
package com.wbrawner.twigs.service.transaction
|
||||
|
||||
import com.wbrawner.twigs.model.Transaction
|
||||
import kotlinx.serialization.Serializable
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.twigs
|
||||
package com.wbrawner.twigs.service.user
|
||||
|
||||
import com.wbrawner.twigs.model.PasswordResetToken
|
||||
import com.wbrawner.twigs.model.Permission
|
|
@ -1,3 +1,4 @@
|
|||
rootProject.name = "twigs"
|
||||
include("core", "api", "app", "storage", "db", "web")
|
||||
include("testhelpers")
|
||||
include("service")
|
||||
|
|
|
@ -8,6 +8,7 @@ plugins {
|
|||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
api(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.html)
|
||||
testImplementation(libs.junit.jupiter.api)
|
||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue