Exercises selection system

This commit is contained in:
2025-08-30 12:54:29 +02:00
parent b69c63ea53
commit 3c13bb88e1
16 changed files with 573 additions and 216 deletions

View File

@@ -9,6 +9,7 @@
"descriptionEn": "Before an attack, it's important to understand how the website is structured. How are the URLs structured? Where are input fields located that execute SQL queries in the backend?", "descriptionEn": "Before an attack, it's important to understand how the website is structured. How are the URLs structured? Where are input fields located that execute SQL queries in the backend?",
"exercises": [ "exercises": [
{ {
"uuid": "getting-known-register",
"nameDe": "Registrieren", "nameDe": "Registrieren",
"nameEn": "Register", "nameEn": "Register",
"exerciseNr": 1, "exerciseNr": 1,
@@ -16,6 +17,7 @@
"descriptionEn": "We'll set up a regular account on the platform. To do this, navigate to the account page and register." "descriptionEn": "We'll set up a regular account on the platform. To do this, navigate to the account page and register."
}, },
{ {
"uuid": "getting-known-profile",
"nameDe": "Profil vervollständigen", "nameDe": "Profil vervollständigen",
"nameEn": "Complete profile", "nameEn": "Complete profile",
"exerciseNr": 2, "exerciseNr": 2,
@@ -23,6 +25,7 @@
"descriptionEn": "Orders are only possible once your account profile is complete. Log in, navigate to your account settings, fill in your name, and add an address and payment method. Finally, save everything." "descriptionEn": "Orders are only possible once your account profile is complete. Log in, navigate to your account settings, fill in your name, and add an address and payment method. Finally, save everything."
}, },
{ {
"uuid": "getting-known-buy-ticket",
"nameDe": "Ein Ticket kaufen", "nameDe": "Ein Ticket kaufen",
"nameEn": "Buy a ticket", "nameEn": "Buy a ticket",
"exerciseNr": 3, "exerciseNr": 3,
@@ -40,6 +43,7 @@
"descriptionEn": "A website contains publicly visible pages and some protected pages. The latter can only be accessed with appropriate permissions. Examples include an admin panel or the personal shopping cart. Access is often controlled via cookies or authentication on a backend server. With broken access control, this security mechanism is either not implemented or is incorrectly implemented. This allows pages to be accessed without authorization via the URL.", "descriptionEn": "A website contains publicly visible pages and some protected pages. The latter can only be accessed with appropriate permissions. Examples include an admin panel or the personal shopping cart. Access is often controlled via cookies or authentication on a backend server. With broken access control, this security mechanism is either not implemented or is incorrectly implemented. This allows pages to be accessed without authorization via the URL.",
"exercises": [ "exercises": [
{ {
"uuid": "broken-access-control-exercise-page",
"nameDe": "Hilfe-Seite aufrufen", "nameDe": "Hilfe-Seite aufrufen",
"nameEn": "Access Help Page", "nameEn": "Access Help Page",
"exerciseNr": 1, "exerciseNr": 1,
@@ -47,6 +51,7 @@
"descriptionEn": "The help page provides insight into the processing status of tasks. It's therefore not secure, but it's also not (yet) accessible as a button in the title bar. Expand the URL in the address bar to access the help page." "descriptionEn": "The help page provides insight into the processing status of tasks. It's therefore not secure, but it's also not (yet) accessible as a button in the title bar. Expand the URL in the address bar to access the help page."
}, },
{ {
"uuid": "broken-access-control-hidden-concert",
"nameDe": "Das versteckte Konzert buchen", "nameDe": "Das versteckte Konzert buchen",
"nameEn": "Book the hidden concert", "nameEn": "Book the hidden concert",
"exerciseNr": 2, "exerciseNr": 2,
@@ -64,6 +69,7 @@
"descriptionEn": "A database uses SQL commands to create, retrieve, modify, and delete records. A server is accessed via API interfaces, executes the commands in the database, and returns the results. The client must not have direct access to the database. SQL injection attempts to circumvent this security mechanism and execute SQL commands directly via the API interfaces.", "descriptionEn": "A database uses SQL commands to create, retrieve, modify, and delete records. A server is accessed via API interfaces, executes the commands in the database, and returns the results. The client must not have direct access to the database. SQL injection attempts to circumvent this security mechanism and execute SQL commands directly via the API interfaces.",
"exercises": [ "exercises": [
{ {
"uuid": "sql-injection-database-scheme",
"nameDe": "Wie sieht die Datenbank aus?", "nameDe": "Wie sieht die Datenbank aus?",
"nameEn": "How does the database look like?", "nameEn": "How does the database look like?",
"exerciseNr": 1, "exerciseNr": 1,
@@ -71,6 +77,7 @@
"descriptionEn": "We'll now attempt to attack the database in the background. Currently, we don't yet know what the database looks like, or which tables it contains. However, we can use an SQL command to inject it. Go to the global search page. Open the Developer Tools using the keyboard shortcut Ctrl + D. Click on the Network tab. Here you can see how the frontend communicates with the server. Now write an SQL injection that ignores the search term and instead returns all records in the sqlite_master table, provided the type='table' condition is met. If the backend responds successfully, copy the table names into a text file so that we can specify the correct table names for future tasks." "descriptionEn": "We'll now attempt to attack the database in the background. Currently, we don't yet know what the database looks like, or which tables it contains. However, we can use an SQL command to inject it. Go to the global search page. Open the Developer Tools using the keyboard shortcut Ctrl + D. Click on the Network tab. Here you can see how the frontend communicates with the server. Now write an SQL injection that ignores the search term and instead returns all records in the sqlite_master table, provided the type='table' condition is met. If the backend responds successfully, copy the table names into a text file so that we can specify the correct table names for future tasks."
}, },
{ {
"uuid": "sql-injection-all-accounts",
"nameDe": "Alle Accounts ausspähen", "nameDe": "Alle Accounts ausspähen",
"nameEn": "Get all accounts", "nameEn": "Get all accounts",
"exerciseNr": 2, "exerciseNr": 2,
@@ -78,6 +85,7 @@
"descriptionEn": "Now write an SQL injection that ignores the search term and instead returns all records in the account table. Execute the attack using the search field. Watch the server's response." "descriptionEn": "Now write an SQL injection that ignores the search term and instead returns all records in the account table. Execute the attack using the search field. Watch the server's response."
}, },
{ {
"uuid": "sql-injection-account-roles",
"nameDe": "Alle Berechtigungsgruppen ausspähen", "nameDe": "Alle Berechtigungsgruppen ausspähen",
"nameEn": "Get all account roles", "nameEn": "Get all account roles",
"exerciseNr": 3, "exerciseNr": 3,
@@ -85,6 +93,7 @@
"descriptionEn": "We now see all the accounts. Each has an authorization ID (accountRoleId) that controls permissions such as access to the admin panel. However, we don't know what the IDs mean. Therefore, write an SQL injection that ignores the search term and instead returns all records in the >>AccountRoles<< table. Execute the attack via the search field. Observe the server's response via the >>Network<< tab." "descriptionEn": "We now see all the accounts. Each has an authorization ID (accountRoleId) that controls permissions such as access to the admin panel. However, we don't know what the IDs mean. Therefore, write an SQL injection that ignores the search term and instead returns all records in the >>AccountRoles<< table. Execute the attack via the search field. Observe the server's response via the >>Network<< tab."
}, },
{ {
"uuid": "sql-injection-upgrade-privileges",
"nameDe": "Eigene Berechtigungen erhöhen", "nameDe": "Eigene Berechtigungen erhöhen",
"nameEn": "Upgrade your privileges", "nameEn": "Upgrade your privileges",
"exerciseNr": 4, "exerciseNr": 4,
@@ -92,6 +101,7 @@
"descriptionEn": "Change the privileges of your account" "descriptionEn": "Change the privileges of your account"
}, },
{ {
"uuid": "sql-injection-capture-account",
"nameDe": "Einen fremden Account übernehmen", "nameDe": "Einen fremden Account übernehmen",
"nameEn": "Capture another account", "nameEn": "Capture another account",
"exerciseNr": 5, "exerciseNr": 5,
@@ -99,6 +109,7 @@
"descriptionEn": "Instead of increasing our own permissions, we can also take over an account that is already a >>super admin<<. To do this, select one from the list of accounts obtained in Task 2.1 that has the >>super admin<< role. Only then can we open the file manager, which we'll need later. Once you've found the account name, go to the login menu (log out if you're still logged in). Now perform an SQL injection to take over this account." "descriptionEn": "Instead of increasing our own permissions, we can also take over an account that is already a >>super admin<<. To do this, select one from the list of accounts obtained in Task 2.1 that has the >>super admin<< role. Only then can we open the file manager, which we'll need later. Once you've found the account name, go to the login menu (log out if you're still logged in). Now perform an SQL injection to take over this account."
}, },
{ {
"uuid": "sql-injection-delete-rating",
"nameDe": "Bewertungen löschen", "nameDe": "Bewertungen löschen",
"nameEn": "Delete ratings", "nameEn": "Delete ratings",
"exerciseNr": 6, "exerciseNr": 6,
@@ -116,6 +127,7 @@
"descriptionEn": "Next, we want to inject malicious code into the web application. First, we'll test whether the website is vulnerable to this. Manipulate the URL of the band's page so that you see a >>Hello World!<< message as an >>alert<<. Note: Use an image tag! Set the number >>1<< as the >>src<<. You can execute the command in the >>onerror<< tag.", "descriptionEn": "Next, we want to inject malicious code into the web application. First, we'll test whether the website is vulnerable to this. Manipulate the URL of the band's page so that you see a >>Hello World!<< message as an >>alert<<. Note: Use an image tag! Set the number >>1<< as the >>src<<. You can execute the command in the >>onerror<< tag.",
"exercises": [ "exercises": [
{ {
"uuid": "cross-site-scripting-hello-world",
"nameDe": "Hallo Welt!", "nameDe": "Hallo Welt!",
"nameEn": "Hello World!", "nameEn": "Hello World!",
"exerciseNr": 1, "exerciseNr": 1,
@@ -123,6 +135,7 @@
"descriptionEn": "Next, we want to inject malicious code into the web application. First, we'll test whether the website is vulnerable to malicious code. To do this, go to the >>All Bands<< page and filter the entries by any genre of your choice. In the URL bar, you'll now see that a parameter is specified after the URL and the resource target (the part after the question mark). We'll replace this parameter with an HTML tag. The trick here: We specify the numeric value >>1<< as the source, which automatically executes what's contained in the >>onerror<< tag. This is exactly where we want to insert an alert message with >>Hello World!<< as JavaScript code. Change the URL so that it displays the message. If you're not familiar with JavaScript, check out the last page for useful commands." "descriptionEn": "Next, we want to inject malicious code into the web application. First, we'll test whether the website is vulnerable to malicious code. To do this, go to the >>All Bands<< page and filter the entries by any genre of your choice. In the URL bar, you'll now see that a parameter is specified after the URL and the resource target (the part after the question mark). We'll replace this parameter with an HTML tag. The trick here: We specify the numeric value >>1<< as the source, which automatically executes what's contained in the >>onerror<< tag. This is exactly where we want to insert an alert message with >>Hello World!<< as JavaScript code. Change the URL so that it displays the message. If you're not familiar with JavaScript, check out the last page for useful commands."
}, },
{ {
"uuid": "cross-site-scripting-external-script",
"nameDe": "Ein externes Script aufrufen", "nameDe": "Ein externes Script aufrufen",
"nameEn": "Run an external script", "nameEn": "Run an external script",
"exerciseNr": 2, "exerciseNr": 2,

View File

@@ -3,6 +3,9 @@ import { ExerciseGroup } from "./exerciseGroup.model";
@Table({ timestamps: false }) @Table({ timestamps: false })
export class Exercise extends Model { export class Exercise extends Model {
@Column
uuid: string
@Column @Column
nameDe: string nameDe: string

View File

@@ -40,9 +40,9 @@ exerciseStore.getAllExercises()
/> />
<v-btn <v-btn
v-if="exerciseStore.helpPageVisible" v-if="exerciseStore.exercisePageVisible"
variant="plain" variant="plain"
icon="mdi-help" icon="mdi-book-open-blank-variant"
to="/help" to="/help"
/> />

View File

@@ -1,7 +1,8 @@
import { ExerciseGroupModel } from "./exerciseGroupModel" import { ExerciseGroupModel } from "./exerciseGroupModel"
export class ExerciseModel { export class ExerciseModel {
id = -1 id: number = -1
uuid: string = ""
nameDe: string = "" nameDe: string = ""
nameEn: string = "" nameEn: string = ""
exerciseNr: number = 0 exerciseNr: number = 0
@@ -9,4 +10,5 @@ export class ExerciseModel {
descriptionEn: string = "" descriptionEn: string = ""
solved: boolean = false solved: boolean = false
exerciseGroup: ExerciseGroupModel exerciseGroup: ExerciseGroupModel
available: boolean = true
} }

View File

@@ -168,6 +168,17 @@
"title": "Auf Werkseinstellungen zurücksetzen?", "title": "Auf Werkseinstellungen zurücksetzen?",
"description": "Sollen alle Einstellungen und Daten auf Werkseinstellungen zurückgesetzt werden? Alle Änderungen und Fortschritte gehen verloren!" "description": "Sollen alle Einstellungen und Daten auf Werkseinstellungen zurückgesetzt werden? Alle Änderungen und Fortschritte gehen verloren!"
} }
},
"exercises": {
"available": "{0} von {1} Aufgaben verfügbar",
"edit": "Verfügbare Aufgaben bearbeiten",
"settings": "Aufgaben-Konfiguration"
},
"importExport": {
"title": "Import/Export Konfiguration",
"selectConfigFile": "Konfigurations-Datei auswählen",
"download": "Konfiguration exportieren",
"upload": "Datei hochladen"
} }
}, },
"help": { "help": {

View File

@@ -168,6 +168,19 @@
"title": "Factory reset?", "title": "Factory reset?",
"description": "Do you really want to reset everything? Every change will be lost!" "description": "Do you really want to reset everything? Every change will be lost!"
} }
},
"exercises": {
"settings": "Exercise Configuration",
"available": "{0} of {1} exercises are available",
"uploadExerciseConfig": "Upload exercise config",
"edit": "Edit available exercises",
"upload": "Upload exercises config"
},
"importExport": {
"title": "Import/Export config",
"selectConfigFile": "Select config file",
"upload": "Upload file",
"download": "Export config"
} }
}, },
"help": { "help": {

View File

@@ -1,55 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { useExerciseStore } from '@/stores/exercise.store'; import { useExerciseStore } from "@/stores/exercise.store";
import outlinedButton from '@/components/basics/outlinedButton.vue'; import outlinedButton from "@/components/basics/outlinedButton.vue";
import { generateResultsPdf } from '@/scripts/pdfScripts'; import { generateResultsPdf } from "@/scripts/pdfScripts";
import { usePreferencesStore } from '@/stores/preferences.store'; import { usePreferencesStore } from "@/stores/preferences.store";
import cardView from '@/components/basics/cardView.vue'; import cardView from "@/components/basics/cardView.vue";
import { LanguageEnum } from '@/data/enums/languageEnum'; import { LanguageEnum } from "@/data/enums/languageEnum";
import { ExerciseModel } from '@/data/models/exercises/exerciseModel'; import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { getExerciseDotColor } from "@/scripts/colorScripts";
import { getExerciseDescriptionLanguage, getExerciseNameLanguage } from "@/scripts/languageScripts";
const exerciseStore = useExerciseStore() const exerciseStore = useExerciseStore();
const preferencesStore = usePreferencesStore() const preferencesStore = usePreferencesStore();
exerciseStore.solveExercise(1, 1) // Mark this exercise as solved if page was opened
exerciseStore.solveExercise(1, 1);
function getDotColor(exerciseGroupNr: number) {
switch(exerciseGroupNr) {
case 0: return "purple"
case 1: return "orange"
case 2: return "blue"
case 3: return "pink"
}
}
function generateExerciseKey() { function generateExerciseKey() {
try { try {
let code = "" let code = "";
for (let i = 0; i < 13; i++) { for (let i = 0; i < 13; i++) {
if (exerciseStore.exercises[i].solved) { if (exerciseStore.exercises[i].solved) {
code += "3" code += "3";
} else { } else {
code += "0" code += "0";
} }
} }
return (Number(code) + Number(preferencesStore.registrationNumber)) * 237 return (Number(code) + Number(preferencesStore.registrationNumber)) * 237;
} catch (e) {} } catch (e) {}
} }
function getNameLanguage(exercise: ExerciseModel) {
switch(preferencesStore.language) {
case LanguageEnum.GERMAN: return exercise.nameDe
case LanguageEnum.ENGLISH: return exercise.nameEn
}
}
function getDescriptionLanguage(exercise: ExerciseModel) {
switch(preferencesStore.language) {
case LanguageEnum.GERMAN: return exercise.descriptionDe
case LanguageEnum.ENGLISH: return exercise.descriptionEn
}
}
</script> </script>
<template> <template>
@@ -61,9 +41,12 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
<outlined-button <outlined-button
prepend-icon="mdi-file-pdf-box" prepend-icon="mdi-file-pdf-box"
@click="generateResultsPdf()" @click="generateResultsPdf()"
:disabled="preferencesStore.studentName.length < 3 || preferencesStore.registrationNumber.length < 7" :disabled="
preferencesStore.studentName.length < 3 ||
preferencesStore.registrationNumber.length < 7
"
> >
{{ $t('help.scoreBoard.generatePdf') }} {{ $t("help.scoreBoard.generatePdf") }}
</outlined-button> </outlined-button>
</v-col> </v-col>
</v-row> </v-row>
@@ -71,7 +54,7 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
<v-row> <v-row>
<v-col class="text-h5 text-center"> <v-col class="text-h5 text-center">
<div> <div>
{{ $t('help.scoreBoard.personalSolutionKey') + ':' }} {{ $t("help.scoreBoard.personalSolutionKey") + ":" }}
</div> </div>
<div> <div>
{{ generateExerciseKey() }} {{ generateExerciseKey() }}
@@ -86,49 +69,55 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
icon="mdi-checkbox-marked-circle-auto-outline" icon="mdi-checkbox-marked-circle-auto-outline"
> >
<template #borderless> <template #borderless>
<v-timeline <v-timeline side="end" class="px-5" align="start">
side="end"
class="px-5"
align="start"
>
<template v-for="exercise of exerciseStore.exercises"> <template v-for="exercise of exerciseStore.exercises">
<!-- Add exercise group description item --> <!-- Add exercise group description item -->
<v-timeline-item v-if="exercise.exerciseNr == 1" <v-timeline-item
:dot-color="getDotColor(exercise.exerciseGroup.groupNr)" v-if="exercise.exerciseNr == 1"
:dot-color="
getExerciseDotColor(exercise.exerciseGroup.groupNr)
"
:icon="exercise.exerciseGroup.icon" :icon="exercise.exerciseGroup.icon"
fill-dot fill-dot
> >
<div <div
:class="`pt-1 text-h5 font-weight-bold text-${getDotColor(exercise.exerciseGroup.groupNr)}`" :class="`pt-1 text-h5 font-weight-bold text-${getExerciseDotColor(
exercise.exerciseGroup.groupNr
)}`"
> >
{{ {{
(preferencesStore.language == LanguageEnum.GERMAN preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.nameDe ? exercise.exerciseGroup.nameDe
: exercise.exerciseGroup.nameEn) : exercise.exerciseGroup.nameEn
}} }}
</div> </div>
<div> <div>
{{ {{
(preferencesStore.language == LanguageEnum.GERMAN preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.descriptionDe ? exercise.exerciseGroup.descriptionDe
: exercise.exerciseGroup.descriptionEn) : exercise.exerciseGroup.descriptionEn
}} }}
</div> </div>
</v-timeline-item> </v-timeline-item>
<!-- Exercise item --> <!-- Exercise item -->
<v-timeline-item <v-timeline-item
v-if="exercise.available"
:dot-color="exercise.solved ? 'green' : 'primary'" :dot-color="exercise.solved ? 'green' : 'primary'"
:icon="exercise.solved ? 'mdi-check' : 'mdi-pencil'" :icon="exercise.solved ? 'mdi-check' : 'mdi-pencil'"
> >
<!-- Right side --> <!-- Right side -->
<card-view <card-view
:title="$t('help.scoreBoard.exerciseNr', [exercise.exerciseGroup.groupNr, exercise.exerciseNr]) + :title="
getNameLanguage(exercise)" $t('help.scoreBoard.exerciseNr', [
exercise.exerciseGroup.groupNr,
exercise.exerciseNr,
]) + getExerciseNameLanguage(exercise)
"
:color="exercise.solved ? 'green' : 'primary'" :color="exercise.solved ? 'green' : 'primary'"
> >
{{ getDescriptionLanguage(exercise) }} {{ getExerciseDescriptionLanguage(exercise) }}
</card-view> </card-view>
</v-timeline-item> </v-timeline-item>
</template> </template>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import actionDialog from "@/components/basics/actionDialog.vue";
import OutlinedButton from "@/components/basics/outlinedButton.vue";
import {
getExerciseGroupNameLanguage,
getExerciseNameLanguage,
} from "@/scripts/languageScripts";
import { useExerciseStore } from "@/stores/exercise.store";
import { usePreferencesStore } from "@/stores/preferences.store";
import { ModelRef } from "vue";
const showDialog: ModelRef<boolean> = defineModel();
const exerciseStore = useExerciseStore();
const preferencesStore = usePreferencesStore();
function saveConfig() {
preferencesStore.notAvailableExercises = []
for (let exercise of exerciseStore.exercises) {
if (!exercise.available) {
preferencesStore.notAvailableExercises.push(exercise.uuid)
}
}
showDialog.value = false
}
</script>
<template>
<action-dialog
v-model="showDialog"
:title="$t('preferences.exercises.edit')"
icon="mdi-pencil"
width="800"
persistent
>
<v-container>
<v-list>
<div v-for="exercise in exerciseStore.exercises">
<div
v-if="exercise.exerciseNr == 1"
>
<v-divider v-if="exercise.exerciseGroup.groupNr != 0"></v-divider>
<v-list-item
type="subheader"
:title="getExerciseGroupNameLanguage(exercise.exerciseGroup)"
/>
</div>
<v-list-item>
<v-checkbox
:label="getExerciseNameLanguage(exercise)"
v-model="exercise.available"
hide-details
density="compact"
/>
</v-list-item>
</div>
</v-list>
</v-container>
<template #actions>
<outlined-button color="warning" prepend-icon="mdi-close" @click="showDialog = false">
{{ $t('misc.actions.cancel') }}
</outlined-button>
<outlined-button color="success" prepend-icon="mdi-content-save" @click="saveConfig()">
{{ $t("misc.actions.save") }}
</outlined-button>
</template>
</action-dialog>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import cardView from "@/components/basics/cardView.vue";
import OutlinedButton from "@/components/basics/outlinedButton.vue";
import { ExerciseGroupModel } from "@/data/models/exercises/exerciseGroupModel";
import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { getExerciseGroupNameLanguage } from "@/scripts/languageScripts";
import { useExerciseStore } from "@/stores/exercise.store";
import { ref, watch } from "vue";
import exerciseDialog from "./exerciseDialog.vue";
const exerciseStore = useExerciseStore();
const exerciseGroups = ref<Array<ExerciseGroupModel>>([]);
const showExerciseDialog = ref(false);
exerciseStore.getAllExercises();
/**
* Extract exercise groups from all exercises
*/
function groupExercises() {
exerciseStore.exercises.forEach((exercise) => {
if (
!exerciseGroups.value.find(
(exerciseGroup) => exerciseGroup.id == exercise.exerciseGroup.id
)
) {
exerciseGroups.value.push(exercise.exerciseGroup);
}
});
}
function filterByExerciseGroup(
exercises: Array<ExerciseModel>,
group: ExerciseGroupModel
) {
return exercises.filter((exercise) => exercise.exerciseGroup.id == group.id);
}
watch(
() => exerciseStore.exercises,
() => groupExercises()
);
</script>
<template>
<card-view
:title="$t('preferences.exercises.settings')"
icon="mdi-book-open-blank-variant"
>
<template #borderless>
<v-list>
<v-list-item
v-for="group in exerciseGroups"
:title="getExerciseGroupNameLanguage(group)"
:subtitle="
$t('preferences.exercises.available', [
filterByExerciseGroup(exerciseStore.exercises, group).filter((exercise) => exercise.available)
.length,
filterByExerciseGroup(exerciseStore.exercises, group).length,
])
"
:prepend-icon="group.icon"
/>
</v-list>
</template>
<template #actions>
<outlined-button prepend-icon="mdi-pencil" @click="showExerciseDialog = true">
{{ $t("preferences.exercises.edit") }}
</outlined-button>
</template>
</card-view>
<exercise-dialog v-model="showExerciseDialog" />
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import OutlinedButton from "@/components/basics/outlinedButton.vue";
</script>
<template>
<card-view
:title="$t('preferences.importExport.title')"
icon="mdi-swap-horizontal-bold"
>
<v-row>
<v-col>
<v-file-input
:label="$t('preferences.importExport.selectConfigFile')"
variant="outlined"
accept=".json"
hide-details
/>
</v-col>
</v-row>
<template #actions>
<outlined-button prepend-icon="mdi-export">
{{ $t("preferences.importExport.download") }}
</outlined-button>
<outlined-button prepend-icon="mdi-upload" color="green">
{{ $t("preferences.importExport.upload") }}
</outlined-button>
</template>
</card-view>
</template>

View File

@@ -2,16 +2,29 @@
import pageSetup from './pageSetupSection.vue'; import pageSetup from './pageSetupSection.vue';
import systemSetup from './systemSetupSection.vue'; import systemSetup from './systemSetupSection.vue';
import aboutSection from './aboutSection.vue'; import aboutSection from './aboutSection.vue';
import exerciseSection from './exerciseConfig/exerciseSection.vue';
import importExportSection from './importExportSection.vue';
</script> </script>
<template> <template>
<v-container max-width="800"> <v-container max-width="800">
<v-row>
<v-col>
<import-export-section />
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<page-setup /> <page-setup />
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col>
<exercise-section />
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<system-setup /> <system-setup />

View File

@@ -1,27 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import cardView from '@/components/basics/cardView.vue'; import cardView from "@/components/basics/cardView.vue";
import outlinedButton from '@/components/basics/outlinedButton.vue'; import outlinedButton from "@/components/basics/outlinedButton.vue";
import confirmDialog from '@/components/basics/confirmDialog.vue'; import confirmDialog from "@/components/basics/confirmDialog.vue";
import { ServerStateEnum } from '@/data/enums/serverStateEnum'; import { ServerStateEnum } from "@/data/enums/serverStateEnum";
import { usePreferencesStore } from '@/stores/preferences.store'; import { usePreferencesStore } from "@/stores/preferences.store";
import ServerStateText from '@/components/pageParts/serverStateText.vue'; import ServerStateText from "@/components/pageParts/serverStateText.vue";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
const preferenceStore = usePreferencesStore() const preferenceStore = usePreferencesStore();
const router = useRouter() const router = useRouter();
preferenceStore.getServerState() preferenceStore.getServerState();
</script> </script>
<template> <template>
<card-view <card-view :title="$t('preferences.systemSetup')" icon="mdi-engine">
:title="$t('preferences.systemSetup')"
icon="mdi-engine"
>
<template #borderless> <template #borderless>
<v-list> <v-list>
<v-list-item class="text-h6 text-center"> <v-list-item class="text-h6 text-center">
{{ $t('preferences.serverState') + ':' }} <server-state-text /> {{ $t("preferences.serverState") + ":" }} <server-state-text />
</v-list-item> </v-list-item>
<v-list-item class="text-center"> <v-list-item class="text-center">
@@ -29,9 +26,12 @@ preferenceStore.getServerState()
@click="preferenceStore.showDeleteDbDialog = true" @click="preferenceStore.showDeleteDbDialog = true"
prepend-icon="mdi-database-refresh" prepend-icon="mdi-database-refresh"
color="warning" color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress" :disabled="
preferenceStore.serverState != ServerStateEnum.ONLINE ||
preferenceStore.fetchInProgress
"
> >
{{ $t('preferences.resetDatabase.resetDatabase') }} {{ $t("preferences.resetDatabase.resetDatabase") }}
</outlined-button> </outlined-button>
</v-list-item> </v-list-item>
@@ -40,9 +40,12 @@ preferenceStore.getServerState()
@click="preferenceStore.showDeleteExerciseDialog = true" @click="preferenceStore.showDeleteExerciseDialog = true"
prepend-icon="mdi-progress-close" prepend-icon="mdi-progress-close"
color="warning" color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress" :disabled="
preferenceStore.serverState != ServerStateEnum.ONLINE ||
preferenceStore.fetchInProgress
"
> >
{{ $t('preferences.resetExerciseProgress.resetExerciseProgress') }} {{ $t("preferences.resetExerciseProgress.resetExerciseProgress") }}
</outlined-button> </outlined-button>
</v-list-item> </v-list-item>
@@ -51,9 +54,12 @@ preferenceStore.getServerState()
@click="preferenceStore.showFactoryResetDialog = true" @click="preferenceStore.showFactoryResetDialog = true"
prepend-icon="mdi-factory" prepend-icon="mdi-factory"
color="warning" color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress" :disabled="
preferenceStore.serverState != ServerStateEnum.ONLINE ||
preferenceStore.fetchInProgress
"
> >
{{ $t('preferences.factoryReset.factoryReset') }} {{ $t("preferences.factoryReset.factoryReset") }}
</outlined-button> </outlined-button>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -85,10 +91,12 @@ preferenceStore.getServerState()
:description="$t('preferences.factoryReset.dialog.description')" :description="$t('preferences.factoryReset.dialog.description')"
v-model="preferenceStore.showFactoryResetDialog" v-model="preferenceStore.showFactoryResetDialog"
icon="mdi-factory" icon="mdi-factory"
:onConfirm="() => { :onConfirm="
preferenceStore.resetToFactorySettings() () => {
router.push('/') preferenceStore.resetToFactorySettings();
}" router.push('/');
}
"
:loading="preferenceStore.fetchInProgress" :loading="preferenceStore.fetchInProgress"
/> />
</template> </template>

View File

@@ -20,3 +20,19 @@ export function getSeatColor(surcharge: number, state: number): string {
case 2: return "orange" case 2: return "orange"
} }
} }
/**
* Get color of exercise group
*
* @param exerciseGroupNr Number of exercise group
*
* @returns Color as string
*/
export function getExerciseDotColor(exerciseGroupNr: number) {
switch(exerciseGroupNr) {
case 0: return "purple"
case 1: return "orange"
case 2: return "blue"
case 3: return "pink"
}
}

View File

@@ -0,0 +1,37 @@
import { LanguageEnum } from "@/data/enums/languageEnum";
import { ExerciseGroupModel } from "@/data/models/exercises/exerciseGroupModel";
import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { usePreferencesStore } from "@/stores/preferences.store";
export function getExerciseNameLanguage(exercise: ExerciseModel) {
let preferencesStore = usePreferencesStore()
switch (preferencesStore.language) {
case LanguageEnum.GERMAN:
return exercise.nameDe;
case LanguageEnum.ENGLISH:
return exercise.nameEn;
}
}
export function getExerciseDescriptionLanguage(exercise: ExerciseModel) {
let preferencesStore = usePreferencesStore()
switch (preferencesStore.language) {
case LanguageEnum.GERMAN:
return exercise.descriptionDe;
case LanguageEnum.ENGLISH:
return exercise.descriptionEn;
}
}
export function getExerciseGroupNameLanguage(exerciseGroup: ExerciseGroupModel) {
let preferencesStore = usePreferencesStore()
switch (preferencesStore.language) {
case LanguageEnum.GERMAN:
return exerciseGroup.nameDe;
case LanguageEnum.ENGLISH:
return exerciseGroup.nameEn;
}
}

View File

@@ -4,6 +4,7 @@ import { ref } from "vue";
import { useFeedbackStore } from "./feedback.store"; import { useFeedbackStore } from "./feedback.store";
import { BannerStateEnum } from "@/data/enums/bannerStateEnum"; import { BannerStateEnum } from "@/data/enums/bannerStateEnum";
import { ExerciseModel } from "@/data/models/exercises/exerciseModel"; import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { usePreferencesStore } from "./preferences.store";
export const useExerciseStore = defineStore("exerciseStore", { export const useExerciseStore = defineStore("exerciseStore", {
state: () => ({ state: () => ({
@@ -13,23 +14,45 @@ export const useExerciseStore = defineStore("exerciseStore", {
/** Request to server sent, waiting for data response */ /** Request to server sent, waiting for data response */
fetchInProgress: ref(false), fetchInProgress: ref(false),
helpPageVisible: ref(false) exercisePageVisible: ref(false),
/** All available exercise uuids are stored here */
exerciseConfig: ref<Array<string>>(),
}), }),
actions: { actions: {
/** /**
* Get all exercises and exercise groups from server * Get all exercises and exercise groups from server
*
* @param firstLoad True sets all exercises as available, for first load after database was initialised
*/ */
async getAllExercises() { async getAllExercises(firstLoad: boolean = false) {
this.fetchInProgress = true const preferencesStore = usePreferencesStore();
this.fetchInProgress = true;
await fetchAllExerciseGroups() await fetchAllExerciseGroups().then((result) => {
.then(result => { this.exercises = result.data;
this.exercises = result.data
this.helpPageVisible = this.getExercise(1, 1).solved if (firstLoad) {
this.fetchInProgress = false preferencesStore.notAvailableExercises = []
}) }
result.data.forEach((exercise) => {
if (firstLoad) {
exercise.available = true
} else {
exercise.available =
preferencesStore.notAvailableExercises.find(
(availableExercise: string) => {
return availableExercise == exercise.uuid;
}
) == undefined;
}
});
this.helpPageVisible = this.getExercise(1, 1).solved;
this.fetchInProgress = false;
});
}, },
/** /**
@@ -42,8 +65,11 @@ export const useExerciseStore = defineStore("exerciseStore", {
*/ */
getExercise(exerciseGroupNr: number, exerciseNr: number): ExerciseModel { getExercise(exerciseGroupNr: number, exerciseNr: number): ExerciseModel {
return this.exercises.find((exercise: ExerciseModel) => { return this.exercises.find((exercise: ExerciseModel) => {
return exercise.exerciseNr == exerciseNr && exercise.exerciseGroup.groupNr == exerciseGroupNr return (
}) exercise.exerciseNr == exerciseNr &&
exercise.exerciseGroup.groupNr == exerciseGroupNr
);
});
}, },
/** /**
@@ -54,24 +80,28 @@ export const useExerciseStore = defineStore("exerciseStore", {
*/ */
async solveExercise(exerciseGroupNr: number, exerciseNr: number) { async solveExercise(exerciseGroupNr: number, exerciseNr: number) {
// Request all exercises from server // Request all exercises from server
await this.getAllExercises() await this.getAllExercises();
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore();
this.fetchInProgress = true this.fetchInProgress = true;
// Change only if the exercise is not solved // Change only if the exercise is not solved
updateExercise(exerciseGroupNr, exerciseNr, true) updateExercise(exerciseGroupNr, exerciseNr, true).then((result) => {
.then(result => {
if (result.data.changed) { if (result.data.changed) {
let bannerState = BannerStateEnum.ERROR;
let bannerState = BannerStateEnum.ERROR
switch (exerciseGroupNr) { switch (exerciseGroupNr) {
case 0: { case 0: {
switch (exerciseNr) { switch (exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED01; break; case 1:
case 2: bannerState = BannerStateEnum.EXERCISESOLVED02; break; bannerState = BannerStateEnum.EXERCISESOLVED01;
case 3: bannerState = BannerStateEnum.EXERCISESOLVED03; break; break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED02;
break;
case 3:
bannerState = BannerStateEnum.EXERCISESOLVED03;
break;
} }
break; break;
@@ -79,8 +109,12 @@ export const useExerciseStore = defineStore("exerciseStore", {
case 1: { case 1: {
switch (exerciseNr) { switch (exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED11; break; case 1:
case 2: bannerState = BannerStateEnum.EXERCISESOLVED12; break; bannerState = BannerStateEnum.EXERCISESOLVED11;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED12;
break;
} }
break; break;
@@ -88,12 +122,24 @@ export const useExerciseStore = defineStore("exerciseStore", {
case 2: { case 2: {
switch (exerciseNr) { switch (exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED21; break; case 1:
case 2: bannerState = BannerStateEnum.EXERCISESOLVED22; break; bannerState = BannerStateEnum.EXERCISESOLVED21;
case 3: bannerState = BannerStateEnum.EXERCISESOLVED23; break; break;
case 4: bannerState = BannerStateEnum.EXERCISESOLVED24; break; case 2:
case 5: bannerState = BannerStateEnum.EXERCISESOLVED25; break; bannerState = BannerStateEnum.EXERCISESOLVED22;
case 6: bannerState = BannerStateEnum.EXERCISESOLVED26; break; break;
case 3:
bannerState = BannerStateEnum.EXERCISESOLVED23;
break;
case 4:
bannerState = BannerStateEnum.EXERCISESOLVED24;
break;
case 5:
bannerState = BannerStateEnum.EXERCISESOLVED25;
break;
case 6:
bannerState = BannerStateEnum.EXERCISESOLVED26;
break;
} }
break; break;
@@ -101,18 +147,22 @@ export const useExerciseStore = defineStore("exerciseStore", {
case 3: { case 3: {
switch (exerciseNr) { switch (exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED31; break; case 1:
case 2: bannerState = BannerStateEnum.EXERCISESOLVED32; break; bannerState = BannerStateEnum.EXERCISESOLVED31;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED32;
break;
} }
break; break;
} }
} }
feedbackStore.addSnackbar(bannerState) feedbackStore.addSnackbar(bannerState);
this.getAllExercises() this.getAllExercises();
} }
}) });
} },
} },
}) });

View File

@@ -3,7 +3,11 @@ import { useLocalStorage } from "@vueuse/core";
import { ThemeEnum } from "../data/enums/themeEnums"; import { ThemeEnum } from "../data/enums/themeEnums";
import { LanguageEnum } from "../data/enums/languageEnum"; import { LanguageEnum } from "../data/enums/languageEnum";
import { ref } from "vue"; import { ref } from "vue";
import { fetchServerState,resetDatabase, resetExerciseProgress } from "@/data/api/mainApi"; import {
fetchServerState,
resetDatabase,
resetExerciseProgress,
} from "@/data/api/mainApi";
import { ServerStateEnum } from "@/data/enums/serverStateEnum"; import { ServerStateEnum } from "@/data/enums/serverStateEnum";
import { BannerStateEnum } from "@/data/enums/bannerStateEnum"; import { BannerStateEnum } from "@/data/enums/bannerStateEnum";
import { useFeedbackStore } from "./feedback.store"; import { useFeedbackStore } from "./feedback.store";
@@ -12,13 +16,19 @@ import { useExerciseStore } from "./exercise.store";
import { useAccountStore } from "./account.store"; import { useAccountStore } from "./account.store";
import { AccountApiModel } from "@/data/models/user/accountApiModel"; import { AccountApiModel } from "@/data/models/user/accountApiModel";
export const usePreferencesStore = defineStore('preferencesStore', { export const usePreferencesStore = defineStore("preferencesStore", {
state: () => ({ state: () => ({
/** Selected theme by user */ /** Selected theme by user */
theme: useLocalStorage<ThemeEnum>("eventMaster/preferencesStore/theme", ThemeEnum.DARK), theme: useLocalStorage<ThemeEnum>(
"eventMaster/preferencesStore/theme",
ThemeEnum.DARK
),
/** Selected language by user */ /** Selected language by user */
language: useLocalStorage<LanguageEnum>("eventMaster/preferencesStore/language", LanguageEnum.GERMAN), language: useLocalStorage<LanguageEnum>(
"eventMaster/preferencesStore/language",
LanguageEnum.GERMAN
),
/** Request to server sent, waiting for data response */ /** Request to server sent, waiting for data response */
fetchInProgress: ref(false), fetchInProgress: ref(false),
@@ -36,13 +46,27 @@ export const usePreferencesStore = defineStore('preferencesStore', {
showFactoryResetDialog: ref(false), showFactoryResetDialog: ref(false),
/** Marks the first run of the app */ /** Marks the first run of the app */
firstStartup: useLocalStorage<Boolean>("eventMaster/preferencesStore/firstStartup", true), firstStartup: useLocalStorage<Boolean>(
"eventMaster/preferencesStore/firstStartup",
true
),
/** Full name of student */ /** Full name of student */
studentName: useLocalStorage<string>("eventMaster/preferencesStore/studentName", ""), studentName: useLocalStorage<string>(
"eventMaster/preferencesStore/studentName",
""
),
/** Matrikel number */ /** Matrikel number */
registrationNumber: useLocalStorage<string>("eventMaster/preferencesStore/registrationNumber", "") registrationNumber: useLocalStorage<string>(
"eventMaster/preferencesStore/registrationNumber",
""
),
notAvailableExercises: useLocalStorage<Array<string>>(
"eventMaster/preferencesStore/notAvailableExercises",
[]
),
}), }),
actions: { actions: {
@@ -50,92 +74,90 @@ export const usePreferencesStore = defineStore('preferencesStore', {
* Request the state of the backend server * Request the state of the backend server
*/ */
async getServerState() { async getServerState() {
this.fetchInProgress = true this.fetchInProgress = true;
fetchServerState() fetchServerState()
.then(result => { .then((result) => {
if (result.status == 200) { if (result.status == 200) {
this.serverState = ServerStateEnum.ONLINE this.serverState = ServerStateEnum.ONLINE;
} else { } else {
this.serverState = ServerStateEnum.OFFLINE this.serverState = ServerStateEnum.OFFLINE;
} }
this.fetchInProgress = false this.fetchInProgress = false;
})
.catch(error => {
this.serverState = ServerStateEnum.OFFLINE
this.fetchInProgress = false
}) })
.catch((error) => {
this.serverState = ServerStateEnum.OFFLINE;
this.fetchInProgress = false;
});
}, },
/** /**
* Resets the database (without exercise tables) * Resets the database (without exercise tables)
*/ */
async resetDb() { async resetDb() {
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore();
const accountStore = useAccountStore() const accountStore = useAccountStore();
this.serverState = ServerStateEnum.PENDING this.serverState = ServerStateEnum.PENDING;
this.fetchInProgress = true this.fetchInProgress = true;
// Logout user // Logout user
accountStore.logout() accountStore.logout();
await resetDatabase() await resetDatabase().then((result) => {
.then(result => {
if (result.status == 200) { if (result.status == 200) {
feedbackStore.addSnackbar(BannerStateEnum.DATABASERESETSUCCESSFUL) feedbackStore.addSnackbar(BannerStateEnum.DATABASERESETSUCCESSFUL);
this.serverState = ServerStateEnum.ONLINE this.serverState = ServerStateEnum.ONLINE;
} }
this.fetchInProgress = false this.fetchInProgress = false;
this.showDeleteDbDialog = false this.showDeleteDbDialog = false;
}) });
}, },
/** /**
* Resets the exercise progress * Resets the exercise progress
*/ */
async resetExerciseProg() { async resetExerciseProg() {
const feedbackStore = useFeedbackStore() const feedbackStore = useFeedbackStore();
const exerciseStore = useExerciseStore() const exerciseStore = useExerciseStore();
this.serverState = ServerStateEnum.PENDING this.serverState = ServerStateEnum.PENDING;
this.fetchInProgress = true this.fetchInProgress = true;
await resetExerciseProgress() await resetExerciseProgress().then((result) => {
.then(result => {
if (result.status == 200) { if (result.status == 200) {
feedbackStore.addSnackbar(BannerStateEnum.EXERCISEPROGRESSRESETSUCCESSFUL) feedbackStore.addSnackbar(
this.serverState = ServerStateEnum.ONLINE BannerStateEnum.EXERCISEPROGRESSRESETSUCCESSFUL
);
this.serverState = ServerStateEnum.ONLINE;
exerciseStore.getAllExercises() exerciseStore.getAllExercises(true);
} }
this.fetchInProgress = false this.fetchInProgress = false;
this.showDeleteExerciseDialog = false this.showDeleteExerciseDialog = false;
}) });
}, },
/** /**
* Reset all store values to factory state * Reset all store values to factory state
*/ */
resetToFactorySettings() { resetToFactorySettings() {
const basketStore = useBasketStore() const basketStore = useBasketStore();
const accountStore = useAccountStore() const accountStore = useAccountStore();
this.firstStartup = true this.firstStartup = true;
this.studentName = "" this.studentName = "";
this.registrationNumber = "" this.registrationNumber = "";
this.theme = "dark" this.theme = "dark";
this.language = LanguageEnum.GERMAN this.language = LanguageEnum.GERMAN;
basketStore.itemsInBasket = [] basketStore.itemsInBasket = [];
accountStore.userAccountToken = "" accountStore.userAccountToken = "";
accountStore.userAccount = new AccountApiModel() accountStore.userAccount = new AccountApiModel();
this.showFactoryResetDialog = false;
},
this.showFactoryResetDialog = false },
} });
}
})