174 Commits

Author SHA1 Message Date
7dc392631d Readme 2025-01-16 10:29:04 +01:00
98f8e0b79b Documentation, upgrade version 2025-01-16 10:23:17 +01:00
4c31ccd021 Backend documentation 2024-12-30 23:13:41 +01:00
57d35a01ba Bugfix search page (Band) 2024-12-21 21:05:06 +01:00
6f6efa5886 More swagger documentation 2024-12-13 16:24:19 +01:00
4498c865f2 Swagger API documentation 2024-12-09 19:06:50 +01:00
a58adbcc8d Bugfix file manager 2024-12-05 12:29:05 +01:00
8a18b95031 Redesign account pages, split payments and addresses to single pages, new dashboard 2024-11-29 13:38:20 +01:00
c867d9d51f Add hint for inputs in Welcome Dialog 2024-11-29 11:34:35 +01:00
4905ef607b Remove test script 2024-11-28 19:48:05 +01:00
2c18c59f51 Rewrite solution code generation 2024-11-28 19:46:02 +01:00
d622fda7a9 Finish order admin page with detail dialog and actions 2024-11-28 13:10:54 +01:00
831a667a27 More server stability 2024-11-28 12:29:36 +01:00
947ed225b6 Remove Super-Admin role, bugfix if user enters buggy SQL injection on search field 2024-11-27 19:29:03 +01:00
b74da2dc3b Implement solution codes for exercises 2024-11-27 18:32:10 +01:00
da484b08a9 Adding new SQL-Injection exercise 2024-11-27 14:21:14 +01:00
22d7b2a11d Light mode improvements, empty state on search page 2024-11-26 20:58:10 +01:00
ac21e8d607 Global color schema, remove name and register number access in preferences, add alert in first startup wizard 2024-11-26 20:13:54 +01:00
7c78226864 More feedback on account creation, bugfix on account creation 2024-11-26 19:39:40 +01:00
fa2c7f2e8b Add more license information of images 2024-11-26 12:51:23 +01:00
07f486c72e Improve exercise solution of 2.1, 2.2, 2.3 and 2.5 2024-11-26 12:40:01 +01:00
b5364639a5 Adding "Test Environment" banner in the bottom right corner 2024-11-26 11:59:37 +01:00
2e15d4a960 License system implemented 2024-11-25 18:55:28 +01:00
1b0f48d374 First steps of license store system 2024-11-23 17:45:14 +01:00
b3ebbff732 Changelog 2024-11-21 09:42:53 +01:00
e8b50da142 Minor bugfixes, new logo 2024-11-20 19:20:22 +01:00
57358fa818 Implementing exercise 3.2 2024-11-20 14:07:41 +01:00
366f1060e3 Exercise 3.1 fully implemented 2024-11-20 12:27:41 +01:00
9d41a14926 Exercise 2.5 added 2024-11-20 11:34:44 +01:00
d4fbda26d7 Password encryption 2024-11-20 10:43:48 +01:00
e02f2d252e Developer window in Electron app, remove edit/delete options in Admin panel (currently not completed) 2024-11-19 18:41:26 +01:00
1dc5740f03 Move software files one directory up, Readme 2024-11-19 16:51:28 +01:00
baf763c4cb Rewrite SQL Injection exercises, change code for search field, exercises 0.1 - 3.1 implemented 2024-11-19 12:25:30 +01:00
7fdecdbc5d Implement exercise 2.1 2024-11-18 16:12:58 +01:00
f2bb4b6954 Add exercise group descriptions to help page, restructure timeline 2024-11-18 16:07:51 +01:00
2d77a793d8 Exercisegroup 0 and 1 complete implemented 2024-11-18 15:41:33 +01:00
7912e38932 Working on exercise 1.2 2024-11-16 16:56:20 +01:00
70e508ce7a Bugfix order process/account update 2024-11-15 11:36:17 +01:00
86acedc8aa Authentification Token 2024-11-14 15:32:28 +01:00
ae577dc023 Improve Snackbar 2024-11-14 09:33:45 +01:00
c0d110f386 Implement exercise 1.1 (open help page) 2024-11-14 08:09:06 +01:00
2b14f76d4b Improve UI for smaller screens 2024-11-14 07:25:06 +01:00
0911cdbe5c Rewrite database access for exercises 2024-11-13 13:56:44 +01:00
24f44e73f4 Redesign file browser, file upload (server) 2024-11-12 23:50:21 +01:00
e690fb984d Bugfix database creation 2024-11-12 05:09:54 +01:00
b97cc1af20 OrderDetailDialog 2024-11-11 08:44:17 +01:00
1b85d0eca9 Documentation 2024-11-11 08:15:21 +01:00
c58be89104 Admin Order Page, refresh ERM diagram of database 2024-11-09 15:19:08 +01:00
b84d542352 PDF Generator for Exercise progress 2024-11-08 20:02:37 +01:00
be1bc85f40 First startup dialog, factory reset 2024-11-08 13:45:09 +01:00
0a1d85a0fe Reduce DB creation time to 5,0 seconds 2024-11-07 19:29:59 +01:00
d10f84750c Massive improvement of database creation time (63s -> 7s) 2024-11-07 19:18:50 +01:00
47e045dde3 OrderStore 2024-11-07 17:18:49 +01:00
844898bb3c Docstrings, AdminDashboard button loading, new BannerStateEnums 2024-11-06 18:22:03 +01:00
9ec8e382cf Finish GenresAdminPanel 2024-11-06 17:55:18 +01:00
80dd2a0ae8 Startup dialog on first run 2024-11-06 16:45:36 +01:00
b7eca62403 File browser on admin page 2024-11-05 19:54:14 +01:00
ce097e2098 Error page 2024-11-05 18:43:47 +01:00
c3d0cc2879 Account itself deletable 2024-11-04 19:05:14 +01:00
ffccd9f2d4 LocationDetailPage: Seat not selectable, loading animation during fetching 2024-11-04 18:15:49 +01:00
a3e8d30b6c Small improvements on account pages, added payment info card 2024-11-04 12:04:15 +01:00
3359400494 Split concertsPage 2024-11-03 17:38:14 +01:00
8da0f01699 Split home page in sections 2024-11-03 17:30:07 +01:00
16fd40f11d Electron & Electron Builder 2024-11-02 18:09:44 +01:00
80a4dcfdc9 Redesign Band highlight section on HomePage, fix Footer spacing bug 2024-11-01 12:52:17 +01:00
ba4f4720d2 Brand logo 2024-11-01 11:06:10 +01:00
9052c59254 New "about" section on PreferencesPage, add new exercise descriptions/steps 2024-11-01 03:56:38 +01:00
b6205f374b Bugfix Carousel on HomePage 2024-11-01 03:08:52 +01:00
82cd2d0e98 License 2024-10-31 22:10:31 +01:00
0f78d3eecc AccountsAdminPage, ConcertsAdminPage 2024-10-30 05:58:53 +01:00
012c544bde Account Dashboard Card 2024-10-27 00:12:27 +02:00
33342345f9 LocationsAdminPage data table 2024-10-26 23:52:38 +02:00
c570a04052 Genre Admin page, new Genre store 2024-10-26 16:23:00 +02:00
cdb3f02156 Creating Band edit page 2024-10-26 14:35:33 +02:00
fedb821a72 Restructure translation files 2024-10-25 13:05:51 +02:00
10d6142fff Reactivate Admin Panel, implement exercise 2.1, add feedback if exercise solved 2024-10-24 20:23:28 +02:00
8de3ca481a Exercise store, mark exercise 0.2 as solved on ticket buy 2024-10-23 13:57:37 +02:00
3faa89a749 Display free tickets in standing areas 2024-10-23 13:36:21 +02:00
1f0933e2a9 Reimplement Carousel on homepage 2024-10-22 21:06:00 +02:00
b1eea15bac Filterbar on Concert page 2024-10-22 20:26:35 +02:00
70227329f2 Band filter by genre 2024-10-22 20:11:09 +02:00
386800f31a Enhance search for band names of concerts 2024-10-22 19:16:30 +02:00
36f87bdbd3 Slight changes on TicketListItem append section 2024-10-22 19:08:52 +02:00
4e6be355ea Streamlined stores 2024-10-22 18:47:27 +02:00
3e53a606a6 Slight changes seat rows on locations 2024-10-22 12:19:15 +02:00
40161a136a Fix order process 2024-10-21 14:57:03 +02:00
7880a444b1 Fixed Account pages 2024-10-21 14:02:51 +02:00
59470f5396 Add addressbar 2024-10-17 19:10:21 +02:00
41106a8686 Update configuration 2024-10-17 18:42:35 +02:00
c8d87f6643 Remove EventModel in frontend 2024-10-12 21:00:42 +02:00
6c33de3d87 New page for all concerts 2024-10-12 19:40:12 +02:00
f8bdb54c33 Simplified json import 2024-10-12 16:22:14 +02:00
203f8428a7 Remove EventTable in database, redesign frontend URL paths 2024-10-12 15:54:03 +02:00
1d4daac9ae Finish search page 2024-10-11 18:59:19 +02:00
0ec11aacf7 Atomize model classes 2024-10-11 17:42:21 +02:00
cfb8fb9d7d Implement global search 2024-10-11 12:59:21 +02:00
49b436d588 UI Bugfixes 2024-10-10 19:23:13 +02:00
913e067ad2 Improve UI of concertListItem and eventListItem 2024-10-10 18:43:38 +02:00
4b745eef99 Bugfixes, reset exercise progress from frontend 2024-10-10 14:29:45 +02:00
58c7282701 Redesign seat map layout 2024-10-10 14:11:09 +02:00
fe2cea5529 Add more locations, add all location images, adjust seatGroups in locations 2024-10-08 20:04:56 +02:00
41a7cbc9da Implement URL XSS attack 2024-10-08 14:30:39 +02:00
3dd7b1d4c6 Implementing Exercise system in database with API and frontend visualization 2024-10-07 13:15:16 +02:00
4b2764e33c Ticket Component 2024-10-06 19:30:12 +02:00
10c0d0838f Redesign and reimplementing account page 2024-10-05 21:00:39 +02:00
d1cdf1f8fb Tickets moveable to basket, Basket shows tickets, removable 2024-10-05 20:23:13 +02:00
e48782b897 SeatSelection page 2024-10-04 20:15:16 +02:00
8165f17fc8 More skeleton loader, repair bandDetailPage 2024-10-04 18:01:37 +02:00
bfffd72a4a More skelton loader, add optional parameters to /locations and /events 2024-10-04 15:20:40 +02:00
e2f6fb9c52 Skeleton loader 2024-10-04 13:16:05 +02:00
17e6b08129 Loading feedback with indeterminate circle 2024-10-03 20:19:03 +02:00
67ed71858c Improve filterBar on eventsPage, improve API access from frontend 2024-10-03 19:56:44 +02:00
14766fb39b Start moving data server handling from pinia store to server 2024-10-03 19:03:36 +02:00
e177cf53e6 Add more artist images 2024-10-03 16:51:30 +02:00
bd53b8edfc Adding Seat plan component and database tables 2024-10-01 15:37:08 +02:00
d6997229c4 New locationDetailPage displays concert in a location, new datasets, images, URL path changed 2024-09-29 21:42:20 +02:00
be5cc090fe Split band detail page in section files 2024-09-29 18:43:37 +02:00
907e0082e1 Creating whole Band details page 2024-09-29 16:56:43 +02:00
a6ca7eedde Filterable tours 2024-09-29 14:28:29 +02:00
0616409f14 Display concerts with card views on "All concerts" page, adding image property for tours 2024-09-28 21:18:25 +02:00
8d0b141217 Display all bands grouped by genre, create m:n association between Band and Genre in database 2024-09-27 23:25:24 +02:00
513c73c5c1 Location page displays city groups with all available concert locations 2024-09-27 20:40:59 +02:00
ef0c48ae17 Redesign home page 2024-09-27 15:52:22 +02:00
c6c8cf0ae8 Move Navigation from NavDrawer to AppBar, redesign page structure and routes 2024-09-27 13:08:43 +02:00
e2dd49e21b Refactor frontend, display tours with cards on ToursPage 2024-09-26 16:06:20 +02:00
169fcdf03c Add ToursTable, update API documentation 2024-09-26 14:40:41 +02:00
da98fc73c0 Rewriting database and API to transform to a ticket shop 2024-09-26 11:04:27 +02:00
d36dbced8e Data tables for brand and category added to admin pages 2024-09-25 15:54:45 +02:00
0856540441 Creating admin pages, new brand API endpoint 2024-09-25 15:42:05 +02:00
6dd49f630d Connect Orders database table with Payments and Addresses, visualize it in the frontend 2024-09-24 23:41:35 +02:00
531f964841 Better validation on text fields, change AlertBanner to Snackbar 2024-09-24 22:18:27 +02:00
3dc4c7af1e Payments and Addresses add- and removeable 2024-09-24 20:53:46 +02:00
fd4c1d5a65 Add more user feedback (loading buttons, empty states) 2024-09-24 16:36:30 +02:00
8329a6ae09 Implement ordering process 2024-09-24 15:40:16 +02:00
5b8f1fbead Multiple addresses & payments of an account configurable in frontend 2024-09-24 13:55:48 +02:00
abe1b496a2 Matching UI with improved API 2024-09-24 13:12:44 +02:00
bc62174566 Readme 2024-09-23 21:27:39 +02:00
b245e3803a Extend database with more tables, rewrite API doc, improve API endpoints 2024-09-23 21:22:45 +02:00
8b4db9ccc8 Add Score board page to visualize progress of exercises 2024-09-22 21:29:23 +02:00
564cf144ff Confirm dialog, fix language change bug, add bank accout information to users 2024-09-22 20:57:28 +02:00
d7eae540b1 New cardView component, add database reset confirm dialog 2024-09-22 15:46:33 +02:00
47fbb564b2 ProductCard redesigned, add property for number of items in stock for product 2024-09-22 15:06:10 +02:00
3863661b3c New products, productDetail dialog offers a specs category 2024-09-20 23:20:03 +02:00
89e91c3213 Fix order view 2024-09-20 15:30:40 +02:00
718dbe30b7 Redesign productDetail dialog 2024-09-20 15:08:17 +02:00
54d13686cf Improved user feedback system, improved Product detail view 2024-09-20 12:57:19 +02:00
ed264ff026 Move banner system to store, migrate login/register API handling to own file, display Account details on accountPage 2024-09-19 16:20:12 +02:00
5b3a753233 Readme 2024-09-19 15:04:09 +02:00
7b245da959 CategoryStore, API calls 2024-09-18 16:21:07 +02:00
9ee344f45f ProductStore, move API calls to separate file 2024-09-18 15:59:16 +02:00
2847bd940f Documenting, restructure and expand API 2024-09-13 12:07:33 +02:00
39ce77ea63 Setting fo ri18n-ally, progress stepper on order page 2024-09-12 18:38:56 +02:00
8594763fa6 Create OrdersPage, load orders from backend, move NavDrawer items to Component 2024-09-11 20:49:55 +02:00
55fd203c7f Account login possible 2024-09-11 14:54:33 +02:00
fd06b8a9a4 User registration completed 2024-09-10 20:28:24 +02:00
f6e4bfdf2f Add dialog to create new user 2024-09-10 18:50:47 +02:00
767269a7cf Moving image source from web to backend 2024-09-10 16:45:49 +02:00
f10c0ef4e9 Translation 2024-09-09 20:55:09 +02:00
7ebc3c1c77 New basket table, add empty state on basket page, new BasketItemModel 2024-09-09 19:47:46 +02:00
6ff577ece1 Store products in a basket, display list of products in basket 2024-09-09 14:33:29 +02:00
2d0dc274bf Moving exmple datasets to json files 2024-09-09 13:45:26 +02:00
20e8ce1024 Working on product detail dialog 2024-09-08 20:35:10 +02:00
f82f6ce9af Add empty state on productsPage 2024-09-08 19:20:54 +02:00
c18f0e0484 Product images 2024-09-06 18:49:22 +02:00
6af4388671 Filter products by category and discount, sort by price and name 2024-09-06 18:26:34 +02:00
babf1c77ce Create product cards, display all on products page 2024-09-06 17:10:21 +02:00
22cc811ae5 Login form 2024-09-05 18:12:08 +02:00
4d1eec023d VueRouter, Pinia state management, add pages, design preferences page 2024-09-05 17:47:02 +02:00
8b1a396f68 Fix db relations, add sample data, show categories in frontend menu 2024-09-05 16:19:23 +02:00
cfd1d29302 Add all database tables with relations 2024-09-04 17:06:41 +02:00
7338bb216a Add SQLite database to backend, interacting with the frontend 2024-09-04 16:42:37 +02:00
8af11151d3 Create a test backend server with ExpressJs 2024-09-03 19:10:18 +02:00
d4be64a0e9 Add Vuetify and basic layout for test purposes 2024-09-03 13:54:19 +02:00
e607a09631 Add Vue.js with TypeScript support 2024-09-03 13:37:46 +02:00
60 changed files with 659 additions and 1400 deletions

