This commit is contained in:
Jonas G 2023-09-17 10:19:43 +02:00
parent d0b1c1d241
commit 00cca5ca9a
11 changed files with 282 additions and 63 deletions

View File

@ -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))
}

View File

@ -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))

View File

@ -1,22 +1,72 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
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>
Miniplan
</header>
<nav>
<div class="left">
Miniplan
</div>
<div class="right">
<button class="flat" @click="showPopup = true"><i>login</i> Einloggen</button>
</div>
</nav>
<RouterView />
<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>

View File

@ -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;
}

View File

@ -1,5 +1,7 @@
<script setup lang="ts">
import {ref} from "vue";
const props = defineProps<{
value: any,
label?: string,
@ -8,44 +10,71 @@ 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>
<style scoped lang="less">
.input {
display: inline-block;
label {
display: block;
padding-left: 15px;
font-size: 14px;
color: #727272;
}
input {
font-family: sans-serif;
width: calc(100% - 30px);
min-width: 0px;
outline: none;
margin: 0 10px;
padding: 5px 5px;
color: #121212;
border: 1px solid transparent;
font-size: 14px;
&:not(:disabled){
border: 1px solid #cecece;
display: inline-block;
position: relative;
label {
display: block;
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;
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;
}
}
}
}
}
</style>
</style>

View File

@ -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>

View File

@ -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>
@ -195,4 +196,4 @@ table{
tr:nth-child(5n) td{
border-bottom: 1px solid black;
}
</style>
</style>

View File

@ -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>

View File

@ -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;

View File

@ -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"
/>

View File

@ -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)
})
}
}