Implement URL XSS attack

This commit is contained in:
2024-10-08 14:30:39 +02:00
parent 3dd7b1d4c6
commit 41a7cbc9da
19 changed files with 243 additions and 61 deletions

View File

@@ -38,6 +38,20 @@ The frontend runs on `http://localhost:5173/` and the backend on `http://localho
TODO TODO
## Exercises
### Group 0
#### Exercise 1
Solution: Create an account by click on the Account symbol (top right) -> Button "Create a new Account" -> "Create Account"
### Group 3
#### Exercise 1
Solution: `http://localhost:5173/events?city=Hannover&genre=<iframe src="javascript:alert(`xss`)">` or `http://localhost:5173/events?city=<iframe src="javascript:alert(`xss`)">`
## Structure ## Structure
### Database ### Database

View File

@@ -1,6 +1,6 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.17 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36" version="24.7.17"> <mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.17 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36" version="24.7.17">
<diagram name="Page-1" id="WevClHWmhzPAQ7FDN5po"> <diagram name="Page-1" id="WevClHWmhzPAQ7FDN5po">
<mxGraphModel dx="3021" dy="1221" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"> <mxGraphModel dx="4728" dy="2205" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
<root> <root>
<mxCell id="0" /> <mxCell id="0" />
<mxCell id="1" parent="0" /> <mxCell id="1" parent="0" />
@@ -862,7 +862,7 @@
<mxPoint x="-561.2099999999999" y="180" as="targetPoint" /> <mxPoint x="-561.2099999999999" y="180" as="targetPoint" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="4QbvcL21_BjxR5MsDnpr-1" value="&lt;blockquote style=&quot;margin: 0px; border: none; padding: 0px;&quot;&gt;&lt;b&gt;&lt;u&gt;Exercises&lt;/u&gt;&lt;/b&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" vertex="1" parent="1"> <mxCell id="4QbvcL21_BjxR5MsDnpr-1" value="&lt;blockquote style=&quot;margin: 0px; border: none; padding: 0px;&quot;&gt;&lt;b&gt;&lt;u&gt;Exercises&lt;/u&gt;&lt;/b&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;fillColor=#647687;strokeColor=#314354;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="-920" y="760" width="160" height="30" as="geometry" /> <mxGeometry x="-920" y="760" width="160" height="30" as="geometry" />
</mxCell> </mxCell>
<mxCell id="4QbvcL21_BjxR5MsDnpr-2" value="&lt;blockquote style=&quot;margin: 0px 0px 0px 8px; border: none; padding: 0px;&quot;&gt;name: String&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1"> <mxCell id="4QbvcL21_BjxR5MsDnpr-2" value="&lt;blockquote style=&quot;margin: 0px 0px 0px 8px; border: none; padding: 0px;&quot;&gt;name: String&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
@@ -871,7 +871,7 @@
<mxCell id="4QbvcL21_BjxR5MsDnpr-3" value="&lt;blockquote style=&quot;margin: 0px 0px 0px 8px; border: none; padding: 0px;&quot;&gt;&lt;u&gt;id:&amp;nbsp;&lt;/u&gt;&lt;u style=&quot;background-color: initial;&quot;&gt;Number&lt;/u&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1"> <mxCell id="4QbvcL21_BjxR5MsDnpr-3" value="&lt;blockquote style=&quot;margin: 0px 0px 0px 8px; border: none; padding: 0px;&quot;&gt;&lt;u&gt;id:&amp;nbsp;&lt;/u&gt;&lt;u style=&quot;background-color: initial;&quot;&gt;Number&lt;/u&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
<mxGeometry x="-920" y="790" width="160" height="30" as="geometry" /> <mxGeometry x="-920" y="790" width="160" height="30" as="geometry" />
</mxCell> </mxCell>
<mxCell id="4QbvcL21_BjxR5MsDnpr-4" value="&lt;blockquote style=&quot;margin: 0px; border: none; padding: 0px;&quot;&gt;&lt;b&gt;&lt;u&gt;ExerciseGroups&lt;/u&gt;&lt;/b&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" vertex="1" parent="1"> <mxCell id="4QbvcL21_BjxR5MsDnpr-4" value="&lt;blockquote style=&quot;margin: 0px; border: none; padding: 0px;&quot;&gt;&lt;b&gt;&lt;u&gt;ExerciseGroups&lt;/u&gt;&lt;/b&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;fillColor=#647687;strokeColor=#314354;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="-920" y="560" width="160" height="30" as="geometry" /> <mxGeometry x="-920" y="560" width="160" height="30" as="geometry" />
</mxCell> </mxCell>
<mxCell id="4QbvcL21_BjxR5MsDnpr-5" value="&lt;blockquote style=&quot;margin: 0px 0px 0px 8px; border: none; padding: 0px;&quot;&gt;&lt;u&gt;id:&amp;nbsp;&lt;/u&gt;&lt;u style=&quot;background-color: initial;&quot;&gt;Number&lt;/u&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1"> <mxCell id="4QbvcL21_BjxR5MsDnpr-5" value="&lt;blockquote style=&quot;margin: 0px 0px 0px 8px; border: none; padding: 0px;&quot;&gt;&lt;u&gt;id:&amp;nbsp;&lt;/u&gt;&lt;u style=&quot;background-color: initial;&quot;&gt;Number&lt;/u&gt;&lt;/blockquote&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">