View File

@@ -11,6 +11,5 @@
"i18n-ally.enabledFrameworks": [
"vue"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.extract.autoDetect": true,
"i18n-ally.keystyle": "nested"
}

View File

@@ -1,28 +1,11 @@
# v.0.4.0 (2025-08-30)
## 🚀 Features
- Exercise selection system
## 🌟 Enhancements
- Tooltips on append icons in toolbar
- Exercise page icon adds open exercises badge
- Finished english translation
- Icons on exercise groups on help page
- Welcome dialog: New page for look and feel, merge database and exercise creation in one step
- Add links to GitHub and project page on settings
# v.0.3.0 (2025-02-28)
# v.0.3.0 (Release Candidate 1)
## 🚀 Features
- Swagger Documentation
- RPM Image
## 🐛 Bugfixes
- Bugfix on search page for Band datasets
# v.0.2.0 (2024-12-05)
# v.0.2.0 (Beta)
## 🚀 Features
- Adding "Test Environment" banner in the bottom right corner
- License handling system
@@ -41,7 +24,8 @@
- More server stability
- Bugfix file manager in Electron application
# v.0.1.0 (2024-11-21)
# v.0.1.0 (Alpha)
## 🚀 Features
- Frontend
- VueJS frontend framework with Vuetify UI library

122
README.md
View File

