feat: implement unauthorized error handling and login popup
Some checks failed
Deploy Miniplan / build (push) Failing after 42s

This commit is contained in:
2026-04-30 22:39:04 +02:00
parent a3e115824b
commit 213065b86c
8 changed files with 70 additions and 17 deletions

View File

@@ -10,6 +10,6 @@
"license": "ISC",
"dependencies": {
"dropzone-vue3": "^1.0.2",
"rxjs": "^7.8.1",
"rxjs": "^7.8.1"
}
}

View File

@@ -59,7 +59,7 @@ fun Payload.mid() = getClaim("id").asInt()
object Security {
fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000 * 60 * 60);
fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 14);
suspend fun authenticateUser(application: Application, username: String, password: String): Ministrant? {
println("Username $username password $password")

View File

@@ -3,6 +3,7 @@ package de.walamana.views
import de.walamana.models.GottesdienstGroup
import de.walamana.models.GottesdienstGroupDao
import de.walamana.models.GottesdienstGroups
import de.walamana.plugins.getJWTEnvironment
import de.walamana.service.PlanDao
import io.ktor.server.application.*
import io.ktor.server.auth.*
@@ -14,12 +15,14 @@ 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, isAnonymous = isAnonymous);
call.respond(plan)
authenticate {
get {
val principal = call.principal<JWTPrincipal>()
val isAnonymous = principal == null
val id = call.parameters.getOrFail("id").toInt()
val plan = PlanDao.constructPlan(id, isAnonymous = isAnonymous);
call.respond(plan)
}
}
}
route("/groups") {

View File

@@ -5,6 +5,7 @@ import LoginPanel from "@/components/LoginPanel.vue";
import {onMounted, ref} from "vue";
import {Auth} from "@/services/auth";
import DialogHost from "@/components/dialog/DialogHost.vue";
import {LoginService} from "@/services/LoginService";
let showPopup = ref(false)
let loggedIn = ref(false)
@@ -25,6 +26,10 @@ onMounted(() => {
Auth.checkForToken()
})
LoginService.subject.subscribe(() => {
showPopup.value = true
})
</script>
<template>

View File

@@ -45,7 +45,9 @@ async function attemptLogin() {
<template>
<div class="container">
<img src="./../assets/minis_logo.png" style="height: 80px"/>
<span class="title">Anmelden</span>
<span class="subtitle">Um den Miniplan einsehen zu können, logge dich bitte ein.</span>
<Input class="input" v-model:value="username" label="Nutzername"/>
<Input class="input" v-model:value="password" label="Passwort" type="password"/>
<span v-if="invalid" style="color: red; text-align: center; margin-bottom: 10px">Benutzername oder<br> Passwort falsch.</span>
@@ -69,11 +71,22 @@ async function attemptLogin() {
.title{
display: inline-block;
font-size: 24px;
margin-bottom: 24px;
margin-bottom: 8px;
margin-top: 16px;
}
.subtitle {
margin-bottom: 30px;
max-width: 300px;
text-align: center;
opacity: 0.5;
font-style: italic;
line-height: 1.3rem;
}
.input {
margin-bottom: 16px;
min-width: 300px;
}
button{
margin-top: 8px;

View File

@@ -0,0 +1,9 @@
import {Subject} from "rxjs";
export namespace LoginService {
export const subject = new Subject<void>();
export function showLoginPopup() {
subject.next()
}
}

View File

@@ -30,11 +30,21 @@ function getToken(): string | null {
return ""
}
export class UnauthorizedError extends Error {
constructor() {
super("UnauthorizedError");
}
}
export namespace API {
export async function getPlan(id: number) {
return api(`/plan?id=${id}`).then(res => res.json())
.then(data => {
return api(`/plan?id=${id}`).then(res => {
if(res.status == 401) {
throw new UnauthorizedError()
}
return res.json()
}).then(data => {
return {
gottesdienste: data.gottesdienste,
ministranten: data.ministranten,

View File

@@ -2,7 +2,7 @@
import {computed, onMounted, reactive, ref, toRaw, watch} from "vue";
import TablePlan from "@/components/TablePlan.vue";
import {API} from "@/services/api";
import {API, UnauthorizedError} from "@/services/api";
import type {Gottesdienst, GottesdienstGroup, Mark, SimplifiedMinistrant} from "@/models/models";
import MobilePlan from "@/components/MobilePlan.vue";
import PlanActionBar from "@/components/PlanActionBar.vue";
@@ -17,6 +17,7 @@ import {useRoute, useRouter} from "vue-router";
import debounce from "underscore/modules/debounce.js"
import SavingIndicator from "@/components/SavingIndicator.vue";
import ImportZelebrationsplanDialog from "@/components/dialog/ImportZelebrationsplanDialog.vue";
import {LoginService} from "@/services/LoginService";
const MAX_WIDTH_MOBILE = 600;
@@ -56,11 +57,23 @@ watch(() => route.params.id, async (value, oldValue) => {
})
async function loadPlan() {
const { ministranten, gottesdienste, marks} = await API.getPlan(planId.value)
plan.ministranten = ministranten
plan.gottesdienste = gottesdienste
plan.marks = marks
Auth.checkForToken()
try {
const { ministranten, gottesdienste, marks} = await API.getPlan(planId.value)
plan.ministranten = ministranten
plan.gottesdienste = gottesdienste
plan.marks = marks
Auth.checkForToken()
} catch (e: any) {
if(e instanceof UnauthorizedError) {
LoginService.showLoginPopup()
let subscription = Auth.loggedInSubject.subscribe((loggedIn) => {
subscription.unsubscribe()
if(loggedIn) {
loadPlan()
}
})
}
}
}
Auth.loggedInSubject.subscribe(async (loggedIn) => {