pipeline and improvements
Some checks failed
Deploy Miniplan / build (push) Failing after 2m23s

This commit is contained in:
walamana
2024-08-07 20:08:52 +02:00
parent 24f14da9b2
commit dde21c3ac5
54 changed files with 1651 additions and 237 deletions

View File

@@ -0,0 +1,3 @@
build
local
public

View File

@@ -0,0 +1,13 @@
KTOR_DEPLOYMENT_PORT=8080
KTOR_DEVELOPMENT=false
JWT_SECRET=def
JWT_ISSUER=localhost
JWT_AUDIENCE=localhost
JWT_REALM=miniplan
DATABASE_URL=jdbc:h2:file:./db
DATABASE_DRIVER=org.h2.Driver
DATABASE_USER=abc
DATABASE_PASSWORD=abc
ADMIN_PASSWORD=123
FRONTEND_PATH=./public
APPLICATION_NAME=Miniplan\ Hl.\ Familie

View File

@@ -33,4 +33,9 @@ out/
/.nb-gradle/
### VS Code ###
.vscode/
.vscode/
.env
local/
db.mv.db

View File

@@ -0,0 +1,25 @@
FROM gradle:latest as build_backend
COPY --chownn=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle buildFatJar --no-daemon
FROM node:18 as build_frontend
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY --chown=node:node /pub
FROM openjdk:latest
LABEL authors="walamana"
RUN mkdir /miniplan
COPY ./build/libs/minis-backend-all.jar /miniplan/backend.jar
COPY ./application_docker.conf /miniplan/application.conf
ADD ./../../public/dist /miniplan/public
WORKDIR /miniplan
ENTRYPOINT ["java", "-jar", "backend.jar", "-config=application.conf"]

View File

