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?",
"exercises": [
{
"uuid": "getting-known-register",
"nameDe": "Registrieren",
"nameEn": "Register",
"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."
},
{
"uuid": "getting-known-profile",
"nameDe": "Profil vervollständigen",
"nameEn": "Complete profile",
"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."
},
{
"uuid": "getting-known-buy-ticket",
"nameDe": "Ein Ticket kaufen",
"nameEn": "Buy a ticket",
"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.",
"exercises": [
{
"uuid": "broken-access-control-exercise-page",
"nameDe": "Hilfe-Seite aufrufen",
"nameEn": "Access Help Page",
"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."
},
{
"uuid": "broken-access-control-hidden-concert",
"nameDe": "Das versteckte Konzert buchen",
"nameEn": "Book the hidden concert",
"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.",
"exercises": [
{
"uuid": "sql-injection-database-scheme",
"nameDe": "Wie sieht die Datenbank aus?",
"nameEn": "How does the database look like?",
"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."
},
{
"uuid": "sql-injection-all-accounts",
"nameDe": "Alle Accounts ausspähen",
"nameEn": "Get all accounts",
"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."
},
{
"uuid": "sql-injection-account-roles",
"nameDe": "Alle Berechtigungsgruppen ausspähen",
"nameEn": "Get all account roles",
"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."
},
{
"uuid": "sql-injection-upgrade-privileges",
"nameDe": "Eigene Berechtigungen erhöhen",
"nameEn": "Upgrade your privileges",
"exerciseNr": 4,
@@ -92,6 +101,7 @@
"descriptionEn": "Change the privileges of your account"
},
{
"uuid": "sql-injection-capture-account",
"nameDe": "Einen fremden Account übernehmen",
"nameEn": "Capture another account",
"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."
},
{
"uuid": "sql-injection-delete-rating",
"nameDe": "Bewertungen löschen",
"nameEn": "Delete ratings",
"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.",
"exercises": [
{
"uuid": "cross-site-scripting-hello-world",
"nameDe": "Hallo Welt!",
"nameEn": "Hello World!",
"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."
},
{
"uuid": "cross-site-scripting-external-script",
"nameDe": "Ein externes Script aufrufen",
"nameEn": "Run an external script",
"exerciseNr": 2,

View File

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

View File

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

View File

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

View File

@@ -168,6 +168,17 @@
"title": "Auf Werkseinstellungen zurücksetzen?",
"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": {

View File

@@ -168,6 +168,19 @@
"title": "Factory reset?",
"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": {

View File

@@ -1,54 +1,34 @@
<script setup lang="ts">
import { useExerciseStore } from '@/stores/exercise.store';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import { generateResultsPdf } from '@/scripts/pdfScripts';
import { usePreferencesStore } from '@/stores/preferences.store';
import cardView from '@/components/basics/cardView.vue';
import { LanguageEnum } from '@/data/enums/languageEnum';
import { ExerciseModel } from '@/data/models/exercises/exerciseModel';
import { useExerciseStore } from "@/stores/exercise.store";
import outlinedButton from "@/components/basics/outlinedButton.vue";
import { generateResultsPdf } from "@/scripts/pdfScripts";
import { usePreferencesStore } from "@/stores/preferences.store";
import cardView from "@/components/basics/cardView.vue";
import { LanguageEnum } from "@/data/enums/languageEnum";
import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { getExerciseDotColor } from "@/scripts/colorScripts";
import { getExerciseDescriptionLanguage, getExerciseNameLanguage } from "@/scripts/languageScripts";
const exerciseStore = useExerciseStore()
const preferencesStore = usePreferencesStore()
const exerciseStore = useExerciseStore();
const preferencesStore = usePreferencesStore();
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"
}
}
// Mark this exercise as solved if page was opened
exerciseStore.solveExercise(1, 1);
function generateExerciseKey() {
try {
let code = ""
let code = "";
for (let i = 0; i < 13; i++) {
if (exerciseStore.exercises[i].solved) {
code += "3"
code += "3";
} else {
code += "0"
code += "0";
}
}
return (Number(code) + Number(preferencesStore.registrationNumber)) * 237
} 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
}
return (Number(code) + Number(preferencesStore.registrationNumber)) * 237;
} catch (e) {}
}
</script>
@@ -61,9 +41,12 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
<outlined-button
prepend-icon="mdi-file-pdf-box"
@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>
</v-col>
</v-row>
@@ -71,7 +54,7 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
<v-row>
<v-col class="text-h5 text-center">
<div>
{{ $t('help.scoreBoard.personalSolutionKey') + ':' }}
{{ $t("help.scoreBoard.personalSolutionKey") + ":" }}
</div>
<div>
{{ generateExerciseKey() }}
@@ -86,49 +69,55 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
icon="mdi-checkbox-marked-circle-auto-outline"
>
<template #borderless>
<v-timeline
side="end"
class="px-5"
align="start"
>
<v-timeline side="end" class="px-5" align="start">
<template v-for="exercise of exerciseStore.exercises">
<!-- Add exercise group description item -->
<v-timeline-item v-if="exercise.exerciseNr == 1"
:dot-color="getDotColor(exercise.exerciseGroup.groupNr)"
<v-timeline-item
v-if="exercise.exerciseNr == 1"
:dot-color="
getExerciseDotColor(exercise.exerciseGroup.groupNr)
"
:icon="exercise.exerciseGroup.icon"
fill-dot
>
<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
? exercise.exerciseGroup.nameDe
: exercise.exerciseGroup.nameEn)
preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.nameDe
: exercise.exerciseGroup.nameEn
}}
</div>
<div>
{{
(preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.descriptionDe
: exercise.exerciseGroup.descriptionEn)
preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.descriptionDe
: exercise.exerciseGroup.descriptionEn
}}
</div>
</v-timeline-item>
<!-- Exercise item -->
<v-timeline-item
v-if="exercise.available"
:dot-color="exercise.solved ? 'green' : 'primary'"
:icon="exercise.solved ? 'mdi-check' : 'mdi-pencil'"
>
<!-- Right side -->
<!-- Right side -->
<card-view
:title="$t('help.scoreBoard.exerciseNr', [exercise.exerciseGroup.groupNr, exercise.exerciseNr]) +
getNameLanguage(exercise)"
:title="
$t('help.scoreBoard.exerciseNr', [
exercise.exerciseGroup.groupNr,
exercise.exerciseNr,
]) + getExerciseNameLanguage(exercise)
"
:color="exercise.solved ? 'green' : 'primary'"
>
{{ getDescriptionLanguage(exercise) }}
{{ getExerciseDescriptionLanguage(exercise) }}
</card-view>
</v-timeline-item>
</template>
@@ -138,4 +127,4 @@ function getDescriptionLanguage(exercise: ExerciseModel) {
</v-col>
</v-row>
</v-container>
</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 systemSetup from './systemSetupSection.vue';
import aboutSection from './aboutSection.vue';
import exerciseSection from './exerciseConfig/exerciseSection.vue';
import importExportSection from './importExportSection.vue';
</script>
<template>
<v-container max-width="800">
<v-row>
<v-col>
<import-export-section />
</v-col>
</v-row>
<v-row>
<v-col>
<page-setup />
</v-col>
</v-row>
<v-row>
<v-col>
<exercise-section />
</v-col>
</v-row>
<v-row>
<v-col>
<system-setup />

View File

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

View File

@@ -19,4 +19,20 @@ export function getSeatColor(surcharge: number, state: number): string {
case 1: return "grey"
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 { BannerStateEnum } from "@/data/enums/bannerStateEnum";
import { ExerciseModel } from "@/data/models/exercises/exerciseModel";
import { usePreferencesStore } from "./preferences.store";
export const useExerciseStore = defineStore("exerciseStore", {
state: () => ({
@@ -13,106 +14,155 @@ export const useExerciseStore = defineStore("exerciseStore", {
/** Request to server sent, waiting for data response */
fetchInProgress: ref(false),
helpPageVisible: ref(false)
exercisePageVisible: ref(false),
/** All available exercise uuids are stored here */
exerciseConfig: ref<Array<string>>(),
}),
actions: {
/**
* 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() {
this.fetchInProgress = true
async getAllExercises(firstLoad: boolean = false) {
const preferencesStore = usePreferencesStore();
this.fetchInProgress = true;
await fetchAllExerciseGroups()
.then(result => {
this.exercises = result.data
await fetchAllExerciseGroups().then((result) => {
this.exercises = result.data;
this.helpPageVisible = this.getExercise(1, 1).solved
this.fetchInProgress = false
})
if (firstLoad) {
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;
});
},
/**
* Get a exercise by group and exercise number
*
*
* @param exerciseGroupNr Number of group of exercise
* @param exerciseNr Number of exercise in group
*
*
* @returns ExerciseModel
*/
getExercise(exerciseGroupNr: number, exerciseNr: number): ExerciseModel {
return this.exercises.find((exercise: ExerciseModel) => {
return exercise.exerciseNr == exerciseNr && exercise.exerciseGroup.groupNr == exerciseGroupNr
})
return (
exercise.exerciseNr == exerciseNr &&
exercise.exerciseGroup.groupNr == exerciseGroupNr
);
});
},
/**
* Mark an exercise as solved
*
*
* @param exerciseGroupNr Exercise group number (0-3)
* @param exerciseNr Exercise number (1-3)
*/
async solveExercise(exerciseGroupNr: number, exerciseNr: number) {
// Request all exercises from server
await this.getAllExercises()
await this.getAllExercises();
const feedbackStore = useFeedbackStore()
this.fetchInProgress = true
const feedbackStore = useFeedbackStore();
this.fetchInProgress = true;
// Change only if the exercise is not solved
updateExercise(exerciseGroupNr, exerciseNr, true)
.then(result => {
if (result.data.changed) {
updateExercise(exerciseGroupNr, exerciseNr, true).then((result) => {
if (result.data.changed) {
let bannerState = BannerStateEnum.ERROR;
let bannerState = BannerStateEnum.ERROR
switch(exerciseGroupNr) {
case 0: {
switch(exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED01; break;
case 2: bannerState = BannerStateEnum.EXERCISESOLVED02; break;
case 3: bannerState = BannerStateEnum.EXERCISESOLVED03; break;
}
break;
switch (exerciseGroupNr) {
case 0: {
switch (exerciseNr) {
case 1:
bannerState = BannerStateEnum.EXERCISESOLVED01;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED02;
break;
case 3:
bannerState = BannerStateEnum.EXERCISESOLVED03;
break;
}
case 1: {
switch(exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED11; break;
case 2: bannerState = BannerStateEnum.EXERCISESOLVED12; break;
}
break;
}
case 2: {
switch(exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED21; break;
case 2: bannerState = BannerStateEnum.EXERCISESOLVED22; 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;
}
case 3: {
switch(exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED31; break;
case 2: bannerState = BannerStateEnum.EXERCISESOLVED32; break;
}
break;
}
break;
}
feedbackStore.addSnackbar(bannerState)
this.getAllExercises()
case 1: {
switch (exerciseNr) {
case 1:
bannerState = BannerStateEnum.EXERCISESOLVED11;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED12;
break;
}
break;
}
case 2: {
switch (exerciseNr) {
case 1:
bannerState = BannerStateEnum.EXERCISESOLVED21;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED22;
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;
}
case 3: {
switch (exerciseNr) {
case 1:
bannerState = BannerStateEnum.EXERCISESOLVED31;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED32;
break;
}
break;
}
}
})
}
}
})
feedbackStore.addSnackbar(bannerState);
this.getAllExercises();
}
});
},
},
});

View File

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