Implement URL XSS attack
This commit is contained in:
14
README.md
14
README.md
@@ -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
|
||||||
|
|||||||
@@ -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="<blockquote style="margin: 0px; border: none; padding: 0px;"><b><u>Exercises</u></b></blockquote>" 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="<blockquote style="margin: 0px; border: none; padding: 0px;"><b><u>Exercises</u></b></blockquote>" 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="<blockquote style="margin: 0px 0px 0px 8px; border: none; padding: 0px;">name: String</blockquote>" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
|
<mxCell id="4QbvcL21_BjxR5MsDnpr-2" value="<blockquote style="margin: 0px 0px 0px 8px; border: none; padding: 0px;">name: String</blockquote>" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
|
||||||
@@ -871,7 +871,7 @@
|
|||||||
<mxCell id="4QbvcL21_BjxR5MsDnpr-3" value="<blockquote style="margin: 0px 0px 0px 8px; border: none; padding: 0px;"><u>id:&nbsp;</u><u style="background-color: initial;">Number</u></blockquote>" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
|
<mxCell id="4QbvcL21_BjxR5MsDnpr-3" value="<blockquote style="margin: 0px 0px 0px 8px; border: none; padding: 0px;"><u>id:&nbsp;</u><u style="background-color: initial;">Number</u></blockquote>" 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="<blockquote style="margin: 0px; border: none; padding: 0px;"><b><u>ExerciseGroups</u></b></blockquote>" 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="<blockquote style="margin: 0px; border: none; padding: 0px;"><b><u>ExerciseGroups</u></b></blockquote>" 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="<blockquote style="margin: 0px 0px 0px 8px; border: none; padding: 0px;"><u>id:&nbsp;</u><u style="background-color: initial;">Number</u></blockquote>" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
|
<mxCell id="4QbvcL21_BjxR5MsDnpr-5" value="<blockquote style="margin: 0px 0px 0px 8px; border: none; padding: 0px;"><u>id:&nbsp;</u><u style="background-color: initial;">Number</u></blockquote>" style="rounded=0;whiteSpace=wrap;html=1;align=left;" vertex="1" parent="1">
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -17,3 +17,11 @@ api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction
|
|||||||
// Step 3: Send status back
|
// Step 3: Send status back
|
||||||
res.status(200).send()
|
res.status(200).send()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.get("/resetExerciseProgress", async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
deleteExerciseProgressTables()
|
||||||
|
|
||||||
|
await prepopulateExerciseDatabase()
|
||||||
|
|
||||||
|
res.status(200).send()
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -21,3 +21,22 @@ exercises.get("/", (req: Request, res: Response) => {
|
|||||||
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
53
software/src/components/navigation/footerItems.vue
Normal file
53
software/src/components/navigation/footerItems.vue
Normal 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>
|
||||||
@@ -5,3 +5,9 @@ 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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> = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user