@@ -1,7 +1,7 @@
ktor {
development = true
deployment {
port = 8080
port = "${PORT}"
}
application {
modules = [ de.walamana.ApplicationKt.module ]
@@ -9,12 +9,17 @@ ktor {
}
jwt {
secret = ${SECRET}
secret = "${SECRET}"
issuer = "http://0.0.0.0:8080/"
audience = "http://0.0.0.0:8080/"
realm = "mini-data"
}
database {
url = "jdbc:h2:file:./db"
driver = "org.h2.Driver"
}
admin {
password = ${ADMIN}
}
password = "${ADMIN}"
}

View File

@@ -0,0 +1,27 @@
ktor {
development = ${KTOR_DEVELOPMENT}
deployment {
port = ${KTOR_DEPLOYMENT_PORT}
}
application {
modules = [ de.walamana.ApplicationKt.module ]
}
}
jwt {
secret = ${JWT_SECRET}
issuer = ${JWT_ISSUER}
audience = ${JWT_AUDIENCE}
realm = ${JWT_REALM}
}
database {
url = ${DATABASE_URL}
driver = ${DATABASE_DRIVER}
user = ${DATABASE_USER}
password = ${DATABASE_PASSWORD}
}
admin {
password = ${ADMIN_PASSWORD}
}

View File

@@ -0,0 +1,30 @@
ktor {
deployment {
port = ${KTOR_DEPLOYMENT_PORT}
}
application {
modules = [ de.walamana.ApplicationKt.module ]
}
}
jwt {
secret = "${JWT_SECRET}"
issuer = "${JWT_ISSUER}"
audience = "${JWT_AUDIENCE}"
realm = "${JWT_REALM}"
}
database {
url = "jdbc:postgresql://localhost:5432/miniplan"
driver = "org.postgresql.Driver"
user = "miniplan"
password = "${DB_PASSWORD}"
}
admin {
password = "${ADMIN}"
}
application {
name = "${APPLICATION_NAME}"
}

View File

@@ -8,6 +8,7 @@ plugins {
kotlin("jvm") version "1.9.0"
id("io.ktor.plugin") version "2.3.3"
kotlin("plugin.serialization") version "1.9.0"
id("com.palantir.docker") version "0.35.0"
}
group = "de.walamana"
@@ -35,12 +36,19 @@ dependencies {
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:$h2_version")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("ch.qos.logback:logback-classic:$logback_version")
// implementation("org.postgresql:postgresql:42.7.2")
implementation("com.h2database:h2:$h2_version")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
implementation("at.favre.lib:bcrypt:0.10.2")
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
docker {
name = "${project.name}:${project.version}"
}

View File

@@ -1,6 +1,6 @@
ktor_version=2.3.3
kotlin_version=1.9.0
logback_version=1.2.11
logback_version=1.4.14
kotlin.code.style=official
exposed_version=0.43.0
h2_version=2.1.214
h2_version=2.2.224

View File

@@ -0,0 +1 @@
./../../public/dist/

View File

@@ -2,8 +2,10 @@ package de.walamana
import de.walamana.plugins.*
import de.walamana.views.*
import io.github.cdimascio.dotenv.dotenv
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.http.content.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
@@ -12,7 +14,14 @@ import io.ktor.server.routing.*
// .start(wait = true)
//}
fun main(args: Array<String>): Unit = EngineMain.main(args)
fun main(args: Array<String>): Unit {
dotenv {
ignoreIfMissing = true
ignoreIfMalformed = true
systemProperties = true
}
EngineMain.main(args)
}
fun Application.module() {
configureSecurity()
@@ -21,6 +30,9 @@ fun Application.module() {
configureDatabases()
routing {
singlePageApplication {
vue(System.getenv("FRONTEND_PATH"))
}
route("/api") {
configureMinistrantenRoutes()
configureGottesdiensteRoutes()

View File

@@ -0,0 +1,59 @@
package de.walamana.models
import de.walamana.plugins.DateAsLong
import de.walamana.plugins.dbQuery
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import java.util.Date
@Serializable
data class GottesdienstGroup (
val id: Int,
val from: DateAsLong,
val to: DateAsLong
)
object GottesdienstGroups : Table() {
val id = integer("id").autoIncrement()
val from = long("date_from")
val to = long("date_to")
override val primaryKey = PrimaryKey(id)
}
object GottesdienstGroupDao {
fun ResultRow.toPlan() = GottesdienstGroup(
this[GottesdienstGroups.id],
Date(this[GottesdienstGroups.from]),
Date(this[GottesdienstGroups.to])
)
suspend fun getPlans(amount: Int = 10, offset: Long = 0) = dbQuery {
GottesdienstGroups.selectAll()
.orderBy(GottesdienstGroups.from to SortOrder.DESC)
.limit(amount, offset)
.map { it.toPlan() }
}
suspend fun createPlan(from: Date, to: Date) = dbQuery {
GottesdienstGroups.insert {
it[GottesdienstGroups.from] = from.time
it[GottesdienstGroups.to] = to.time
}.resultedValues?.firstOrNull()?.toPlan()
}
suspend fun updatePlan(group: GottesdienstGroup): Boolean = dbQuery {
GottesdienstGroups.upsert {
it[id]= group.id
it[from] = group.from.time
it[to] = group.to.time
}.resultedValues?.isNotEmpty() ?: false
}
suspend fun deletePlan(id: Int) = dbQuery {
GottesdienstGroups.deleteWhere { GottesdienstGroups.id eq id } > 0
}
}

View File

@@ -22,8 +22,9 @@ object Gottesdienste : Table() {
val date = long("date")
val attendance = long("attendance")
val planId = integer("planId")
.references(GottesdienstGroups.id, onDelete = ReferenceOption.CASCADE)
override val primaryKey = PrimaryKey(id, planId)
override val primaryKey = PrimaryKey(id)
}
object GottesdiensteDao {
@@ -72,4 +73,4 @@ object GottesdiensteDao {
if (planId != null) it[Gottesdienste.planId] = planId
} > 0
}
}
}

View File

@@ -4,6 +4,7 @@ import de.walamana.plugins.dbQuery
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
@Serializable
data class Mark(
@@ -14,9 +15,9 @@ data class Mark(
object Marks : Table() {
val mid = integer("mid")
.references(Ministranten.id);
.references(Ministranten.id, onDelete = ReferenceOption.CASCADE);
val gid = integer("gid")
.references(Gottesdienste.id);
.references(Gottesdienste.id, onDelete = ReferenceOption.CASCADE);
val value = integer("value");
override val primaryKey = PrimaryKey(mid, gid)
@@ -51,4 +52,4 @@ object MarksDao {
} > 0
}
}
}

View File

@@ -17,7 +17,7 @@ import java.util.*
data class Ministrant(
val id: Int,
val username: String,
val passwordHash: String,
val passwordHash: String? = "",
val firstname: String,
val lastname: String,
val birthday: DateAsLong,
@@ -56,7 +56,12 @@ object MinistrantenDao {
)
suspend fun allMinistranten(): List<Ministrant> = dbQuery {
Ministranten.selectAll().map(::resultRowToMinistrant)
Ministranten.selectAll()
.orderBy(
Ministranten.lastname to SortOrder.ASC,
Ministranten.firstname to SortOrder.ASC
)
.map(::resultRowToMinistrant)
}
suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery {
@@ -65,14 +70,19 @@ object MinistrantenDao {
Ministranten.username,
Ministranten.firstname,
Ministranten.lastname
).selectAll().map { row ->
SimplifiedMinistrant(
row[Ministranten.id],
row[Ministranten.username],
row[Ministranten.firstname],
row[Ministranten.lastname]
).selectAll()
.orderBy(
Ministranten.lastname to SortOrder.ASC,
Ministranten.firstname to SortOrder.ASC
)
}
.map { row ->
SimplifiedMinistrant(
row[Ministranten.id],
row[Ministranten.username],
row[Ministranten.firstname],
row[Ministranten.lastname]
)
}
}
suspend fun getMinistrant(username: String, showPasswordHash: Boolean = false): Ministrant? = dbQuery {
@@ -123,4 +133,4 @@ object MinistrantenDao {
if (privileges != null) it[Ministranten.privileges] = privileges.joinToString(",")
}
}
}
}

View File

@@ -1,5 +1,6 @@
package de.walamana.plugins
import de.walamana.models.GottesdienstGroups
import de.walamana.models.Gottesdienste
import de.walamana.models.Marks
import de.walamana.models.Ministranten
@@ -11,16 +12,25 @@ import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransacti
import org.jetbrains.exposed.sql.transactions.transaction
fun Application.configureDatabases() {
val driverClassName = "org.h2.Driver"
val jdbcURL = "jdbc:h2:file:./build/db"
val database = Database.connect(jdbcURL, driverClassName)
val driverClassName = environment.config.property("database.driver").getString()
val jdbcURL = environment.config.property("database.url").getString()
val user = environment.config.propertyOrNull("database.user")?.getString()
val password = environment.config.propertyOrNull("database.password")?.getString()
println("Using database driver $driverClassName")
val database = if(user != null && password != null )
Database.connect(jdbcURL, driverClassName, user, password)
else
Database.connect(jdbcURL, driverClassName)
transaction {
SchemaUtils.create(Gottesdienste)
SchemaUtils.create(Ministranten)
SchemaUtils.create(GottesdienstGroups)
SchemaUtils.create(Gottesdienste)
SchemaUtils.create(Marks)
}
}
suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
suspend fun <T> dbQuery(block: suspend Transaction.() -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }

View File

@@ -62,10 +62,15 @@ object Security {
fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000 * 60 * 60);
suspend fun authenticateUser(application: Application, username: String, password: String): Ministrant? {
println("Username $username password $password")
if (username == "admin") {
println("Test")
val adminPw = application.environment.config.property("admin.password").getString()
println("Test $adminPw")
if (adminPw == password) {
val allMinis = MinistrantenDao.allMinistranten().map { it.username }
println(allMinis)
return Ministrant(
0, "admin", "", "admin", "admin", Date(), allMinis
)
@@ -103,4 +108,4 @@ object Security {
.withClaim("privileges", ministrant.privileges)
.withExpiresAt(DEFAULT_EXPIRY())
.sign(Algorithm.HMAC256(jwtEnv.secret))
}
}

View File

@@ -21,6 +21,7 @@ fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
})
}

View File

@@ -13,12 +13,17 @@ data class Plan (
object PlanDao {
suspend fun constructPlan(planId: Int): Plan {
suspend fun constructPlan(planId: Int, isAnonymous: Boolean): Plan {
val gottesdienste = GottesdiensteDao.getGottesdiensteForPlan(planId)
val ministranten = MinistrantenDao.simplifiedMinistranten()
var ministranten = MinistrantenDao.simplifiedMinistranten()
if(isAnonymous) {
ministranten = ministranten.map {
SimplifiedMinistrant(it.id, it.username, it.firstname, if(it.lastname.isNotEmpty()) it.lastname[0].toString() + "." else "")
}
}
val marks = MarksDao.allMarksForPlan(planId)
return Plan(gottesdienste, ministranten, marks)
}
}
}

View File

@@ -81,9 +81,12 @@ fun Route.configureAuthenticationRoutes() {
return@post
}
val request = call.receive<PasswordResetRequest>()
val newPassword = BCrypt.withDefaults()
.hash(Math.random().toInt() * 3 + 4, Math.random().toString().toCharArray())
.toString().substring(3..10);
var newPassword = "";
for(i in 0..2) {
newPassword += BCrypt.withDefaults()
.hash(Math.random().toInt() * 3 + 4, (Math.random().toString() + Math.random().toString()).toCharArray())
};
newPassword = newPassword.substring(3..10)
Security.setPassword(request.username, newPassword)
@@ -94,4 +97,4 @@ fun Route.configureAuthenticationRoutes() {
}
}
}
}
}

