diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 186cfe8..cc041ed 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -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) diff --git a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt deleted file mode 100644 index cb186c9..0000000 --- a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt +++ /dev/null @@ -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.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.budgetWithPermission( - budgetRepository: BudgetRepository, - permissionRepository: PermissionRepository, - budgetId: String, - permission: Permission, - block: suspend (Budget) -> Unit -) { - val session = call.principal()!! - 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.errorResponse( - httpStatusCode: HttpStatusCode = HttpStatusCode.NotFound, - message: String? = null -) { - message?.let { - call.respond(httpStatusCode, ErrorResponse(message)) - } ?: call.respond(httpStatusCode) -} diff --git a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt index 2246830..3cc5898 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt @@ -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,111 +10,39 @@ 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()!! - val budgetIds = permissionRepository.findAll(userId = session.userId).map { it.budgetId } - if (budgetIds.isEmpty()) { - call.respond(emptyList()) - 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 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 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()!! val request = call.receive() - 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 request = call.receive() - 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 session = requireNotNull(call.principal()) { "session was null" } + val request = call.receive() + 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) - call.respond(HttpStatusCode.NoContent) - } + val session = requireNotNull(call.principal()) { "session was null" } + val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" } + budgetService.delete(budgetId = budgetId, userId = session.userId) + call.respond(HttpStatusCode.NoContent) } } } diff --git a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt index 2e3e9af..2cee3e7 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt @@ -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.* diff --git a/api/src/main/kotlin/com/wbrawner/twigs/ErrorResponse.kt b/api/src/main/kotlin/com/wbrawner/twigs/ErrorResponse.kt deleted file mode 100644 index 4145f53..0000000 --- a/api/src/main/kotlin/com/wbrawner/twigs/ErrorResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.wbrawner.twigs - -import kotlinx.serialization.Serializable - -@Serializable -data class ErrorResponse(val message: String) \ No newline at end of file diff --git a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt index 32fb046..452a94a 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt @@ -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.* diff --git a/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt index d918903..61bb24b 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt @@ -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.* diff --git a/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt index c4ea8e6..083072a 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt @@ -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.* diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a457969..10a4eb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/service/build.gradle.kts b/service/build.gradle.kts new file mode 100644 index 0000000..191bf56 --- /dev/null +++ b/service/build.gradle.kts @@ -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") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/service/src/main/java/com/wbrawner/twigs/service/ApiUtils.kt b/service/src/main/java/com/wbrawner/twigs/service/ApiUtils.kt new file mode 100644 index 0000000..2d0e605 --- /dev/null +++ b/service/src/main/java/com/wbrawner/twigs/service/ApiUtils.kt @@ -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.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) +} \ No newline at end of file diff --git a/service/src/main/java/com/wbrawner/twigs/service/ErrorResponse.kt b/service/src/main/java/com/wbrawner/twigs/service/ErrorResponse.kt new file mode 100644 index 0000000..e08223d --- /dev/null +++ b/service/src/main/java/com/wbrawner/twigs/service/ErrorResponse.kt @@ -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" }) +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/wbrawner/twigs/BudgetApi.kt b/service/src/main/java/com/wbrawner/twigs/service/budget/BudgetApi.kt similarity index 79% rename from api/src/main/kotlin/com/wbrawner/twigs/BudgetApi.kt rename to service/src/main/java/com/wbrawner/twigs/service/budget/BudgetApi.kt index 1d83da9..bfc16a6 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/BudgetApi.kt +++ b/service/src/main/java/com/wbrawner/twigs/service/budget/BudgetApi.kt @@ -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 ) { constructor(budget: Budget, users: Iterable) : this( - Objects.requireNonNull(budget.id), + requireNotNull(budget.id), budget.name, budget.description, users.map { userPermission: UserPermission -> diff --git a/service/src/main/java/com/wbrawner/twigs/service/budget/BudgetService.kt b/service/src/main/java/com/wbrawner/twigs/service/budget/BudgetService.kt new file mode 100644 index 0000000..ec31e90 --- /dev/null +++ b/service/src/main/java/com/wbrawner/twigs/service/budget/BudgetService.kt @@ -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 + + 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 { + 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)) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt b/service/src/main/java/com/wbrawner/twigs/service/category/CategoryApi.kt similarity index 94% rename from api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt rename to service/src/main/java/com/wbrawner/twigs/service/category/CategoryApi.kt index 18427fb..d88e47a 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/CategoryApi.kt +++ b/service/src/main/java/com/wbrawner/twigs/service/category/CategoryApi.kt @@ -1,4 +1,4 @@ -package com.wbrawner.twigs +package com.wbrawner.twigs.service.category import com.wbrawner.twigs.model.Category import kotlinx.serialization.Serializable diff --git a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt b/service/src/main/java/com/wbrawner/twigs/service/recurringtransaction/RecurringTransactionsApi.kt similarity index 95% rename from api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt rename to service/src/main/java/com/wbrawner/twigs/service/recurringtransaction/RecurringTransactionsApi.kt index 3bc7542..cdf1825 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt +++ b/service/src/main/java/com/wbrawner/twigs/service/recurringtransaction/RecurringTransactionsApi.kt @@ -1,4 +1,4 @@ -package com.wbrawner.twigs +package com.wbrawner.twigs.service.recurringtransaction import com.wbrawner.twigs.model.RecurringTransaction import kotlinx.serialization.Serializable diff --git a/api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt b/service/src/main/java/com/wbrawner/twigs/service/transaction/TransactionApi.kt similarity index 95% rename from api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt rename to service/src/main/java/com/wbrawner/twigs/service/transaction/TransactionApi.kt index 4062858..130319d 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt +++ b/service/src/main/java/com/wbrawner/twigs/service/transaction/TransactionApi.kt @@ -1,4 +1,4 @@ -package com.wbrawner.twigs +package com.wbrawner.twigs.service.transaction import com.wbrawner.twigs.model.Transaction import kotlinx.serialization.Serializable diff --git a/api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt b/service/src/main/java/com/wbrawner/twigs/service/user/UserApi.kt similarity index 97% rename from api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt rename to service/src/main/java/com/wbrawner/twigs/service/user/UserApi.kt index f8e9f91..7443a51 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/UserApi.kt +++ b/service/src/main/java/com/wbrawner/twigs/service/user/UserApi.kt @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index a1af3bb..f54ab1e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "twigs" include("core", "api", "app", "storage", "db", "web") -include("testhelpers") \ No newline at end of file +include("testhelpers") +include("service") diff --git a/web/build.gradle.kts b/web/build.gradle.kts index 40bd3cb..502837e 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -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) }