View File

@@ -1,5 +1,5 @@
import { Request, Response, NextFunction, Router } from 'express' import { Request, Response, NextFunction, Router } from 'express'
import { deleteAllTables, prepopulateDatabase } from '../scripts/databaseHelper' import { deleteAllTables, deleteExerciseProgressTables, prepopulateDatabase, prepopulateExerciseDatabase } from '../scripts/databaseHelper'
export const api = Router() export const api = Router()
@@ -15,5 +15,13 @@ api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction
await prepopulateDatabase() await prepopulateDatabase()
// Step 3: Send status back // Step 3: Send status back
res.status(200).send()
})
api.get("/resetExerciseProgress", async (req: Request, res: Response, next: NextFunction) => {
deleteExerciseProgressTables()
await prepopulateExerciseDatabase()
res.status(200).send() res.status(200).send()
}) })

View File

@@ -43,6 +43,7 @@ events.get("/", async (req: Request, res: Response) => {
include: [ include: [
{ {
model: Concert, model: Concert,
required: true,
include: [ include: [
{ {
model: Location, model: Location,

View File

@@ -20,4 +20,23 @@ exercises.get("/", (req: Request, res: Response) => {
).then(result => { ).then(result => {
res.status(200).json(result) res.status(200).json(result)
}) })
})
exercises.post("/:groupNr/:exerciseNr/:state", (req: Request, res: Response) => {
console.log(req.params.groupNr)
ExerciseGroup.findOne({
where: { groupNr: req.params.groupNr }
})
.then(group => {
Exercise.findOne({
where: {
exerciseNr: req.params.exerciseNr,
exerciseGroupId: group.id
}
})
.then(exercise => {
exercise.update({ solved: req.params.state == "1"})
res.status(200).send()
})
})
}) })

View File

@@ -42,6 +42,7 @@ export function deleteAllTables() {
Band.destroy({ truncate: true }) Band.destroy({ truncate: true })
Event.destroy({ truncate: true }) Event.destroy({ truncate: true })
City.destroy({ truncate: true })
Location.destroy({ truncate: true }) Location.destroy({ truncate: true })
Concert.destroy({ truncate: true }) Concert.destroy({ truncate: true })
SeatGroup.destroy({ truncate: true }) SeatGroup.destroy({ truncate: true })
@@ -52,11 +53,26 @@ export function deleteAllTables() {
Payment.destroy({ truncate: true }) Payment.destroy({ truncate: true })
Account.destroy({ truncate: true }) Account.destroy({ truncate: true })
AccountRole.destroy({ truncate: true}) AccountRole.destroy({ truncate: true})
}
export function deleteExerciseProgressTables() {
Exercise.destroy({truncate: true}) Exercise.destroy({truncate: true})
ExerciseGroup.destroy({truncate: true}) ExerciseGroup.destroy({truncate: true})
} }
export async function prepopulateExerciseDatabase() {
for (let exerciseGroup of exercises.data) {
ExerciseGroup.create(exerciseGroup)
.then(async dataset => {
for (let exercise of exerciseGroup.exercises) {
exercise["exerciseGroupId"] = dataset.id
await Exercise.create(exercise)
}
})
}
}
/** /**
* Insert default datasets in the database tables * Insert default datasets in the database tables
*/ */
@@ -195,15 +211,4 @@ export async function prepopulateDatabase() {
} }
}) })
} }
for (let exerciseGroup of exercises.data) {
ExerciseGroup.create(exerciseGroup)
.then(async dataset => {
for (let exercise of exerciseGroup.exercises) {
exercise["exerciseGroupId"] = dataset.id
await Exercise.create(exercise)
}
})
}
} }

