WIP
This commit is contained in:
parent
d0b1c1d241
commit
00cca5ca9a
@ -58,6 +58,8 @@ fun Payload.mid() = getClaim("id").asInt()
|
||||
|
||||
|
||||
object Security {
|
||||
fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000*60*60);
|
||||
|
||||
suspend fun authenticateUser(application: Application, username: String, password: String): Ministrant? {
|
||||
if(username == "admin") {
|
||||
val adminPw = application.environment.config.property("admin.password").getString()
|
||||
@ -97,6 +99,6 @@ object Security {
|
||||
.withIssuer(jwtEnv.issuer)
|
||||
.withClaim("username", ministrant.username)
|
||||
.withClaim("id", ministrant.id)
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + 1000*60*60))
|
||||
.withExpiresAt(DEFAULT_EXPIRY())
|
||||
.sign(Algorithm.HMAC256(jwtEnv.secret))
|
||||
}
|
||||
@ -28,7 +28,7 @@ data class AuthenticationRequest(
|
||||
@Serializable
|
||||
data class AuthenticationResult(
|
||||
val success: Boolean,
|
||||
val token: String? = null
|
||||
val privileges: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -54,8 +54,14 @@ fun Route.configureAuthenticationRoutes() {
|
||||
}
|
||||
|
||||
val token = Security.createToken(jwtEnv, ministrant)
|
||||
val expiry = Security.DEFAULT_EXPIRY().toGMTString()
|
||||
|
||||
call.respond(AuthenticationResult(true, token.toString()))
|
||||
call.response.header(
|
||||
"Set-Cookie",
|
||||
"token=$token; HttpOnly; Expires=$expiry"
|
||||
)
|
||||
|
||||
call.respond(AuthenticationResult(true, ministrant.privileges))
|
||||
}
|
||||
|
||||
authenticate {
|
||||
@ -80,6 +86,7 @@ fun Route.configureAuthenticationRoutes() {
|
||||
|
||||
Security.setPassword(request.username, newPassword)
|
||||
|
||||
|
||||
call.respond(hashMapOf("password" to newPassword))
|
||||
|
||||
|
||||
|
||||
@ -1,22 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import {RouterLink, RouterView} from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import LoginPanel from "@/components/LoginPanel.vue";
|
||||
import {ref} from "vue";
|
||||
|
||||
const showPopup = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<nav>
|
||||
<div class="left">
|
||||
Miniplan
|
||||
</header>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button class="flat" @click="showPopup = true"><i>login</i> Einloggen</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<RouterView/>
|
||||
<div class="popup-container" :class="{show: showPopup}" @click.self="showPopup = false">
|
||||
<LoginPanel :active="showPopup"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
header{
|
||||
nav {
|
||||
display: flex;
|
||||
padding: 20px 32px;
|
||||
align-items: center;
|
||||
padding: 10px 32px;
|
||||
border-bottom: 1px solid #d7d5d5;
|
||||
font-weight: 700;
|
||||
|
||||
.left {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: 200ms opacity;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.show{
|
||||
pointer-events: auto;
|
||||
* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -26,17 +26,23 @@ html, body {
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 14px 8px 10px;
|
||||
margin: 0 4px;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
background: #d7eaf3;
|
||||
color: #0e2c48;
|
||||
border: 1px solid #bed4e0;
|
||||
transition: 100ms border-color;
|
||||
}
|
||||
|
||||
button.flat {
|
||||
background: #ffffff;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
button i {
|
||||
@ -45,10 +51,14 @@ button i {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
button.flat:hover{
|
||||
border-color: #e5e5e5;
|
||||
}
|
||||
|
||||
button:not(.flat):hover {
|
||||
background: #e4eff6;
|
||||
}
|
||||
|
||||
button:active {
|
||||
button:not(.flat):active {
|
||||
background: #d0e3f1;
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
value: any,
|
||||
label?: string,
|
||||
@ -8,17 +10,20 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["update:value"])
|
||||
const focus = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input">
|
||||
<label v-if="label">{{ label }}</label>
|
||||
<label v-if="label" :class="{up: props.value != '' || focus, focus}">{{ label }}</label>
|
||||
<input
|
||||
:value="value"
|
||||
@input="$emit('update:value', $event.target.value)"
|
||||
:type="props.type ? props.type : 'text'"
|
||||
:disabled="disabled">
|
||||
:disabled="disabled"
|
||||
@focusin="focus = true"
|
||||
@focusout="focus = false">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -26,24 +31,48 @@ const emit = defineEmits(["update:value"])
|
||||
|
||||
.input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
padding-left: 15px;
|
||||
font-size: 14px;
|
||||
padding: 2px 4px;
|
||||
color: #727272;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
background: #ffffff;
|
||||
pointer-events: none;
|
||||
transition: 100ms top;
|
||||
&.up {
|
||||
top: -8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.focus {
|
||||
color: #464646;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
position: relative;
|
||||
font-family: sans-serif;
|
||||
width: calc(100% - 30px);
|
||||
min-width: 0px;
|
||||
outline: none;
|
||||
margin: 0 10px;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
padding: 15px 15px;
|
||||
color: #121212;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: 100ms border-color;
|
||||
|
||||
&:not(:disabled) {
|
||||
border: 1px solid #cecece;
|
||||
&:focus{
|
||||
border-color: #919191;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,70 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Input from "@/components/Input.vue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {API} from "@/views/api";
|
||||
|
||||
const props = defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["success"])
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", ev => {
|
||||
if(props.active && ev.key == "Enter") {
|
||||
attemptLogin()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const username = ref("")
|
||||
const password = ref("")
|
||||
|
||||
async function attemptLogin() {
|
||||
let login = await API.login(username.value, password.value)
|
||||
if(login.success){
|
||||
console.log("success", login)
|
||||
emit("success", login)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="container">
|
||||
<span class="title">Anmelden</span>
|
||||
<Input class="input" v-model:value="username" label="Nutzername"/>
|
||||
<Input class="input" v-model:value="password" label="Passwort" type="password"/>
|
||||
<button><i>login</i>Anmelden</button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
.container{
|
||||
background: #ffffff;
|
||||
padding: 20px 32px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
pointer-events: auto;
|
||||
|
||||
.title{
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
button{
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -125,6 +125,7 @@ function toggleMark(gid, mid) {
|
||||
<tr>
|
||||
<th></th>
|
||||
<th v-for="godi in props.gottesdienste">{{ formatWeekday(godi.date) }}</th>
|
||||
<th class="edit" v-if="props.edit"></th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(["addPlan", "save"])
|
||||
const props = defineProps<{
|
||||
save: boolean,
|
||||
plan: boolean,
|
||||
@ -10,8 +11,7 @@ const props = defineProps<{
|
||||
|
||||
<div class="action-bar" :class="{save: props.save}">
|
||||
<div class="other-action">
|
||||
<button class="add-plan" :class="{show: props.plan}" @click="$emit('save')"> <i class="icon">add_box</i> Neuer Plan</button>
|
||||
<button class="add-godi" :class="{show: props.godi}" @click="$emit('save')"> <i class="icon">add</i> Neuer Gottesdienst</button>
|
||||
<button class="add-plan" :class="{show: props.plan}" @click="$emit('addPlan')"> <i class="icon">add_box</i> Neuer Plan</button>
|
||||
</div>
|
||||
<button class="save" :class="{show: props.save}" @click="$emit('save')"><i class="icon">save</i> Änderungen speichern </button>
|
||||
</div>
|
||||
|
||||
@ -3,10 +3,32 @@
|
||||
import {API} from "@/views/api";
|
||||
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import type {PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||
import Input from "@/components/Input.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
gottesdienste: Gottesdienst[],
|
||||
ministranten: SimplifiedMinistrant[]
|
||||
marks: Mark[],
|
||||
editable: number[]
|
||||
edit: boolean
|
||||
}>()
|
||||
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit"])
|
||||
|
||||
const data = reactive({
|
||||
godi: {}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keypress", ev => {
|
||||
if(ev.key == "Enter" && props.edit){
|
||||
emit("added", data.godi, () => {
|
||||
data.godi = {}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const props = defineProps<PlanModel>()
|
||||
defineEmits(["toggleMark"])
|
||||
|
||||
function getIconForMark(gid, mid) {
|
||||
const mark = getMark(gid, mid).value
|
||||
@ -100,25 +122,36 @@ function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
||||
|
||||
<thead>
|
||||
|
||||
<tr v-if="props.edit">
|
||||
<th></th>
|
||||
<th v-for="godi in props.gottesdienste"><i @click="$emit('delete', godi.id)">delete</i></th>
|
||||
<th><i @click="$emit('endEdit')">close</i></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th v-for="godi in props.gottesdienste">{{ godi.name }}</th>
|
||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.name"/></th>
|
||||
</tr>
|
||||
<tr class="bold">
|
||||
<th>Datum</th>
|
||||
<th v-for="godi in props.gottesdienste">{{ formatDay(godi.date) }}</th>
|
||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.date" type="date"/></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Uhrzeit</th>
|
||||
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.date) }}</th>
|
||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.time" type="time"/></th>
|
||||
</tr>
|
||||
<tr class="bold">
|
||||
<th>Anwesenheit</th>
|
||||
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.attendance) }}</th>
|
||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.attendance" type="time"/></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Wochentag</th>
|
||||
<th v-for="godi in props.gottesdienste">{{ formatWeekday(godi.date) }}</th>
|
||||
<th class="edit" v-if="props.edit"></th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
@ -134,6 +167,7 @@ function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
||||
<i class="icon"> {{ getIconForMark(godi.id, mini.id) }} </i><br>
|
||||
<span class="hint">{{ getHintForMark(godi.id, mini.id) }}</span>
|
||||
</td>
|
||||
<td class="edit" v-if="props.edit"></td>
|
||||
|
||||
|
||||
</tr>
|
||||
@ -151,6 +185,9 @@ table {
|
||||
tr{
|
||||
th{
|
||||
font-weight: 400;
|
||||
&.edit{
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
&.bold th{
|
||||
font-weight: 700;
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, reactive, ref, toRaw} from "vue";
|
||||
import {onMounted, reactive, ref, toRaw, computed} from "vue";
|
||||
import TablePlan from "@/components/TablePlan.vue";
|
||||
import {API} from "@/views/api";
|
||||
import type {Mark, PlanModel} from "@/models/models";
|
||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||
import MobilePlan from "@/components/MobilePlan.vue";
|
||||
import PlanActionBar from "@/components/PlanActionBar.vue";
|
||||
|
||||
import {computed, onMounted, reactive, ref} from "vue";
|
||||
import Plan from "@/components/Plan.vue";
|
||||
import {API} from "@/views/api";
|
||||
import type {PlanModel} from "@/models/models";
|
||||
|
||||
const plan = reactive<PlanModel>({
|
||||
const plan = reactive<{
|
||||
gottesdienste: Gottesdienst[],
|
||||
ministranten: SimplifiedMinistrant[],
|
||||
marks: Mark[],
|
||||
editable: number[]
|
||||
}>({
|
||||
gottesdienste: [],
|
||||
ministranten: [],
|
||||
marks: []
|
||||
marks: [],
|
||||
editable: []
|
||||
})
|
||||
const mobile = ref(false)
|
||||
const editedMarks = reactive<Mark[]>([]);
|
||||
const editPlanAdmin = ref(false)
|
||||
|
||||
const sortedGottesdienste = computed(() => {
|
||||
return plan.gottesdienste.sort((a, b) => {
|
||||
@ -27,7 +29,8 @@ const sortedGottesdienste = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
async function addGodi(data) {
|
||||
async function addGodi(data, validate) {
|
||||
console.log("Test")
|
||||
console.log(data)
|
||||
let date = Date.parse(data.date + "T" + data.time);
|
||||
let attendance = data.attendance && data.attendance != ""
|
||||
@ -41,7 +44,8 @@ async function addGodi(data) {
|
||||
0
|
||||
)
|
||||
console.log(newGodi)
|
||||
plan.gottesdienste.push(newGodi)
|
||||
plan.gottesdienste.push(newGodi);
|
||||
validate()
|
||||
}
|
||||
|
||||
async function deleteGottedienst(id) {
|
||||
@ -53,13 +57,6 @@ async function deleteGottedienst(id) {
|
||||
}
|
||||
|
||||
|
||||
async function addGodi() {
|
||||
let time = 1692104646066 + ((1000 * 60 * 60 * 24) * Math.random() * 6)
|
||||
let newGodi = await API.addGottesdienst("Godi", new Date(time), new Date(time - 1000 * 60 * 30), 0)
|
||||
plan.gottesdienste.push(newGodi)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
let fetchedPlan = await API.getPlan(0)
|
||||
plan.gottesdienste = fetchedPlan.gottesdienste
|
||||
@ -111,9 +108,11 @@ function toggleMark(gid, mid) {
|
||||
:ministranten="plan.ministranten"
|
||||
:marks="getMarks()"
|
||||
:editable="plan.editable"
|
||||
:edit="editPlanAdmin"
|
||||
@added="addGodi"
|
||||
@delete="deleteGottesdienst"
|
||||
@delete="deleteGottedienst"
|
||||
@toggle-mark="toggleMark"
|
||||
@end-edit="editPlanAdmin = false"
|
||||
class="plan table"
|
||||
v-if="!mobile">
|
||||
|
||||
@ -134,7 +133,7 @@ function toggleMark(gid, mid) {
|
||||
class="action-bar"
|
||||
:save="getDif().length > 0"
|
||||
:plan="false"
|
||||
:godi="false"
|
||||
:godi="true"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
@ -16,6 +16,14 @@ async function api(endpoint: string, method: string = "GET", body?: any ) {
|
||||
})
|
||||
}
|
||||
|
||||
function setToken(token: string) {
|
||||
let expires = new Date((new Date()).getTime() + (1000*60*60*24*5)).toUTCString()
|
||||
document.cookie = `token=${token};expires=${expires};HTTPOnly`
|
||||
}
|
||||
|
||||
function getToken(): string | null {
|
||||
return ""
|
||||
}
|
||||
|
||||
export namespace API {
|
||||
|
||||
@ -59,6 +67,23 @@ export namespace API {
|
||||
.then(data => data.status == 200)
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string): Promise<{
|
||||
success: boolean,
|
||||
token?: string
|
||||
}> {
|
||||
return api("/auth", "POST", {
|
||||
username, password
|
||||
}).then(res => res.json() as Promise<{
|
||||
success: boolean,
|
||||
token?: string
|
||||
}>)
|
||||
.then(res => {
|
||||
if(res.success) {
|
||||
console.log("test")
|
||||
}
|
||||
return Promise.resolve(res)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user