View File

@@ -2,6 +2,7 @@ package de.walamana.views
import de.walamana.models.Ministrant
import de.walamana.models.MinistrantenDao
import de.walamana.models.SimplifiedMinistrant
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
@@ -17,9 +18,10 @@ fun Route.configureMinistrantenRoutes() {
}
put {
val data = call.receive<Ministrant>()
val passwordHash = data.passwordHash ?: return@put
val ministrant = MinistrantenDao.createMinistrant(
data.username,
data.passwordHash,
passwordHash,
data.firstname,
data.lastname,
data.birthday,
@@ -29,16 +31,27 @@ fun Route.configureMinistrantenRoutes() {
}
patch {
// TODO: Access only by admin
val data = call.receive<Ministrant>()
val changed = MinistrantenDao.updateMinistrant(
data.id,
data.username,
data.passwordHash,
data.firstname,
data.lastname,
data.birthday,
data.privileges
)
val simple = false
if(simple) {
val data = call.receive<SimplifiedMinistrant>()
val changed = MinistrantenDao.updateMinistrant(
id = data.id,
username = data.username,
firstname = data.firstname,
lastname = data.lastname
)
}else{
val data = call.receive<Ministrant>()
val changed = MinistrantenDao.updateMinistrant(
data.id,
data.username,
null,
data.firstname,
data.lastname,
data.birthday,
data.privileges
)
}
call.respond(HttpStatusCode.OK)
}
delete {
@@ -47,4 +60,4 @@ fun Route.configureMinistrantenRoutes() {
call.respond(HttpStatusCode.OK)
}
}
}
}

