diff --git a/docker-compose.yml b/docker-compose.yml index eb5128f..c29f97a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: - POSTGRES_PASSWORD=minis - POSTGRES_USER=minis - POSTGRES_DB=minis + ports: + - 5432:5432 networks: - backend volumes: diff --git a/private/minis-backend/.env b/private/minis-backend/.env index 0805f7f..7a59ace 100644 --- a/private/minis-backend/.env +++ b/private/minis-backend/.env @@ -11,3 +11,5 @@ DATABASE_PASSWORD=abc ADMIN_PASSWORD=123 FRONTEND_PATH=./public APPLICATION_NAME=Miniplan\ Hl.\ Familie +PORT=8080 + diff --git a/private/minis-backend/application_base.conf b/private/minis-backend/application_base.conf index 86b86ba..62f770f 100644 --- a/private/minis-backend/application_base.conf +++ b/private/minis-backend/application_base.conf @@ -1,7 +1,7 @@ ktor { development = true deployment { - port = "${PORT}" + port = ${PORT} } application { modules = [ de.walamana.ApplicationKt.module ] @@ -9,7 +9,7 @@ ktor { } jwt { - secret = "${SECRET}" + secret = ${SECRET} issuer = "http://0.0.0.0:8080/" audience = "http://0.0.0.0:8080/" realm = "mini-data" @@ -21,5 +21,5 @@ database { } admin { - password = "${ADMIN}" + password = ${ADMIN} } diff --git a/private/minis-backend/src/main/kotlin/de/walamana/models/Ministranten.kt b/private/minis-backend/src/main/kotlin/de/walamana/models/Ministranten.kt index 25cbec5..97b06a6 100644 --- a/private/minis-backend/src/main/kotlin/de/walamana/models/Ministranten.kt +++ b/private/minis-backend/src/main/kotlin/de/walamana/models/Ministranten.kt @@ -20,7 +20,7 @@ data class Ministrant( val passwordHash: String? = "", val firstname: String, val lastname: String, - val birthday: DateAsLong, + val birthday: DateAsLong? = null, val privileges: CommaSeperatedStringList ) @@ -38,7 +38,7 @@ object Ministranten : Table() { val passwordHash = varchar("passwordHash", 1024) val firstname = varchar("firstname", 128) val lastname = varchar("lastname", 128) - val birthday = long("birthday") + val birthday = long("birthday").nullable() val privileges = varchar("privileges", 1024) override val primaryKey = PrimaryKey(id) @@ -51,7 +51,7 @@ object MinistrantenDao { if (showPasswordHash) row[Ministranten.passwordHash] else "", row[Ministranten.firstname], row[Ministranten.lastname], - Date(row[Ministranten.birthday]), + row[Ministranten.birthday]?.let(::Date), row[Ministranten.privileges].split(",") ) @@ -96,7 +96,7 @@ object MinistrantenDao { passwordHash: String, firstname: String, lastname: String, - birthday: Date, + birthday: Date?, privileges: List ) = dbQuery { val statement = Ministranten.insert { @@ -104,7 +104,7 @@ object MinistrantenDao { it[Ministranten.passwordHash] = "" it[Ministranten.firstname] = firstname it[Ministranten.lastname] = lastname - it[Ministranten.birthday] = birthday.time + it[Ministranten.birthday] = birthday?.time it[Ministranten.privileges] = privileges.joinToString(",") } Security.setPassword(username, passwordHash) diff --git a/private/minis-backend/src/main/kotlin/de/walamana/plugins/Databases.kt b/private/minis-backend/src/main/kotlin/de/walamana/plugins/Databases.kt index 65fb568..e0d82b4 100644 --- a/private/minis-backend/src/main/kotlin/de/walamana/plugins/Databases.kt +++ b/private/minis-backend/src/main/kotlin/de/walamana/plugins/Databases.kt @@ -29,6 +29,8 @@ fun Application.configureDatabases() { SchemaUtils.create(GottesdienstGroups) SchemaUtils.create(Gottesdienste) SchemaUtils.create(Marks) + // TODO Add proper versioning and migration for database + exec("ALTER TABLE MINISTRANTEN ALTER COLUMN birthday BIGINT NULL;") } } diff --git a/private/minis-backend/src/main/kotlin/de/walamana/plugins/Security.kt b/private/minis-backend/src/main/kotlin/de/walamana/plugins/Security.kt index a92c6b8..eea46b6 100644 --- a/private/minis-backend/src/main/kotlin/de/walamana/plugins/Security.kt +++ b/private/minis-backend/src/main/kotlin/de/walamana/plugins/Security.kt @@ -59,7 +59,7 @@ fun Payload.mid() = getClaim("id").asInt() object Security { - fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 14); + fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 40); suspend fun authenticateUser(application: Application, username: String, password: String): Ministrant? { println("Username $username password $password") diff --git a/private/minis-backend/src/main/kotlin/de/walamana/views/AuthenticationView.kt b/private/minis-backend/src/main/kotlin/de/walamana/views/AuthenticationView.kt index 8a5f1ee..eb0f1d3 100644 --- a/private/minis-backend/src/main/kotlin/de/walamana/views/AuthenticationView.kt +++ b/private/minis-backend/src/main/kotlin/de/walamana/views/AuthenticationView.kt @@ -3,6 +3,7 @@ package de.walamana.views import at.favre.lib.crypto.bcrypt.BCrypt import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm +import de.walamana.models.Ministrant import de.walamana.models.MinistrantenDao import de.walamana.plugins.Security import de.walamana.plugins.getJWTEnvironment @@ -98,6 +99,28 @@ fun Route.configureAuthenticationRoutes() { call.respond(hashMapOf("password" to newPassword)) } + post("refresh") { + val principal = call.principal()!! + val jwtEnv = application.getJWTEnvironment() + val username = principal.payload.username() + + val ministrant = if (username == "admin") { + val allMinis = MinistrantenDao.allMinistranten().map { it.username } + Ministrant(0, "admin", "", "admin", "admin", null, allMinis) + } else { + MinistrantenDao.getMinistrant(username) ?: return@post + } + + val newToken = Security.createToken(jwtEnv, ministrant) + val expiry = Security.DEFAULT_EXPIRY().toGMTString() + + call.response.header( + "Set-Cookie", + "token=$newToken; HttpOnly; Expires=$expiry" + ) + + call.respond(AuthenticationResult(true, newToken, ministrant.privileges)) + } } } } diff --git a/public/src/App.vue b/public/src/App.vue index f900d66..1b95328 100644 --- a/public/src/App.vue +++ b/public/src/App.vue @@ -22,8 +22,11 @@ function print(){ window.print() } -onMounted(() => { +onMounted(async () => { Auth.checkForToken() + if (Auth.getToken()) { + await Auth.refreshToken() + } }) LoginService.subject.subscribe(() => { diff --git a/public/src/components/GottesdienstCard.vue b/public/src/components/GottesdienstCard.vue new file mode 100644 index 0000000..7e4b6c7 --- /dev/null +++ b/public/src/components/GottesdienstCard.vue @@ -0,0 +1,474 @@ + + + + + diff --git a/public/src/components/GroupView.vue b/public/src/components/GroupView.vue index 0fb3722..04fd8bf 100644 --- a/public/src/components/GroupView.vue +++ b/public/src/components/GroupView.vue @@ -133,31 +133,33 @@ function dateToValueString(time) { @@ -165,56 +167,119 @@ function dateToValueString(time) { .bar { display: flex; - width: calc(100% - 30px); + flex-direction: column; align-items: center; - justify-content: center; border-bottom: 1px solid #d7d5d5; z-index: 10; - padding: 15px; - position: relative; + padding: 8px 12px; + gap: 6px; - .controls { + .top-row { + display: grid; + align-items: center; + width: 100%; + gap: 6px; + grid-template-columns: 60px 1fr 60px; + } + + .arrow { display: flex; - justify-content: space-between; - position: absolute; - top: 0; - left: 0; - width: calc(100% - 10px); - height: calc(100% - 10px); - padding: 5px; - } - - .left, .right{ + align-items: center; + gap: 4px; flex-shrink: 0; - } + padding: 6px 8px 6px 6px; - .left { - margin-left: 10px; - } - - .right { i { - margin-left: 10px; + font-size: 22px; } - } - span { - font-weight: 700; - } - - button { - margin-right: 10px; - } - - .input { - margin: 0 10px - } - - @media(max-width: 800px) { - .hide-mobile { + .date-label { display: none; } + } + .title { + flex: 1; + text-align: center; + font-weight: 700; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + margin: 0 auto; + + .prefix { + display: none; + } + } + + .admin-row { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + padding-top: 4px; + border-top: 1px solid #eee; + + button { + font-size: 13px; + padding: 4px 10px 4px 6px; + + i { + font-size: 18px; + margin-right: 4px; + } + + &.icon { + padding: 4px 8px; + + i { + margin-right: 0; + } + } + } + } + + @media(min-width: 800px) { + padding: 10px 16px; + gap: 0; + + .top-row { + flex: 1; + gap: 10px; + grid-template-columns: 180px 1fr 180px; + } + + .arrow { + padding: 6px 14px 6px 10px; + + .date-label { + display: inline; + font-size: 13px; + font-weight: 600; + white-space: nowrap; + } + } + + .title { + font-size: 16px; + white-space: normal; + + .prefix { + display: inline; + font-weight: 400; + color: #777; + } + } + + .admin-row { + width: auto; + border-top: none; + padding-top: 0; + margin-left: 12px; + } } } diff --git a/public/src/components/ListView.vue b/public/src/components/ListView.vue new file mode 100644 index 0000000..dca9868 --- /dev/null +++ b/public/src/components/ListView.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/public/src/components/PlanActionBar.vue b/public/src/components/PlanActionBar.vue index d9b66b4..cc302c1 100644 --- a/public/src/components/PlanActionBar.vue +++ b/public/src/components/PlanActionBar.vue @@ -25,17 +25,23 @@ const props = defineProps<{