save changed marks
This commit is contained in:
parent
00cca5ca9a
commit
24f14da9b2
@ -7,5 +7,8 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,5 @@ ktor_version=2.3.3
|
|||||||
kotlin_version=1.9.0
|
kotlin_version=1.9.0
|
||||||
logback_version=1.2.11
|
logback_version=1.2.11
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
exposed_version=0.41.1
|
exposed_version=0.43.0
|
||||||
h2_version=2.1.214
|
h2_version=2.1.214
|
||||||
|
|||||||
@ -37,7 +37,7 @@ object MarksDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setMark(ministrantId: Int, gottesdienstId: Int, value: Int): Boolean = dbQuery {
|
suspend fun setMark(ministrantId: Int, gottesdienstId: Int, value: Int): Boolean = dbQuery {
|
||||||
Marks.insert {
|
Marks.upsert {
|
||||||
it[Marks.mid] = ministrantId
|
it[Marks.mid] = ministrantId
|
||||||
it[Marks.gid] = gottesdienstId
|
it[Marks.gid] = gottesdienstId
|
||||||
it[Marks.value] = value
|
it[Marks.value] = value
|
||||||
|
|||||||
@ -27,6 +27,7 @@ data class Ministrant(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class SimplifiedMinistrant(
|
data class SimplifiedMinistrant(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val username: String,
|
||||||
val firstname: String,
|
val firstname: String,
|
||||||
val lastname: String
|
val lastname: String
|
||||||
)
|
)
|
||||||
@ -59,8 +60,18 @@ object MinistrantenDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery {
|
suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery {
|
||||||
Ministranten.selectAll().map { row ->
|
Ministranten.slice(
|
||||||
SimplifiedMinistrant(row[Ministranten.id], row[Ministranten.firstname], row[Ministranten.lastname])
|
Ministranten.id,
|
||||||
|
Ministranten.username,
|
||||||
|
Ministranten.firstname,
|
||||||
|
Ministranten.lastname
|
||||||
|
).selectAll().map { row ->
|
||||||
|
SimplifiedMinistrant(
|
||||||
|
row[Ministranten.id],
|
||||||
|
row[Ministranten.username],
|
||||||
|
row[Ministranten.firstname],
|
||||||
|
row[Ministranten.lastname]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +81,14 @@ object MinistrantenDao {
|
|||||||
}.firstOrNull()
|
}.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createMinistrant(username: String, passwordHash: String, firstname: String, lastname: String, birthday: Date, privileges: List<String>) = dbQuery {
|
suspend fun createMinistrant(
|
||||||
|
username: String,
|
||||||
|
passwordHash: String,
|
||||||
|
firstname: String,
|
||||||
|
lastname: String,
|
||||||
|
birthday: Date,
|
||||||
|
privileges: List<String>
|
||||||
|
) = dbQuery {
|
||||||
val statement = Ministranten.insert {
|
val statement = Ministranten.insert {
|
||||||
it[Ministranten.username] = username
|
it[Ministranten.username] = username
|
||||||
it[Ministranten.passwordHash] = ""
|
it[Ministranten.passwordHash] = ""
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import java.util.*
|
|||||||
|
|
||||||
|
|
||||||
const val SALT_ROUNDS = 10;
|
const val SALT_ROUNDS = 10;
|
||||||
|
|
||||||
data class JWTEnvironment(
|
data class JWTEnvironment(
|
||||||
val secret: String,
|
val secret: String,
|
||||||
val issuer: String,
|
val issuer: String,
|
||||||
@ -99,6 +100,7 @@ object Security {
|
|||||||
.withIssuer(jwtEnv.issuer)
|
.withIssuer(jwtEnv.issuer)
|
||||||
.withClaim("username", ministrant.username)
|
.withClaim("username", ministrant.username)
|
||||||
.withClaim("id", ministrant.id)
|
.withClaim("id", ministrant.id)
|
||||||
|
.withClaim("privileges", ministrant.privileges)
|
||||||
.withExpiresAt(DEFAULT_EXPIRY())
|
.withExpiresAt(DEFAULT_EXPIRY())
|
||||||
.sign(Algorithm.HMAC256(jwtEnv.secret))
|
.sign(Algorithm.HMAC256(jwtEnv.secret))
|
||||||
}
|
}
|
||||||
@ -28,6 +28,7 @@ data class AuthenticationRequest(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class AuthenticationResult(
|
data class AuthenticationResult(
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
|
val token: String? = null,
|
||||||
val privileges: List<String>? = null,
|
val privileges: List<String>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ fun Route.configureAuthenticationRoutes() {
|
|||||||
"token=$token; HttpOnly; Expires=$expiry"
|
"token=$token; HttpOnly; Expires=$expiry"
|
||||||
)
|
)
|
||||||
|
|
||||||
call.respond(AuthenticationResult(true, ministrant.privileges))
|
call.respond(AuthenticationResult(true, token, ministrant.privileges))
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate {
|
authenticate {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import io.ktor.server.response.*
|
|||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.util.*
|
import io.ktor.server.util.*
|
||||||
|
|
||||||
|
|
||||||
fun Route.configureMarksView() {
|
fun Route.configureMarksView() {
|
||||||
route("/marks") {
|
route("/marks") {
|
||||||
get {
|
get {
|
||||||
@ -15,22 +16,26 @@ fun Route.configureMarksView() {
|
|||||||
call.respond(data)
|
call.respond(data)
|
||||||
}
|
}
|
||||||
put {
|
put {
|
||||||
val data = call.receive<Mark>()
|
val changedMarks = call.receive<List<Mark>>()
|
||||||
|
for(changedMark in changedMarks) {
|
||||||
val mark = MarksDao.setMark(
|
val mark = MarksDao.setMark(
|
||||||
data.mid,
|
changedMark.mid,
|
||||||
data.gid,
|
changedMark.gid,
|
||||||
data.value
|
changedMark.value
|
||||||
)
|
)
|
||||||
|
}
|
||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
patch {
|
patch {
|
||||||
// TODO: Access only by admin
|
// TODO: Access only by admin
|
||||||
val data = call.receive<Mark>()
|
val changedMarks = call.receive<List<Mark>>()
|
||||||
val changed = MarksDao.setMark(
|
for(changedMark in changedMarks) {
|
||||||
data.mid,
|
val mark = MarksDao.setMark(
|
||||||
data.gid,
|
changedMark.mid,
|
||||||
data.value
|
changedMark.gid,
|
||||||
|
changedMark.value
|
||||||
)
|
)
|
||||||
|
}
|
||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
delete {
|
delete {
|
||||||
|
|||||||
@ -2,9 +2,23 @@
|
|||||||
import {RouterLink, RouterView} from 'vue-router'
|
import {RouterLink, RouterView} from 'vue-router'
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import HelloWorld from './components/HelloWorld.vue'
|
||||||
import LoginPanel from "@/components/LoginPanel.vue";
|
import LoginPanel from "@/components/LoginPanel.vue";
|
||||||
import {ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
|
import {Auth} from "@/services/auth";
|
||||||
|
|
||||||
const showPopup = ref(false)
|
let showPopup = ref(false)
|
||||||
|
let loggedIn = ref(false)
|
||||||
|
|
||||||
|
Auth.loggedInSubject.subscribe((isLoggedIn) => {
|
||||||
|
loggedIn.value = isLoggedIn
|
||||||
|
})
|
||||||
|
|
||||||
|
function logout(){
|
||||||
|
Auth.logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Auth.checkForToken()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -14,13 +28,14 @@ const showPopup = ref(false)
|
|||||||
Miniplan
|
Miniplan
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<button class="flat" @click="showPopup = true"><i>login</i> Einloggen</button>
|
<button v-if="!loggedIn" class="flat" @click="showPopup = true"><i>login</i> Einloggen</button>
|
||||||
|
<button v-if="loggedIn" class="flat" @click="logout"><i>logout</i> Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<RouterView/>
|
<RouterView/>
|
||||||
<div class="popup-container" :class="{show: showPopup}" @click.self="showPopup = false">
|
<div class="popup-container" :class="{show: showPopup}" @click.self="showPopup = false">
|
||||||
<LoginPanel :active="showPopup"/>
|
<LoginPanel :active="showPopup" @success="showPopup = false"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Input from "@/components/Input.vue";
|
import Input from "@/components/Input.vue";
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import {API} from "@/views/api";
|
import {API} from "@/services/api";
|
||||||
|
import {Auth} from "@/services/auth";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
active: boolean
|
active: boolean
|
||||||
@ -22,7 +23,7 @@ const username = ref("")
|
|||||||
const password = ref("")
|
const password = ref("")
|
||||||
|
|
||||||
async function attemptLogin() {
|
async function attemptLogin() {
|
||||||
let login = await API.login(username.value, password.value)
|
let login = await Auth.login(username.value, password.value)
|
||||||
if(login.success){
|
if(login.success){
|
||||||
console.log("success", login)
|
console.log("success", login)
|
||||||
emit("success", login)
|
emit("success", login)
|
||||||
@ -36,7 +37,7 @@ async function attemptLogin() {
|
|||||||
<span class="title">Anmelden</span>
|
<span class="title">Anmelden</span>
|
||||||
<Input class="input" v-model:value="username" label="Nutzername"/>
|
<Input class="input" v-model:value="username" label="Nutzername"/>
|
||||||
<Input class="input" v-model:value="password" label="Passwort" type="password"/>
|
<Input class="input" v-model:value="password" label="Passwort" type="password"/>
|
||||||
<button><i>login</i>Anmelden</button>
|
<button @click="attemptLogin"><i>login</i>Anmelden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {API} from "@/views/api";
|
import {API} from "@/services/api";
|
||||||
|
|
||||||
import {onMounted, reactive, ref} from "vue";
|
import {onMounted, reactive, ref} from "vue";
|
||||||
import type {PlanModel, SimplifiedMinistrant} from "@/models/models";
|
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||||
|
|
||||||
const props = defineProps<PlanModel>()
|
const props = defineProps<{
|
||||||
|
gottesdienste: Gottesdienst[],
|
||||||
|
ministranten: SimplifiedMinistrant[],
|
||||||
|
marks: Mark[],
|
||||||
|
editable: string[]
|
||||||
|
}>()
|
||||||
defineEmits(["toggleMark"])
|
defineEmits(["toggleMark"])
|
||||||
|
|
||||||
function getIconForMark(gid, mid) {
|
function getIconForMark(gid, mid) {
|
||||||
@ -89,12 +94,12 @@ function getMark(gid, mid) {
|
|||||||
|
|
||||||
function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
||||||
return {
|
return {
|
||||||
edit: props.editable.includes(mini.id)
|
edit: props.editable.includes(mini.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMinis() {
|
function getMinis() {
|
||||||
return props.ministranten.filter(m => props.editable.includes(m.id))
|
return props.ministranten.filter(m => props.editable.includes(m.username))
|
||||||
}
|
}
|
||||||
function getMiniName(mini) {
|
function getMiniName(mini) {
|
||||||
return mini.firstname + " " + mini.lastname
|
return mini.firstname + " " + mini.lastname
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {API} from "@/views/api";
|
import {API} from "@/services/api";
|
||||||
|
|
||||||
import {onMounted, reactive, ref} from "vue";
|
import {onMounted, reactive, ref} from "vue";
|
||||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {API} from "@/views/api";
|
import {API} from "@/services/api";
|
||||||
|
|
||||||
import {onMounted, reactive, ref} from "vue";
|
import {onMounted, reactive, ref} from "vue";
|
||||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||||
@ -10,10 +10,11 @@ const props = defineProps<{
|
|||||||
gottesdienste: Gottesdienst[],
|
gottesdienste: Gottesdienst[],
|
||||||
ministranten: SimplifiedMinistrant[]
|
ministranten: SimplifiedMinistrant[]
|
||||||
marks: Mark[],
|
marks: Mark[],
|
||||||
editable: number[]
|
editable: string[]
|
||||||
edit: boolean
|
edit: boolean,
|
||||||
|
smallMode: boolean
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit"])
|
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit", "resetPassword"])
|
||||||
|
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
godi: {}
|
godi: {}
|
||||||
@ -47,15 +48,12 @@ function getIconForMark(gid, mid) {
|
|||||||
|
|
||||||
function getClassForMark(gid, mid) {
|
function getClassForMark(gid, mid) {
|
||||||
const mark = getMark(gid, mid).value
|
const mark = getMark(gid, mid).value
|
||||||
switch (mark) {
|
return {
|
||||||
case -1:
|
minus: mark == -1,
|
||||||
return "minus";
|
neutral: mark == 0,
|
||||||
case 0:
|
cross: mark == 1,
|
||||||
return "neutral";
|
showIcon: !props.smallMode
|
||||||
case 1:
|
|
||||||
return "cross"
|
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHintForMark(gid, mid) {
|
function getHintForMark(gid, mid) {
|
||||||
@ -111,7 +109,7 @@ function getMark(gid, mid) {
|
|||||||
|
|
||||||
function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
||||||
return {
|
return {
|
||||||
edit: props.editable.includes(mini.id)
|
edit: props.editable.includes(mini.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -158,7 +156,7 @@ function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr v-for="mini in props.ministranten" class="ministrant" :class="getMinistrantClasses(mini)">
|
<tr v-for="mini in props.ministranten" class="ministrant" :class="getMinistrantClasses(mini)">
|
||||||
<td class="name">{{ mini.id }} {{ mini.firstname }} {{ mini.lastname }}</td>
|
<td class="name"><i v-if="edit" style="margin-right: 10px" @click="$emit('resetPassword', mini.username)">lock_reset</i>{{ mini.id }} {{ mini.firstname }} {{ mini.lastname }}</td>
|
||||||
<td
|
<td
|
||||||
v-for="godi in props.gottesdienste"
|
v-for="godi in props.gottesdienste"
|
||||||
class="mark"
|
class="mark"
|
||||||
@ -199,7 +197,7 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
td:first-child, th:first-child {
|
td:first-child, th:first-child {
|
||||||
padding: 6px 60px 6px 12px;
|
padding: 6px 30px 6px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,6 +214,7 @@ td:nth-child(2n), th:nth-child(2n){
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
|
||||||
i {
|
i {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
@ -253,6 +252,14 @@ td:nth-child(2n), th:nth-child(2n){
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
//mix-blend-mode: difference;
|
//mix-blend-mode: difference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&:not(.showIcon){
|
||||||
|
padding: 0 !important;
|
||||||
|
.hint, br{
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ministrant.edit {
|
.ministrant.edit {
|
||||||
@ -264,6 +271,11 @@ td:nth-child(2n), th:nth-child(2n){
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name{
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.mark{
|
.mark{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&.neutral i {
|
&.neutral i {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export interface Gottesdienst {
|
|||||||
|
|
||||||
export interface SimplifiedMinistrant {
|
export interface SimplifiedMinistrant {
|
||||||
id: number,
|
id: number,
|
||||||
|
username: string,
|
||||||
firstname: string,
|
firstname: string,
|
||||||
lastname: string
|
lastname: string
|
||||||
}
|
}
|
||||||
@ -21,6 +22,5 @@ export interface Mark {
|
|||||||
export interface PlanModel {
|
export interface PlanModel {
|
||||||
gottesdienste: Gottesdienst[],
|
gottesdienste: Gottesdienst[],
|
||||||
ministranten: SimplifiedMinistrant[],
|
ministranten: SimplifiedMinistrant[],
|
||||||
marks: Mark[],
|
marks: Mark[]
|
||||||
editable: number[]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import type {Gottesdienst} from "@/models/models";
|
import type {Gottesdienst, Mark} from "@/models/models";
|
||||||
|
import {Auth} from "@/services/auth";
|
||||||
|
|
||||||
|
|
||||||
const API_ENDPOINT = "http://0.0.0.0:8080/api"
|
const API_ENDPOINT = "http://0.0.0.0:8080/api"
|
||||||
|
|
||||||
async function api(endpoint: string, method: string = "GET", body?: any ) {
|
export async function api(endpoint: string, method: string = "GET", body?: any ) {
|
||||||
let isJson = (typeof body == "object")
|
let isJson = (typeof body == "object")
|
||||||
return fetch(API_ENDPOINT + endpoint, {
|
return fetch(API_ENDPOINT + endpoint, {
|
||||||
method: method,
|
method: method,
|
||||||
@ -11,7 +12,8 @@ async function api(endpoint: string, method: string = "GET", body?: any ) {
|
|||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": (isJson ? "application/json" : "text/plain"),
|
"Content-Type": (isJson ? "application/json" : "text/plain"),
|
||||||
"Access-Control-Allow-Origin": "*"
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Authorization": "Bearer " + Auth.getToken()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -67,23 +69,16 @@ export namespace API {
|
|||||||
.then(data => data.status == 200)
|
.then(data => data.status == 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(username: string, password: string): Promise<{
|
export async function setMarks(marks: Mark[]): Promise<boolean> {
|
||||||
success: boolean,
|
return api("/marks", "PATCH", marks)
|
||||||
token?: string
|
.then(res => Promise.resolve(res.status == 200))
|
||||||
}> {
|
|
||||||
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)
|
|
||||||
})
|
export async function resetPassword(username: String): Promise<any> {
|
||||||
|
return api("/auth/reset", "POST", { username })
|
||||||
|
.then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
85
public/src/services/auth.ts
Normal file
85
public/src/services/auth.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import {api} from "@/services/api";
|
||||||
|
import {Subject} from "rxjs";
|
||||||
|
|
||||||
|
export namespace Auth {
|
||||||
|
|
||||||
|
let loggedIn = false
|
||||||
|
let user = ""
|
||||||
|
let privileges: string[] = []
|
||||||
|
export const loggedInSubject: Subject<boolean> = new Subject<boolean>()
|
||||||
|
|
||||||
|
export function isLoggedIn() {
|
||||||
|
return loggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrivileges() {
|
||||||
|
return privileges
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToken(token: string) {
|
||||||
|
window.localStorage.setItem("token", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken(): string {
|
||||||
|
return window.localStorage.getItem("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
privileges?: string[]
|
||||||
|
}>).then(res => {
|
||||||
|
if(res.success) {
|
||||||
|
loggedIn = true
|
||||||
|
user = username;
|
||||||
|
privileges = res.privileges ?? []
|
||||||
|
privileges.push(username)
|
||||||
|
setToken(res.token ?? "")
|
||||||
|
loggedInSubject.next(true)
|
||||||
|
}
|
||||||
|
return Promise.resolve(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout(){
|
||||||
|
setToken("")
|
||||||
|
loggedIn = false;
|
||||||
|
user = "";
|
||||||
|
privileges = []
|
||||||
|
loggedInSubject.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkForToken() {
|
||||||
|
const token = getToken()
|
||||||
|
if(token && token != ""){
|
||||||
|
const payload = parseJwt(token)
|
||||||
|
loggedIn = true
|
||||||
|
user = payload.username
|
||||||
|
privileges = payload.privileges
|
||||||
|
privileges.push(user)
|
||||||
|
loggedInSubject.next(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJwt (token) {
|
||||||
|
var base64Url = token.split('.')[1];
|
||||||
|
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(''));
|
||||||
|
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -2,23 +2,26 @@
|
|||||||
|
|
||||||
import {onMounted, reactive, ref, toRaw, computed} from "vue";
|
import {onMounted, reactive, ref, toRaw, computed} from "vue";
|
||||||
import TablePlan from "@/components/TablePlan.vue";
|
import TablePlan from "@/components/TablePlan.vue";
|
||||||
import {API} from "@/views/api";
|
import {API} from "@/services/api";
|
||||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||||
import MobilePlan from "@/components/MobilePlan.vue";
|
import MobilePlan from "@/components/MobilePlan.vue";
|
||||||
import PlanActionBar from "@/components/PlanActionBar.vue";
|
import PlanActionBar from "@/components/PlanActionBar.vue";
|
||||||
|
import {Auth} from "@/services/auth";
|
||||||
|
|
||||||
|
const MAX_WIDTH_MOBILE = 600;
|
||||||
|
|
||||||
const plan = reactive<{
|
const plan = reactive<{
|
||||||
gottesdienste: Gottesdienst[],
|
gottesdienste: Gottesdienst[],
|
||||||
ministranten: SimplifiedMinistrant[],
|
ministranten: SimplifiedMinistrant[],
|
||||||
marks: Mark[],
|
marks: Mark[],
|
||||||
editable: number[]
|
editable: string[]
|
||||||
}>({
|
}>({
|
||||||
gottesdienste: [],
|
gottesdienste: [],
|
||||||
ministranten: [],
|
ministranten: [],
|
||||||
marks: [],
|
marks: [],
|
||||||
editable: []
|
editable: []
|
||||||
})
|
})
|
||||||
const mobile = ref(false)
|
const mobile = ref(window.innerWidth <= MAX_WIDTH_MOBILE)
|
||||||
const editedMarks = reactive<Mark[]>([]);
|
const editedMarks = reactive<Mark[]>([]);
|
||||||
const editPlanAdmin = ref(false)
|
const editPlanAdmin = ref(false)
|
||||||
|
|
||||||
@ -62,6 +65,23 @@ onMounted(async () => {
|
|||||||
plan.gottesdienste = fetchedPlan.gottesdienste
|
plan.gottesdienste = fetchedPlan.gottesdienste
|
||||||
plan.ministranten = fetchedPlan.ministranten
|
plan.ministranten = fetchedPlan.ministranten
|
||||||
plan.marks = fetchedPlan.marks
|
plan.marks = fetchedPlan.marks
|
||||||
|
Auth.checkForToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
Auth.loggedInSubject.subscribe(loggedIn => {
|
||||||
|
if (loggedIn) {
|
||||||
|
plan.editable = Auth.getPrivileges()
|
||||||
|
if(Auth.getUser() == "admin"){
|
||||||
|
editPlanAdmin.value = true
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
editPlanAdmin.value = false
|
||||||
|
plan.editable = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener("resize", (ev) => {
|
||||||
|
mobile.value = window.innerWidth <= MAX_WIDTH_MOBILE
|
||||||
})
|
})
|
||||||
|
|
||||||
function getMarks(): Mark[] {
|
function getMarks(): Mark[] {
|
||||||
@ -77,13 +97,29 @@ function getDif(): Mark[]{
|
|||||||
return (!sameMark && mark.value != 0) || (sameMark && mark.value != sameMark.value)
|
return (!sameMark && mark.value != 0) || (sameMark && mark.value != sameMark.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function canEdit(mid) {
|
|
||||||
return plan.editable.includes(mid)
|
async function saveChanges() {
|
||||||
|
const saved = await API.setMarks(getDif())
|
||||||
|
if(saved) {
|
||||||
|
plan.marks = getMarks()
|
||||||
|
editedMarks.length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPassword(username: string) {
|
||||||
|
const result = await API.resetPassword(username)
|
||||||
|
alert("Neues Passwort für " + username + "\n\n" + result.password)
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canEdit(username: string) {
|
||||||
|
return plan.editable.includes(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMark(gid, mid) {
|
function toggleMark(gid, mid) {
|
||||||
// TODO: track changes
|
// TODO: track changes
|
||||||
if(!canEdit(mid)) return;
|
const username = plan.ministranten.find(m => m.id == mid)?.username
|
||||||
|
if (!canEdit(username)) return;
|
||||||
|
|
||||||
let mark = editedMarks.find(m => m.mid == mid && m.gid == gid);
|
let mark = editedMarks.find(m => m.mid == mid && m.gid == gid);
|
||||||
if (mark) {
|
if (mark) {
|
||||||
@ -97,29 +133,30 @@ function toggleMark(gid, mid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<TablePlan
|
<TablePlan
|
||||||
:gottesdienste="plan.gottesdienste"
|
:gottesdienste="sortedGottesdienste"
|
||||||
:ministranten="plan.ministranten"
|
:ministranten="plan.ministranten"
|
||||||
:marks="getMarks()"
|
:marks="getMarks()"
|
||||||
:editable="plan.editable"
|
:editable="plan.editable"
|
||||||
:edit="editPlanAdmin"
|
:edit="editPlanAdmin"
|
||||||
|
:small-mode="editPlanAdmin"
|
||||||
@added="addGodi"
|
@added="addGodi"
|
||||||
@delete="deleteGottedienst"
|
@delete="deleteGottedienst"
|
||||||
@toggle-mark="toggleMark"
|
@toggle-mark="toggleMark"
|
||||||
@end-edit="editPlanAdmin = false"
|
@end-edit="editPlanAdmin = false"
|
||||||
|
@reset-password="resetPassword"
|
||||||
class="plan table"
|
class="plan table"
|
||||||
v-if="!mobile">
|
v-if="!mobile">
|
||||||
|
|
||||||
</TablePlan>
|
</TablePlan>
|
||||||
|
|
||||||
<MobilePlan
|
<MobilePlan
|
||||||
:gottesdienste="plan.gottesdienste"
|
:gottesdienste="sortedGottesdienste"
|
||||||
:ministranten="plan.ministranten"
|
:ministranten="plan.ministranten"
|
||||||
:marks="getMarks()"
|
:marks="getMarks()"
|
||||||
:editable="plan.editable"
|
:editable="plan.editable"
|
||||||
@ -134,6 +171,7 @@ function toggleMark(gid, mid) {
|
|||||||
:save="getDif().length > 0"
|
:save="getDif().length > 0"
|
||||||
:plan="false"
|
:plan="false"
|
||||||
:godi="true"
|
:godi="true"
|
||||||
|
@save="saveChanges()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
@ -146,6 +184,7 @@ function toggleMark(gid, mid) {
|
|||||||
.plan {
|
.plan {
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan.table {
|
.plan.table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user