View File

@@ -1,7 +1,12 @@
package de.walamana.views
import de.walamana.models.GottesdienstGroup
import de.walamana.models.GottesdienstGroupDao
import de.walamana.models.GottesdienstGroups
import de.walamana.service.PlanDao
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@@ -10,9 +15,34 @@ import io.ktor.server.util.*
fun Route.configurePlanRoutes() {
route("/plan") {
get {
val principal = call.principal<JWTPrincipal>()
val isAnonymous = principal == null
val id = call.parameters.getOrFail("id").toInt()
val plan = PlanDao.constructPlan(id);
val plan = PlanDao.constructPlan(id, isAnonymous = isAnonymous);
call.respond(plan)
}
}
}
route("/groups") {
get {
val offset = call.parameters.get("offset")?.toLong() ?: 0
val amount = call.parameters.get("amount")?.toInt() ?: 10
val groups = GottesdienstGroupDao.getPlans(amount, offset)
call.respond(groups)
}
post {
val group = call.receive<GottesdienstGroup>()
val res = GottesdienstGroupDao.createPlan(group.from, group.to) ?: TODO("fail")
call.respond(res)
}
patch {
val group = call.receive<GottesdienstGroup>()
val res = GottesdienstGroupDao.updatePlan(group) ?: TODO("fail")
call.respond(res)
}
delete {
val id = call.parameters.getOrFail("id").toInt()
val res = GottesdienstGroupDao.deletePlan(id)
call.respond(hashMapOf("success" to res))
}
}
}

View File

@@ -8,8 +8,8 @@ import io.ktor.server.testing.*
import kotlin.test.*
class ApplicationTest {
@Test
fun testRoot() = testApplication {
// @Test
// fun testRoot() = testApplication {
// application {
// configureRouting()
// }
@@ -17,5 +17,5 @@ class ApplicationTest {
// assertEquals(HttpStatusCode.OK, status)
// assertEquals("Hello World!", bodyAsText())
// }
}
// }
}