View File

@@ -8,11 +8,13 @@ import { usePreferencesStore } from './data/stores/preferencesStore';
import { useFeedbackStore } from './data/stores/feedbackStore'; import { useFeedbackStore } from './data/stores/feedbackStore';
import { useConcertStore } from './data/stores/concertStore'; import { useConcertStore } from './data/stores/concertStore';
import { LocationModel } from './data/models/locations/locationModel'; import { LocationModel } from './data/models/locations/locationModel';
import { useShoppingStore } from './data/stores/shoppingStore';
import footerItems from './components/navigation/footerItems.vue';
const preferencesStore = usePreferencesStore() const preferencesStore = usePreferencesStore()
const concertStore = useConcertStore() const concertStore = useConcertStore()
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore()
const shoppingStore = useShoppingStore()
const theme = useTheme() const theme = useTheme()
theme.global.name.value = preferencesStore.theme theme.global.name.value = preferencesStore.theme
@@ -80,9 +82,19 @@ watch(() => concertStore.genreFilter, () => {
<!-- Here changes the router the content --> <!-- Here changes the router the content -->
<v-container max-width="1400" class="py-0" height="100%"> <v-container max-width="1400" class="py-0" height="100%">
<v-sheet color="sheet" height="100%"> <v-sheet color="sheet" height="100%">
<v-sheet color="primary" >
<v-breadcrumbs class="position-absolute">
<v-breadcrumbs-item />
</v-breadcrumbs>
</v-sheet>
<router-view></router-view> <router-view></router-view>
</v-sheet> </v-sheet>
</v-container> </v-container>
<v-footer color="secondary">
<footer-items />
</v-footer>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>

View File

@@ -23,11 +23,13 @@ function confirmPressed() {
max-width="400" max-width="400"
v-model="showDialog" v-model="showDialog"
> >
<v-row> <v-container>
<v-col> <v-row>
{{ description }} <v-col>
</v-col> {{ description }}
</v-row> </v-col>
</v-row>
</v-container>
<template #actions> <template #actions>
<outlined-button <outlined-button

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { getAllExerciseGroups, updateExercise } from '@/data/api/exerciseApi';
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const routeItems = ref(route.path.split('/'))
function solveExerciseXssInUrl() {
updateExercise(3, 1, true)
}
watch(() => route.path, () => {
routeItems.value = route.path.split("/")
routeItems.value = routeItems.value.filter(value => value != "")
for (let item in routeItems.value) {
item.charAt(0).toUpperCase() + item.slice(1)
}
})
</script>
<template>
<v-row>
<v-spacer />
<v-col>
{{ $t('youAreHere') }}
<v-breadcrumbs :items="routeItems">
<template v-slot:title="{ item }">
{{ item.title.charAt(0).toUpperCase() + item.title.slice(1) }}
</template>
<template v-slot:divider>
<v-icon icon="mdi-forward"></v-icon>
</template>
</v-breadcrumbs>
</v-col>
<v-col>
Filter:
<div v-for="query in route.query" v-html="query" />
<div v-for="query in route.query">
<span v-if="String(query).startsWith('<iframe')">
{{ solveExerciseXssInUrl() }}
</span>
</div>
</v-col>
<v-spacer />
</v-row>
</template>

View File

@@ -4,4 +4,10 @@ const BASE_URL = "http://localhost:3000/exercises"
export async function getAllExerciseGroups() { export async function getAllExerciseGroups() {
return await axios.get(BASE_URL) return await axios.get(BASE_URL)
}
export async function updateExercise(exerciseGroupNr: number, exerciseNr: number, state: boolean) {
let url = BASE_URL + "/" + exerciseGroupNr + "/" + exerciseNr + "/" + (state ? "1" : "0")
return await axios.post(url)
} }

View File

@@ -13,8 +13,8 @@ export const useShoppingStore = defineStore("shoppingStore", {
events: ref<Array<EventModel>>([]), events: ref<Array<EventModel>>([]),
cities: ref<Array<CityModel>>([]), cities: ref<Array<CityModel>>([]),
genres: ref<Array<GenreModel>>([]), genres: ref<Array<GenreModel>>([]),
cityFilterName: ref<String>(), cityFilterName: ref<string>(),
genreFilterName: ref<String>() genreFilterName: ref<string>()
}), }),
actions: { actions: {
@@ -23,8 +23,8 @@ export const useShoppingStore = defineStore("shoppingStore", {
feedbackStore.fetchDataFromServerInProgress = true feedbackStore.fetchDataFromServerInProgress = true
await fetchEvents( await fetchEvents(
this.cityFilterName != null ? this.cityFilterName : "", this.cityFilterName != null && this.cityFilterName != "undefined" && !this.cityFilterName.startsWith("<") ? this.cityFilterName : "",
this.genreFilterName != null ? this.genreFilterName : "" this.genreFilterName != null && this.genreFilterName != "undefined" && !this.genreFilterName.startsWith("<") ? this.genreFilterName : ""
) )
.then(result => { .then(result => {
this.events = result.data this.events = result.data

View File

@@ -5,13 +5,17 @@
"topLocations": "Top Veranstaltungsorte", "topLocations": "Top Veranstaltungsorte",
"tickets": "Ticket | Tickets", "tickets": "Ticket | Tickets",
"concert": "Konzert | Konzerte", "concert": "Konzert | Konzerte",
"resetPreferences": "Einstellungen zurücksetzen",
"resetDatabase": "Datenbank zurücksetzen",
"resetConfirm": {
"title": "Datenbank zurücksetzen?",
"description": "Soll die Datenbank des Servers wirklich zurückgesetzt werden? Dies kann nicht rückgänig gemacht werden! Der Bearbeitungsfortschritt der Aufgaben wird nicht gelöscht."
},
"preferences": { "preferences": {
"pageSetup": "Seiteneinstellungen", "pageSetup": "Seiteneinstellungen",
"selectedTheme": "Ausgewähltes Theme", "selectedTheme": "Ausgewähltes Theme",
"language": "Sprache", "language": "Sprache",
"systemSetup": "Systemeinstellungen", "systemSetup": "Systemeinstellungen",
"resetDatabase": "Datenbank zurücksetzen",
"resetPreferences": "Einstellungen zurücksetzen",
"resetConfirm": "Soll die Datenbank wirklich zurückgesetzt werden?" "resetConfirm": "Soll die Datenbank wirklich zurückgesetzt werden?"
}, },
"product": { "product": {
@@ -93,10 +97,6 @@
"deleteAccount": { "deleteAccount": {
"title": "Account löschen?", "title": "Account löschen?",
"description": "Soll der Account wirklich gelöscht werden? Dieser kann nicht mehr wiederhergestellt werden!" "description": "Soll der Account wirklich gelöscht werden? Dieser kann nicht mehr wiederhergestellt werden!"
},
"resetConfirm": {
"title": "Datenbank zurücksetzen?",
"description": "Soll die Datenbank des Servers wirklich zurückgesetzt werden? Dies kann nicht rückgänig gemacht werden!"
} }
}, },
"scoreBoard": { "scoreBoard": {
@@ -161,5 +161,7 @@
"price": "Preis", "price": "Preis",
"standingArea": "Stehbereich", "standingArea": "Stehbereich",
"exerciseGroup": "Aufgabengruppe", "exerciseGroup": "Aufgabengruppe",
"exercise": "Aufgabe" "exercise": "Aufgabe",
"resetProgress": "Aufgabenfortschritt zurücksetzen",
"youAreHere": "Du bist hier:"
} }

View File

@@ -5,13 +5,17 @@
"topLocations": "Top Locations", "topLocations": "Top Locations",
"tickets": "Ticket | Tickets", "tickets": "Ticket | Tickets",
"concert": "Concert | Concerts", "concert": "Concert | Concerts",
"resetPreferences": "Reset preferences",
"resetDatabase": "Reset database",
"resetDatabaseConfirm": {
"title": "Reset database?",
"description": "Do you really want to reset the server database? This can't be undone! Progress will not be deleted."
},
"preferences": { "preferences": {
"pageSetup": "Page setup", "pageSetup": "Page setup",
"selectedTheme": "Selected theme", "selectedTheme": "Selected theme",
"language": "Language", "language": "Language",
"systemSetup": "System setup", "systemSetup": "System setup",
"resetDatabase": "Reset database",
"resetPreferences": "Reset preferences",
"resetConfirm": "Really reset the database?" "resetConfirm": "Really reset the database?"
}, },
"product": { "product": {
@@ -93,10 +97,6 @@
"deleteAccount": { "deleteAccount": {
"title": "Delete account?", "title": "Delete account?",
"description": "Do you really want to delete the account? This can't be undone!" "description": "Do you really want to delete the account? This can't be undone!"
},
"resetConfirm": {
"title": "Reset database?",
"description": "Do you really want to reset the server database? This can't be undone!"
} }
}, },
"scoreBoard": { "scoreBoard": {
@@ -161,5 +161,7 @@
"price": "Price", "price": "Price",
"standingArea": "Standing Area", "standingArea": "Standing Area",
"exerciseGroup": "Exercise group", "exerciseGroup": "Exercise group",
"exercise": "Exercise" "exercise": "Exercise",
"resetProgress": "Reset Exercise Progress",
"youAreHere": "You are here:"
} }

View File

@@ -30,7 +30,7 @@ defineProps({
readonly readonly
/> />
<div class="px-3">{{ band.ratings.length }} Bewertungen</div> <div class="px-3">{{ band.ratings.length }} {{ $t('rating', band.ratings.length) }}</div>
</div> </div>
</v-col> </v-col>

View File

@@ -4,8 +4,10 @@ import outlinedButton from '@/components/basics/outlinedButton.vue';
import { GenreModel } from '@/data/models/acts/genreModel'; import { GenreModel } from '@/data/models/acts/genreModel';
import { CityModel } from '@/data/models/locations/cityModel'; import { CityModel } from '@/data/models/locations/cityModel';
import { useShoppingStore } from '@/data/stores/shoppingStore'; import { useShoppingStore } from '@/data/stores/shoppingStore';
import { useRoute, useRouter } from 'vue-router';
const shoppingStore = useShoppingStore() const shoppingStore = useShoppingStore()
const router = useRouter()
shoppingStore.getCities() shoppingStore.getCities()
shoppingStore.getGenres() shoppingStore.getGenres()
@@ -21,6 +23,22 @@ function itemPropsGenre(genre: GenreModel) {
title: genre.name title: genre.name
} }
} }
function filter() {
let queries = {}
if (shoppingStore.cityFilterName != null && shoppingStore.cityFilterName != "undefined") {
queries["city"] = shoppingStore.cityFilterName
}
if (shoppingStore.genreFilterName != null && shoppingStore.genreFilterName != "undefined") {
queries["genre"] = shoppingStore.genreFilterName
}
router.push({ path: '/events', query: queries})
shoppingStore.getEvents()
}
</script> </script>
<template> <template>
@@ -64,7 +82,7 @@ function itemPropsGenre(genre: GenreModel) {
<outlined-button <outlined-button
height="100%" height="100%"
append-icon="mdi-chevron-right" append-icon="mdi-chevron-right"
@click="shoppingStore.getEvents()" @click="filter"
> >
{{ $t('filtering') }} {{ $t('filtering') }}
</outlined-button> </outlined-button>

View File

@@ -1,15 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { createDateRangeString, lowestTicketPrice } from '@/scripts/concertScripts'; import { createDateRangeString, lowestTicketPrice } from '@/scripts/concertScripts';
import filterBar from './filterBar.vue'; import filterBar from './filterBar.vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useShoppingStore } from '@/data/stores/shoppingStore'; import { useShoppingStore } from '@/data/stores/shoppingStore';
import { useFeedbackStore } from '@/data/stores/feedbackStore'; import { useFeedbackStore } from '@/data/stores/feedbackStore';
import concertListItem from '@/components/pageParts/concertListItem.vue'; import concertListItem from '@/components/pageParts/concertListItem.vue';
import { useTemplateRef } from 'vue';
const route = useRoute()
const router = useRouter() const router = useRouter()
const shoppingStore = useShoppingStore() const shoppingStore = useShoppingStore()
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore()
// Load query attributes
shoppingStore.cityFilterName = String(route.query.city)
shoppingStore.genreFilterName = String(route.query.genre)
shoppingStore.getEvents() shoppingStore.getEvents()
</script> </script>
@@ -17,6 +23,7 @@ shoppingStore.getEvents()
<v-container> <v-container>
<v-row> <v-row>
<v-spacer /> <v-spacer />
<!-- <div v-html="route.query.genre" /> -->
<v-col cols="10"> <v-col cols="10">
<v-row> <v-row>
@@ -44,6 +51,7 @@ shoppingStore.getEvents()
<div class="text-h5"> <div class="text-h5">
{{ createDateRangeString(event) }} {{ createDateRangeString(event) }}
<!-- {{ console.log(event.concerts) }} -->
</div> </div>
<div class="text-h5"> <div class="text-h5">

View File

@@ -19,7 +19,10 @@ function changeLanguage() {
</script> </script>
<template> <template>
<card-view :title="$t('preferences.pageSetup')" prepend-icon="mdi-view-dashboard" elevation="8"> <card-view
:title="$t('preferences.pageSetup')"
icon="mdi-view-dashboard"
>
<v-row> <v-row>
<v-col> <v-col>
<v-select <v-select
@@ -27,15 +30,20 @@ function changeLanguage() {
:items="themeEnums" :items="themeEnums"
:label="$t('preferences.selectedTheme')" :label="$t('preferences.selectedTheme')"
@update:model-value="changeTheme" @update:model-value="changeTheme"
hide-details
/> />
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col> <v-col>
<v-select v-model="preferencesStore.language" :items="$i18n.availableLocales" :label="$t('preferences.language')" <v-select
@update:model-value="changeLanguage" v-model="preferencesStore.language"
/> :items="$i18n.availableLocales"
:label="$t('preferences.language')"
@update:model-value="changeLanguage"
hide-details
/>
</v-col> </v-col>
</v-row> </v-row>
</card-view> </card-view>

View File

@@ -7,6 +7,7 @@ import { ref } from 'vue';
import confirmDialog from '@/components/basics/confirmDialog.vue'; import confirmDialog from '@/components/basics/confirmDialog.vue';
import { getServerState, resetDatabase } from '@/data/api/mainApi'; import { getServerState, resetDatabase } from '@/data/api/mainApi';
import { ServerStateEnum } from '@/data/enums/serverStateEnum'; import { ServerStateEnum } from '@/data/enums/serverStateEnum';
import packageJson from './../../../../package.json'
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore()
const showConfirmDialog = ref(false) const showConfirmDialog = ref(false)
@@ -38,16 +39,12 @@ async function resetDb() {
showConfirmDialog.value = false showConfirmDialog.value = false
// todo: Request all data // todo: Request all data
} }
function resetSettings() {
// todo
}
</script> </script>
<template> <template>
<card-view <card-view
:title="$t('preferences.systemSetup')" :title="$t('preferences.systemSetup')"
prepend-icon="mdi-engine" icon="mdi-engine"
> >
<v-row> <v-row>
<v-col> <v-col>
@@ -68,6 +65,13 @@ function resetSettings() {
</span> </span>
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col>
Software Version: {{ packageJson.version }}
</v-col>
</v-row>
<v-row> <v-row>
<v-col class="d-flex justify-center align-center"> <v-col class="d-flex justify-center align-center">
<outlined-button <outlined-button
@@ -76,23 +80,27 @@ function resetSettings() {
color="red" color="red"
:disabled="serverOnline != ServerStateEnum.ONLINE" :disabled="serverOnline != ServerStateEnum.ONLINE"
> >
{{ $t('preferences.resetDatabase') }} {{ $t('resetDatabase') }}
</outlined-button> </outlined-button>
</v-col> </v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-center align-center"> <v-col class="d-flex justify-center align-center">
<outlined-button <outlined-button
@click="resetSettings" prepend-icon="mdi-progress-close"
prepend-icon="mdi-cog-counterclockwise" color="red"
> >
{{ $t('preferences.resetPreferences') }} {{ $t('resetProgress') }}
</outlined-button> </outlined-button>
</v-col> </v-col>
</v-row> </v-row>
</card-view> </card-view>
<confirm-dialog <confirm-dialog
:title="$t('dialog.resetConfirm.title')" :title="$t('resetDatabaseConfirm.title')"
:description="$t('dialog.resetConfirm.description')" :description="$t('resetDatabaseConfirm.description')"
v-model="showConfirmDialog" v-model="showConfirmDialog"
:onConfirm="resetDb" :onConfirm="resetDb"
/> />

View File

@@ -49,15 +49,23 @@ export function calcRatingValues(ratings: Array<RatingModel>) {
return ratingValues return ratingValues
} }
export function createDateRangeString(tour: TourModel) {
const dateArray = []
for (let concert of tour.concerts) { /**
* Create a date range string of all concerts from an Event
*
* @param event EventModel with a list of concerts
*
* @returns A date string. If one concert: dd.MM.YYYY, if two or more: dd.MM.YYYY - dd.MM.YYYY
*/
export function createDateRangeString(event: EventModel) {
const dateArray: Array<Date> = []
for (let concert of event.concerts) {
dateArray.push(new Date(concert.date)) dateArray.push(new Date(concert.date))
} }
dateArray.sort(function (a, b) { dateArray.sort(function (a, b) {
return a - b return a.getUTCMilliseconds() - b.getUTCMilliseconds()
}) })
@@ -69,6 +77,14 @@ export function createDateRangeString(tour: TourModel) {
} }
} }
/**
* Search in all concerts of an Event for the show with the lowest price
*
* @param event EventModel with a list of concerts
*
* @returns Lowest ticket price, rounded to two floating point digits
*/
export function lowestTicketPrice(event: EventModel): string { export function lowestTicketPrice(event: EventModel): string {
const priceArray : Array<number> = [] const priceArray : Array<number> = []