@@ -1,116 +1,30 @@
<p align="center">
<img src="misc/images/logo.png" width="300" />
</p>
# EventMaster
<h1 align="center">EventMaster</h1>
<h2 align="center">The most hackable Ticket-Shop!</h2>
![Hompage](/misc/images/homepage.png)
## About the project
EventMaster is a desktop application for simulating various hacking methods for web applications. It's the perfect tool to understand vulnerabilities in modern web applications.
The shop offers tickets for concerts of popular bands with a complete ordering system, account management and admin interface. The program has been deliberately provided with vulnerabilities for teaching purposes, e.g. SQL injections. If a task is solved and a vulnerability is identified, the system provides visual feedback. The progress can be viewed on an overview page.
This project is created during my Bachelor thesis. It will be used at Leibniz University Hannover for foundation lectures.
## Important notes!
- ⚠️ The project is under development, bugs may occur
- ⚠️ The software is developed for educational purposes, don't use the attack techniques without permission on other applications and systems!
- ⚠️ Works 100% offline, no data transmission to or from external services
## Features
- 13 exercises
- 3 attack techniques
- SQL-Injections
- Cross-Site-Scripting
- Broken Access Control
- Exercise control system - check the progress and get feedback for solved exercises!
- Full working ticket shop:
- Vuetify design
- Account management system
- Order process system
- Event locations with seat plan and seat reservation system during ordering process
- Global search
- Admin panel
- Product pages
- ExpressJs-Backend server
- REST-API
- SQLite Database
- URL simulation bar for electron application
- Works 100% offline
- Multi language support (German, English)
- Dark/Light-Mode
- Exercise progress PDF generation
- API-Documentation with Swagger
- Desktop application packed with Electron
- Database can be resetted complete or partially
### Techstack
- Axios
- Electron
- Electron Builder
- Express.js
- JsonWebToken
- jspdf
- Moment.js
- NPM
- Nodemon
- Pinia
- Sequelize
- SQLite 3
- Swagger
- TypeScript
- Vite
- Vue i18n
- Vue.js
- VueRouter
- Vuetify
### Planned features
- More attack techniques
- More exercises
- More languages
- Customize the offered set of exercises (for trainer)
- Image license view system (currently only in admin panel)
### Screenshots
#### Band Detail page
![Band Detail page](/misc/images/band-detail-seite.png)
#### Booking page
![Booking page](/misc/images/booking-concert.png)
#### Admin panel
![Admin panel](/misc/images/admin-panel.png)
#### Global search
![Global search](/misc/images/search.png)
#### Feedback on exercise solution
![Snackbar Feedback](/misc/images/snackbar-feedback.png)
The most hackable Ticket-Shop!
## How to use
### Download pre-builded images
### Prepare development environment
Go to releases and download one of the pre-builded images for your operating system.
1. Install node.js
### Build yourself
```bash
sudo apt install npm
1. Download + extract the project
2. Open the root folder with VS Code (recommended)
3. Open the bash inside VS Code and install all necessary packages:
# If outdated version:
sudo npm install -g n
sudo n stable
```
2. Download + extract the project
3. Open the root folder with VS Code (recommended)
4. Open the bash inside VS Code and install all necessary packages:
```bash
npm i
```
#### Test/development
### Test/development
There are multiple commands to test parts or the whole project:
@@ -120,7 +34,7 @@ There are multiple commands to test parts or the whole project:
The frontend runs on `http://localhost:5173/` and the backend on `http://localhost:3000/`
#### Build
### Build
- `npm run vite:build`: Build Vue frontend only
- `npm run server:build`: Build ExpressJs backend only
@@ -133,7 +47,3 @@ The frontend runs on `http://localhost:5173/` and the backend on `http://localho
### Database
![database-erm](misc/images/database.png)
### Frontend-Backend-System
![frontend-backend-system](misc/images/frontend-backend-system.png)

View File

@@ -3,144 +3,127 @@
{
"nameDe": "Den Shop kennenlernen",
"nameEn": "Getting to know the shop",
"icon": "mdi-human-greeting",
"groupNr": 0,
"descriptionDe": "Vor einem Angriff ist es wichtig zu verstehen, wie die Webseite aufgebaut ist. Wie sind die URLs strukturiert? Wo befinden sich Eingabefelder welche im Backend eine SQL Abfrage stellen?",
"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": "todo",
"exercises": [
{
"uuid": "getting-known-register",
"nameDe": "Registrieren",
"nameEn": "Register",
"exerciseNr": 1,
"descriptionDe": "Wir richten uns einen gewöhnlichen Account auf der Plattform ein. Navigiere hierzu auf die Account-Seite und registriere dich.",
"descriptionEn": "We'll set up a regular account on the platform. To do this, navigate to the account page and register."
"descriptionEn": "Create a new account in the online shop"
},
{
"uuid": "getting-known-profile",
"nameDe": "Profil vervollständigen",
"nameEn": "Complete profile",
"exerciseNr": 2,
"descriptionDe": "Bestellungen sind erst möglich, wenn das Account-Profil vervollständigt ist. Logge dich ein, navigiere zu den Account-Einstellungen, fülle den Namen aus und füge je eine Adresse und Bezahlart hinzu. Speichere alles zum Schluss ab.",
"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": "Search for an event of choice and buy a ticket for"
},
{
"uuid": "getting-known-buy-ticket",
"nameDe": "Ein Ticket kaufen",
"nameEn": "Buy a ticket",
"exerciseNr": 3,
"descriptionDe": "Wir führen nun einen Bestellvorgang durch. Wähle hierzu ein Konzert deiner Wahl und lege Tickets in den Warenkorb. Öffne diesen und schließe die Bestellung ab. Beachte die Struktur der URL wenn du ein Konzert buchen willst. Sieh dir ruhig 2-3 Buchungsseiten an, wie sich die URL jeweils verändert.",
"descriptionEn": "We'll now complete the order process. Select a concert of your choice and add tickets to your shopping cart. Open the shopping cart and complete your order. Pay attention to the URL structure when booking a concert. Feel free to look at two or three booking pages to see how the URL changes each time."
"descriptionEn": "Search for an event of choice and buy a ticket for"
}
]
},
{
"nameDe": "Broken Access Control",
"nameEn": "Broken Access Control",
"icon": "mdi-application-outline",
"groupNr": 1,
"descriptionDe": "Eine Webseite beinhaltet öffentlich einsehbare und einige geschützte Seiten. Letztere sind nur mit passenden Berechtigungen erreichbar. Beispiele hierfür sind ein Admin-Panel oder der persönliche Warenkorb. Der Zugriff wird oft über Cookies oder eine Authentifizierung an einem Backend-Server geregelt. Bei Broken Access Control ist dieser Sicherheits-Mechanismus nicht oder fehlerhaft implementiert. Somit lassen sich Seiten unberechtigterweise über die URL erreichen.",
"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": "todo",
"exercises": [
{
"uuid": "broken-access-control-exercise-page",
"nameDe": "Hilfe-Seite aufrufen",
"nameEn": "Access Help Page",
"exerciseNr": 1,
"descriptionDe": "Die Hilfe-Seite erlaubt dir einen Einblick auf den Bearbeitungszustand der Aufgaben. Sie ist dementsprechend nicht abgesichert, aber auch (noch) nicht in der Titel-Leiste als Button erreichbar. Erweitere die URL in der Adresszeile so, dass du auf die Hilfeseite gelangst.",
"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": "Manipulate the URL and access the help page"
},
{
"uuid": "broken-access-control-hidden-concert",
"nameDe": "Das versteckte Konzert buchen",
"nameEn": "Book the hidden concert",
"exerciseNr": 2,
"descriptionDe": "Die Band >>Arctic Monkeys<< will auf ihrer >>European Tour<< drei Konzerte spielen. Im Shop finden sich allerdings nur zwei Einträge. Zwischen den beiden Tourdaten soll eine Show in der Lanxess Arena in Köln stattfinden, der Datensatz hierfür ist bereits angelegt, jedoch nicht freigeschaltet. Besuche die Seite der Band. Sieh dir den Zeitraum zwischen beiden Konzerten an, in denen das versteckte Event liegen könnte. Öffne eine Buchungsseite eines anderen Konzertes und ändere die URL so ab, dass du das versteckte Konzert buchen kannst. Reserviere dir mindestens ein Ticket und schließe den Bestellprozess ab.",
"descriptionEn": "The band >>Arctic Monkeys<< plans to play three shows on their >>European Tour<<. However, there are only two entries in the shop. A show at the Lanxess Arena in Cologne is scheduled to take place between the two tour dates. The dataset for this has already been created but is not yet activated. Visit the band's website. Look at the time period between the two concerts, where the hidden event could take place. Open a booking page for another concert and change the URL so that you can book the hidden concert. Reserve at least one ticket and complete the order process."
"descriptionEn": "Manipulate the URL and access the sold out concert and buy a ticket"
}
]
},
{
"nameDe": "SQL Injections",
"nameEn": "SQL Injections",
"icon": "mdi-needle",
"groupNr": 2,
"descriptionDe": "Eine Datenbank arbeitet mit SQL Befehlen um Datensätze anzulegen, abzurufen, zu verändern und löschen. Ein Server wird über API-Schnittstellen angesprochen, führt die Befehle in der Datenbank aus und liefert das Ergebnis zurück. Der Client darf keinen direkten Zugriff auf die Datenbank haben. Bei SQL Injections wird versucht, diesen Sicherheitsmechanismus zu umgehen und über die API-Schnittstellen direkte SQL Befehle auszuführen.",
"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": "todo",
"exercises": [
{
"uuid": "sql-injection-database-scheme",
"nameDe": "Wie sieht die Datenbank aus?",
"nameEn": "How does the database look like?",
"exerciseNr": 1,
"descriptionDe": "Wir versuchen nun die Datenbank im Hintergrund anzugreifen. Aktuell wissen wir aber noch nicht wie die Datenbank aussieht, also welche Tabellen sie beinhaltet. Wir können uns aber mit einem SQL-Befehl ausgeben. Gehe zur globalen Suchseite. Öffne mit der Tastenkombination >>Strg<< + >>D<< die >>Developer Tools<<. Klicke auf den Reiter >>Network<<. Hier siehst du, wie das Frontend mit dem Server kommuniziert. Schreibe nun eine SQL-Injection, welche den Suchbegriff ignoriert und dir stattdessen alle Datensätze der Tabelle >>sqlite_master<< zurück gibt, sofern die Bedingung >>type='table'<< erfüllt ist. Kopiere dir bei erfolgreicher Rückmeldung des Backends die Namen der Tabellen in eine Text-Datei, damit wir für die kommenden Aufgaben die richtigen Namen der Tabellen angeben können.",
"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": "todo"
},
{
"uuid": "sql-injection-all-accounts",
"nameDe": "Alle Accounts ausspähen",
"nameEn": "Get all accounts",
"exerciseNr": 2,
"descriptionDe": "Schreibe nun eine SQL-Injection, welche den Suchbegriff ignoriert und dir stattdessen alle Datensätze der Account-Tabelle zurück liefert. Führe den Angriff über das Suchfeld aus. Sieh dir die Rückmeldung des Servers an.",
"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": "Execute an SQL-Injection on the Search page to get all datasets from >>Accounts<< table."
},
{
"uuid": "sql-injection-account-roles",
"nameDe": "Alle Berechtigungsgruppen ausspähen",
"nameEn": "Get all account roles",
"exerciseNr": 3,
"descriptionDe": "Wir sehen nun alle Accounts. Jeder hat eine Berechtigungs-ID (accountRoleId) mit der Berechtigungen wie der Zugriff aufs Admin-Panel geregelt werden. Wir wissen aber nicht, was die ID's bedeuten. Schreibe darum eine SQL-Injection, welche den Suchbegriff ignoriert und dir stattdessen alle Datensätze der Tabelle >>AccountRoles<< zurück liefert. Führe den Angriff über das Suchfeld aus. Beobachte die Rückmeldung des Servers über den >>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."
"descriptionEn": "Execute an SQL-Injection on the Search page to get all datasets from >>AccountRoles<< table."
},
{
"uuid": "sql-injection-upgrade-privileges",
"nameDe": "Eigene Berechtigungen erhöhen",
"nameEn": "Upgrade your privileges",
"exerciseNr": 4,
"descriptionDe": "Jetzt bearbeiten wir unseren eigenen Account. Schreibe hierfür einen >>UPDATE<<-SQL-Befehl, welcher die >>accountRoleId<< auf das Niveau eines >>Admin<< erhöht für deinen Account-Namen.",
"descriptionEn": "Now we'll edit our own account. To do this, write an >>UPDATE<< SQL command that elevates the >>accountRoleId<< to the level of >>Admin<< for your account name."
"descriptionEn": "Change the privileges of your account"
},
{
"uuid": "sql-injection-capture-account",
"nameDe": "Einen fremden Account übernehmen",
"nameEn": "Capture another account",
"exerciseNr": 5,
"descriptionDe": "Statt unsere eigenen Berechtigungen zu erhöhen, können wir auch einen Account übernehmen, welcher bereits ein >>Super-Admin<< ist. Suche dir dafür aus der Liste der in Aufgabe 2.1 erhaltenen Accounts einen aus, welcher die Rolle >>Super-Admin<< inne hat. Nur damit lässt sich die Dateiverwaltung öffnen, welche wir später brauchen. Hast du den Account-Namen gefunden, gehe ins Login-Menü (logge dich aus, falls du noch angemeldet bist). Führe nun einen SQL-Injektion durch um diesen Account zu übernehmen.",
"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": "todo"
},
{
"uuid": "sql-injection-delete-rating",
"nameDe": "Bewertungen löschen",
"nameEn": "Delete ratings",
"exerciseNr": 6,
"descriptionDe": "Jede Band hat Bewertungen auf einer Skala von eins bis fünf Sternen erhalten. Wir wollen alle Fünf-Sterne Bewertungen aus der Datenbank löschen. Schreibe eine SQL Injection, welche in der Bewertungs-Tabelle alle Einträge mit der Bedingung >>rating = 5<< entfernt. Führe die Injection über die globale Suche aus.",
"descriptionEn": "Each band has received ratings on a scale of one to five stars. We want to delete all five-star ratings from the database. Write an SQL injection that removes all entries in the ratings table with the condition >>rating = 5<<. Execute the injection via the global search."
"descriptionEn": "todo"
}
]
},
{
"nameDe": "Cross-Site Scripting (XSS)",
"nameEn": "Cross-Site Scripting (XSS)",
"icon": "mdi-code-brackets",
"groupNr": 3,
"descriptionDe": "Als nächstes wollen wir Schadcode in die Web-Applikation einschleusen. Zunächst testen wir, ob die Webseite hierfür anfällig ist. Manipuliere die URL der Band-Seite so, dass du eine >>Hallo Welt!<<-Nachricht als >>alert<< siehst. Hinweis: Nutze einen image tag! Setze als >>src<< die Zahl >>1<<. Den Befehl kannst du im Tag >>onerror<< ausführen.",
"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": "todo",
"exercises": [
{
"uuid": "cross-site-scripting-hello-world",
"nameDe": "Hallo Welt!",
"nameEn": "Hello World!",
"exerciseNr": 1,
"descriptionDe": "Als nächstes wollen wir Schadcode in die Web-Applikation einschleusen. Zunächst testen wir, ob die Webseite hierfür anfällig ist. Gehe hierzu auf die Seite >>Alle Bands<< und filtere die Einträge nach einem beliebigen Genre deiner Wahl. In der URL-Leiste siehst du nun, dass hinter der URL und dem Ressourcen-Ziel ein Parameter angegeben ist (der Part hinter dem Fragezeichen). Wir tauschen diesen Parameter gegen einen HTML Tag aus. Der Trick hierbei: Als Quelle geben wir den Zahlenwert >>1<< an, wodurch automatisch das ausgeführt wird, was im >>onerror<<-Tag drinnen steht. Genau hier soll eine Alert-Meldung mit >>Hello World!<< als JavaScript Code eingefügt werden. Verändere die URL so, dass sie die Meldung ausgibt. Falls du nicht mit JavaScript vertraut bist, sieh dir die letzte Seite mit nützlichen Befehlen an.",
"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": "Take an URL of the shop and extend it with JavaScript code so that a 'Hello World' message appears whent the link is opened"
},
{
"uuid": "cross-site-scripting-external-script",
"nameDe": "Ein externes Script aufrufen",
"nameEn": "Run an external script",
"exerciseNr": 2,
"descriptionDe": "Wir haben festgestellt, dass die Seite für Cross-Site-Scripting durch Reflected XSS angreifbar ist! Im zweiten Schritt binden wir nun das Script ein. Es wurde bereits auf den Server hochgeladen. Logge dich wahlweise mit einem Admin-Account (Aufgabe 2.5) oder deinem eigenen nun berechtigten Account (Aufgabe 2.4) ein. Öffne nun das Admin-Panel über den Button rechts oben. Suche über die Dateiverwaltung im Admin-Panel nach dem Skript und notiere dir die darunter angezeigte Adresse auf dem Backend-Server. Logge dich aus. Wir wollen das Skript auf der nun sichtbaren Login-Seite über eine veränderte URL einbinden. Nutze hierfür das gleiche Prinzip wie in Aufgabe 3.1. Statt >>genreName<< kannst du einen beliebigen anderen Parameter-Namen verwenden. Nutze die Konsole mit der Tastenkombination Strg + D vor dem Abschicken der URL.Logge dich nach erfolgreicher Aufgabenlösung ein und sieh in der Konsole, wie deine Login-Daten abgegriffen werden.",
"descriptionEn": "We've determined that the page is vulnerable to cross-site scripting through Reflected XSS! In the second step, we'll integrate the script. It's already been uploaded to the server. Log in either with an admin account (Task 2.5) or your own, now authorized account (Task 2.4). Now open the admin panel using the button in the top right. Search for the script using the file manager in the admin panel and note the address displayed below it on the backend server. Log out. We want to integrate the script into the now visible login page using a modified URL. Use the same principle as in Task 3.1. Instead of >>genreName<<, you can use any other parameter name. Use the console by pressing Ctrl + D before submitting the URL. After successfully completing the task, log in and watch in the console how your login data is being retrieved."
"descriptionEn": "Create an URL of the shop, which calls the script"
}
]
}

View File

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

View File

@@ -9,9 +9,6 @@ export class ExerciseGroup extends Model {
@Column
nameEn: string
@Column
icon: string
@Column
groupNr: number

View File

@@ -36,7 +36,7 @@ app.use("/files", files)
// Add delay for more realistic response times
app.use((req, res, next) => {
setTimeout(next, Math.floor((Math.random() * 500) + 100))
setTimeout(next, Math.floor((Math.random() * 1000) + 100))
})
// Routes

View File

@@ -19,7 +19,7 @@
"icon": "public/logo-small.png"
},
"linux": {
"target": ["deb", "rpm"],
"target": ["deb"],
"maintainer": "Tobias Zoghaib",
"icon": "public/logo-small.png",
"category": "Education"
@@ -35,8 +35,6 @@
"!release",
"!src",
"!dist",
"!out",
"!misc",
"!database.sqlite"
"!out"
]
}

View File

@@ -1,19 +0,0 @@
{
"theme": "dark",
"language": "en",
"notAvailableExercises": [
"getting-known-register",
"getting-known-profile",
"getting-known-buy-ticket",
"broken-access-control-exercise-page",
"broken-access-control-hidden-concert",
"sql-injection-database-schema",
"sql-injection-all-accounts",
"sql-injection-account-roles",
"sql-injection-upgrade-privileges",
"sql-injection-capture-account",
"sql-injection-delete-rating",
"cross-site-scripting-hello-world",
"cross-site-scripting-external-script"
]
}

View File

@@ -1,84 +0,0 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.0.9 Chrome/128.0.6613.186 Electron/32.2.5 Safari/537.36" version="26.0.9">
<diagram name="Page-1" id="6ideKt-3XiqBCAVgAsyQ">
<mxGraphModel dx="1195" dy="689" 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>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="KDqfIFqn06qLZiIO6-10-49" value="Frontend" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="90" width="400" height="220" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-52" value="Subpage&lt;div&gt;&quot;All Bands&quot;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="50" y="120" width="150" height="180" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-50" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="630" y="90" width="210" height="280" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-55" value="API Endpoint" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="650" y="120" width="170" height="100" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-46" value="bandStore" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="270" y="120" width="160" height="160" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#00CC00;" parent="1" source="KDqfIFqn06qLZiIO6-10-47" target="KDqfIFqn06qLZiIO6-10-65" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-71" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#00CC00;" parent="1" source="KDqfIFqn06qLZiIO6-10-47" target="KDqfIFqn06qLZiIO6-10-66" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-73" value="&lt;font color=&quot;#00cc00&quot;&gt;Handles&lt;/font&gt;&lt;div&gt;&lt;font color=&quot;#00cc00&quot;&gt;changes&lt;/font&gt;&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#00CC00;" parent="1" source="KDqfIFqn06qLZiIO6-10-47" target="KDqfIFqn06qLZiIO6-10-72" edge="1">
<mxGeometry x="0.168" y="24" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-47" value="bands" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="290" y="230" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-59" value="&lt;div style=&quot;&quot;&gt;&lt;font style=&quot;color: rgb(204, 0, 0);&quot;&gt;Stores&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;&quot;&gt;&lt;font style=&quot;color: rgb(204, 0, 0);&quot;&gt;results&lt;/font&gt;&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;align=left;fillColor=#e51400;strokeColor=#B20000;" parent="1" source="KDqfIFqn06qLZiIO6-10-48" target="KDqfIFqn06qLZiIO6-10-47" edge="1">
<mxGeometry x="-0.2" y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-67" value="&lt;font style=&quot;color: rgb(0, 29, 188);&quot;&gt;requests&lt;/font&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;verticalAlign=bottom;fillColor=#0050ef;strokeColor=#001DBC;" parent="1" source="KDqfIFqn06qLZiIO6-10-48" target="KDqfIFqn06qLZiIO6-10-56" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-48" value="getBands()" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="290" y="150" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-53" value="&lt;font color=&quot;#bd7000&quot;&gt;Call function&lt;/font&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;verticalAlign=bottom;fillColor=#f0a30a;strokeColor=#BD7000;" parent="1" source="KDqfIFqn06qLZiIO6-10-52" target="KDqfIFqn06qLZiIO6-10-48" edge="1">
<mxGeometry x="-0.2148" y="7" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-68" value="&lt;font style=&quot;color: rgb(0, 87, 0);&quot;&gt;send http code &amp;amp;&lt;/font&gt;&lt;div&gt;&lt;span style=&quot;color: light-dark(rgb(0, 87, 0), rgb(0, 87, 0)); background-color: light-dark(#ffffff, var(--ge-dark-color, #121212));&quot;&gt;data sets&lt;/span&gt;&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;verticalAlign=top;fillColor=#008a00;strokeColor=#005700;" parent="1" source="KDqfIFqn06qLZiIO6-10-56" target="KDqfIFqn06qLZiIO6-10-48" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-56" value="/bands" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="670" y="150" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-63" value="&lt;font style=&quot;color: rgb(0, 153, 153);&quot;&gt;get raw&lt;/font&gt;&lt;div&gt;&lt;font color=&quot;#009999&quot;&gt;datasets&lt;/font&gt;&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.855;exitY=0;exitDx=0;exitDy=4.35;exitPerimeter=0;align=left;strokeColor=#009999;" parent="1" source="KDqfIFqn06qLZiIO6-10-61" edge="1">
<mxGeometry x="-0.2154" y="-9" relative="1" as="geometry">
<mxPoint as="offset" />
<mxPoint x="751" y="180" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-61" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
<mxGeometry x="700" y="280" width="60" height="80" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-62" value="&lt;font style=&quot;color: rgb(255, 0, 255);&quot;&gt;sends&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(255, 0, 255);&quot;&gt;SQL&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(255, 0, 255);&quot;&gt;command&lt;/font&gt;&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.321;exitY=1.017;exitDx=0;exitDy=0;entryX=0.145;entryY=0;entryDx=0;entryDy=4.35;entryPerimeter=0;align=right;strokeColor=#FF00FF;exitPerimeter=0;" parent="1" source="KDqfIFqn06qLZiIO6-10-56" target="KDqfIFqn06qLZiIO6-10-61" edge="1">
<mxGeometry x="0.2159" y="-9" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-65" value="Coldplay" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="65" y="170" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-66" value="Muse" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="65" y="210" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KDqfIFqn06qLZiIO6-10-72" value="Radiohead" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="65" y="250" width="120" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,181 @@
<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="z4ePnmX8JLkG3N1GDPOr">
<mxGraphModel dx="1783" dy="1720" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="Fk8QGSgGJ1bhqam2HO4i-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="Fk8QGSgGJ1bhqam2HO4i-15" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" target="fY-Wzga6seKQwhded4A5-7">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-360" y="700" as="targetPoint" />
<mxPoint x="-720" y="240" as="sourcePoint" />
<Array as="points">
<mxPoint x="-720" y="540" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="Fk8QGSgGJ1bhqam2HO4i-13">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="fY-Wzga6seKQwhded4A5-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-680" y="-20" as="targetPoint" />
<Array as="points">
<mxPoint x="-720" y="-20" />
<mxPoint x="-680" y="-20" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="fY-Wzga6seKQwhded4A5-1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="Fk8QGSgGJ1bhqam2HO4i-14">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-1" target="Fk8QGSgGJ1bhqam2HO4i-4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-1" value="Homepage&lt;div&gt;&lt;font face=&quot;Courier New&quot;&gt;&lt;b&gt;/&lt;/b&gt;&lt;/font&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-800" y="-120" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-2" target="Fk8QGSgGJ1bhqam2HO4i-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-2" target="Fk8QGSgGJ1bhqam2HO4i-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-2" target="Fk8QGSgGJ1bhqam2HO4i-7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;fillColor=#6d8764;strokeColor=#3A5431;" edge="1" parent="1" target="Fk8QGSgGJ1bhqam2HO4i-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-679.6666666666667" y="70" as="targetPoint" />
<mxPoint x="-680" y="-20" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-43" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-2" target="fY-Wzga6seKQwhded4A5-42">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-2" value="AccountHomePage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/account&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-600" y="-40" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-3" target="Fk8QGSgGJ1bhqam2HO4i-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-3" value="BandsPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/bands&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-600" y="280" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-4" value="PreferencesPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/preferences&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-600" y="600" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-5" target="fY-Wzga6seKQwhded4A5-5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-280" y="330" />
<mxPoint x="-280" y="330" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-5" value="BandDetailPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/bands/detail/:bandName&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-360" y="280" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-41" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;fillColor=#e51400;strokeColor=#B20000;" edge="1" parent="1" target="Fk8QGSgGJ1bhqam2HO4i-7">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-200" y="-220" as="targetPoint" />
<mxPoint x="-680" y="-20" as="sourcePoint" />
<Array as="points">
<mxPoint x="-680" y="140" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-7" value="LoginPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/account/login&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-360" y="120" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-8" value="AccountData&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/account/data&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-360" y="40" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-9" value="OrdersPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/account/orders&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-360" y="-40" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-13" value="AdminHome&lt;span style=&quot;background-color: initial;&quot;&gt;Page&lt;/span&gt;&lt;div&gt;&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/admin&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-600" y="760" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-14" value="HelpPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/help&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-600" y="680" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-15" target="Fk8QGSgGJ1bhqam2HO4i-16">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-15" value="LocationsPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/locations&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-600" y="440" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="Fk8QGSgGJ1bhqam2HO4i-16" target="fY-Wzga6seKQwhded4A5-5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-280" y="420" />
<mxPoint x="-280" y="420" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="Fk8QGSgGJ1bhqam2HO4i-16" value="LocationsDetailPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/locations/detail/:name&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-360" y="440" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-30" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="fY-Wzga6seKQwhded4A5-1" target="fY-Wzga6seKQwhded4A5-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-1" value="ConcertsPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/concerts&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-600" y="360" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-3" value="if logged in" style="text;strokeColor=none;align=center;fillColor=none;html=1;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="-680" y="-60" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-4" value="else" style="text;strokeColor=none;align=center;fillColor=none;html=1;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="-690" y="110" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-34" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="fY-Wzga6seKQwhded4A5-5" target="fY-Wzga6seKQwhded4A5-7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-160" y="380" />
<mxPoint x="-160" y="540" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-5" value="ConcertsBookingPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/concerts/booking/:id&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-360" y="360" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-7" value="BasketPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/basket&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-600" y="520" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="fY-Wzga6seKQwhded4A5-8" target="Fk8QGSgGJ1bhqam2HO4i-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-38" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="fY-Wzga6seKQwhded4A5-8" target="fY-Wzga6seKQwhded4A5-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="fY-Wzga6seKQwhded4A5-8" target="Fk8QGSgGJ1bhqam2HO4i-16">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-8" value="SearchPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/search&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-600" y="200" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-14" value="if ticket add to basket" style="text;strokeColor=none;align=center;fillColor=none;html=1;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="-290" y="510" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-29" value="on log out" style="text;strokeColor=none;align=center;fillColor=none;html=1;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="-460" y="90" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="fY-Wzga6seKQwhded4A5-42" value="AccountRatingsPage&lt;div&gt;&lt;b style=&quot;font-family: &amp;quot;Courier New&amp;quot;;&quot;&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;/account/ratings&lt;/font&gt;&lt;/b&gt;&lt;br&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-360" y="-120" width="160" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,10 +1,10 @@
{
"name": "eventmaster",
"version": "0.4.0",
"version": "0.3.0",
"author": "Tobias Zoghaib",
"description": "Hackable ticket store for educational purposes",
"license": "MIT",
"homepage": "https://www.itsec.uni-hannover.de/de/usec/forschung/eventmaster-learning-web-attacks",
"homepage": "www.uni-hannover.de",
"main": "build/src/electron/index.js",
"private": true,
"scripts": {

View File

@@ -3,33 +3,17 @@ defineProps({
/** Displayed smaller text on the left side */
descriptionText: {
type: String,
default: "",
},
loading: {
type: Boolean,
default: false,
default: ""
},
/** Displayed bigger text on the right side */
valueText: [String, Number],
});
valueText: [ String, Number ]
})
</script>
<template>
<v-card variant="outlined" class="my-1 px-2">
<v-row v-if="loading">
<v-col>
<v-skeleton-loader
type="heading"
:loading="loading"
style="background-color: transparent"
>
sdasd
</v-skeleton-loader>
</v-col>
</v-row>
<v-row class="d-flex justify-center align-center" v-else>
<v-row class="d-flex justify-center align-center">
<v-col class="text-caption text-left" v-if="descriptionText.length > 0">
{{ descriptionText }}
</v-col>

View File

@@ -1,122 +1,50 @@
<script setup lang="ts">
import { useAccountStore } from "@/stores/account.store";
import { useBasketStore } from "@/stores/basket.store";
import { useExerciseStore } from "@/stores/exercise.store";
import { ref, watch } from "vue";
import { useAccountStore } from '@/stores/account.store';
import { useBasketStore } from '@/stores/basket.store';
import { useExerciseStore } from '@/stores/exercise.store';
const accountStore = useAccountStore();
const basketStore = useBasketStore();
const exerciseStore = useExerciseStore();
const basketItems = ref(0);
const accountStore = useAccountStore()
const basketStore = useBasketStore()
const exerciseStore = useExerciseStore()
exerciseStore.getAllExercises();
watch(
() => basketStore.itemsInBasket,
() => {
basketItems.value = basketStore.itemsInBasket.reduce((tot, item) => {
return tot + item.seats.length;
}, 0);
}
);
exerciseStore.getAllExercises()
</script>
<template>
<!-- Global search -->
<v-tooltip :text="$t('misc.search.globalsearch')" location="bottom">
<template #activator="{ props }">
<v-btn v-bind="props" variant="plain" icon="mdi-magnify" to="/search" />
</template>
</v-tooltip>
<v-btn variant="plain" icon="mdi-magnify" to="/search" />
<!-- Account -->
<v-tooltip :text="$t('account.account')" location="bottom">
<template #activator="{ props }">
<v-btn
v-if="accountStore.userAccountToken == ''"
v-bind="props"
variant="plain"
icon="mdi-account"
to="/account/login"
/>
<v-btn
v-else
v-bind="props"
variant="plain"
icon="mdi-account-check"
to="/account/home"
/>
</template>
</v-tooltip>
<v-btn v-else variant="plain" icon="mdi-account-check" to="/account/home" />
<!-- Basket -->
<v-tooltip :text="$t('basket.basket')" location="bottom">
<template #activator="{ props }">
<div>
<v-badge
v-if="basketItems > 0"
:content="basketItems"
color="error"
offset-x="8"
offset-y="8"
>
<v-btn v-bind="props" variant="plain" icon="mdi-cart" to="/basket" />
:content="basketStore.itemsInBasket.reduce((tot, item) => {
return tot + item.seats.length
}, 0)"
color="error" offset-x="8" offset-y="8">
<v-btn variant="plain" icon="mdi-cart" to="/basket" />
</v-badge>
</div>
<v-btn
v-else
v-bind="props"
variant="plain"
icon="mdi-cart"
to="/basket"
/>
</template>
</v-tooltip>
<!-- Exercise page -->
<v-tooltip :text="$t('misc.firstStartup.exercises')" location="bottom">
<template #activator="{ props }">
<v-badge
v-if="exerciseStore.exercisePageVisible"
:content="
exerciseStore.exercises.reduce((tot, exercise) => {
if (exercise.available && !exercise.solved) {
return tot + 1;
} else {
return tot;
}
}, 0)
"
color="error"
offset-x="8"
offset-y="8"
>
<v-btn
v-bind="props"
variant="plain"
icon="mdi-book-open-blank-variant"
to="/help"
/>
</v-badge>
</template>
</v-tooltip>
<!-- Admin panel -->
<v-tooltip :text="$t('admin.adminpanel')" location="bottom">
<template #activator="{ props }">
<v-btn
v-if="accountStore.adminPanelVisible"
v-bind="props"
variant="plain"
icon="mdi-table-cog"
to="/admin"
/>
</template>
</v-tooltip>
<v-tooltip :text="$t('preferences.preferences')" location="bottom">
<template #activator="{ props }">
<v-btn v-bind="props" variant="plain" icon="mdi-cog" to="/preferences" />
</template>
</v-tooltip>
<v-btn
v-if="exerciseStore.helpPageVisible"
variant="plain"
icon="mdi-help"
to="/help"
/>
<v-btn variant="plain" icon="mdi-cog" to="/preferences"/>
</template>

View File

@@ -1,27 +1,27 @@
<script setup lang="ts">
import cardViewHorizontal from "@/components/basics/cardViewHorizontal.vue";
import { BandModel } from "@/data/models/acts/bandModel";
import { ConcertModel } from "@/data/models/acts/concertModel";
import { LocationModel } from "@/data/models/locations/locationModel";
import { useRouter } from "vue-router";
import cardViewHorizontal from '@/components/basics/cardViewHorizontal.vue';
import { BandModel } from '@/data/models/acts/bandModel';
import { ConcertModel } from '@/data/models/acts/concertModel';
import { LocationModel } from '@/data/models/locations/locationModel';
import { useRouter } from 'vue-router';
const router = useRouter();
const router = useRouter()
defineProps({
/** Concert to display */
concert: {
type: ConcertModel,
required: true,
required: true
},
band: {
type: BandModel,
required: true,
required: true
},
location: {
type: LocationModel,
required: true,
required: true
},
/** Display text parts as skeleton */
@@ -30,9 +30,9 @@ defineProps({
/** Show or hide the button on the right side */
showButton: {
type: Boolean,
default: true,
},
});
default: true
}
})
</script>
<template>
@@ -40,13 +40,7 @@ defineProps({
:title="concert.name"
v-if="!loading"
:link="showButton && concert.inStock > 0"
@click="console.log(concert.date);
showButton && concert.inStock > 0
? router.push(
'/concerts/booking/' + location.urlName + '/' + concert.date
)
: () => {}
"
@click="showButton && concert.inStock > 0 ? router.push('/concerts/booking/' + location.urlName + '/' + concert.date) : () => {}"
>
<template #prepend>
<div>
@@ -55,9 +49,7 @@ defineProps({
</div>
<div class="text-h6">
{{
new Date(concert.date).toLocaleString("default", { month: "long" })
}}
{{ new Date(concert.date).toLocaleString('default', { month: 'long' }) }}
</div>
<div class="text-h6">
@@ -79,23 +71,28 @@ defineProps({
<template #append>
<div>
<div class="text-secondary font-weight-medium text-h6 pb-1">
{{ $t("misc.from") + " " + concert.price.toFixed(2) + "" }}
{{ $t('misc.from') + ' ' + concert.price.toFixed(2) + '' }}
</div>
<div v-if="concert.inStock == 0 && showButton" class="text-h6">
{{ $t("concert.concertSoldOut") }}
{{ $t('concert.concertSoldOut') }}
</div>
<div v-else-if="showButton">
<v-btn variant="flat" color="secondary">
{{ $t("concert.goToTheConcert") }}
{{ $t('concert.goToTheConcert') }}
</v-btn>
</div>
</div>
</template>
</card-view-horizontal>
<card-view-horizontal v-else :loading="loading">
<v-skeleton-loader type="text" />
<card-view-horizontal
v-else
:loading="loading"
>
<v-skeleton-loader
type="text" />
</card-view-horizontal>
</template>

View File

@@ -2,7 +2,6 @@ export class ExerciseGroupModel {
id = -1
nameDe: string = ""
nameEn: string = ""
icon: string = ""
groupNr: number = 0
descriptionDe: string = ""
descriptionEn: string = ""

View File

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

View File

@@ -97,8 +97,7 @@
"postalCode": "Postleitzahl",
"placeOfResidence": "Wohnort",
"bankName": "Name der Bank",
"iban": "IBAN",
"actions": "Aktionen"
"iban": "IBAN"
},
"deleteAccount": {
"deleteAccount": "Account löschen",
@@ -131,8 +130,7 @@
"noOrdersText": "Bisher wurden keine Bestellungen von diesem Account getätigt. Gehe zum Warenkorb und bestelle!",
"ordersDescription": "Übersicht aller getätigten Bestellungen",
"order": "Bestellung | Bestellungen",
"notShipped": "noch nicht versendet",
"orderState": "Bestellstatus"
"notShipped": "noch nicht versendet"
},
"basket": {
"addToBasket": "Zum Warenkorb hinzufügen",
@@ -168,26 +166,12 @@
"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"
},
"preferences": "Einstellungen"
}
},
"help": {
"scoreBoard": {
"exerciseGroupNr": "Aufgabengruppe {0}: ",
"exerciseNr": "Aufgabe {0}.{1}: ",
"generatePdf": "PDF generieren",
"personalSolutionKey": "Persönlicher Lösungsschlüssel"
"exerciseNr": "Aufgabe {0}.{1}: "
}
},
"bannerMessages": {
@@ -241,7 +225,6 @@
"cancel": "Abbrechen",
"more": "Mehr",
"confirm": "Bestätigen",
"back": "Zurück",
"next": "Weiter"
},
"validation": {
@@ -258,7 +241,7 @@
"firstStartup": {
"title": "Ersteinrichtung",
"description": "Die Datenbank wird eingerichtet. Bitte warten...",
"createDatabase": "Datenbank Einrichtung",
"createDatabase": "Erstelle Datenbank...",
"complete": "Fertig",
"finished": "Abgeschlossen",
"createExercises": "Erstelle Aufgaben...",
@@ -266,7 +249,6 @@
"database": "Datenbank",
"exercises": "Aufgaben",
"userData": "Persönliche Daten",
"lookAndFeel": "Look and feel",
"enterYourPersonalData": "Bitte gebe nun deinen Namen und deine Matrikelnummer von der Universität ein. Überprüfe die Angaben vor dem Absenden genau! Die Angaben können später nicht ohne Verlust des Bearbeitungsfortschrittes geändert werden!"
},
"user": "Angaben zur Person",
@@ -281,25 +263,10 @@
"empty": {
"headline": "So leer hier..."
},
"searchterm": "Suchbegriff",
"globalsearch": "Globale Suche"
},
"submit": "Absenden",
"content": "Inhalt",
"source": "Quelle",
"softwareVersion": "Software Version",
"license": "Lizenz",
"developer": "Entwickler",
"developedFor": "Entwickelt im Auftrag",
"copyright": "Copyright",
"githubRepository": "GitHub Repository",
"projectPage": "Projektseite"
"searchterm": "Suchbegriff"
}
},
"genre": {
"withoutBand": "ohne Band",
"popular": "Beliebte Genres"
},
"admin": {
"adminpanel": "Admin Panel"
"withoutBand": "ohne Band"
}
}

View File

@@ -97,8 +97,7 @@
"postalCode": "Postal code",
"placeOfResidence": "Place of residence",
"bankName": "Name of bank",
"iban": "IBAN",
"actions": "Actions"
"iban": "IBAN"
},
"deleteAccount": {
"deleteAccount": "Delete Account",
@@ -128,11 +127,10 @@
"takeOrder": "Execute order",
"noOrders": "No orders found",
"orderedAt": "Ordered at",
"noOrdersText": "No orders have been placed with this account yet. Go to your shopping cart and place your order!",
"noOrdersText": "Bisher wurden keine Bestellungen von diesem Account getätigt. Gehe zum Warenkorb und bestelle!",
"ordersDescription": "Overview of all placed orders",
"order": "Order | Orders",
"notShipped": "don't shipped",
"orderState": "Order state"
"notShipped": "don't shipped"
},
"basket": {
"addToBasket": "Add to basket",
@@ -168,28 +166,12 @@
"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"
},
"preferences": "Preferences"
}
},
"help": {
"scoreBoard": {
"exerciseGroupNr": "Exercise Group {0}: ",
"exerciseNr": "Exercise {0}.{1}: ",
"generatePdf": "Generate PDF",
"personalSolutionKey": "Personal solution key"
"exerciseNr": "Exercise {0}.{1}: "
}
},
"bannerMessages": {
@@ -243,7 +225,6 @@
"cancel": "Cancel",
"more": "More",
"confirm": "Confirm",
"back": "Back",
"next": "Next"
},
"validation": {
@@ -260,13 +241,14 @@
"firstStartup": {
"title": "First startup",
"description": "Creating database. Please wait...",
"createDatabase": "Create Database",
"createDatabase": "Create Database...",
"complete": "Complete",
"createExercises": "Create Exercises...",
"finished": "Finished",
"connectToServer": "Server",
"database": "Database",
"exercises": "Exercises",
"userData": "User data",
"lookAndFeel": "Look and feel",
"enterYourPersonalData": "Please enter your name and your Matrikel number from your university. Check it twice! You can't change it later without loosing your exercise progress!"
},
"user": "About person",
@@ -281,25 +263,10 @@
"empty": {
"headline": "So empty here..."
},
"searchterm": "Search term",
"globalsearch": "Global Search"
},
"submit": "Submit",
"content": "Content",
"source": "Source",
"softwareVersion": "Software Version",
"license": "License",
"developer": "Developer",
"developedFor": "Developed for",
"copyright": "Copyright",
"githubRepository": "GitHub Repository",
"projectPage": "Project page"
"searchterm": "Search term"
}
},
"genre": {
"withoutBand": "without Band",
"popular": "Popular Genres"
},
"admin": {
"adminpanel": "Admin Panel"
"withoutBand": "without Band"
}
}

View File

@@ -12,7 +12,7 @@ const headers = [
{ title: feedbackStore.i18n.t('account.userData.houseNumber'), value: "houseNumber" },
{ title: feedbackStore.i18n.t('account.userData.postalCode'), value: "postalCode" },
{ title: feedbackStore.i18n.t('account.userData.placeOfResidence'), value: "city" },
{ title: feedbackStore.i18n.t('account.userData.actions'), value: "actions", width: 130 }
{ title: "Aktionen", value: "actions", width: 130 }
]
accountStore.refreshAccount()

View File

@@ -10,7 +10,7 @@ const feedbackStore = useFeedbackStore()
const headers = [
{ title: feedbackStore.i18n.t('account.userData.bankName'), value: "bankName" },
{ title: feedbackStore.i18n.t('account.userData.iban'), value: "iban" },
{ title: feedbackStore.i18n.t('account.userData.actions'), value: "actions", width: 130 }
{ title: "Aktionen", value: "actions", width: 130 }
]
accountStore.refreshAccount()

View File

@@ -40,7 +40,7 @@ concertStore.getConcerts()
</template>
<template #item.price="{ item }">
{{ item.price.toFixed(2) + '€' }}
{{ item.price.toFixed(2) }}
</template>
<template #item.image="{ item }">

View File

@@ -46,7 +46,7 @@ const test = ref()
</v-row>
</v-container>
<v-btn type="submit">{{ $t('misc.submit') }}</v-btn>
<v-btn type="submit">Submit</v-btn>
</v-form>
<template #actions>

View File

@@ -67,7 +67,7 @@ filesStore.getStaticFolders()
v-if="filesStore.selectedFile != undefined && filesStore.selectedFile.name.endsWith('js')"
:model-value="filesStore.selectedFile.content"
variant="outlined"
:label="$t('misc.content')"
label="Content"
height="300"
rows="30"
/>
@@ -104,7 +104,7 @@ filesStore.getStaticFolders()
prepend-icon="mdi-web"
v-if="filesStore.selectedFile.copyright.url.length > 0"
>
<a :href="filesStore.selectedFile.copyright.url" target="_blank" >{{ $t('misc.source') }}</a>
<a :href="filesStore.selectedFile.copyright.url" target="_blank" >Quelle</a>
</v-list-item>
</template>
</v-list>

View File

@@ -2,14 +2,12 @@
import dataLayout from '@/layouts/dataLayout.vue';
import genreEditDialog from './genreEditDialog.vue';
import { useGenreStore } from '@/stores/genre.store';
import { useFeedbackStore } from '@/stores/feedback.store';
const genreStore = useGenreStore()
const feedbackStore = useFeedbackStore()
const headers = [
{ title: feedbackStore.i18n.t('band.genre'), value: "name" },
{ title: feedbackStore.i18n.t('band.name'), value: "bands" },
{ title: "Name", value: "name" },
{ title: "Bands", value: "bands" },
{ title: "", value: "edit", width: 130 }
]

View File

@@ -3,19 +3,17 @@ import dataLayout from '@/layouts/dataLayout.vue';
import { useOrderStore } from '@/stores/order.store';
import moment from 'moment';
import OrderDetailDialog from './orderDetailDialog.vue';
import { useFeedbackStore } from '@/stores/feedback.store';
const orderStore = useOrderStore()
const feedbackStore = useFeedbackStore()
const headers = [
{ title: feedbackStore.i18n.t('account.userData.username'), value: "account.username" },
{ title: feedbackStore.i18n.t('account.userData.firstName'), value: "account" },
{ title: feedbackStore.i18n.t('order.orderedAt'), value: "orderedAt" },
{ title: feedbackStore.i18n.t('account.userData.address'), value: "street" },
{ title: feedbackStore.i18n.t('account.userData.placeOfResidence'), value: "city" },
{ title: feedbackStore.i18n.t('order.orderState'), value: "shipped" },
{ title: "", value: "edit", width: 130 }
{ title: "Account", value: "account.username" },
{ title: "Name", value: "account" },
{ title: "Bestellt am", value: "orderedAt" },
{ title: "Adresse", value: "street" },
{ title: "Stadt", value: "city" },
{ title: "Versendet", value: "shipped" },
{ title: "Aktionen", value: "edit", width: 130 }
]
orderStore.getAllOrders()

View File

@@ -7,13 +7,9 @@ import concertSection from './concertSection.vue';
import heroImage from '@/components/pageParts/heroImage.vue';
import { useBandStore } from '@/stores/band.store';
import { onMounted, watch } from 'vue';
import { useConcertStore } from '@/stores/concert.store';
const router = useRouter()
const bandStore = useBandStore()
const concertStore = useConcertStore()
concertStore.getConcerts()
onMounted(async () => {
bandStore.getBand(String(router.currentRoute.value.params.name).replaceAll('-', ' '))

View File

@@ -39,8 +39,8 @@ watch(() => router.currentRoute.value.query, () => {
</v-row>
<v-row
v-else-if="bandStore.filteredBands.length > 0"
v-for="band in bandStore.filteredBands"
v-else-if="bandStore.bands.length > 0"
v-for="band in bandStore.bands"
>
<v-col>
<band-list-item

View File

@@ -72,7 +72,7 @@ watch(() => router.currentRoute.value.params.date, () => {
<circular-progress-indeterminate />
<div class="pt-5 text-h3">
{{ $t('misc.loading') }}
{{ $t('misc.loading') }}...
</div>
</v-col>

View File

@@ -1,15 +1,17 @@
<script setup lang="ts">
import { useConcertStore } from "@/stores/concert.store";
import concertListItem from "@/components/pageParts/concertListItem.vue";
import cardViewHorizontal from "@/components/basics/cardViewHorizontal.vue";
import sectionDivider from "@/components/basics/sectionDivider.vue";
import concertFilterbar from "./concertFilterbar.vue";
import { useConcertStore } from '@/stores/concert.store';
import concertListItem from '@/components/pageParts/concertListItem.vue';
import cardViewHorizontal from '@/components/basics/cardViewHorizontal.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import concertFilterbar from './concertFilterbar.vue';
const concertStore = useConcertStore();
const concertStore = useConcertStore()
</script>
<template>
<div v-if="concertStore.fetchInProgress">
<div
v-if="concertStore.fetchInProgress"
>
<section-divider :loading="true" />
<v-row v-for="i in 3">
<v-col>
@@ -24,21 +26,13 @@ const concertStore = useConcertStore();
>
<div v-if="concert.offered">
<v-row
v-if="
index == 0 ||
v-if="index == 0 ||
new Date(concertStore.concerts[index - 1].date).getMonth() !=
new Date(concertStore.concerts[index].date).getMonth()
"
new Date(concertStore.concerts[index].date).getMonth()"
>
<v-col>
<section-divider
:title="
new Date(concert.date).toLocaleString('default', {
month: 'long',
}) +
' ' +
new Date(concert.date).getFullYear()
"
:title="new Date(concert.date).toLocaleString('default', { month: 'long' }) + ' ' + new Date(concert.date).getFullYear()"
/>
</v-col>
</v-row>

View File

@@ -43,7 +43,7 @@ const showOrderingDialog = ref()
</template>
<v-card-text class="text-right text-h5" v-if="basketStore.itemsInBasket.length > 0">
{{ $t('misc.totalPrice') }}: {{ (basketStore.getTotalPrice).toFixed(2) + '' }}
{{ $t('misc.totalPrice') }}: {{ (basketStore.getTotalPrice).toFixed(2) }}
</v-card-text>

View File

@@ -44,19 +44,19 @@ function removeFromBasket(basketItem: BasketItemModel) {
<!-- Quantity -->
<td class="text-center">
{{ basketItem.seats.length + 'x' }}
{{ basketItem.seats.length }}x
</td>
<!-- Price per event -->
<td class="text-right">
<div v-if="basketItem.seats">
{{ basketItem.price.toFixed(2) + '' }}
{{ basketItem.price.toFixed(2) }}
</div>
</td>
<!-- Total price -->
<td class="text-right">
{{ (calcPrice(basketItem.concert.price, basketItem.seats.length)).toFixed(2) + '€' }}
{{ (calcPrice(basketItem.concert.price, basketItem.seats.length)).toFixed(2) }}
</td>
<td class="text-right">

View File

@@ -1,34 +1,39 @@
<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 { getExerciseDotColor } from "@/scripts/colorScripts";
import { getExerciseDescriptionLanguage, getExerciseNameLanguage } from "@/scripts/languageScripts";
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';
const exerciseStore = useExerciseStore();
const preferencesStore = usePreferencesStore();
const exerciseStore = useExerciseStore()
const preferencesStore = usePreferencesStore()
// Mark this exercise as solved if page was opened
exerciseStore.solveExercise(1, 1);
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() {
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) {}
return (Number(code) + Number(preferencesStore.registrationNumber)) * 237
} catch(e) {}
}
</script>
@@ -41,12 +46,9 @@ function generateExerciseKey() {
<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") }}
PDF generieren
</outlined-button>
</v-col>
</v-row>
@@ -54,7 +56,7 @@ function generateExerciseKey() {
<v-row>
<v-col class="text-h5 text-center">
<div>
{{ $t("help.scoreBoard.personalSolutionKey") + ":" }}
Persönlicher Lösungsschlüssel:
</div>
<div>
{{ generateExerciseKey() }}
@@ -69,55 +71,46 @@ function generateExerciseKey() {
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="
getExerciseDotColor(exercise.exerciseGroup.groupNr)
"
:icon="exercise.exerciseGroup.icon"
<v-timeline-item v-if="exercise.exerciseNr == 1"
dot-color="grey"
fill-dot
>
<div
:class="`pt-1 text-h5 font-weight-bold text-${getExerciseDotColor(
exercise.exerciseGroup.groupNr
)}`"
:class="`pt-1 text-h5 font-weight-bold text-${getDotColor(exercise.exerciseGroup.groupNr)}`"
>
{{
preferencesStore.language == LanguageEnum.GERMAN
(preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.nameDe
: exercise.exerciseGroup.nameEn
: exercise.exerciseGroup.nameEn)
}}
</div>
<div>
{{
preferencesStore.language == LanguageEnum.GERMAN
(preferencesStore.language == LanguageEnum.GERMAN
? exercise.exerciseGroup.descriptionDe
: exercise.exerciseGroup.descriptionEn
: exercise.exerciseGroup.descriptionEn)
}}
</div>
</v-timeline-item>
<!-- Exercise item -->
<v-timeline-item
v-if="exercise.available"
:dot-color="exercise.solved ? 'green' : 'primary'"
:dot-color="getDotColor(exercise.exerciseGroup.groupNr)"
:icon="exercise.solved ? 'mdi-check' : 'mdi-pencil'"
>
<!-- Right side -->
<card-view
:title="
$t('help.scoreBoard.exerciseNr', [
exercise.exerciseGroup.groupNr,
exercise.exerciseNr,
]) + getExerciseNameLanguage(exercise)
"
:title="$t('help.scoreBoard.exerciseNr', [exercise.exerciseGroup.groupNr, exercise.exerciseNr]) +
(preferencesStore.language == LanguageEnum.GERMAN ? exercise.nameDe : exercise.nameEn)"
:color="exercise.solved ? 'green' : 'primary'"
>
{{ getExerciseDescriptionLanguage(exercise) }}
{{ preferencesStore.language == LanguageEnum.GERMAN ? exercise.descriptionDe : exercise.descriptionEn }}
</card-view>
</v-timeline-item>
</template>

View File

@@ -1,48 +0,0 @@
<script setup lang="ts">
import CardView from "@/components/basics/cardView.vue";
import CardViewOneLine from "@/components/basics/cardViewOneLine.vue";
import SectionDivider from "@/components/basics/sectionDivider.vue";
import { GenreApiModel } from "@/data/models/acts/genreApiModel";
import { useGenreStore } from "@/stores/genre.store";
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
const genreStore = useGenreStore();
const genresByNumberOfBands = ref<Array<GenreApiModel>>([]);
const router = useRouter();
genreStore.getGenres();
watch(
() => genreStore.genres,
() => {
genresByNumberOfBands.value = genreStore.genres;
genresByNumberOfBands.value.sort((a, b) => {
return b.bands.length - a.bands.length;
});
}
);
</script>
<template>
<v-row>
<v-col>
<section-divider :title="$t('genre.popular')" />
</v-col>
</v-row>
<v-row>
<v-col v-if="genreStore.fetchInProgress" v-for="n in 4" cols="6" md="">
<v-skeleton-loader :loading="true" type="card" />
</v-col>
<v-col v-else v-for="genre in genresByNumberOfBands" cols="6" md="3">
<card-view
@click="router.push({ path: '/bands', query: { genreName: genre.name }})"
:title="genre.name"
:subtitle="genre.bands.length + ' ' + $t('band.band', genre.bands.length)"
/>
</v-col>
</v-row>
</template>

View File

@@ -1,25 +1,24 @@
<script setup lang="ts">
import { useConcertStore } from "@/stores/concert.store";
import { useLocationStore } from "@/stores/location.store";
import bandSection from "./bandsSection.vue";
import UpcomingConcertsSection from "./upcomingConcertsSection.vue";
import TopLocationsSection from "./topLocationsSection.vue";
import { usePreferencesStore } from "@/stores/preferences.store";
import welcomeDialog from "./welcomeDialog/dialog.vue";
import { ref } from "vue";
import genresSection from "./genresSection.vue";
import { useConcertStore } from '@/stores/concert.store';
import { useLocationStore } from '@/stores/location.store';
import bandSection from './bandsSection.vue';
import UpcomingConcertsSection from './upcomingConcertsSection.vue';
import TopLocationsSection from './topLocationsSection.vue';
import { usePreferencesStore } from '@/stores/preferences.store';
import welcomeDialog from './welcomeDialog.vue';
import { ref } from 'vue';
const concertStore = useConcertStore();
const locationStore = useLocationStore();
const preferencesStore = usePreferencesStore();
const showWelcomeDialog = ref(false);
const concertStore = useConcertStore()
const locationStore = useLocationStore()
const preferencesStore = usePreferencesStore()
const showWelcomeDialog = ref(false)
concertStore.getUpcomingConcerts();
locationStore.getTopLocations();
concertStore.getUpcomingConcerts()
locationStore.getTopLocations()
// First startup
if (preferencesStore.firstStartup) {
showWelcomeDialog.value = true;
showWelcomeDialog.value = true
}
</script>
@@ -35,8 +34,6 @@ if (preferencesStore.firstStartup) {
<v-col cols="10">
<upcoming-concerts-section />
<genres-section />
<top-locations-section />
</v-col>

View File

@@ -1,36 +1,43 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import ServerStateText from '@/components/pageParts/serverStateText.vue';
import { getRegisterNumberRules, getStringRules } from '@/scripts/validationRules';
import { useFeedbackStore } from '@/stores/feedback.store';
import { usePreferencesStore } from '@/stores/preferences.store';
import { ref, watch } from 'vue';
import step1 from './step1.vue';
import step2 from './step2.vue';
import step3 from './step3.vue';
import step4 from './step4.vue';
const preferencesStore = usePreferencesStore()
const feedbackStore = useFeedbackStore()
const showDialog = defineModel()
const currentStep = ref(1)
const databaseCreated = ref(false)
const currentStep = ref(0)
const steps = [
feedbackStore.i18n.t('misc.firstStartup.connectToServer'),
feedbackStore.i18n.t('misc.firstStartup.lookAndFeel'),
feedbackStore.i18n.t('misc.firstStartup.database'),
feedbackStore.i18n.t('misc.firstStartup.exercises'),
feedbackStore.i18n.t('misc.firstStartup.userData'),
]
preferencesStore.getServerState()
watch(() => currentStep.value, async () => {
if (currentStep.value == 3 && !databaseCreated.value) {
await preferencesStore.resetDb();
await preferencesStore.resetExerciseProg();
watch(() => currentStep.value, () => {
switch(currentStep.value) {
case 2: {
preferencesStore.resetDb();
break;
}
databaseCreated.value = true;
case 3: {
preferencesStore.resetExerciseProg();
break;
}
case 4: {
break;
}
}
})
</script>
@@ -70,31 +77,83 @@ watch(() => currentStep.value, async () => {
<!-- Step 1: Check connection to backend server -->
<v-stepper-window-item
:value="1"
class="text-h4 text-center"
>
<step1 />
<div>
{{ $t('preferences.serverState') }}:
</div>
<server-state-text />
</v-stepper-window-item>
<!-- Step 2: Select theme and language -->
<!-- Step 2: Reset the database -->
<v-stepper-window-item
:value="2"
>
<step2 />
<div v-if="preferencesStore.fetchInProgress" class="text-center text-h4 pb-4">
<div class="pb-4">
{{ $t('misc.firstStartup.createDatabase') }}
</div>
<v-progress-linear indeterminate />
</div>
<div v-else class="text-center text-h4 pb-4 text-green">
<v-icon icon="mdi-check" /> {{ $t('misc.firstStartup.finished') }}
</div>
</v-stepper-window-item>
<!-- Step 3: Reset the database -->
<!-- Step 3: Create exercises -->
<v-stepper-window-item
:value="3"
>
<step3 />
<div v-if="preferencesStore.fetchInProgress" class="text-center text-h4 pb-4">
<div class="pb-4">
{{ $t('misc.firstStartup.createExercises') }}
</div>
<v-progress-linear indeterminate />
</div>
<div v-else class="text-center text-h4 pb-4 text-green">
<v-icon icon="mdi-check" /> {{ $t('misc.firstStartup.finished') }}
</div>
</v-stepper-window-item>
<!-- Step 4: Personal data -->
<v-stepper-window-item
:value="4"
>
<step4 />
<v-container class="px-0 py-2">
<v-row>
<v-col>
<v-alert color="warning" icon="mdi-alert">
{{ $t('misc.firstStartup.enterYourPersonalData') }}
</v-alert>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
:label="$t('misc.yourFullName')"
v-model="preferencesStore.studentName"
:rules="getStringRules(4)"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
:label="$t('misc.registrationNumber')"
v-model="preferencesStore.registrationNumber"
:rules="getRegisterNumberRules()"
/>
</v-col>
</v-row>
</v-container>
</v-stepper-window-item>
</v-stepper-window>
@@ -102,25 +161,16 @@ watch(() => currentStep.value, async () => {
<!-- Next/Previous buttons -->
<v-stepper-actions
@click:next="next"
@click:prev="prev"
>
<template #prev="{ props }">
<outlined-button
@click="props.onClick()"
:disabled="currentStep == 1 || preferencesStore.fetchInProgress"
color="grey"
prepend-icon="mdi-arrow-left"
>
{{ $t('misc.actions.back') }}
</outlined-button>
<v-spacer />
</template>
<template #next="{ props }">
<outlined-button
v-if="currentStep < steps.length"
v-if="currentStep < 4"
@click="props.onClick()"
:disabled="preferencesStore.fetchInProgress"
append-icon="mdi-arrow-right"
>
{{ $t('misc.actions.next') }}
</outlined-button>
@@ -130,7 +180,7 @@ watch(() => currentStep.value, async () => {
@click="showDialog = false; preferencesStore.firstStartup = false"
:disabled="preferencesStore.studentName.length < 5 ||
preferencesStore.registrationNumber.length < 8"
append-icon="mdi-check"
prepend-icon="mdi-check"
color="success"
>
{{ $t('misc.firstStartup.complete') }}

View File

@@ -1,23 +0,0 @@
<script setup lang="ts">
import ServerStateText from '@/components/pageParts/serverStateText.vue';
</script>
<template>
<v-container class="text-h4 text-center">
<v-row>
<v-col>
<v-icon icon="mdi-server" />
</v-col>
</v-row>
<v-row>
<v-col>
<div>
{{ $t('preferences.serverState') + ':' }}
</div>
<server-state-text />
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -1,48 +0,0 @@
<script setup lang="ts">
import { ThemeEnum } from '@/data/enums/themeEnums';
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
const themeEnums = Object.values(ThemeEnum)
</script>
<template>
<v-container width="600" class="text-h4 text-center">
<v-row>
<v-col>
<v-icon icon="mdi-palette" />
</v-col>
</v-row>
<v-row>
<v-col>
{{ $t('misc.firstStartup.lookAndFeel') }}
</v-col>
</v-row>
<v-row>
<v-col>
<v-select
v-model="preferencesStore.theme"
:items="themeEnums"
:label="$t('preferences.selectedTheme')"
variant="outlined"
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-select
v-model="preferencesStore.language"
:items="$i18n.availableLocales"
:label="$t('preferences.language')"
variant="outlined"
hide-details
/>
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -1,31 +0,0 @@
<script setup lang="ts">
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
</script>
<template>
<v-container width="600" class="text-h4 text-center">
<v-row>
<v-col>
<v-icon icon="mdi-database" />
</v-col>
</v-row>
<v-row>
<v-col>
{{ $t('misc.firstStartup.createDatabase') }}
</v-col>
</v-row>
<v-row>
<v-col v-if="preferencesStore.fetchInProgress">
<v-progress-linear indeterminate />
</v-col>
<v-col v-else class="text-green">
<v-icon icon="mdi-check" /> {{ $t('misc.firstStartup.finished') }}
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -1,54 +0,0 @@
<script setup lang="ts">
import { getRegisterNumberRules, getStringRules } from '@/scripts/validationRules';
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
</script>
<template>
<v-container class="px-0 py-2" width="600">
<v-row>
<v-col class="text-h4 text-center">
<v-icon icon="mdi-account" />
</v-col>
</v-row>
<v-row>
<v-col class="text-h4 text-center">
{{ $t('misc.firstStartup.userData') }}
</v-col>
</v-row>
<v-row>
<v-col>
<v-alert color="warning" icon="mdi-alert">
{{ $t('misc.firstStartup.enterYourPersonalData') }}
</v-alert>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
:label="$t('misc.yourFullName')"
v-model="preferencesStore.studentName"
:rules="getStringRules(4)"
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
:label="$t('misc.registrationNumber')"
v-model="preferencesStore.registrationNumber"
:rules="getRegisterNumberRules()"
hide-details
/>
</v-col>
</v-row>
</v-container>
</template>

View File

@@ -1,55 +1,41 @@
<script setup lang="ts">
import cardView from "@/components/basics/cardView.vue";
import packageJson from "./../../../../package.json";
import cardView from '@/components/basics/cardView.vue';
import packageJson from './../../../../package.json'
function openExternal(url: string) {
window.open(url, "_blank");
}
</script>
<template>
<card-view :title="$t('preferences.aboutProject')" icon="mdi-information">
<card-view
:title="$t('preferences.aboutProject')"
icon="mdi-information"
>
<template #borderless>
<v-list>
<v-list-item
:title="$t('misc.softwareVersion')"
title="Software Version"
:subtitle="packageJson.version"
prepend-icon="mdi-counter"
/>
<v-list-item
:title="$t('misc.license')"
title="Lizenz"
subtitle="MIT"
prepend-icon="mdi-license"
/>
<v-list-item
:title="$t('misc.developer')"
title="Entwickler"
subtitle="Tobias Zoghaib"
prepend-icon="mdi-account"
/>
<v-list-item
:title="$t('misc.developedFor')"
title="Entwickelt im Auftrag"
subtitle="Uni Hannover, Institut für IT-Sicherheit, Fachgebiet Usable Security and Privacy"
prepend-icon="mdi-school"
/>
<v-list-item
:title="$t('misc.copyright')"
subtitle="2024-2025"
title="Copyright"
subtitle="2024"
prepend-icon="mdi-copyright"
/>
<v-list-item
:title="$t('misc.githubRepository')"
prepend-icon="mdi-web"
@click="openExternal('https://github.com/TobiZog/eventmaster')"
/>
<v-list-item
:title="$t('misc.projectPage')"
prepend-icon="mdi-web"
@click="
openExternal(
'https://www.itsec.uni-hannover.de/de/usec/forschung/eventmaster-learning-web-attacks'
)
"
/>
</v-list>
</template>
</card-view>

View File

@@ -1,71 +0,0 @@
<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

@@ -1,75 +0,0 @@
<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

@@ -1,34 +0,0 @@
<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,29 +2,16 @@
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,24 +1,27 @@
<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">
@@ -26,12 +29,9 @@ 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,12 +40,9 @@ 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>
@@ -54,12 +51,9 @@ 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>
@@ -91,12 +85,10 @@ 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>

View File

@@ -20,19 +20,3 @@ export function getSeatColor(surcharge: number, state: number): string {
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

@@ -1,37 +0,0 @@
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

@@ -12,9 +12,6 @@ export const useBandStore = defineStore("bandStore", {
/** All available bands */
bands: ref<Array<BandApiModel>>([]),
/** Available bands filtered by parameters */
filteredBands: ref<Array<BandApiModel>>([]),
/** All information about a single band */
band: ref<BandDetailsApiModel>(new BandDetailsApiModel()),
@@ -35,9 +32,7 @@ export const useBandStore = defineStore("bandStore", {
await fetchAllBands()
.then(result => {
this.bands = result.data
this.filteredBands = result.data.filter((band: BandApiModel) => {
this.bands = result.data.filter((band: BandApiModel) => {
if (genreStore.genre == null) {
return true
}

View File

@@ -64,9 +64,6 @@ export const useConcertStore = defineStore("concertStore", {
const feedbackStore = useFeedbackStore()
this.fetchInProgress = true
console.log("LOcation & Date:")
console.log(this.concerts)
let id = this.concerts.find((concert: ConcertApiModel) => {
return (concert.location.urlName == location && concert.date == date)
}).id
@@ -78,7 +75,6 @@ export const useConcertStore = defineStore("concertStore", {
})
.catch(res => {
feedbackStore.notFound = true
this.fetchInProgress = false
})
},

View File

@@ -4,7 +4,6 @@ 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: () => ({
@@ -14,45 +13,23 @@ export const useExerciseStore = defineStore("exerciseStore", {
/** Request to server sent, waiting for data response */
fetchInProgress: ref(false),
exercisePageVisible: ref(false),
/** All available exercise uuids are stored here */
exerciseConfig: ref<Array<string>>(),
helpPageVisible: ref(false)
}),
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(firstLoad: boolean = false) {
const preferencesStore = usePreferencesStore();
this.fetchInProgress = true;
async getAllExercises() {
this.fetchInProgress = true
await fetchAllExerciseGroups().then((result) => {
this.exercises = result.data;
await fetchAllExerciseGroups()
.then(result => {
this.exercises = result.data
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.exercisePageVisible = this.getExercise(1, 1).solved;
this.fetchInProgress = false;
});
this.helpPageVisible = this.getExercise(1, 1).solved
this.fetchInProgress = false
})
},
/**
@@ -65,11 +42,8 @@ export const useExerciseStore = defineStore("exerciseStore", {
*/
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
})
},
/**
@@ -80,89 +54,65 @@ export const useExerciseStore = defineStore("exerciseStore", {
*/
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) => {
updateExercise(exerciseGroupNr, exerciseNr, true)
.then(result => {
if (result.data.changed) {
let bannerState = BannerStateEnum.ERROR;
switch (exerciseGroupNr) {
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;
switch(exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED01; break;
case 2: bannerState = BannerStateEnum.EXERCISESOLVED02; break;
case 3: bannerState = BannerStateEnum.EXERCISESOLVED03; break;
}
break;
}
case 1: {
switch (exerciseNr) {
case 1:
bannerState = BannerStateEnum.EXERCISESOLVED11;
break;
case 2:
bannerState = BannerStateEnum.EXERCISESOLVED12;
break;
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;
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;
switch(exerciseNr) {
case 1: bannerState = BannerStateEnum.EXERCISESOLVED31; break;
case 2: bannerState = BannerStateEnum.EXERCISESOLVED32; break;
}
break;
}
}
feedbackStore.addSnackbar(bannerState);
this.getAllExercises();
feedbackStore.addSnackbar(bannerState)
this.getAllExercises()
}
});
},
},
});
})
}
}
})

View File

@@ -3,11 +3,7 @@ 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";
@@ -16,19 +12,13 @@ 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),
@@ -46,27 +36,13 @@ 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",
""
),
notAvailableExercises: useLocalStorage<Array<string>>(
"eventMaster/preferencesStore/notAvailableExercises",
[]
),
registrationNumber: useLocalStorage<string>("eventMaster/preferencesStore/registrationNumber", "")
}),
actions: {
@@ -74,90 +50,92 @@ 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;
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)
*/
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) => {
await resetDatabase()
.then(result => {
if (result.status == 200) {
feedbackStore.addSnackbar(BannerStateEnum.DATABASERESETSUCCESSFUL);
this.serverState = ServerStateEnum.ONLINE;
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) => {
await resetExerciseProgress()
.then(result => {
if (result.status == 200) {
feedbackStore.addSnackbar(
BannerStateEnum.EXERCISEPROGRESSRESETSUCCESSFUL
);
this.serverState = ServerStateEnum.ONLINE;
feedbackStore.addSnackbar(BannerStateEnum.EXERCISEPROGRESSRESETSUCCESSFUL)
this.serverState = ServerStateEnum.ONLINE
exerciseStore.getAllExercises(true);
exerciseStore.getAllExercises()
}
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
}
}
})