diff --git a/README.md b/README.md index 61ef8d6..b02c141 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,20 @@ running, you can run the app from the command line with gradle: Some parameters of Twigs can be configured via environment variables: -Environment Variable|Default Value|Note -:---:|:---:|:--- -`TWIGS_PORT`|`8080`|Port for web server to listen on -`TWIGS_DB_HOST`|`localhost`|PostgreSQL server host -`TWIGS_DB_PORT`|`5432`|PostgreSQL server port -`TWIGS_DB_NAME`|`twigs`|PostgreSQL database name -`TWIGS_DB_USER`|`twigs`|PostgreSQL database user -`TWIGS_DB_PASS`|`twigs`|PostgreSQL database password -`TWIGS_PW_SALT`||Salt to use for password, generated if empty or null -`TWIGS_SMTP_FROM`||From email address for automated emails sent from Twigs -`TWIGS_SMTP_HOST`||SMTP server host for sending emails -`TWIGS_SMTP_PORT`||SMTP server port for sending emails -`TWIGS_SMTP_USER`||SMTP server username for sending emails -`TWIGS_SMTP_PASS`||SMTP server password for sending emails +| Environment Variable | Default Value | Note | +|:--------------------:|:-------------:|:--------------------------------------------------------| +| `TWIGS_PORT` | `8080` | Port for web server to listen on | +| `TWIGS_DB_HOST` | `localhost` | PostgreSQL server host | +| `TWIGS_DB_PORT` | `5432` | PostgreSQL server port | +| `TWIGS_DB_NAME` | `twigs` | PostgreSQL database name | +| `TWIGS_DB_USER` | `twigs` | PostgreSQL database user | +| `TWIGS_DB_PASS` | `twigs` | PostgreSQL database password | +| `TWIGS_PW_SALT` | | Salt to use for password, generated if empty or null | +| `TWIGS_SMTP_FROM` | | From email address for automated emails sent from Twigs | +| `TWIGS_SMTP_HOST` | | SMTP server host for sending emails | +| `TWIGS_SMTP_PORT` | | SMTP server port for sending emails | +| `TWIGS_SMTP_USER` | | SMTP server username for sending emails | +| `TWIGS_SMTP_PASS` | | SMTP server password for sending emails | ## Building diff --git a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt index fff5f72..cb186c9 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt @@ -5,10 +5,10 @@ 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.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.response.* +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( @@ -58,5 +58,5 @@ suspend inline fun PipelineContext.errorResponse( ) { message?.let { call.respond(httpStatusCode, ErrorResponse(message)) - }?: call.respond(httpStatusCode) + } ?: 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 277b943..2246830 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt @@ -6,12 +6,12 @@ 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 io.ktor.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* fun Application.budgetRoutes( budgetRepository: BudgetRepository, @@ -34,7 +34,12 @@ fun Application.budgetRoutes( } get("/{id}") { - budgetWithPermission(budgetRepository, permissionRepository, call.parameters["id"]!!, Permission.READ) { budget -> + budgetWithPermission( + budgetRepository, + permissionRepository, + call.parameters["id"]!!, + Permission.READ + ) { budget -> val users = permissionRepository.findAll(budgetIds = listOf(budget.id)) call.respond(BudgetResponse(budget, users)) } @@ -77,7 +82,12 @@ fun Application.budgetRoutes( } put("/{id}") { - budgetWithPermission(budgetRepository, permissionRepository, call.parameters["id"]!!, Permission.MANAGE) { budget -> + 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 @@ -99,7 +109,12 @@ fun Application.budgetRoutes( } delete("/{id}") { - budgetWithPermission(budgetRepository, permissionRepository, budgetId = call.parameters["id"]!!, Permission.OWNER) { budget -> + budgetWithPermission( + budgetRepository, + permissionRepository, + budgetId = call.parameters["id"]!!, + Permission.OWNER + ) { budget -> budgetRepository.delete(budget) 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 e37e469..2e3e9af 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt @@ -5,12 +5,12 @@ import com.wbrawner.twigs.model.Permission import com.wbrawner.twigs.model.Session import com.wbrawner.twigs.storage.CategoryRepository import com.wbrawner.twigs.storage.PermissionRepository -import io.ktor.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* fun Application.categoryRoutes( categoryRepository: CategoryRepository, diff --git a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt index 67745fc..32fb046 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt @@ -5,12 +5,12 @@ import com.wbrawner.twigs.model.RecurringTransaction import com.wbrawner.twigs.model.Session import com.wbrawner.twigs.storage.PermissionRepository import com.wbrawner.twigs.storage.RecurringTransactionRepository -import io.ktor.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import io.ktor.util.pipeline.* import java.time.Instant diff --git a/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt index 4d1cb66..d918903 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt @@ -5,12 +5,12 @@ import com.wbrawner.twigs.model.Session import com.wbrawner.twigs.model.Transaction import com.wbrawner.twigs.storage.PermissionRepository import com.wbrawner.twigs.storage.TransactionRepository -import io.ktor.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import java.time.Instant fun Application.transactionRoutes( @@ -32,7 +32,7 @@ fun Application.transactionRoutes( from = call.request.queryParameters["from"]?.let { Instant.parse(it) }, to = call.request.queryParameters["to"]?.let { Instant.parse(it) }, expense = call.request.queryParameters["expense"]?.toBoolean(), - ).map { it.asResponse() }) + ).map { it.asResponse() }) } get("/{id}") { diff --git a/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt index c6b300e..8a4144f 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/UserRoutes.kt @@ -7,12 +7,12 @@ import com.wbrawner.twigs.storage.PasswordResetRepository import com.wbrawner.twigs.storage.PermissionRepository import com.wbrawner.twigs.storage.SessionRepository import com.wbrawner.twigs.storage.UserRepository -import io.ktor.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import java.time.Instant fun Application.userRoutes( diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90b5281..49e0a3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,9 +22,7 @@ dependencies { implementation(project(":db")) implementation(project(":web")) implementation(libs.kotlin.reflect) - implementation(libs.ktor.server.core) - implementation(libs.ktor.server.cio) - implementation(libs.ktor.server.sessions) + implementation(libs.bundles.ktor.server) implementation(libs.kotlinx.coroutines.core) implementation(libs.logback) implementation(libs.mail) diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt index 7c12465..ec8012f 100644 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt +++ b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt @@ -8,13 +8,15 @@ import com.wbrawner.twigs.storage.* import com.wbrawner.twigs.web.webRoutes import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource -import io.ktor.application.* -import io.ktor.auth.* -import io.ktor.features.* import io.ktor.http.* -import io.ktor.response.* -import io.ktor.serialization.* -import io.ktor.sessions.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.plugins.callloging.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.response.* +import io.ktor.server.sessions.* import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -80,14 +82,14 @@ fun Application.moduleWithDependencies( call.respond(HttpStatusCode.Unauthorized) } validate { session -> - environment.log.info("Validating session") + application.environment.log.info("Validating session") val storedSession = sessionRepository.findAll(session.token) .firstOrNull() if (storedSession == null) { - environment.log.info("Did not find session!") + application.environment.log.info("Did not find session!") return@validate null } else { - environment.log.info("Found session!") + application.environment.log.info("Found session!") } return@validate if (twoWeeksFromNow.isAfter(storedSession.expiration)) { sessionRepository.save(storedSession.copy(expiration = twoWeeksFromNow)) @@ -101,7 +103,7 @@ fun Application.moduleWithDependencies( header("Authorization") { serializer = object : SessionSerializer { override fun deserialize(text: String): Session { - environment.log.info("Deserializing session!") + this@moduleWithDependencies.environment.log.info("Deserializing session!") return Session(token = text.substringAfter("Bearer ")) } @@ -122,28 +124,28 @@ fun Application.moduleWithDependencies( }) } install(CORS) { - host("twigs.wbrawner.com", listOf("http", "https")) // TODO: Make configurable - method(HttpMethod.Options) - method(HttpMethod.Put) - method(HttpMethod.Delete) - header(HttpHeaders.Authorization) - header(HttpHeaders.Accept) - header(HttpHeaders.AcceptEncoding) - header(HttpHeaders.AcceptLanguage) - header(HttpHeaders.Connection) - header(HttpHeaders.ContentType) - header(HttpHeaders.Host) - header(HttpHeaders.Origin) - header(HttpHeaders.AccessControlRequestHeaders) - header(HttpHeaders.AccessControlRequestMethod) - header("Sec-Fetch-Dest") - header("Sec-Fetch-Mode") - header("Sec-Fetch-Site") - header("sec-ch-ua") - header("sec-ch-ua-mobile") - header("sec-ch-ua-platform") - header(HttpHeaders.UserAgent) - header("DNT") + allowHost("twigs.wbrawner.com", listOf("http", "https")) // TODO: Make configurable + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowHeader(HttpHeaders.Authorization) + allowHeader(HttpHeaders.Accept) + allowHeader(HttpHeaders.AcceptEncoding) + allowHeader(HttpHeaders.AcceptLanguage) + allowHeader(HttpHeaders.Connection) + allowHeader(HttpHeaders.ContentType) + allowHeader(HttpHeaders.Host) + allowHeader(HttpHeaders.Origin) + allowHeader(HttpHeaders.AccessControlRequestHeaders) + allowHeader(HttpHeaders.AccessControlRequestMethod) + allowHeader("Sec-Fetch-Dest") + allowHeader("Sec-Fetch-Mode") + allowHeader("Sec-Fetch-Site") + allowHeader("sec-ch-ua") + allowHeader("sec-ch-ua-mobile") + allowHeader("sec-ch-ua-platform") + allowHeader(HttpHeaders.UserAgent) + allowHeader("DNT") allowCredentials = true } budgetRoutes(budgetRepository, permissionRepository) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0c3db56..a93ffc5 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -5,7 +5,7 @@ plugins { dependencies { implementation(kotlin("stdlib")) - api(libs.ktor.auth) + api(libs.ktor.server.auth) api(libs.bcrypt) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) diff --git a/core/src/main/kotlin/com/wbrawner/twigs/model/Session.kt b/core/src/main/kotlin/com/wbrawner/twigs/model/Session.kt index 6e4ea97..41e82fb 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/model/Session.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/model/Session.kt @@ -3,7 +3,7 @@ package com.wbrawner.twigs.model import com.wbrawner.twigs.Identifiable import com.wbrawner.twigs.randomString import com.wbrawner.twigs.twoWeeksFromNow -import io.ktor.auth.* +import io.ktor.server.auth.* import java.time.Instant data class Session( diff --git a/core/src/main/kotlin/com/wbrawner/twigs/model/User.kt b/core/src/main/kotlin/com/wbrawner/twigs/model/User.kt index 883786e..cd5ab9f 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/model/User.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/model/User.kt @@ -2,7 +2,7 @@ package com.wbrawner.twigs.model import com.wbrawner.twigs.Identifiable import com.wbrawner.twigs.randomString -import io.ktor.auth.* +import io.ktor.server.auth.* data class User( override val id: String = randomString(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a29a41..0967818 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ hikari = "5.0.1" junit = "5.8.2" kotlin = "1.6.21" kotlinx-coroutines = "1.6.2" -ktor = "1.6.6" +ktor = "2.0.2" logback = "1.2.11" mail = "1.6.2" postgres = "42.3.4" @@ -15,19 +15,32 @@ bcrypt = { module = "at.favre.lib:bcrypt", version.ref = "bcrypt" } hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -ktor-auth = { module = "io.ktor:ktor-auth", version.ref = "ktor" } -ktor-serialization = { module = "io.ktor:ktor-serialization", version.ref = "ktor" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" } +ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" } 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-sessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } mail = { module = "com.sun.mail:javax.mail", version.ref = "mail" } postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } +[bundles] +ktor-server = [ + "ktor-server-call-logging", + "ktor-server-cio", + "ktor-server-content-negotiation", + "ktor-server-core", + "ktor-server-cors", + "ktor-server-sessions" +] + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/web/src/main/kotlin/com/wbrawner/twigs/web/WebRoutes.kt b/web/src/main/kotlin/com/wbrawner/twigs/web/WebRoutes.kt index 144d698..63f4723 100644 --- a/web/src/main/kotlin/com/wbrawner/twigs/web/WebRoutes.kt +++ b/web/src/main/kotlin/com/wbrawner/twigs/web/WebRoutes.kt @@ -1,10 +1,10 @@ package com.wbrawner.twigs.web -import io.ktor.application.* -import io.ktor.http.content.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.http.content.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* fun Application.webRoutes() { routing {