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 {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
api(project(":core"))
|
api(project(":core"))
|
||||||
implementation(project(":storage"))
|
implementation(project(":service"))
|
||||||
api(libs.ktor.server.core)
|
api(libs.ktor.server.core)
|
||||||
api(libs.ktor.serialization)
|
api(libs.ktor.serialization)
|
||||||
api(libs.kotlinx.coroutines.core)
|
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
|
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.Session
|
||||||
import com.wbrawner.twigs.model.UserPermission
|
import com.wbrawner.twigs.service.budget.BudgetRequest
|
||||||
import com.wbrawner.twigs.storage.BudgetRepository
|
import com.wbrawner.twigs.service.budget.BudgetService
|
||||||
import com.wbrawner.twigs.storage.PermissionRepository
|
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
|
@ -13,113 +10,41 @@ import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
fun Application.budgetRoutes(
|
fun Application.budgetRoutes(budgetService: BudgetService) {
|
||||||
budgetRepository: BudgetRepository,
|
|
||||||
permissionRepository: PermissionRepository
|
|
||||||
) {
|
|
||||||
routing {
|
routing {
|
||||||
route("/api/budgets") {
|
route("/api/budgets") {
|
||||||
authenticate(optional = false) {
|
authenticate(optional = false) {
|
||||||
get {
|
get {
|
||||||
val session = call.principal<Session>()!!
|
val session = requireNotNull(call.principal<Session>()) { "session is required" }
|
||||||
val budgetIds = permissionRepository.findAll(userId = session.userId).map { it.budgetId }
|
call.respond(budgetService.budgetsForUser(userId = session.userId))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
budgetWithPermission(
|
val session = requireNotNull(call.principal<Session>()) { "session is required" }
|
||||||
budgetRepository,
|
val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" }
|
||||||
permissionRepository,
|
call.respond(budgetService.budget(budgetId = budgetId, userId = session.userId))
|
||||||
call.parameters["id"]!!,
|
|
||||||
Permission.READ
|
|
||||||
) { budget ->
|
|
||||||
val users = permissionRepository.findAll(budgetIds = listOf(budget.id))
|
|
||||||
call.respond(BudgetResponse(budget, users))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
val session = call.principal<Session>()!!
|
val session = call.principal<Session>()!!
|
||||||
val request = call.receive<BudgetRequest>()
|
val request = call.receive<BudgetRequest>()
|
||||||
if (request.name.isNullOrBlank()) {
|
call.respond(budgetService.save(request = request, userId = session.userId))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
put("/{id}") {
|
put("/{id}") {
|
||||||
budgetWithPermission(
|
val session = requireNotNull(call.principal<Session>()) { "session was null" }
|
||||||
budgetRepository,
|
|
||||||
permissionRepository,
|
|
||||||
call.parameters["id"]!!,
|
|
||||||
Permission.MANAGE
|
|
||||||
) { budget ->
|
|
||||||
val request = call.receive<BudgetRequest>()
|
val request = call.receive<BudgetRequest>()
|
||||||
val name = request.name ?: budget.name
|
val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" }
|
||||||
val description = request.description ?: budget.description
|
call.respond(budgetService.save(request = request, userId = session.id, budgetId = budgetId))
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete("/{id}") {
|
delete("/{id}") {
|
||||||
budgetWithPermission(
|
val session = requireNotNull(call.principal<Session>()) { "session was null" }
|
||||||
budgetRepository,
|
val budgetId = requireNotNull(call.parameters["id"]) { "budgetId is required" }
|
||||||
permissionRepository,
|
budgetService.delete(budgetId = budgetId, userId = session.userId)
|
||||||
budgetId = call.parameters["id"]!!,
|
|
||||||
Permission.OWNER
|
|
||||||
) { budget ->
|
|
||||||
budgetRepository.delete(budget)
|
|
||||||
call.respond(HttpStatusCode.NoContent)
|
call.respond(HttpStatusCode.NoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -3,6 +3,10 @@ package com.wbrawner.twigs
|
||||||
import com.wbrawner.twigs.model.Category
|
import com.wbrawner.twigs.model.Category
|
||||||
import com.wbrawner.twigs.model.Permission
|
import com.wbrawner.twigs.model.Permission
|
||||||
import com.wbrawner.twigs.model.Session
|
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.CategoryRepository
|
||||||
import com.wbrawner.twigs.storage.PermissionRepository
|
import com.wbrawner.twigs.storage.PermissionRepository
|
||||||
import io.ktor.http.*
|
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.Permission
|
||||||
import com.wbrawner.twigs.model.RecurringTransaction
|
import com.wbrawner.twigs.model.RecurringTransaction
|
||||||
import com.wbrawner.twigs.model.Session
|
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.PermissionRepository
|
||||||
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
|
|
@ -3,6 +3,10 @@ package com.wbrawner.twigs
|
||||||
import com.wbrawner.twigs.model.Permission
|
import com.wbrawner.twigs.model.Permission
|
||||||
import com.wbrawner.twigs.model.Session
|
import com.wbrawner.twigs.model.Session
|
||||||
import com.wbrawner.twigs.model.Transaction
|
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.PermissionRepository
|
||||||
import com.wbrawner.twigs.storage.TransactionRepository
|
import com.wbrawner.twigs.storage.TransactionRepository
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
|
|
@ -3,6 +3,11 @@ package com.wbrawner.twigs
|
||||||
import com.wbrawner.twigs.model.PasswordResetToken
|
import com.wbrawner.twigs.model.PasswordResetToken
|
||||||
import com.wbrawner.twigs.model.Session
|
import com.wbrawner.twigs.model.Session
|
||||||
import com.wbrawner.twigs.model.User
|
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 com.wbrawner.twigs.storage.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
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-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-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
|
||||||
ktor-server-cors = { module = "io.ktor:ktor-server-cors", 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-sessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" }
|
||||||
ktor-server-test = { module = "io.ktor:ktor-server-test-host", 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" }
|
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.Budget
|
||||||
import com.wbrawner.twigs.model.UserPermission
|
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 kotlinx.serialization.Serializable
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BudgetRequest(
|
data class BudgetRequest(
|
||||||
|
@ -20,7 +21,7 @@ data class BudgetResponse(
|
||||||
val users: List<UserPermissionResponse>
|
val users: List<UserPermissionResponse>
|
||||||
) {
|
) {
|
||||||
constructor(budget: Budget, users: Iterable<UserPermission>) : this(
|
constructor(budget: Budget, users: Iterable<UserPermission>) : this(
|
||||||
Objects.requireNonNull<String>(budget.id),
|
requireNotNull(budget.id),
|
||||||
budget.name,
|
budget.name,
|
||||||
budget.description,
|
budget.description,
|
||||||
users.map { userPermission: UserPermission ->
|
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 com.wbrawner.twigs.model.Category
|
||||||
import kotlinx.serialization.Serializable
|
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 com.wbrawner.twigs.model.RecurringTransaction
|
||||||
import kotlinx.serialization.Serializable
|
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 com.wbrawner.twigs.model.Transaction
|
||||||
import kotlinx.serialization.Serializable
|
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.PasswordResetToken
|
||||||
import com.wbrawner.twigs.model.Permission
|
import com.wbrawner.twigs.model.Permission
|
|
@ -1,3 +1,4 @@
|
||||||
rootProject.name = "twigs"
|
rootProject.name = "twigs"
|
||||||
include("core", "api", "app", "storage", "db", "web")
|
include("core", "api", "app", "storage", "db", "web")
|
||||||
include("testhelpers")
|
include("testhelpers")
|
||||||
|
include("service")
|
||||||
|
|
|
@ -8,6 +8,7 @@ plugins {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
api(libs.ktor.server.core)
|
api(libs.ktor.server.core)
|
||||||
|
implementation(libs.ktor.server.html)
|
||||||
testImplementation(libs.junit.jupiter.api)
|
testImplementation(libs.junit.jupiter.api)
|
||||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue