24 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
112 changed files with 4966 additions and 1314 deletions

View File

@@ -1,3 +1,30 @@
# v.0.3.0 (Release Candidate 1)
## 🚀 Features
- Swagger Documentation
## 🐛 Bugfixes
- Bugfix on search page for Band datasets
# v.0.2.0 (Beta)
## 🚀 Features
- Adding "Test Environment" banner in the bottom right corner
- License handling system
- New SQL-Injection exercise 2.1
- Solution code based on Matrikelnummer and number of completed exercises
## 🌟 Enhancements
- Improve exercise solution of 2.1, 2.2, 2.3, 2.4 and 2.6
- Light mode improvements
- Global color schema
- More feedback through notifications
- More hints on text fields
- Redesign account pages, split payments and addresses, new dashboard
## 🐛 Bugfixes
- More server stability
- Bugfix file manager in Electron application
# v.0.1.0 (Alpha) # v.0.1.0 (Alpha)
## 🚀 Features ## 🚀 Features
- Frontend - Frontend

425
README.md
View File

@@ -1,6 +1,6 @@
# HackMyCart # EventMaster
The most hackable Web Shop! The most hackable Ticket-Shop!
## How to use ## How to use
@@ -47,424 +47,3 @@ The frontend runs on `http://localhost:5173/` and the backend on `http://localho
### Database ### Database
![database-erm](misc/images/database.png) ![database-erm](misc/images/database.png)
### Backend API endpoints
The application host it's data in a SQLite database. The access is managed by an [ExpressJs](https://expressjs.com/) server which offers many REST-API endpoints for the frontend. The REST-API server runs on port 3000.
---
#### Listing existing
<details open>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/accounts/</b></code> <code> (Get all Accounts)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Account + AccountRole>` |
##### Example Response
```json
[
{
"id": 421,
"username": "hagemeister93",
"password": "Xjt3qb5t",
"email": "hagemeister93@gmail.com",
"firstName": "Laurin",
"lastName": "Hagemeister",
"accountRoleId": 2,
"accountRole": {
"id": 2,
"name": "Admin",
"privilegeBuy": true,
"privilegeAdminPanel": true
}
}
]
```
</details>
<details open>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/api/files</b></code> <code> (Get all public files)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<{folder: String, files: Array<{name: String, size: Number, url: String}> }>` |
##### Example Response
```json
[
{
"folder": "artists",
"files": [
{
"name": "alex-turner.jpg",
"size": 56473,
"url": "http://localhost:3000/static/artists/alex-turner.jpg"
},
{
"name": "andy-nicholson.jpg",
"size": 68983,
"url": "http://localhost:3000/static/artists/andy-nicholson.jpg"
}
]
}
]
```
</details>
<details open>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/bands/</b></code> <code> (Get all bands)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | `sort` | optional | string | Sort by number of concerts ascending (asc) or descending (desc) |
> | `count` | optional | number | Number of items to responde |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<>` |
##### Example Response
```json
[
{
"folder": "artists",
"files": [
{
"name": "alex-turner.jpg",
"size": 56473,
"url": "http://localhost:3000/static/artists/alex-turner.jpg"
},
{
"name": "andy-nicholson.jpg",
"size": 68983,
"url": "http://localhost:3000/static/artists/andy-nicholson.jpg"
}
]
}
]
```
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/events?city=cityName&genre=genreName&count=nrOfItems&sort=sortDirection</b></code> <code> (Get all events, filtered by city and genre)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | `cityName` | optional | string | Name of the city to filter for |
> | `genreName` | optional | string | Name of the genre to filter for |
> | `nrOfItems` | optional | number | Limits number of results |
> | `sortDirection` | optional | string | Sort by number of concerts, 'asc' or 'desc' |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Event + Array<Concert + Location + City> + Band & Genre>` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/locations?count=nrOfItems&sort=sortDirection</b></code> <code> (Get all locations)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | `nrOfItems` | optional | number | Limits number of results |
> | `sortDirection` | optional | string | Sort by number of concerts, 'asc' or 'desc' |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Location + City + Array<Concert + Event>>` |
</details>
Down here: todo!
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/bands</b></code> <code> (Get all bands)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Band>` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/bands/:id</b></code> <code> (Get all information about one band)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | `id` | required | string | ID of product in the database |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Band` + `Array<Rating>` + `Array<Member>` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/shows/:id</b></code> <code> (Get all information about one show)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | `id` | required | string | ID of product in the database |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Show` + `Tour` + `Location` + `City` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/tours</b></code> <code> (Get all tours)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Tours>` + `Band` + `Show` + `Location` + `City` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/genres</b></code> <code> (Get all genres)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Genre>` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/orders/:id</b></code> <code> (Get all orders of an user)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | --- | --- | --- | --- |
> | `id` | required | string | ID of userAccount in the database |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `ProductModel` + `Order`, `OrderItem`, `Product` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/categories/</b></code> <code> (Get all Categories)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Categories>` |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/brands</b></code> <code> (Get all Brands)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `Array<Brand>` |
</details>
---
#### Creating new
<details>
<summary><code><span style="color:#69CA92"><b>POST</b></span></code> <code><b>/accounts/</b></code> <code> (Create a new account)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | None | required | object (JSON) | Model of an Account |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `201` | `application/json` | `AccountModel` |
> | `400` | `application/json` | `{code: 400, message: "Username too short!"}` |
> | `400` | `application/json` | `{code: 400, message: "Password too short!"}` |
> | `409` | `application/json` | `{code: 409, message: "Username already in use"}` |
</details>
<details>
<summary><code><span style="color:#69CA92"><b>POST</b></span></code> <code><b>/orders/</b></code> <code> (Create a new order)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | None | required | object (JSON) | Model of an Order |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `201` | `application/json` | `OrderModel` |
</details>
---
#### Updating existing
<details>
<summary><code><span style="color:#F3A63D"><b>PATCH</b></span></code> <code><b>/accounts/</b></code> <code> (Update data of an existing account)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | None | required | object (JSON) | Model of an Account |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | AccountModel |
> | `400` | `application/json` | `{code: 400, message: "..."}` |
</details>
---
#### Delete existing
<!-- <details>
<summary><code><span style="color:#EB5246"><b>DELETE</b></span></code> <code><b>/product/:id</b></code> <code> (Delete a product)</code>
</summary>
##### Parameters
> | name | type | data type | description |
> | :---: | --- | --- | --- |
> | `id` | required | string | ID of product in the database |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `ProductModel` |
> | `400` | `application/json` | `{code: 400, message: "..."}` |
</details> -->
---
#### Miscs
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/api/</b></code> <code> (Check if server runs)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | None | None |
</details>
<details>
<summary><code><span style="color:#70AFFD"><b>GET</b></span></code> <code><b>/resetDatabase/</b></code> <code> (Reset the database to it's default values)</code>
</summary>
##### Parameters
> None
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | None | None |
</details>
#### Validate
<details>
<summary><code><span style="color:#69CA92"><b>POST</b></span></code> <code><b>/accounts/login</b></code> <code> (Login for user)</code></summary>
##### Parameters
> | name | type | data type | description |
> | --- | --- | --- | --- |
> | username | required | string | Username of the account |
> | password | required | string | Password of the account |
##### Responses
> | http code | content-type | response |
> | :---: | --- | --- |
> | `200` | `application/json` | `AccountObject` + `Addresses`, `Payments`, `AccountRole` |
> | `400` | `application/json` | `{code: 400, message: "Bad Request"}` |
> | `401` | `application/json` | `{code: 401, message: "Unauthorized"}` |
</details>
---

View File

@@ -4,29 +4,19 @@
"id": 0, "id": 0,
"name": "Unregistered", "name": "Unregistered",
"privilegeBuy": false, "privilegeBuy": false,
"privilegeAdminPanel": false, "privilegeAdminPanel": false
"privilegeFileAccess": false
}, },
{ {
"id": 1, "id": 1,
"name": "User", "name": "User",
"privilegeBuy": true, "privilegeBuy": true,
"privilegeAdminPanel": false, "privilegeAdminPanel": false
"privilegeFileAccess": false
}, },
{ {
"id": 2, "id": 2,
"name": "Admin", "name": "Admin",
"privilegeBuy": true, "privilegeBuy": true,
"privilegeAdminPanel": true, "privilegeAdminPanel": true
"privilegeFileAccess": false
},
{
"id": 3,
"name": "Super-Admin",
"privilegeBuy": true,
"privilegeAdminPanel": true,
"privilegeFileAccess": true
} }
] ]
} }

View File

@@ -19,7 +19,7 @@
"iban": "DE92500105175721645777" "iban": "DE92500105175721645777"
} }
], ],
"accountRoleId": 2 "accountRoleId": 1
}, },
{ {
"username": "katjaStoiber", "username": "katjaStoiber",
@@ -94,7 +94,7 @@
"iban": "DE41500105172184936679" "iban": "DE41500105172184936679"
} }
], ],
"accountRoleId": 3 "accountRoleId": 2
}, },
{ {
"username": "guitarhero", "username": "guitarhero",

View File

@@ -78,25 +78,25 @@
"location": "Swiss Life Hall" "location": "Swiss Life Hall"
}, },
{ {
"date": "4", "date": "8",
"price": 92, "price": 92,
"inStock": 170, "inStock": 170,
"location": "Swiss Life Hall" "location": "Swiss Life Hall"
}, },
{ {
"date": "8", "date": "12",
"price": 119.90, "price": 119.90,
"inStock": 8736, "inStock": 8736,
"location": "Olympiahalle München" "location": "Olympiahalle München"
}, },
{ {
"date": "12", "date": "19",
"price": 114.90, "price": 114.90,
"inStock": 2793, "inStock": 2793,
"location": "Barclays Arena" "location": "Barclays Arena"
}, },
{ {
"date": "13", "date": "31",
"price": 124.90, "price": 124.90,
"inStock": 3079, "inStock": 3079,
"location": "Uber Arena Berlin" "location": "Uber Arena Berlin"
@@ -468,7 +468,7 @@
"location": "ZAG Arena" "location": "ZAG Arena"
}, },
{ {
"date": "6", "date": "15",
"price": 84.90, "price": 84.90,
"inStock": 192, "inStock": 192,
"location": "Muffatwerk" "location": "Muffatwerk"
@@ -613,25 +613,25 @@
"image": "concerts/will-of-the-people-tour.jpg", "image": "concerts/will-of-the-people-tour.jpg",
"concerts": [ "concerts": [
{ {
"date": "32", "date": "2",
"price": 67.90, "price": 67.90,
"inStock": 847, "inStock": 847,
"location": "ZAG Arena" "location": "ZAG Arena"
}, },
{ {
"date": "39", "date": "17",
"price": 67.90, "price": 67.90,
"inStock": 847, "inStock": 847,
"location": "ZAG Arena" "location": "ZAG Arena"
}, },
{ {
"date": "41", "date": "31",
"price": 64.90, "price": 64.90,
"inStock": 245, "inStock": 245,
"location": "Olympiastadion Berlin" "location": "Olympiastadion Berlin"
}, },
{ {
"date": "45", "date": "43",
"price": 64.90, "price": 64.90,
"inStock": 245, "inStock": 245,
"location": "Astra Kulturhaus" "location": "Astra Kulturhaus"

View File

@@ -60,39 +60,46 @@
"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.", "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": "todo", "descriptionEn": "todo",
"exercises": [ "exercises": [
{
"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": "todo"
},
{ {
"nameDe": "Alle Accounts ausspähen", "nameDe": "Alle Accounts ausspähen",
"nameEn": "Get all accounts", "nameEn": "Get all accounts",
"exerciseNr": 1, "exerciseNr": 2,
"descriptionDe": "Wir versuchen nun die Datenbank im Hintergrund anzugreifen. 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 >>Accounts<< zurück liefert. Führe den Angriff über das Suchfeld aus. Sieh dir die Rückmeldung des Servers an, indem du im >>Network<<-Tab auf die zurück gegebenen Objekte klickst (z.B. >>locations<<).", "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": "Execute an SQL-Injection on the Search page to get all datasets from >>Accounts<< table." "descriptionEn": "Execute an SQL-Injection on the Search page to get all datasets from >>Accounts<< table."
}, },
{ {
"nameDe": "Alle Berechtigungsgruppen ausspähen", "nameDe": "Alle Berechtigungsgruppen ausspähen",
"nameEn": "Get all account roles", "nameEn": "Get all account roles",
"exerciseNr": 2, "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.", "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": "Execute an SQL-Injection on the Search page to get all datasets from >>AccountRoles<< table." "descriptionEn": "Execute an SQL-Injection on the Search page to get all datasets from >>AccountRoles<< table."
}, },
{ {
"nameDe": "Eigene Berechtigungen erhöhen", "nameDe": "Eigene Berechtigungen erhöhen",
"nameEn": "Upgrade your privileges", "nameEn": "Upgrade your privileges",
"exerciseNr": 3, "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.", "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": "Change the privileges of your account" "descriptionEn": "Change the privileges of your account"
}, },
{ {
"nameDe": "Einen fremden Account übernehmen", "nameDe": "Einen fremden Account übernehmen",
"nameEn": "Capture another account", "nameEn": "Capture another account",
"exerciseNr": 4, "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.", "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": "todo" "descriptionEn": "todo"
}, },
{ {
"nameDe": "Bewertungen löschen", "nameDe": "Bewertungen löschen",
"nameEn": "Delete ratings", "nameEn": "Delete ratings",
"exerciseNr": 5, "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 Tabelle >>Ratings<< alle Einträge mit der Bedingung >>rating = 5<< entfernt. Führe die Injection über die globale Suche aus.", "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": "todo" "descriptionEn": "todo"
} }
] ]
@@ -108,14 +115,14 @@
"nameDe": "Hallo Welt!", "nameDe": "Hallo Welt!",
"nameEn": "Hello World!", "nameEn": "Hello World!",
"exerciseNr": 1, "exerciseNr": 1,
"descriptionDe": "Nimm dir eine URL des Shops und erweitere sie mit JavaScript Code so, dass beim Öffnen des Links eine 'Hallo Welt' Nachricht erscheint", "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": "Take an URL of the shop and extend it with JavaScript code so that a 'Hello World' message appears whent the link is opened" "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"
}, },
{ {
"nameDe": "Ein externes Script aufrufen", "nameDe": "Ein externes Script aufrufen",
"nameEn": "Run an external script", "nameEn": "Run an external script",
"exerciseNr": 2, "exerciseNr": 2,
"descriptionDe": "Wir haben festgestellt, dass die Seite für Cross-Site-Scripting angreifbar ist! Im zweiten Schritt binden wir nun das Script ein. Es wurde bereits auf den Server hochgeladen. Suche über die Dateiverwaltung im Admin-Panel nach dem Skript. Kopiere dir die URL der Ressource heraus. Gehe zum Login und log dich aus. Füge einen beliebig benannten Query Parameter zur URL hinzu um das Skript zu laden. Nutze ein >>import<<-Statement um das Skript einzubinden. Logge dich nun ganz normal ein und sieh dir über die Konsole (Strg + D) an, wie deine Login-Daten abgegriffen werden. Hinweis: Wenn das Skript richtig eingebunden wurde, erscheint in der Konsole eine >>Active<<-Benachrichtigung. Da das >>import<<-Statement nur beim erstmaligen Laden der Seite genutzt wird, musst nach Eingabe der URL eventuell den Neu-Laden-Button neben der URL-Leiste betätigen.", "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": "Create an URL of the shop, which calls the script" "descriptionEn": "Create an URL of the shop, which calls the script"
} }
] ]

200
backend/data/licenses.json Normal file
View File

@@ -0,0 +1,200 @@
[
{
"image": "alex-turner.jpg",
"license": "CC BY 2.0",
"creator": "Raph_PH",
"url": "https://upload.wikimedia.org/wikipedia/commons/9/95/Alex_Turner%2C_Way_Out_West_2018.jpg"
},
{
"image": "andy-nicholson.jpg",
"license": "CC BY 2.0",
"creator": "Lola's Big Adventure!",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/6c/Andy_Nicholson_%28cropped%29.jpg"
},
{
"image": "anthony-kiedis.jpg",
"license": "CC BY 2.0",
"creator": "Hel Davies",
"url": "https://upload.wikimedia.org/wikipedia/commons/c/ca/Anthony_Kiedis_2022.jpg"
},
{
"image": "chris-martin.jpg",
"license": "CC BY 2.0",
"creator": "Raph_PH",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/68/ChrisMartinManch030623_%28cropped%29.jpg"
},
{
"image": "chris-wolstenholme.jpg",
"license": "CC BY-SA 4.0",
"creator": "Markus Felix",
"url": "https://upload.wikimedia.org/wikipedia/commons/e/eb/2018_Chris_Wolstenholme_%28cropped%29.jpg"
},
{
"image": "flea.jpg",
"license": "CC BY 2.0",
"creator": "Piyush Kumar",
"url": "https://upload.wikimedia.org/wikipedia/commons/8/8e/Flea_1012_%282%29.jpg"
},
{
"image": "chad-smith.jpg",
"license": "Gemeinfrei",
"creator": "Bojosoto",
"url": "https://upload.wikimedia.org/wikipedia/commons/4/42/Chadsmithclinic.jpg"
},
{
"image": "john-frusciante.jpg",
"license": "CC BY-SA 2.0",
"creator": "Hel Davies",
"url": "https://upload.wikimedia.org/wikipedia/commons/1/1f/John_Frusciante_%2852279466415%29.jpg"
},
{
"image": "logo.png",
"license": "MIT",
"creator": "Tobias Zoghaib",
"url": ""
},
{
"image": "lanxess-arena-indoor.jpg",
"license": "CC BY-SA 3.0",
"creator": "Admin Kübelbeck",
"url": "https://upload.wikimedia.org/wikipedia/commons/f/f3/Koelnarena_inside.jpg"
},
{
"image": "lanxess-arena-outdoor.jpg",
"license": "CC BY-SA 2.0",
"creator": "Rolf H.",
"url": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lanxess_Arena_Flight_over_Cologne.jpg"
},
{
"image": "red-hot-chili-peppers-1.jpg",
"license": "CC BY-SA 4.0",
"creator": "Kreepin Deth",
"url": "https://upload.wikimedia.org/wikipedia/commons/1/14/RHCP_Live_in_London_26_June_2022.jpg"
},
{
"image": "swiss-life-hall-indoor.jpg",
"license": "CC BY-SA 3.0",
"creator": "Bernd Schwabe in Hannover",
"url": "https://upload.wikimedia.org/wikipedia/commons/8/89/2013-09-18_Besuch_14._Dalai_Lama_Tendzin_Gyatsho_in_Hannover%2C_future4children%2C_Swiss_Life_Hall%2C_%2876%29.JPG"
},
{
"image": "swiss-life-hall-outdoor.jpg",
"license": "Public Domain",
"creator": "AxelHH",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/66/AWD_Hall_Seite.jpg"
},
{
"image": "astra-kulturhaus-outdoor.jpg",
"license": "CC BY 2.0",
"creator": "Marcus Grbac",
"url": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Astra_Kulturhaus_Biergarten_RAW_Berlin_July_2017.jpg"
},
{
"image": "thom-yorke.jpg",
"license": "CC BY 2.0",
"creator": "Raph_PH",
"url": "https://upload.wikimedia.org/wikipedia/commons/2/25/RadioheadMontreal170718-70_%2843600493681%29_%28cropped%29.jpg"
},
{
"image": "rami-jaffee.jpg",
"license": "CC BY 2.0",
"creator": "Raph_PH",
"url": "https://upload.wikimedia.org/wikipedia/commons/f/fa/Rami_Jaffee_1.jpg"
},
{
"image": "philip-selway.jpg",
"license": "CC BY-SA 2.0",
"creator": "Michell Zappa",
"url": "https://upload.wikimedia.org/wikipedia/commons/1/1f/Phil_Selway.jpg"
},
{
"image": "phil-harvey.jpg",
"license": "CC BY-SA 3.0",
"creator": "Hayley St. James",
"url": "https://upload.wikimedia.org/wikipedia/commons/3/36/PhilHarveyNewYork17062021.png"
},
{
"image": "pat-smear.jpg",
"license": "GNU v.1.2",
"creator": "Andrew Burns",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/66/Patsmear.jpg"
},
{
"image": "mike-kerr.jpg",
"license": "CC BY 4.0",
"creator": "Dena Flows",
"url": "https://upload.wikimedia.org/wikipedia/commons/8/85/017-BIME-2017-Royal-Blood-27X17-por-Dena-Flows.jpg"
},
{
"image": "matthew-bellamy.jpg",
"license": "CC BY 3.0",
"creator": "Minerva97",
"url": "https://upload.wikimedia.org/wikipedia/commons/d/d0/2009_Matthew_Bellamy_%28cropped%29.jpg"
},
{
"image": "capitol-outside.jpg",
"license": "",
"creator": "AxelHH",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/6c/Hannover_Capitol_ganz.jpg"
},
{
"image": "red-hot-chili-peppers-logo.png",
"license": "",
"creator": "Viiticus",
"url": "https://upload.wikimedia.org/wikipedia/commons/3/31/Red_Hot_Chili_Peppers_logo.svg"
},
{
"image": "red-hot-chili-peppers-2.jpg",
"license": "CC BY-SA 4.0",
"creator": "Roberto Gianardi",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/64/Red_Hot_Chili_Peppers_Bologna_2016.jpg"
},
{
"image": "arctic-monkeys-1.jpg",
"license": "CC BY 3.0",
"creator": "Bill Ebbesen",
"url": "https://upload.wikimedia.org/wikipedia/commons/0/04/Arctic_Monkeys_-_Orange_Stage_-_Roskilde_Festival_2014.jpg"
},
{
"image": "arctic-monkeys-2.jpg",
"license": "CC BY-SA 3.0",
"creator": "Kennysun",
"url": "https://upload.wikimedia.org/wikipedia/commons/6/65/Arctic_Monkeys_Playing_at_MSG.jpg"
},
{
"image": "arctic-monkeys-3.jpg",
"license": "CC BY-SA 2.0",
"creator": "Aurelien Guichard",
"url": "https://upload.wikimedia.org/wikipedia/commons/f/f8/Arctic_Monkeys_%40_Shepherds_Bush_Empire.jpg"
},
{
"image": "european-tour-arctic-monkeys.jpg",
"license": "Gemeinfrei",
"creator": "Matthew Cooper",
"url": "https://upload.wikimedia.org/wikipedia/commons/e/e7/%22AM%22_%28Arctic_Monkeys%29.jpg"
},
{
"image": "billy-talent-1.jpg",
"license": "CC BY-SA 4.0",
"creator": "Biha",
"url": "https://upload.wikimedia.org/wikipedia/commons/2/2b/Billy_Talent_-_Frequency_Festival_-_2017-08-15-21-51-04.jpg"
},
{
"image": "billy-talent-2.jpg",
"license": "CC BY-SA 4.0",
"creator": "Markus Maier",
"url": "https://upload.wikimedia.org/wikipedia/commons/3/3f/Southside_Festival_-_Billy_Talent_-_DSC05306.jpg"
},
{
"image": "billy-talent-3.jpg",
"license": "CC BY-SA 2.0",
"creator": "sebi ryffel",
"url": "https://upload.wikimedia.org/wikipedia/commons/e/ec/Billy_Talent_at_Rock_Am_See_2007.jpg"
},
{
"image": "coldplay-members.jpg",
"license": "CC BY 2.0",
"creator": "Raph_PH",
"url": "https://upload.wikimedia.org/wikipedia/commons/2/2e/ColdplayBBC071221_%28cropped%29.jpg"
}
]

View File

@@ -18,7 +18,7 @@
"username": "duranduran", "username": "duranduran",
"tickets": [ "tickets": [
{ {
"date": "4", "date": "8",
"concertGroupName": "Unlimited Love", "concertGroupName": "Unlimited Love",
"orderPrice": 184, "orderPrice": 184,
"seatGroup": "A", "seatGroup": "A",
@@ -26,7 +26,7 @@
"seat": 2 "seat": 2
}, },
{ {
"date": "4", "date": "8",
"concertGroupName": "Unlimited Love", "concertGroupName": "Unlimited Love",
"orderPrice": 184, "orderPrice": 184,
"seatGroup": "A", "seatGroup": "A",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 677 KiB

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 276 KiB

View File

@@ -12,9 +12,6 @@ export class AccountRole extends Model {
@Column @Column
privilegeAdminPanel: boolean privilegeAdminPanel: boolean
@Column
privilegeFileAccess: boolean
// Relations // Relations
@HasMany(() => Account) @HasMany(() => Account)

View File

@@ -1,10 +1,15 @@
/**
* @swagger
* tags:
* name: Account
* description: API to manage accounts
*/
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { Account } from "../models/user/account.model"; import { Account } from "../models/user/account.model";
import { validateString } from "../scripts/validateHelper"; import { validateString } from "../scripts/validateHelper";
import { Address } from "../models/user/address.model"; import { Address } from "../models/user/address.model";
import { Payment } from "../models/user/payment.model"; import { Payment } from "../models/user/payment.model";
import { AccountRole } from "../models/user/accountRole.model"; import { AccountRole } from "../models/user/accountRole.model";
import { Exercise } from "../models/exercises/exercise.model";
import { sequelize } from "../database"; import { sequelize } from "../database";
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
import { verifyToken } from "../middlewares/auth.middleware"; import { verifyToken } from "../middlewares/auth.middleware";
@@ -12,61 +17,133 @@ import { encryptString } from "../scripts/encryptScripts";
export const account = Router() export const account = Router()
account.get("/", (req: Request, res: Response) => { /**
Account.findAll({ * @swagger
include: [ AccountRole ] * /accounts/login:
}) * get:
.then(accounts => { * summary: Start login process
res.status(200).json(accounts) * tags: [Account]
}) * parameters:
}) * - in: query
* name: username
// Login user * schema:
* type: string
* required: true
* description: Username
* - in: query
* name: password
* schema:
* type: string
* required: true
* description: User password
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/loginResponse'
* 401:
* description: Wrong credentials
* 500:
* description: Internal server error
*/
account.get("/login", async (req: Request, res: Response) => { account.get("/login", async (req: Request, res: Response) => {
const encryptedPassword = encryptString(String(req.query.password)) const encryptedPassword = encryptString(String(req.query.password))
// Using raw SQL code for SQL injections! try {
const [results, metadata] = // Using raw SQL code for SQL injections!
await sequelize.query( const [results, metadata] =
"SELECT * FROM Accounts " + await sequelize.query(
"WHERE (username='" + req.query.username + "SELECT * FROM Accounts " +
"' AND password='" + encryptedPassword + "')" "WHERE (username='" + req.query.username +
) "' AND password='" + encryptedPassword + "')"
)
if (results.length != 0) { if (results.length != 0) {
// Creating session token // Creating session token
const token = jwt.sign({ userId: results[0]["id"] }, 'sjcucjdkdf') const token = jwt.sign({ userId: results[0]["id"] }, 'sjcucjdkdf')
// Status: 200 OK // Status: 200 OK
res.status(200).json({ res.status(200).json({
success: true, success: true,
token: token token: token
}) })
} else { } else {
// Status: 401 Unauthorized // Status: 401 Unauthorized
res.status(401).json({ res.status(401).send()
code: 401, }
message: "Unauthorized" } catch (e) {
}) res.status(500).send()
} }
}) })
/**
* @swagger
* /accounts/account:
* get:
* summary: Get all data about an user account
* tags: [Account]
* security:
* - JWT: []
* responses:
* 200:
* description: Success
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/useraccount'
* 401:
* description: Unauthorized
* 500:
* description: Internal server error
*/
account.get("/account", verifyToken, async(req: Request, res: Response) => { account.get("/account", verifyToken, async(req: Request, res: Response) => {
Account.findOne({ Account.findOne({
where: { where: {
id: req["id"] id: req["id"]
}, },
include: [ Address, AccountRole, Payment ] include: [ Address, AccountRole, Payment ],
attributes: {
exclude: [ "accountRoleId" ]
}
}) })
.then(account => { .then(account => {
res.status(200).json(account) res.status(200).json(account)
}) })
.catch(error => {
res.status(500).send()
})
}) })
// Creating a new user /**
account.post("/", async (req: Request, res: Response) => { * @swagger
* /accounts/account:
* post:
* summary: Create a new user account
* tags: [Account]
* requestBody:
* description: Minimal user data body
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/minimalAccount'
* responses:
* 201:
* description: Created
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/useraccount'
* 400:
* description: Username/password too short
* 409:
* description: Username already in use
*/
account.post("/account", async (req: Request, res: Response) => {
// Check if username is valid // Check if username is valid
if (!validateString(req.body.username, 4)) if (!validateString(req.body.username, 4))
{ {
@@ -85,9 +162,10 @@ account.post("/", async (req: Request, res: Response) => {
code: 400, code: 400,
message: "Password too short!" message: "Password too short!"
}) })
return
} }
// Create account // User on creation gets User role
await AccountRole.findOne({ await AccountRole.findOne({
where: { where: {
name: "User" name: "User"
@@ -97,11 +175,12 @@ account.post("/", async (req: Request, res: Response) => {
req.body["accountRoleId"] = role.id req.body["accountRoleId"] = role.id
}) })
// Create account
Account.create(req.body) Account.create(req.body)
.then(account => { .then(account => {
// Status: 201 Created // Status: 201 Created
res.status(201).json(account) res.status(201).json(account)
}).catch(reason => { }).catch(error => {
// Status: 409 Conflict // Status: 409 Conflict
res.status(409).json({ res.status(409).json({
code: 409, code: 409,
@@ -110,38 +189,56 @@ account.post("/", async (req: Request, res: Response) => {
}) })
}) })
account.patch("/", verifyToken, (req: Request, res: Response) => {
/**
* @swagger
* /accounts/account:
* patch:
* summary: Update an user accounts data
* tags: [Account]
* security:
* - JWT: []
* responses:
* 200:
* description: Success
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/useraccount'
* 401:
* description: Unauthorized
* 500:
* description: Internal server error
*/
account.patch("/account", verifyToken, (req: Request, res: Response) => {
Account.update(req.body, Account.update(req.body,
{ {
where: { id: req.body.id } where: { id: req.body.id }
}) })
.then(async result => { .then(async result => {
for (let payment of req.body.payments) { Payment.destroy({
if (payment.id == undefined) { where: {
payment["accountId"] = req.body.id accountId: req.body.id
await Payment.create(payment)
} else {
await Payment.update(payment,
{
where: { id: payment.id }
}
)
} }
})
Address.destroy({
where: {
accountId: req.body.id
}
})
for (let payment of req.body.payments) {
payment["accountId"] = req.body.id
await Payment.create(payment)
} }
for (let address of req.body.addresses) { for (let address of req.body.addresses) {
if (address.id == undefined) { address["accountId"] = req.body.id
address["accountId"] = req.body.id
await Address.create(address) await Address.create(address)
} else {
await Address.update(address,
{
where: { id: address.id }
}
)
}
} }
// Status: 200 OK // Status: 200 OK
@@ -157,7 +254,31 @@ account.patch("/", verifyToken, (req: Request, res: Response) => {
}) })
}) })
account.delete("/:id", (req: Request, res: Response) => {
/**
* @swagger
* /accounts/account/{id}:
* delete:
* summary: Delete an user account
* tags: [Account]
* security:
* - JWT: []
* parameters:
* - in: path
* name: id
* schema:
* type: number
* required: true
* description: ID of user account
* responses:
* 200:
* description: Success
* 401:
* description: Unauthorized
* 500:
* description: Internal server error
*/
account.delete("/account/:id", verifyToken, (req: Request, res: Response) => {
Account.destroy({ Account.destroy({
where: { where: {
id: req.params.id id: req.params.id
@@ -166,4 +287,40 @@ account.delete("/:id", (req: Request, res: Response) => {
.then(account => { .then(account => {
res.status(200).send() res.status(200).send()
}) })
.catch(error => {
res.status(500).send()
})
})
/**
* @swagger
* /accounts/:
* get:
* summary: Request all user accounts
* tags: [Account]
* security:
* - JWT: []
* responses:
* 200:
* description: Success
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/useraccount'
* 401:
* description: Unauthorized
* 500:
* description: Internal server error
*/
account.get("/", verifyToken, (req: Request, res: Response) => {
Account.findAll({
include: [ AccountRole ]
})
.then(accounts => {
res.status(200).json(accounts)
})
.catch(error => {
res.status(500).send()
})
}) })

View File

@@ -1,18 +1,38 @@
/**
* @swagger
* tags:
* name: Api
* description: Main API access point for misc events
*/
import { Request, Response, NextFunction, Router } from 'express' import { Request, Response, NextFunction, Router } from 'express'
import { deleteAllTables, deleteExerciseProgressTables, prepopulateDatabase, prepopulateExerciseDatabase } from '../scripts/databaseHelper' import { deleteAllTables, deleteExerciseProgressTables, prepopulateDatabase, prepopulateExerciseDatabase } from '../scripts/databaseHelper'
export const api = Router() export const api = Router()
/** /**
* Status check endpoint * @swagger
* /api:
* get:
* summary: Status check endpoint
* tags: [Api]
* responses:
* 200:
* description: Server is up and running
*/ */
api.get("/", (req: Request, res: Response, next: NextFunction) => { api.get("/", (req: Request, res: Response, next: NextFunction) => {
res.status(200).send() res.status(200).send()
}) })
/** /**
* Reset the whole database to factory state * @swagger
* Doesn't effect ExerciseTable and ExerciseGroupTable * /api/resetdatabase:
* get:
* summary: Reset the database to factory state
* description: Doesn't effect ExerciseTable and ExerciseGroupTable
* tags: [Api]
* responses:
* 200:
* description: Reset successful
*/ */
api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction) => { api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction) => {
// Step 1: Delete all data tables // Step 1: Delete all data tables
@@ -26,7 +46,15 @@ api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction
}) })
/** /**
* Reset ExerciseTable and ExerciseGroupTable to factory state * @swagger
* /api/resetExerciseProgress:
* get:
* summary: Reset exercises to factory state
* description: Reset ExerciseTable and ExerciseGroupTable to factory state
* tags: [Api]
* responses:
* 200:
* description: Reset successful
*/ */
api.get("/resetExerciseProgress", async (req: Request, res: Response, next: NextFunction) => { api.get("/resetExerciseProgress", async (req: Request, res: Response, next: NextFunction) => {
deleteExerciseProgressTables() deleteExerciseProgressTables()

View File

@@ -1,3 +1,9 @@
/**
* @swagger
* tags:
* name: Bands
* description: API to manage the bands
*/
import { Member } from "../models/acts/member.model"; import { Member } from "../models/acts/member.model";
import { Band } from "../models/acts/band.model"; import { Band } from "../models/acts/band.model";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
@@ -13,7 +19,33 @@ import { sequelize } from "../database";
export const band = Router() export const band = Router()
/** /**
* Get all bands * @swagger
* /bands:
* get:
* summary: Download all available bands
* tags: [Bands]
* parameters:
* - in: query
* name: sort
* schema:
* type: string
* required: false
* description: Sort bands by number of concerts ascending (asc) or descending (desc)
* - in: query
* name: count
* schema:
* type: number
* required: false
* description: Limit number of results
* responses:
* 200:
* description: List of band objects
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/band'
* 500:
* description: Internal server error
*/ */
band.get("/", (req: Request, res: Response) => { band.get("/", (req: Request, res: Response) => {
let sort = req.query.sort let sort = req.query.sort
@@ -21,16 +53,14 @@ band.get("/", (req: Request, res: Response) => {
Band.findAll({ Band.findAll({
include: [ include: [
{
model: Rating,
},
{ {
model: Genre, model: Genre,
attributes: { attributes: {
exclude: [ "id" ] exclude: [ "id" ]
} }
}, },
Concert Concert,
Rating
] ]
}) })
.then(bands => { .then(bands => {
@@ -65,6 +95,9 @@ band.get("/", (req: Request, res: Response) => {
res.status(200).json(bands) res.status(200).json(bands)
}) })
.catch(error => {
res.status(500).send()
})
}) })
/** /**
@@ -122,7 +155,7 @@ band.get("/band/:name", (req: Request, res: Response) => {
res.status(200).json(band) res.status(200).json(band)
}) })
.catch(e => { .catch(error => {
res.status(404).send() res.status(404).send()
}) })
}) })
@@ -137,11 +170,13 @@ band.get("/search", async (req: Request, res: Response) => {
// On stacked prompts, execute last prompt // On stacked prompts, execute last prompt
if (prompts.length > 1) { if (prompts.length > 1) {
console.log(prompts[prompts.length - 2]) try {
const [results, metadata] = const [results, metadata] =
await sequelize.query(prompts[prompts.length - 2]) await sequelize.query(prompts[prompts.length - 2])
res.status(200).json(results)
res.status(200).json(results) } catch (e) {
res.status(400).send()
}
} else { } else {
Band.findAll({ Band.findAll({
where: { where: {
@@ -154,6 +189,9 @@ band.get("/search", async (req: Request, res: Response) => {
.then(bands => { .then(bands => {
res.status(200).json(bands) res.status(200).json(bands)
}) })
.catch(error => {
res.status(200).send()
})
} }
}) })
@@ -170,6 +208,9 @@ band.patch("/", (req: Request, res: Response) => {
.then(result => { .then(result => {
res.status(200).json(result) res.status(200).json(result)
}) })
.catch(error => {
res.status(500).send()
})
}) })
@@ -181,6 +222,9 @@ band.post("/", (req: Request, res: Response) => {
.then(result => { .then(result => {
res.status(200).json(result) res.status(200).json(result)
}) })
.catch(error => {
res.status(500).send()
})
}) })
/** /**

View File

@@ -1,11 +1,36 @@
/**
* @swagger
* tags:
* name: Cities
* description: API to manage the cities
*/
import { City } from "../models/locations/city.model"; import { City } from "../models/locations/city.model";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
export const city = Router() export const city = Router()
/**
* @swagger
* /cities:
* get:
* summary: Download all cities
* tags: [Cities]
* responses:
* 200:
* description: List of all cities as objects
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/city'
* 500:
* description: Internal server error
*/
city.get("/", (req: Request, res: Response) => { city.get("/", (req: Request, res: Response) => {
City.findAll() City.findAll()
.then(cities => { .then(cities => {
res.status(200).json(cities) res.status(200).json(cities)
}) })
.catch(error => {
res.status(500).send()
})
}) })

View File

@@ -1,3 +1,9 @@
/**
* @swagger
* tags:
* name: Concerts
* description: API to manage the concerts
*/
import { Location } from "../models/locations/location.model"; import { Location } from "../models/locations/location.model";
import { Concert } from "../models/acts/concert.model"; import { Concert } from "../models/acts/concert.model";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
@@ -11,18 +17,68 @@ import { Op } from "sequelize";
export const concert = Router() export const concert = Router()
const concertStructure = [
{
model: Band
},
{
model: Location,
include: [
{
model: City
},
{
model: SeatGroup,
include: [
{
model: SeatRow,
include: [
{
model: Seat,
include: [
{
model: Ticket
}
]
}
]
}
]
}
],
attributes: {
exclude: [ "cityId" ]
}
}
]
/**
* @swagger
* /concerts:
* get:
* summary: Get all available concerts
* tags: [Concerts]
* parameters:
* - in: query
* name: count
* schema:
* type: number
* required: false
* description: Limit number of results
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/concert'
*/
concert.get("/", (req: Request, res: Response) => { concert.get("/", (req: Request, res: Response) => {
let count = req.query.count let count = req.query.count
Concert.findAll({ Concert.findAll({
include: [ include: concertStructure,
{
model: Location,
include: [ City ]
},
Band
],
order: [ order: [
[ 'date', 'ASC' ] [ 'date', 'ASC' ]
] ]
@@ -35,50 +91,37 @@ concert.get("/", (req: Request, res: Response) => {
res.status(200).json(concerts) res.status(200).json(concerts)
}) })
.catch(error => {
res.status(500).send()
})
}) })
// Get all available data about a band by it's ID /**
* @swagger
* /concerts/concert/{id}:
* get:
* summary: Download all available informations about a specific concert
* tags: [Concerts]
* parameters:
* - in: path
* name: id
* schema:
* type: number
* required: true
* description: ID of concert in database
* responses:
* 200:
* description: Single concert object
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/concert'
* 404:
* description: Not found
*/
concert.get("/concert/:id", (req: Request, res: Response) => { concert.get("/concert/:id", (req: Request, res: Response) => {
Concert.findByPk(req.params.id, { Concert.findByPk(req.params.id, { include: concertStructure })
include: [
{
model: Band,
},
{
model: Location,
include: [
{
model: City
},
{
model: SeatGroup,
include: [
{
model: SeatRow,
include: [
{
model: Seat,
include: [
{
model: Ticket
}
]
}
]
}
]
}
],
attributes: {
exclude: [ "cityId" ]
}
}
],
attributes: {
exclude: [ "locationId", "tourId" ]
}
})
.then(concert => { .then(concert => {
concert.dataValues["capacity"] = 0 concert.dataValues["capacity"] = 0
@@ -116,7 +159,29 @@ concert.get("/concert/:id", (req: Request, res: Response) => {
}) })
// Concert search /**
* @swagger
* /concerts/search:
* get:
* summary: Search for concerts
* tags: [Concerts]
* parameters:
* - in: query
* name: value
* schema:
* type: string
* required: true
* description: Search term
* responses:
* 200:
* description: List of concert objects
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/concert'
* 500:
* description: Internal server error
*/
concert.get("/search", (req: Request, res: Response) => { concert.get("/search", (req: Request, res: Response) => {
Concert.findAll({ Concert.findAll({
where: { where: {
@@ -154,4 +219,7 @@ concert.get("/search", (req: Request, res: Response) => {
.then(concerts => { .then(concerts => {
res.status(200).json(concerts) res.status(200).json(concerts)
}) })
.catch(error => {
res.status(500).send()
})
}) })

View File

@@ -1,3 +1,9 @@
/**
* @swagger
* tags:
* name: Exercises
* description: API to manage the exercise progress
*/
import { Op } from "sequelize"; import { Op } from "sequelize";
import { Exercise } from "../models/exercises/exercise.model"; import { Exercise } from "../models/exercises/exercise.model";
import { ExerciseGroup } from "../models/exercises/exerciseGroup.model"; import { ExerciseGroup } from "../models/exercises/exerciseGroup.model";
@@ -6,26 +12,75 @@ import { Request, Response, Router } from "express";
export const exercises = Router() export const exercises = Router()
/** /**
* Get all Exercises grouped in ExerciseGroups * @swagger
* /exercises:
* get:
* summary: Download all exercises
* tags: [Exercises]
* responses:
* 200:
* description: Array of all exercises
* type: array
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/exercise'
* 500:
* description: Internal server error
*/ */
exercises.get("/", (req: Request, res: Response) => { exercises.get("/", (req: Request, res: Response) => {
Exercise.findAll({ Exercise.findAll({
include: [ ExerciseGroup ] include: [ ExerciseGroup ],
}).then(result => { attributes: {
result.sort((a, b) => { exclude: [ "exerciseGroupId" ]
return (a.dataValues.exerciseGroup.dataValues.groupNr * 10 + a.dataValues.exerciseNr) > (b.dataValues.exerciseGroup.dataValues.groupNr * 10 + b.dataValues.exerciseNr) ? 1 : -1 }
})
res.status(200).json(result)
}) })
.then(result => {
result.sort((a, b) => {
return (a.dataValues.exerciseGroup.dataValues.groupNr * 10 + a.dataValues.exerciseNr) > (b.dataValues.exerciseGroup.dataValues.groupNr * 10 + b.dataValues.exerciseNr) ? 1 : -1
})
res.status(200).json(result)
})
.catch(error => {
res.status(500).send()
})
}) })
/** /**
* Update state of an Exercise * @swagger
* * /exercises/{groupNr}/{exerciseNr}/{state}:
* @param groupNr Number of exercise group (not ID) * post:
* @param exerciseNr Number of exercise (not ID) * summary: Update an exercise solved state
* @param state New state boolean * tags: [Exercises]
* parameters:
* - in: path
* name: groupNr
* schema:
* type: number
* required: true
* description: Number of exercise group (not ID)
* - in: path
* name: exerciseNr
* schema:
* type: number
* required: true
* description: Number of exercise (not ID)
* - in: path
* name: state
* schema:
* type: number
* required: true
* description: 1 = Solved, 0 = Unsolved
* responses:
* 200:
* description: Edited exercise
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/exercise'
* 500:
* description: Internal server error
*/ */
exercises.post("/:groupNr/:exerciseNr/:state", (req: Request, res: Response) => { exercises.post("/:groupNr/:exerciseNr/:state", (req: Request, res: Response) => {
Exercise.findOne({ Exercise.findOne({
@@ -39,7 +94,10 @@ exercises.post("/:groupNr/:exerciseNr/:state", (req: Request, res: Response) =>
} }
] ]
}, },
include: [ ExerciseGroup ] include: [ ExerciseGroup ],
attributes: {
exclude: [ "exerciseGroupId" ]
}
}) })
.then(async exercise => { .then(async exercise => {
let changed = false let changed = false
@@ -54,21 +112,7 @@ exercises.post("/:groupNr/:exerciseNr/:state", (req: Request, res: Response) =>
changed: changed changed: changed
}) })
}) })
.catch(error => {
res.status(500).send()
// ExerciseGroup.findOne({ })
// where: { groupNr: req.params.groupNr }
// })
// .then(group => {
// Exercise.findOne({
// where: {
// exerciseNr: req.params.exerciseNr,
// exerciseGroupId: group.id
// }
// })
// .then(exercise => {
// exercise.update({ solved: req.params.state == "1"})
// res.status(200).send()
// })
// })
}) })

View File

@@ -1,21 +1,40 @@
/**
* @swagger
* tags:
* name: Files
* description: API for handling static files
*/
import { Request, Response, NextFunction, Router } from 'express' import { Request, Response, NextFunction, Router } from 'express'
import fs from "fs" import fs from "fs"
import multer from "multer" import multer from "multer"
const upload = multer({ dest: './backend/images/' }) const upload = multer({ dest: './backend/images/' })
import licenses from "../data/licenses.json"
import path from 'path'
export const files = Router() export const files = Router()
/** /**
* Get all folders * @swagger
* /files/folders:
* get:
* summary: Get all static folders
* tags: [Files]
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/folder'
*/ */
files.get("/folders", async (req: Request, res: Response) => { files.get("/folders", async (req: Request, res: Response) => {
let dirNames = fs.readdirSync("./backend/images") let dirNames = fs.readdirSync(path.resolve(__dirname, "../images"))
let result = [] let result = []
dirNames.forEach(dir => { dirNames.forEach(dir => {
result.push({ result.push({
name: dir, name: dir,
nrOfItems: fs.readdirSync("./backend/images/" + dir).length nrOfItems: fs.readdirSync(path.resolve(__dirname, "../images/" + dir)).length
}) })
}) })
@@ -24,25 +43,52 @@ files.get("/folders", async (req: Request, res: Response) => {
/** /**
* Get all uploaded file names by file name * @swagger
* /files/{folder}:
* get:
* summary: Get all files in one folder
* tags: [Files]
* parameters:
* - in: path
* name: folder
* schema:
* type: string
* required: true
* description: Name of folder
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/file'
*/ */
files.get("/:folder", async (req: Request, res: Response) => { files.get("/:folder", async (req: Request, res: Response) => {
let result = [] let result = []
let fileNames = fs.readdirSync("./backend/images/" + req.params.folder + "/") let fileNames = fs.readdirSync(path.resolve(__dirname, "../images/" + req.params.folder))
try {
fileNames.forEach(file => {
let resData = ""
let url = "http://localhost:3000/static/" + req.params.folder + "/" + file
fileNames.forEach(file => { if (file.endsWith("html") || file.endsWith("js")) {
let resData = fs.readFileSync("./backend/images/" + req.params.folder + "/" + file, "utf8") resData = fs.readFileSync(path.resolve(__dirname, "../images/" + req.params.folder + "/" + file), "utf8")
}
result.push({ result.push({
name: file, name: file,
size: fs.statSync("./backend/images/" + req.params.folder + "/" + file).size, size: fs.statSync(path.resolve(__dirname, "../images/" + req.params.folder + "/" + file)).size,
content: resData, content: resData,
url: "http://localhost:3000/static/" + req.params.folder + "/" + file url: url,
copyright: licenses.find(data => data.image == file)
})
}) })
})
res.status(200).json(result) res.status(200).json(result)
} catch (error) {
res.status(400).json(error)
}
}) })
@@ -50,7 +96,5 @@ files.get("/:folder", async (req: Request, res: Response) => {
* Upload a file * Upload a file
*/ */
files.post("/", upload.single("file"), function (req: Request, res: Response, next: NextFunction) { files.post("/", upload.single("file"), function (req: Request, res: Response, next: NextFunction) {
console.log(req.file)
res.status(200).send() res.status(200).send()
}) })

View File

@@ -1,3 +1,9 @@
/**
* @swagger
* tags:
* name: Genres
* description: API to manage the music genres
*/
import { Band } from "../models/acts/band.model"; import { Band } from "../models/acts/band.model";
import { Genre } from "../models/acts/genre.model"; import { Genre } from "../models/acts/genre.model";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
@@ -5,7 +11,20 @@ import { Request, Response, Router } from "express";
export const genre = Router() export const genre = Router()
/** /**
* Get all available Genres * @swagger
* /genres:
* get:
* summary: Get all available genres
* tags: [Genres]
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/genre'
* 500:
* description: Internal Server Error
*/ */
genre.get("/", (req: Request, res: Response) => { genre.get("/", (req: Request, res: Response) => {
Genre.findAll({ Genre.findAll({
@@ -19,8 +38,22 @@ genre.get("/", (req: Request, res: Response) => {
}) })
}) })
/** /**
* Update a Genre entry * @swagger
* /genres:
* patch:
* summary: Update the dataset of a genre
* tags: [Genres]
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/genre'
* 500:
* description: Internal Server Error
*/ */
genre.patch("/", (req: Request, res: Response) => { genre.patch("/", (req: Request, res: Response) => {
Genre.update(req.body, { Genre.update(req.body, {
@@ -36,8 +69,22 @@ genre.patch("/", (req: Request, res: Response) => {
}) })
}) })
/** /**
* Create a new Genre entry * @swagger
* /genres:
* post:
* summary: Add a new dataset of a genre
* tags: [Genres]
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/genre'
* 500:
* description: Internal Server Error
*/ */
genre.post("/", (req: Request, res: Response) => { genre.post("/", (req: Request, res: Response) => {
Genre.create(req.body) Genre.create(req.body)
@@ -49,8 +96,22 @@ genre.post("/", (req: Request, res: Response) => {
}) })
}) })
/** /**
* Delete a Genre entry * @swagger
* /genres:
* delete:
* summary: Delete the dataset of a genre
* tags: [Genres]
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/genre'
* 500:
* description: Internal Server Error
*/ */
genre.delete("/", (req: Request, res: Response) => { genre.delete("/", (req: Request, res: Response) => {
Genre.destroy({ Genre.destroy({

View File

@@ -1,3 +1,9 @@
/**
* @swagger
* tags:
* name: Locations
* description: API to manage the event locations
*/
import { Concert } from "../models/acts/concert.model"; import { Concert } from "../models/acts/concert.model";
import { City } from "../models/locations/city.model"; import { City } from "../models/locations/city.model";
import { Location } from "../models/locations/location.model"; import { Location } from "../models/locations/location.model";
@@ -10,24 +16,57 @@ import { Op } from "sequelize";
export const location = Router() export const location = Router()
// Response include rules
const locationStructure = [
City,
{
model: Concert,
include: [ Band ]
},
{
model: SeatGroup,
include: [
{
model: SeatRow,
include: [ Seat ]
}
]
}
]
/** /**
* Get all available Locations * @swagger
* * /locations:
* @query sort Sort results ascending (asc) or descending (desc) * get:
* @query count Limit number of results * summary: Get all available locations
* tags: [Locations]
* parameters:
* - in: query
* name: sort
* schema:
* type: string
* required: false
* description: Sort locations by number of concerts ascending (asc) or descending (desc)
* - in: query
* name: count
* schema:
* type: number
* required: false
* description: Limit number of results
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/location'
*/ */
location.get("/", (req: Request, res: Response) => { location.get("/", (req: Request, res: Response) => {
let sort = req.query.sort let sort = req.query.sort
let count = req.query.count let count = req.query.count
Location.findAll({ Location.findAll({
include: [ include: locationStructure,
City,
{
model: Concert,
include: [ Band ],
}
],
attributes: { attributes: {
exclude: [ "cityId" ] exclude: [ "cityId" ]
} }
@@ -60,29 +99,32 @@ location.get("/", (req: Request, res: Response) => {
/** /**
* Get all data about a specific location * @swagger
* * /locations/{urlName}:
* @param urlName UrlName of the band (e.g. Red Hot Chili Peppers => red-hot-chili-peppers) * get:
* summary: Download all available informations about a specific locations
* tags: [Locations]
* parameters:
* - in: path
* name: urlName
* schema:
* type: string
* required: true
* description: Url name of the location to request for
* responses:
* 200:
* description: Single of location objects
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/location'
* 500:
* description: Internal server error
*/ */
location.get("/location/:urlName", (req: Request, res: Response) => { location.get("/location/:urlName", (req: Request, res: Response) => {
Location.findOne({ Location.findOne({
where: { urlName: req.params.urlName }, where: { urlName: req.params.urlName },
include: [ include: locationStructure,
City,
{
model: Concert,
include: [ Band ],
},
{
model: SeatGroup,
include: [
{
model: SeatRow,
include: [ Seat ]
}
]
}
],
attributes: { attributes: {
exclude: [ "cityId" ] exclude: [ "cityId" ]
} }
@@ -98,16 +140,34 @@ location.get("/location/:urlName", (req: Request, res: Response) => {
res.status(200).json(location) res.status(200).json(location)
}) })
.catch(e => { .catch(error => {
res.status(404).send() res.status(404).send()
}) })
}) })
/** /**
* Search for Locations * @swagger
* * /locations/search:
* @query value Search term to look for * get:
* summary: Search for locations
* tags: [Locations]
* parameters:
* - in: query
* name: value
* schema:
* type: string
* required: true
* description: Search term
* responses:
* 200:
* description: List of band objects
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/location'
* 500:
* description: Internal server error
*/ */
location.get("/search", (req: Request, res: Response) => { location.get("/search", (req: Request, res: Response) => {
Location.findAll({ Location.findAll({
@@ -128,9 +188,12 @@ location.get("/search", (req: Request, res: Response) => {
} }
] ]
}, },
include: [ City, Concert ] include: locationStructure
}) })
.then(locations => { .then(locations => {
res.status(200).json(locations) res.status(200).json(locations)
}) })
.catch(error => {
res.status(500).send()
})
}) })

View File

@@ -1,3 +1,9 @@
/**
* @swagger
* tags:
* name: Orders
* description: API to manage orders
*/
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { Order } from "../models/ordering/order.model"; import { Order } from "../models/ordering/order.model";
import { Concert } from "../models/acts/concert.model"; import { Concert } from "../models/acts/concert.model";
@@ -10,17 +16,41 @@ import { City } from "../models/locations/city.model";
import { Seat } from "../models/locations/seat.model"; import { Seat } from "../models/locations/seat.model";
import { SeatRow } from "../models/locations/seatRow.model"; import { SeatRow } from "../models/locations/seatRow.model";
import { SeatGroup } from "../models/locations/seatGroup.model"; import { SeatGroup } from "../models/locations/seatGroup.model";
import { verifyToken } from "../middlewares/auth.middleware";
import { Account } from "../models/user/account.model"; import { Account } from "../models/user/account.model";
import { Exercise } from "backend/models/exercises/exercise.model";
export const order = Router() export const order = Router()
// Get all orders /**
order.get("/", (req: Request, res: Response) => { * @swagger
* /orders:
* get:
* summary: Get orders of an account or all available
* tags: [Orders]
* security:
* - JWT: []
* parameters:
* - in: query
* name: id
* schema:
* type: string
* required: false
* description: User account id to filter the orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/order'
* 500:
* description: Internal server error
*/
order.get("/", verifyToken, (req: Request, res: Response) => {
const accountId = req.query.id
Order.findAll({ Order.findAll({
include: [ include: [
Account,
Address,
{ {
model: Ticket, model: Ticket,
include: [ include: [
@@ -35,42 +65,6 @@ order.get("/", (req: Request, res: Response) => {
include: [ City ] include: [ City ]
} }
] ]
}
]
}
]
})
.then(orders => {
res.status(200).json(orders)
})
})
// Get all orders of one account by it's user id
order.get("/:id", (req: Request, res: Response) => {
Order.findAll({
where: { accountId: req.params.id },
include: [
{
model: Ticket,
include: [
{
model: Concert,
include: [
{
model: Band
},
{
model: Location,
include: [ City ]
}
],
attributes: {
exclude: [
"categoryId",
"brandId"
]
}
}, },
{ {
model: Seat, model: Seat,
@@ -83,17 +77,58 @@ order.get("/:id", (req: Request, res: Response) => {
} }
] ]
}, },
Address,
Payment, Payment,
Address Account,
] ],
attributes: {
exclude: [ "accountId", "addressId", "paymentId" ]
}
}) })
.then(orders => { .then(orders => {
res.status(200).json(orders) if (accountId != undefined) {
let filteredOrders = orders.filter(order => {
return order.id == accountId
})
res.status(200).json(filteredOrders)
} else {
res.status(200).json(orders)
}
})
.catch(error => {
res.status(500).send()
}) })
}) })
// Place a new order /**
order.post("/", (req: Request, res: Response) => { * @swagger
* /orders:
* post:
* summary: Place a new order
* tags: [Orders]
* security:
* - JWT: []
* parameters:
* - in: query
* name: id
* schema:
* type: string
* required: false
* description: User account id to filter the orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/order'
* 500:
* description: Internal server error
*/
order.post("/", verifyToken, (req: Request, res: Response) => {
req.body["accountId"] = req["id"]
Order.create(req.body) Order.create(req.body)
.then(async order => { .then(async order => {
for (let ticket of req.body.tickets) { for (let ticket of req.body.tickets) {
@@ -116,4 +151,45 @@ order.post("/", (req: Request, res: Response) => {
// Created // Created
res.status(201).json(order) res.status(201).json(order)
}) })
.catch(error => {
res.status(500).send()
})
})
/**
* @swagger
* /orders:
* patch:
* summary: Update an order
* tags: [Orders]
* parameters:
* - in: body
* name: order
* schema:
* type: object
* required: true
* description: Updated order object
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/order'
* 500:
* description: Internal server error
*/
order.patch("/", (req: Request, res: Response) => {
Order.update(req.body, {
where: {
id: req.body.id
}
})
.then(affectedCount => {
res.status(200).send()
})
.catch(error => {
res.status(500).send()
})
}) })

View File

@@ -1,6 +1,8 @@
import express from 'express' import express from 'express'
import cors from 'cors' import cors from 'cors'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import swaggerJsdoc from "swagger-jsdoc"
import swaggerUi from "swagger-ui-express"
import { api } from './routes/api.routes' import { api } from './routes/api.routes'
import { startDatabase } from './database' import { startDatabase } from './database'
import { order } from './routes/order.routes' import { order } from './routes/order.routes'
@@ -12,6 +14,7 @@ import { location } from './routes/location.routes'
import { city } from './routes/city.routes' import { city } from './routes/city.routes'
import { exercises } from './routes/exercise.routes' import { exercises } from './routes/exercise.routes'
import { files } from './routes/files.routes' import { files } from './routes/files.routes'
import swaggerFile from './swagger.json'
const app = express() const app = express()
const port = 3000 const port = 3000
@@ -29,6 +32,7 @@ startDatabase()
const path = require('path') const path = require('path')
app.use('/static', express.static(path.join(__dirname, 'images'))) app.use('/static', express.static(path.join(__dirname, 'images')))
app.use("/exercises", exercises) app.use("/exercises", exercises)
app.use("/files", files)
// Add delay for more realistic response times // Add delay for more realistic response times
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -44,7 +48,17 @@ app.use("/orders", order)
app.use("/accounts", account) app.use("/accounts", account)
app.use("/cities", city) app.use("/cities", city)
app.use("/concerts", concert) app.use("/concerts", concert)
app.use("/files", files)
// Swagger API documentation
const specs = swaggerJsdoc(swaggerFile);
app.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(specs, { explorer: true })
)
// Start server // Start server
const server = app.listen(port, () => { const server = app.listen(port, () => {

796
backend/swagger.json Normal file
View File

@@ -0,0 +1,796 @@
{
"swagger": "2.0",
"definition": {
"openapi": "3.1.0",
"info": {
"title": "EventMaster API",
"version": "0.2.0",
"description": "Dokumentation über alle API-Endpunkte des Backends",
"license": {
"name": "MIT",
"url": "https://spdx.org/licenses/MIT.html"
}
},
"servers": [
{
"url": "http://localhost:3000"
}
],
"components": {
"securitySchemes": {
"JWT": {
"type": "apiKey",
"in": "header",
"name": "Json Web Token"
}
},
"schemas": {
"city": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"name": {
"type": "string",
"description": "Name of the city"
},
"country": {
"type": "string",
"description": "Name of country of the city"
}
},
"example": {
"id": 2,
"name": "Hannover",
"country": "Germany"
}
},
"loginResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "Login successful state"
},
"token": {
"type": "string",
"description": "Individual created access token"
}
},
"example": {
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjI2MiwiaWF0IjoxNzMzNzYwOTY3fQ.I3rR71c-k2Y2WB0dkd1QEgHxsIRGl4s69YprBNuhX7w"
}
},
"minimalAccount": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Account username"
},
"password": {
"type": "string",
"description": "Encrypted password"
},
"email": {
"type": "string",
"description": "E-Mail address of user"
}
},
"example": {
"username": "maxmustermann",
"password": "supersecret",
"email": "tijjji@didjhli.de"
}
},
"minimalAccountResponse": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"username": {
"type": "string",
"description": "Account username"
},
"password": {
"type": "string",
"description": "Encrypted password"
},
"email": {
"type": "string",
"description": "E-Mail address of user"
},
"accountRoleId": {
"type": "number",
"description": "ID of account role"
}
},
"example": {
"id": 263,
"username": "maxmustermann",
"password": "8746fb88adbae61ffa68193ee0bb8050",
"email": "tijjji@didjhli.de",
"accountRoleId": 1
}
},
"placeOrderBody": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Account username"
},
"password": {
"type": "string",
"description": "Encrypted password"
},
"email": {
"type": "string",
"description": "E-Mail address of user"
},
"accountRoleId": {
"type": "number",
"description": "ID of account role"
}
},
"example": {
"id": 263,
"username": "maxmustermann",
"password": "8746fb88adbae61ffa68193ee0bb8050",
"email": "tijjji@didjhli.de",
"accountRoleId": 1
}
},
"genre": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"name": {
"type": "string",
"description": "Name of the genre"
},
"bands": {
"type": "object",
"description": "Bands with this genre object"
}
},
"example": [
{
"id": 562,
"name": "Funk Rock",
"bands": [
{
"images": [
"http://localhost:3000/static/bands/red-hot-chili-peppers-1.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-2.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-3.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-4.jpg"
],
"id": 265,
"name": "Red Hot Chili Peppers",
"foundingYear": 1983,
"descriptionEn": "The Red Hot Chili Peppers are an American rock band formed in Los Angeles in 1983, comprising vocalist Anthony Kiedis, bassist Flea, drummer Chad Smith, and guitarist John Frusciante. Their music incorporates elements of alternative rock, funk, punk rock, hard rock, hip hop, and psychedelic rock. Their eclectic range has influenced genres such as funk metal, rap metal, rap rock, and nu metal. With over 120 million records sold worldwide, the Red Hot Chili Peppers are one of the top-selling bands of all time.",
"descriptionDe": "Red Hot Chili Peppers (Abkürzung: RHCP) ist eine 1983 gegründete US-amerikanische Funk- und Alternative-Rockband. Sie zählt zu den kommerziell erfolgreichsten Vertretern des Crossover. Ihr Album Blood Sugar Sex Magik gilt als eines der bedeutendsten dieses Genres.",
"imageMembers": "http://localhost:3000/static/bands/red-hot-chili-peppers-members.jpg",
"logo": "http://localhost:3000/static/bands/red-hot-chili-peppers-logo.png",
"BandGenre": {
"id": 793,
"genreId": 562,
"bandId": 265
}
}
]
},
{
"id": 563,
"name": "Alternative Rock",
"bands": [
{
"images": [
"http://localhost:3000/static/bands/red-hot-chili-peppers-1.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-2.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-3.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-4.jpg"
],
"id": 265,
"name": "Red Hot Chili Peppers",
"foundingYear": 1983,
"descriptionEn": "The Red Hot Chili Peppers are an American rock band formed in Los Angeles in 1983, comprising vocalist Anthony Kiedis, bassist Flea, drummer Chad Smith, and guitarist John Frusciante. Their music incorporates elements of alternative rock, funk, punk rock, hard rock, hip hop, and psychedelic rock. Their eclectic range has influenced genres such as funk metal, rap metal, rap rock, and nu metal. With over 120 million records sold worldwide, the Red Hot Chili Peppers are one of the top-selling bands of all time.",
"descriptionDe": "Red Hot Chili Peppers (Abkürzung: RHCP) ist eine 1983 gegründete US-amerikanische Funk- und Alternative-Rockband. Sie zählt zu den kommerziell erfolgreichsten Vertretern des Crossover. Ihr Album Blood Sugar Sex Magik gilt als eines der bedeutendsten dieses Genres.",
"imageMembers": "http://localhost:3000/static/bands/red-hot-chili-peppers-members.jpg",
"logo": "http://localhost:3000/static/bands/red-hot-chili-peppers-logo.png",
"BandGenre": {
"id": 794,
"genreId": 563,
"bandId": 265
}
}
]
}
]
},
"location": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"name": {
"type": "string",
"description": "Name of the genre"
},
"bands": {
"type": "object",
"description": "Bands with this genre object"
}
},
"example": {
"id": 562,
"name": "Funk Rock",
"bands": [
{
"images": [
"http://localhost:3000/static/bands/red-hot-chili-peppers-1.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-2.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-3.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-4.jpg"
],
"id": 265,
"name": "Red Hot Chili Peppers",
"foundingYear": 1983,
"descriptionEn": "The Red Hot Chili Peppers are an American rock band formed in Los Angeles in 1983, comprising vocalist Anthony Kiedis, bassist Flea, drummer Chad Smith, and guitarist John Frusciante. Their music incorporates elements of alternative rock, funk, punk rock, hard rock, hip hop, and psychedelic rock. Their eclectic range has influenced genres such as funk metal, rap metal, rap rock, and nu metal. With over 120 million records sold worldwide, the Red Hot Chili Peppers are one of the top-selling bands of all time.",
"descriptionDe": "Red Hot Chili Peppers (Abkürzung: RHCP) ist eine 1983 gegründete US-amerikanische Funk- und Alternative-Rockband. Sie zählt zu den kommerziell erfolgreichsten Vertretern des Crossover. Ihr Album Blood Sugar Sex Magik gilt als eines der bedeutendsten dieses Genres.",
"imageMembers": "http://localhost:3000/static/bands/red-hot-chili-peppers-members.jpg",
"logo": "http://localhost:3000/static/bands/red-hot-chili-peppers-logo.png",
"BandGenre": {
"id": 793,
"genreId": 562,
"bandId": 265
}
}
]
}
},
"order": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"orderedAt": {
"type": "string",
"description": "Timestamp of order"
},
"tickets": {
"type": "array",
"description": "Array of Ticket objects"
},
"addresses": {
"type": "object",
"description": "Address object"
},
"payment": {
"type": "object",
"description": "Payment object"
},
"account": {
"type": "object",
"description": "Account object"
}
},
"example": {
"id": 112,
"orderedAt": "2024-11-29T12:38:36.381Z",
"shipped": false,
"tickets": [
{
"id": 144,
"orderId": 112,
"orderPrice": 184,
"concertId": 892,
"seatId": 106331,
"concert": {
"id": 892,
"date": "2024-11-30",
"name": "Unlimited Love",
"price": 92,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 170,
"offered": true,
"bandId": 265,
"locationId": 834,
"band": {
"images": [
"http://localhost:3000/static/bands/red-hot-chili-peppers-1.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-2.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-3.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-4.jpg"
],
"id": 265,
"name": "Red Hot Chili Peppers",
"foundingYear": 1983,
"descriptionEn": "The Red Hot Chili Peppers are an American rock band formed in Los Angeles in 1983, comprising vocalist Anthony Kiedis, bassist Flea, drummer Chad Smith, and guitarist John Frusciante. Their music incorporates elements of alternative rock, funk, punk rock, hard rock, hip hop, and psychedelic rock. Their eclectic range has influenced genres such as funk metal, rap metal, rap rock, and nu metal. With over 120 million records sold worldwide, the Red Hot Chili Peppers are one of the top-selling bands of all time.",
"descriptionDe": "Red Hot Chili Peppers (Abkürzung: RHCP) ist eine 1983 gegründete US-amerikanische Funk- und Alternative-Rockband. Sie zählt zu den kommerziell erfolgreichsten Vertretern des Crossover. Ihr Album Blood Sugar Sex Magik gilt als eines der bedeutendsten dieses Genres.",
"imageMembers": "http://localhost:3000/static/bands/red-hot-chili-peppers-members.jpg",
"logo": "http://localhost:3000/static/bands/red-hot-chili-peppers-logo.png"
},
"location": {
"id": 834,
"urlName": "swiss-life-hall",
"name": "Swiss Life Hall",
"address": "Ferdinand-Wilhelm-Fricke-Weg 8",
"cityId": 246,
"imageIndoor": "http://localhost:3000/static/locations/swiss-life-hall-indoor.jpg",
"imageOutdoor": "http://localhost:3000/static/locations/swiss-life-hall-outdoor.jpg",
"layout": 2,
"capacity": 180,
"city": {
"id": 246,
"name": "Hannover",
"country": "Germany"
}
}
},
"seat": {
"id": 106331,
"seatNr": 1,
"seatRowId": 14701,
"seatRow": {
"id": 14701,
"row": 0,
"seatGroupId": 3872,
"seatGroup": {
"id": 3872,
"name": "A",
"surcharge": 30,
"capacity": 40,
"standingArea": true,
"locationId": 834
}
}
}
}
],
"address": {
"id": 342,
"accountId": 255,
"street": "Laportestraße",
"houseNumber": 22,
"postalCode": 30449,
"city": "Hannover"
},
"payment": {
"id": 247,
"accountId": 255,
"bankName": "Deutsche Bank",
"iban": "DE92500105175721645777"
},
"account": {
"id": 255,
"username": "hagemeister93",
"password": "e1e3981e5b0c009c018c5726a4be5eee",
"email": "hagemeister93@gmail.com",
"firstName": "Laurin",
"lastName": "Hagemeister",
"accountRoleId": 1
}
}
},
"useraccount": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"username": {
"type": "string",
"description": "Account username"
},
"password": {
"type": "string",
"description": "Encrypted password"
},
"email": {
"type": "string",
"description": "E-Mail address of user"
},
"firstName": {
"type": "string",
"description": "First name of user"
},
"lastName": {
"type": "string",
"description": "Last name of user"
},
"addresses": {
"type": "array",
"description": "Array of Address objects"
},
"accountRole": {
"type": "object",
"description": "Account role object"
},
"payments": {
"type": "array",
"description": "Array of Payments objects"
}
},
"example": {
"id": 262,
"username": "max",
"password": "06f7a5f329fed099ad36026f9623e6ce",
"email": "titi@didi.de",
"firstName": "Max",
"lastName": "Mustermann",
"accountRoleId": 1,
"addresses": [
{
"id": 352,
"accountId": 262,
"street": "Musterstraße",
"houseNumber": 21,
"postalCode": 30167,
"city": "Hannover"
}
],
"accountRole": {
"id": 1,
"name": "User",
"privilegeBuy": true,
"privilegeAdminPanel": false
},
"payments": [
{
"id": 254,
"accountId": 262,
"bankName": "Deutsche Bank",
"iban": "DE293948484738383829"
}
]
}
},
"exercise": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"nameDe": {
"type": "string",
"description": "German exercise name"
},
"nameEn": {
"type": "string",
"description": "English exercise name"
},
"exerciseNr": {
"type": "number",
"description": "Number of exercise in group"
},
"descriptionDe": {
"type": "string",
"description": "German description text"
},
"descriptionEn": {
"type": "string",
"description": "English description text"
},
"solved": {
"type": "boolean",
"description": "State of solved"
},
"exerciseGroup": {
"type": "object",
"description": "Exercise group object"
}
},
"example": {
"id": 350,
"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": "Create a new account in the online shop",
"solved": true,
"exerciseGroup": {
"id": 113,
"nameDe": "Den Shop kennenlernen",
"nameEn": "Getting to know the shop",
"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": "todo"
}
}
},
"folder": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of folder"
},
"nrOrItems": {
"type": "number",
"description": "Number of files in folder"
}
},
"example": {
"name": "artists",
"description": 41
}
},
"file": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of file"
},
"size": {
"type": "number",
"description": "File size in Bytes"
},
"content": {
"type": "string",
"description": "Text content, only for Text/Code files"
},
"url": {
"type": "string",
"description": "Resource URL"
},
"copyright": {
"type": "object",
"description": "Copyright object"
}
},
"example": {
"name": "alex-turner.jpg",
"size": 551625,
"content": "",
"url": "http://localhost:3000/static/artists/alex-turner.jpg",
"copyright": {
"image": "alex-turner.jpg",
"license": "CC BY 2.0",
"creator": "Raph_PH",
"url": "https://upload.wikimedia.org/wikipedia/commons/9/95/Alex_Turner%2C_Way_Out_West_2018.jpg"
}
}
},
"concert": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The auto-generated id"
},
"date": {
"type": "string",
"description": "Date of the concert"
},
"name": {
"type": "string",
"description": "Name of concert"
},
"price": {
"type": "string",
"description": "Lowest price of concert"
},
"image": {
"type": "number",
"description": "Concert image"
},
"inStock": {
"type": "string",
"description": "Amount of available tickets"
},
"offered": {
"type": "string",
"description": "Display concert in UI"
},
"band": {
"type": "object",
"description": "Band object"
},
"location": {
"type": "object",
"description": "Location object"
}
},
"example": [
{
"id": 892,
"date": "2024-11-30",
"name": "Unlimited Love",
"price": 92,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 169,
"offered": true,
"bandId": 265,
"locationId": 834,
"band": {
"images": [
"http://localhost:3000/static/bands/red-hot-chili-peppers-1.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-2.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-3.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-4.jpg"
],
"id": 265,
"name": "Red Hot Chili Peppers",
"foundingYear": 1983,
"descriptionEn": "The Red Hot Chili Peppers are an American rock band formed in Los Angeles in 1983, comprising vocalist Anthony Kiedis, bassist Flea, drummer Chad Smith, and guitarist John Frusciante. Their music incorporates elements of alternative rock, funk, punk rock, hard rock, hip hop, and psychedelic rock. Their eclectic range has influenced genres such as funk metal, rap metal, rap rock, and nu metal. With over 120 million records sold worldwide, the Red Hot Chili Peppers are one of the top-selling bands of all time.",
"descriptionDe": "Red Hot Chili Peppers (Abkürzung: RHCP) ist eine 1983 gegründete US-amerikanische Funk- und Alternative-Rockband. Sie zählt zu den kommerziell erfolgreichsten Vertretern des Crossover. Ihr Album Blood Sugar Sex Magik gilt als eines der bedeutendsten dieses Genres.",
"imageMembers": "http://localhost:3000/static/bands/red-hot-chili-peppers-members.jpg",
"logo": "http://localhost:3000/static/bands/red-hot-chili-peppers-logo.png"
},
"location": {
"id": 834,
"urlName": "swiss-life-hall",
"name": "Swiss Life Hall",
"address": "Ferdinand-Wilhelm-Fricke-Weg 8",
"imageIndoor": "http://localhost:3000/static/locations/swiss-life-hall-indoor.jpg",
"imageOutdoor": "http://localhost:3000/static/locations/swiss-life-hall-outdoor.jpg",
"layout": 2,
"capacity": 180,
"city": {
"id": 246,
"name": "Hannover",
"country": "Germany"
},
"seatGroups": []
}
}
]
},
"band": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "The auto-generated id"
},
"name": {
"type": "string",
"description": "Name of the band"
},
"foundingYear": {
"type": "string",
"description": "Founding year of the band"
},
"descriptionEn": {
"type": "string",
"description": "English description text"
},
"descriptionDe": {
"type": "string",
"description": "German description text"
},
"imageMembers": {
"type": "string",
"description": "URL to image of band members"
},
"logo": {
"type": "string",
"description": "URL to image of band logo"
},
"genres": {
"type": "array",
"description": "Array of Genre objects which fits the bands music"
},
"concerts": {
"type": "array",
"description": "Array of Concert objects"
},
"nrOfConcerts": {
"type": "number",
"description": "Number of concerts"
},
"rating": {
"type": "number",
"description": "Average rating of the band"
}
},
"example": {
"images": [
"http://localhost:3000/static/bands/red-hot-chili-peppers-1.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-2.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-3.jpg",
"http://localhost:3000/static/bands/red-hot-chili-peppers-4.jpg"
],
"id": 265,
"name": "Red Hot Chili Peppers",
"foundingYear": 1983,
"descriptionEn": "The Red Hot Chili Peppers are an American rock band formed in Los Angeles in 1983, comprising vocalist Anthony Kiedis, bassist Flea, drummer Chad Smith, and guitarist John Frusciante. Their music incorporates elements of alternative rock, funk, punk rock, hard rock, hip hop, and psychedelic rock. Their eclectic range has influenced genres such as funk metal, rap metal, rap rock, and nu metal. With over 120 million records sold worldwide, the Red Hot Chili Peppers are one of the top-selling bands of all time.",
"descriptionDe": "Red Hot Chili Peppers (Abkürzung: RHCP) ist eine 1983 gegründete US-amerikanische Funk- und Alternative-Rockband. Sie zählt zu den kommerziell erfolgreichsten Vertretern des Crossover. Ihr Album Blood Sugar Sex Magik gilt als eines der bedeutendsten dieses Genres.",
"imageMembers": "http://localhost:3000/static/bands/red-hot-chili-peppers-members.jpg",
"logo": "http://localhost:3000/static/bands/red-hot-chili-peppers-logo.png",
"genres": [
{
"name": "Funk Rock"
},
{
"name": "Alternative Rock"
},
{
"name": "Crossover"
}
],
"concerts": [
{
"id": 892,
"date": "2024-11-30",
"name": "Unlimited Love",
"price": 92,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 170,
"offered": true,
"bandId": 265,
"locationId": 834
},
{
"id": 893,
"date": "2024-12-07",
"name": "Unlimited Love",
"price": 92,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 170,
"offered": true,
"bandId": 265,
"locationId": 834
},
{
"id": 894,
"date": "2024-12-11",
"name": "Unlimited Love",
"price": 119.9,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 8736,
"offered": true,
"bandId": 265,
"locationId": 838
},
{
"id": 895,
"date": "2024-12-18",
"name": "Unlimited Love",
"price": 114.9,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 2793,
"offered": true,
"bandId": 265,
"locationId": 842
},
{
"id": 896,
"date": "2024-12-30",
"name": "Unlimited Love",
"price": 124.9,
"image": "http://localhost:3000/static/concerts/unlimited-love-tour.jpg",
"inStock": 3079,
"offered": true,
"bandId": 265,
"locationId": 845
}
],
"nrOfConcerts": 5,
"rating": 4.428571428571429
}
}
}
}
},
"apis": [
"./backend/routes/*.ts"
]
}

File diff suppressed because it is too large Load Diff

381
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "eventmaster", "name": "eventmaster",
"version": "0.1.0", "version": "0.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "eventmaster", "name": "eventmaster",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
@@ -14,6 +14,10 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv": "^6.3.11",
"csv-reader": "^1.0.12",
"exif-js": "^2.3.0",
"exifreader": "^4.25.0",
"express": "^4.21.1", "express": "^4.21.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jspdf": "^2.5.2", "jspdf": "^2.5.2",
@@ -25,6 +29,9 @@
"sequelize": "^6.37.4", "sequelize": "^6.37.4",
"sequelize-typescript": "^2.1.6", "sequelize-typescript": "^2.1.6",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"swagger-autogen": "^2.23.7",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-i18n": "^10.0.4", "vue-i18n": "^10.0.4",
"vue-router": "^4.4.5", "vue-router": "^4.4.5",
@@ -40,6 +47,8 @@
"@types/jsonwebtoken": "^9.0.7", "@types/jsonwebtoken": "^9.0.7",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"@types/node": "^22.9.0", "@types/node": "^22.9.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.7",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@@ -52,6 +61,50 @@
"vue-tsc": "^2.1.10" "vue-tsc": "^2.1.10"
} }
}, },
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.25.7", "version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz",
@@ -1542,6 +1595,12 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"license": "MIT"
},
"node_modules/@malept/cross-spawn-promise": { "node_modules/@malept/cross-spawn-promise": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
@@ -1929,6 +1988,13 @@
"win32" "win32"
] ]
}, },
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@sideway/address": { "node_modules/@sideway/address": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -2134,6 +2200,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": { "node_modules/@types/jsonwebtoken": {
"version": "9.0.7", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
@@ -2252,6 +2324,24 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"node_modules/@types/swagger-jsdoc": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
"integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz",
"integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/validator": { "node_modules/@types/validator": {
"version": "13.12.2", "version": "13.12.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
@@ -3046,7 +3136,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/array-flatten": { "node_modules/array-flatten": {
@@ -3631,6 +3720,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"license": "MIT"
},
"node_modules/canvg": { "node_modules/canvg": {
"version": "3.0.10", "version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
@@ -4284,6 +4379,48 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/csv": {
"version": "6.3.11",
"resolved": "https://registry.npmjs.org/csv/-/csv-6.3.11.tgz",
"integrity": "sha512-a8bhT76Q546jOElHcTrkzWY7Py925mfLO/jqquseH61ThOebYwOjLbWHBqdRB4K1VpU36sTyIei6Jwj7QdEZ7g==",
"license": "MIT",
"dependencies": {
"csv-generate": "^4.4.2",
"csv-parse": "^5.6.0",
"csv-stringify": "^6.5.2",
"stream-transform": "^3.3.3"
},
"engines": {
"node": ">= 0.1.90"
}
},
"node_modules/csv-generate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.4.2.tgz",
"integrity": "sha512-W6nVsf+rz0J3yo9FOjeer7tmzBJKaTTxf7K0uw6GZgRocZYPVpuSWWa5/aoWWrjQZj4/oNIKTYapOM7hiNjVMA==",
"license": "MIT"
},
"node_modules/csv-parse": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz",
"integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
"license": "MIT"
},
"node_modules/csv-reader": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/csv-reader/-/csv-reader-1.0.12.tgz",
"integrity": "sha512-0AAgazKJUywtjvZbclNuovIiQY/WyvojWw15Y2k3kPixE+pDiOFnfg5FcH3CfDqqnrB2f3p5oPAc446EXD01Tw==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/csv-stringify": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.2.tgz",
"integrity": "sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==",
"license": "MIT"
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -4324,6 +4461,15 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/defaults": { "node_modules/defaults": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@@ -4552,6 +4698,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.5.7", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz",
@@ -5014,6 +5172,15 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/etag": { "node_modules/etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -5023,6 +5190,32 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/exif-js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/exif-js/-/exif-js-2.3.0.tgz",
"integrity": "sha512-1Og9pAzG2FZRVlaavH8bB8BTeHcjMdJhKmeQITkX+uLRCD0xPtKAdZ2clZmQdJ56p9adXtJ8+jwrGp/4505lYg==",
"license": "MIT"
},
"node_modules/exifreader": {
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.25.0.tgz",
"integrity": "sha512-lPyPXWTUuYgoKdKf3rw2EDoE9Zl7xHoy/ehPNeQ4gFVNLzfLyNMP4oEI+sP0/Czp5r/2i7cFhqg5MHsl4FYtyw==",
"hasInstallScript": true,
"license": "MPL-2.0",
"optionalDependencies": {
"@xmldom/xmldom": "^0.9.4"
}
},
"node_modules/exifreader/node_modules/@xmldom/xmldom": {
"version": "0.9.6",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.6.tgz",
"integrity": "sha512-Su4xcxR0CPGwlDHNmVP09fqET9YxbyDXHaSob6JlBH7L6reTYaeim6zbk9o08UarO0L5GTRo3uzl0D+9lSxmvw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14.6"
}
},
"node_modules/expand-template": { "node_modules/expand-template": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -6138,7 +6331,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -6180,7 +6372,6 @@
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
@@ -6372,6 +6563,12 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"license": "MIT"
},
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -6384,6 +6581,12 @@
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"license": "MIT"
},
"node_modules/lodash.isinteger": { "node_modules/lodash.isinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -6408,6 +6611,12 @@
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/lodash.once": { "node_modules/lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -7157,6 +7366,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT",
"peer": true
},
"node_modules/ora": { "node_modules/ora": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
@@ -8590,6 +8806,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/stream-transform": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.3.3.tgz",
"integrity": "sha512-dALXrXe+uq4aO5oStdHKlfCM/b3NBdouigvxVPxCdrMRAU6oHh3KNss20VbTPQNQmjAHzZGKGe66vgwegFEIog==",
"license": "MIT"
},
"node_modules/streamsearch": { "node_modules/streamsearch": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -8738,6 +8960,116 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/swagger-autogen": {
"version": "2.23.7",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz",
"integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==",
"license": "MIT",
"dependencies": {
"acorn": "^7.4.1",
"deepmerge": "^4.2.2",
"glob": "^7.1.7",
"json5": "^2.2.3"
}
},
"node_modules/swagger-autogen/node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"license": "MIT",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"license": "MIT",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.18.2",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz",
"integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"license": "MIT",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/tar": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -9564,6 +9896,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -9627,6 +9968,36 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"license": "MIT",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "eventmaster", "name": "eventmaster",
"version": "0.1.0", "version": "0.3.0",
"author": "Tobias Zoghaib", "author": "Tobias Zoghaib",
"description": "Hackable ticket store for educational purposes", "description": "Hackable ticket store for educational purposes",
"license": "MIT", "license": "MIT",
@@ -38,6 +38,10 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv": "^6.3.11",
"csv-reader": "^1.0.12",
"exif-js": "^2.3.0",
"exifreader": "^4.25.0",
"express": "^4.21.1", "express": "^4.21.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jspdf": "^2.5.2", "jspdf": "^2.5.2",
@@ -49,6 +53,9 @@
"sequelize": "^6.37.4", "sequelize": "^6.37.4",
"sequelize-typescript": "^2.1.6", "sequelize-typescript": "^2.1.6",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"swagger-autogen": "^2.23.7",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-i18n": "^10.0.4", "vue-i18n": "^10.0.4",
"vue-router": "^4.4.5", "vue-router": "^4.4.5",
@@ -64,6 +71,8 @@
"@types/jsonwebtoken": "^9.0.7", "@types/jsonwebtoken": "^9.0.7",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"@types/node": "^22.9.0", "@types/node": "^22.9.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.7",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",

View File

@@ -26,7 +26,6 @@ watch(() => preferencesStore.language, () => {
// Watch for theme change // Watch for theme change
watch(() => preferencesStore.theme, () => { watch(() => preferencesStore.theme, () => {
theme.global.name.value = preferencesStore.theme theme.global.name.value = preferencesStore.theme
feedbackStore.addSnackbar(BannerStateEnum.ERROR)
}) })
// Watch for 404 page directions // Watch for 404 page directions
@@ -80,9 +79,33 @@ function calcMargin(i) {
<v-sheet color="sheet" height="100%"> <v-sheet color="sheet" height="100%">
<router-view></router-view> <router-view></router-view>
</v-sheet> </v-sheet>
</v-container> </v-container>
<v-btn
fab
dark
fixed
bottom
right
color="primary"
>
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
<company-footer /> <company-footer />
</v-main> </v-main>
</v-app> </v-app>
<!-- Test Environment sticker in bottom right corner -->
<v-sheet
color="error"
position="fixed"
location="bottom right"
class="pa-3 mb-12 mr-n16 text-center text-h5"
width="300"
style="rotate: 315deg; z-index: 1008;"
>
{{ $t('misc.testEnvironment') }}
</v-sheet>
</template> </template>

View File

@@ -40,12 +40,16 @@ defineProps({
type="image" type="image"
:loading="loading" :loading="loading"
> >
<v-img <v-sheet
:src="image" color="sheet"
:height="height" >
:width="height" <v-img
cover :src="image"
/> :height="height"
:width="height"
cover
/>
</v-sheet>
</v-skeleton-loader> </v-skeleton-loader>
<v-skeleton-loader <v-skeleton-loader
@@ -55,6 +59,7 @@ defineProps({
> >
<v-sheet <v-sheet
:height="height" :height="height"
color="sheet"
width="100%" width="100%"
class="text-center d-flex justify-center align-center" class="text-center d-flex justify-center align-center"
> >
@@ -75,6 +80,8 @@ defineProps({
> >
<v-sheet <v-sheet
:height="height" :height="height"
color="sheet"
class="w-100"
> >
<div> <div>
<div class="text-h4 font-weight-black pt-2 h-100"> <div class="text-h4 font-weight-black pt-2 h-100">

View File

@@ -12,7 +12,7 @@ defineProps({
</script> </script>
<template> <template>
<v-card variant="outlined" class="my-1 mx-2 px-2"> <v-card variant="outlined" class="my-1 px-2">
<v-row class="d-flex justify-center align-center"> <v-row class="d-flex justify-center align-center">
<v-col class="text-caption text-left" v-if="descriptionText.length > 0"> <v-col class="text-caption text-left" v-if="descriptionText.length > 0">
{{ descriptionText }} {{ descriptionText }}

View File

@@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ import { loadLicense } from '@/scripts/imageScripts';
import { ref, watch } from 'vue';
const props = defineProps({
image: String, image: String,
errorImage: { errorImage: {
type: String, type: String,
@@ -16,6 +19,14 @@ defineProps({
}, },
loading: Boolean loading: Boolean
}) })
const license = ref("")
loadLicense(props.image)
.then(result => {
console.log(result)
license.value = result
})
</script> </script>
<template> <template>

View File

@@ -20,7 +20,7 @@ function confirmPressed() {
<template> <template>
<action-dialog <action-dialog
:title="title" :title="title"
max-width="400" max-width="500"
v-model="showDialog" v-model="showDialog"
persistent persistent
> >
@@ -36,7 +36,7 @@ function confirmPressed() {
<outlined-button <outlined-button
@click="showDialog = false" @click="showDialog = false"
prepend-icon="mdi-close" prepend-icon="mdi-close"
color="orange" color="warning"
:loading="loading" :loading="loading"
> >
{{ $t("misc.actions.cancel") }} {{ $t("misc.actions.cancel") }}
@@ -45,7 +45,7 @@ function confirmPressed() {
<outlined-button <outlined-button
@click="confirmPressed" @click="confirmPressed"
prepend-icon="mdi-check" prepend-icon="mdi-check"
color="red" color="error"
:loading="loading" :loading="loading"
> >
{{ $t("misc.actions.confirm") }} {{ $t("misc.actions.confirm") }}

View File

@@ -8,20 +8,28 @@ defineProps({
<template> <template>
<v-row class="pt-3 d-none d-md-flex"> <v-row class="pt-3 d-none d-md-flex">
<!-- Left line -->
<v-col class="d-flex justify-center align-center"> <v-col class="d-flex justify-center align-center">
<v-sheet height="12" width="100%" color="primary" class="rounded-s-lg" /> <v-sheet height="12" width="100%" color="primary" class="rounded-s-lg" />
</v-col> </v-col>
<!-- Title -->
<v-col class="v-col-auto"> <v-col class="v-col-auto">
<v-skeleton-loader <v-skeleton-loader
type="heading" type="heading"
:loading="loading" :loading="loading"
width="300" width="300"
> >
<span class="text-h4">{{ title }}</span> <v-sheet
class="text-h4"
color="sheet"
>
{{ title }}
</v-sheet>
</v-skeleton-loader> </v-skeleton-loader>
</v-col> </v-col>
<!-- Right line -->
<v-col class="d-flex justify-center align-center"> <v-col class="d-flex justify-center align-center">
<v-sheet height="12" width="100%" color="primary" class="rounded-e-lg" /> <v-sheet height="12" width="100%" color="primary" class="rounded-e-lg" />
</v-col> </v-col>

View File

@@ -27,7 +27,7 @@ exerciseStore.getAllExercises()
:content="basketStore.itemsInBasket.reduce((tot, item) => { :content="basketStore.itemsInBasket.reduce((tot, item) => {
return tot + item.seats.length return tot + item.seats.length
}, 0)" }, 0)"
color="red" offset-x="8" offset-y="8"> color="error" offset-x="8" offset-y="8">
<v-btn variant="plain" icon="mdi-cart" to="/basket" /> <v-btn variant="plain" icon="mdi-cart" to="/basket" />
</v-badge> </v-badge>
</div> </div>

View File

@@ -37,7 +37,7 @@ defineProps({
{{ secondLine }} {{ secondLine }}
</v-skeleton-loader> </v-skeleton-loader>
<template #actions> <template #actions v-if="!$slots.actions">
<outlined-button <outlined-button
@click="router.push(buttonRoute)" @click="router.push(buttonRoute)"
:loading="loading" :loading="loading"
@@ -45,6 +45,10 @@ defineProps({
{{ $t('misc.actions.more') }} {{ $t('misc.actions.more') }}
</outlined-button> </outlined-button>
</template> </template>
<template #actions v-else>
<slot name="actions"></slot>
</template>
</card-view> </card-view>
</v-col> </v-col>
</template> </template>

View File

@@ -56,6 +56,7 @@ defineProps({
type="heading" type="heading"
:loading="loading" :loading="loading"
width="500" width="500"
class="text-white"
> >
<span class="text-h3 font-weight-bold"> <span class="text-h3 font-weight-bold">
{{ title }} {{ title }}
@@ -77,11 +78,18 @@ defineProps({
<!-- Description --> <!-- Description -->
<p class="text-h6 text-medium-emphasis" v-if="!$slots.description"> <p
class="text-h6 text-white"
style="opacity: 0.7;"
v-if="!$slots.description"
>
{{ description }} {{ description }}
</p> </p>
<p class="text-h6 text-medium-emphasis"> <p
class="text-h6 text-white"
style="opacity: 0.7;"
>
<slot name="description"></slot> <slot name="description"></slot>
</p> </p>
</v-skeleton-loader> </v-skeleton-loader>

View File

@@ -3,14 +3,41 @@ import { AccountModel } from "../models/user/accountModel"
const BASE_URL = "http://localhost:3000/accounts" const BASE_URL = "http://localhost:3000/accounts"
export async function fetchAllAccounts() { /**
return await axios.get(BASE_URL) * Fetch all accounts from server
*
* @param token Validation token of current logged in user. User needs to have the right privileges
*
* @returns Response from server with list of all account body
*/
export async function fetchAllAccounts(token: string) {
return await axios.get(BASE_URL, {
headers: {
"Authorization": token
}
})
} }
export async function login(username: string, password: string) { /**
* Start the login process
*
* @param username Username of the account
* @param password Password of the account
*
* @returns Response from server with token body
*/
export async function getLogin(username: string, password: string) {
return await axios.get(BASE_URL + "/login?username=" + username + "&password=" + password) return await axios.get(BASE_URL + "/login?username=" + username + "&password=" + password)
} }
/**
* Get all data about a single account
*
* @param token Validation token
*
* @returns Response from server with account body
*/
export async function getAccount(token: string) { export async function getAccount(token: string) {
return await axios.get(BASE_URL + "/account", { return await axios.get(BASE_URL + "/account", {
headers: { headers: {
@@ -19,18 +46,48 @@ export async function getAccount(token: string) {
}) })
} }
/**
* Register a new account in servers database
*
* @param account Account data for new dataset
*
* @returns Response from server
*/
export async function registerAccount(account: AccountModel) { export async function registerAccount(account: AccountModel) {
return await axios.post(BASE_URL, account) return await axios.post(BASE_URL + "/account", account)
} }
/**
* Update data of an account
*
* @param account Account data to update
* @param token Validation token
*
* @returns Response from server
*/
export async function updateAccount(account: AccountModel, token: string) { export async function updateAccount(account: AccountModel, token: string) {
return await axios.patch(BASE_URL, account, { return await axios.patch(BASE_URL + "/account", account, {
headers: { headers: {
"Authorization": token "Authorization": token
} }
}) })
} }
export async function deleteAccount(account: AccountModel) { /**
return await axios.delete(BASE_URL + "/" + account.id) * Delete an account in servers database
*
* @param account Account to delete
* @param token Validation token
*
* @returns Response from server
*/
export async function deleteAccount(account: AccountModel, token: string) {
return await axios.delete(BASE_URL + "/account", {
headers: {
"Authorization": token
},
data: {
account: account
}
})
} }

View File

@@ -1,17 +1,22 @@
import axios from "axios" import axios from "axios"
import { BasketItemModel } from "../models/ordering/basketItemModel" import { BasketItemModel } from "../models/ordering/basketItemModel"
import { OrderApiModel } from "../models/apiEndpoints/orderApiModel"
const BASE_URL = "http://localhost:3000/orders" const BASE_URL = "http://localhost:3000/orders"
export async function fetchUserOrders(userId: number) { export async function fetchUserOrders(userId: number, token: string) {
return axios.get(BASE_URL + "/" + userId) return axios.get(BASE_URL + "?id=" + userId, {
headers: {
"Authorization": token
}
})
} }
export async function createOrder( export async function createOrder(
accountId: number,
basketItem: Array<BasketItemModel>, basketItem: Array<BasketItemModel>,
paymentId: number, paymentId: number,
addressId: number addressId: number,
token: string
) { ) {
let tickets = [] let tickets = []
@@ -25,21 +30,25 @@ export async function createOrder(
} }
} }
console.log({
accountId: accountId,
tickets: tickets,
paymentId: paymentId,
addressId: addressId
})
return axios.post(BASE_URL, { return axios.post(BASE_URL, {
accountId: accountId,
tickets: tickets, tickets: tickets,
paymentId: paymentId, paymentId: paymentId,
addressId: addressId addressId: addressId,
}, {
headers: {
"Authorization": token
}
}) })
} }
export async function fetchAllOrders() { export async function fetchAllOrders(token: string) {
return axios.get(BASE_URL) return axios.get(BASE_URL, {
headers: {
"Authorization": token
}
})
}
export async function patchOrder(order: OrderApiModel) {
return axios.patch(BASE_URL, order)
} }

View File

@@ -1,12 +1,14 @@
export enum BannerStateEnum { export enum BannerStateEnum {
////////// System feedback ////////// ////////// System feedback //////////
// Some error // Unknown error
ERROR, ERROR,
BASKETPRODUCTADDED, // Ticket added to basket
BASKETTICKETADDED,
BASKETPRODUCTREMOVED, // Ticket removed from basket
BASKETTICKETREMOVED,
////////// Exercise feedback ////////// ////////// Exercise feedback //////////
@@ -30,6 +32,8 @@ export enum BannerStateEnum {
EXERCISESOLVED24, EXERCISESOLVED24,
EXERCISESOLVED25, EXERCISESOLVED25,
EXERCISESOLVED26,
EXERCISESOLVED31, EXERCISESOLVED31,
@@ -63,11 +67,20 @@ export enum BannerStateEnum {
ACCOUNTREGISTERERROR, ACCOUNTREGISTERERROR,
// Status: 409 Conflict // Status: 409 Conflict
ACCOUNTREGISTERUSERNAMEINUSE, ACCOUNTREGISTERUSERNAMEORMAILINUSE,
// Status: 200 OK // Status: 200 OK
ACCOUNTUPDATESUCCESSFUL, ACCOUNTUPDATESUCCESSFUL,
// Local check on unvalid username
ACCOUNTUSERNAMETOOSHORT,
// Local check on unvalid password
ACCOUNTPASSWORDTOOSHORT,
// Local check on unvalid mail address
ACCOUNTMAILADDRESSUNVALID,
// No status code, runs in local cache // No status code, runs in local cache
ACCOUNTLOGOUTSUCCESSFUL, ACCOUNTLOGOUTSUCCESSFUL,

View File

@@ -0,0 +1,13 @@
export class FilesApiModel {
name: string
size: number
content: string
url: string
copyright: CopyRightModel = new CopyRightModel()
}
class CopyRightModel {
license: string = ""
creator: string = ""
url: string = ""
}

View File

@@ -34,7 +34,7 @@ defineProps({
<outlined-button <outlined-button
v-if="!hideAddButton" v-if="!hideAddButton"
prepend-icon="mdi-plus" prepend-icon="mdi-plus"
color="green" color="success"
:disabled="fetchInProgress" :disabled="fetchInProgress"
@click="onAddClick()" @click="onAddClick()"
> >

View File

@@ -74,7 +74,7 @@
"emailIsNotValid": "Ungültige E-Mail Addresse", "emailIsNotValid": "Ungültige E-Mail Addresse",
"emailRequired": "E-Mail-Adresse benötigt", "emailRequired": "E-Mail-Adresse benötigt",
"accountManagement": "Account verwalten", "accountManagement": "Account verwalten",
"accountManagementDescription": "Persönliche Daten, Adressen, Bezahlmethoden", "accountManagementDescription": "Persönliche Daten, Konto löschen",
"login": { "login": {
"pleaseLoginToOrder": "Bitte anmelden zum bestellen", "pleaseLoginToOrder": "Bitte anmelden zum bestellen",
"backToLogin": "Zurück zum Login", "backToLogin": "Zurück zum Login",
@@ -109,7 +109,16 @@
"addNewAccount": "Neuen Account hinzufügen", "addNewAccount": "Neuen Account hinzufügen",
"accountRole": "Account Rolle", "accountRole": "Account Rolle",
"noRealPaymentsNeeded": "Keine echten Kontodaten nötig!", "noRealPaymentsNeeded": "Keine echten Kontodaten nötig!",
"administrator": "Administrator | Administratoren" "administrator": "Administrator | Administratoren",
"managePaymentsDescription": "Bezahlarten hinzufügen, ändern, löschen",
"paymentsManagement": "Bezahlarten verwalten",
"payments": {
"editPayment": "Bezahlart bearbeiten",
"editAddress": "Adresse bearbeiten"
},
"addressManagementDetails": "Adressen hinzufügen, ändern, löschen",
"addressManagement": "Adressen verwalten",
"sessionTime": "Session time"
}, },
"order": { "order": {
"oclock": "Uhr", "oclock": "Uhr",
@@ -154,7 +163,7 @@
"factoryReset": { "factoryReset": {
"factoryReset": "Zurücksetzen auf Werkseinstellungen", "factoryReset": "Zurücksetzen auf Werkseinstellungen",
"dialog": { "dialog": {
"title": "Aus Werkseinstellungen zurücksetzen?", "title": "Auf Werkseinstellungen zurücksetzen?",
"description": "Sollen alle Einstellungen und Daten auf Werkseinstellungen zurückgesetzt werden? Alle Änderungen und Fortschritte gehen verloren!" "description": "Sollen alle Einstellungen und Daten auf Werkseinstellungen zurückgesetzt werden? Alle Änderungen und Fortschritte gehen verloren!"
} }
} }
@@ -173,7 +182,6 @@
"exerciseProgressResetSuccessful": "Aufgabenfortschritt erfolgreich zurück gesetzt!", "exerciseProgressResetSuccessful": "Aufgabenfortschritt erfolgreich zurück gesetzt!",
"registerSuccessful": "Account erfolgreich erstellt!", "registerSuccessful": "Account erfolgreich erstellt!",
"registerError": "Fehler beim Erstellen des Accounts", "registerError": "Fehler beim Erstellen des Accounts",
"usernameInUse": "Der Accountname ist bereits vergeben!",
"accountUpdated": "Account erfolgreich aktualisiert", "accountUpdated": "Account erfolgreich aktualisiert",
"logoutSuccessful": "Logout erfolgreich", "logoutSuccessful": "Logout erfolgreich",
"orderPlaceSuccessfull": "Bestellung erfolgreich aufgegeben", "orderPlaceSuccessfull": "Bestellung erfolgreich aufgegeben",
@@ -187,7 +195,11 @@
"genreDeleteError": "Fehler beim Löschen des Genres", "genreDeleteError": "Fehler beim Löschen des Genres",
"genreDeleteSuccessful": "Genre erfolgreich gelöscht", "genreDeleteSuccessful": "Genre erfolgreich gelöscht",
"genreSavedError": "Fehler beim Speichern des Genres", "genreSavedError": "Fehler beim Speichern des Genres",
"genreSavedSuccessful": "Genre erfolgreich gespeichert" "genreSavedSuccessful": "Genre erfolgreich gespeichert",
"accountPasswordTooShort": "Passwort ist zu kurz",
"accountUsernameTooShort": "Username ist zu kurz",
"accountMailAddressUnvalid": "Mail-Adresse ungültig",
"usernameOrMailInUse": "Der Accountname und/oder die Mail-Adresse sind bereits vergeben!"
}, },
"misc": { "misc": {
"404": { "404": {
@@ -218,7 +230,7 @@
"validation": { "validation": {
"required": "Darf nicht leer bleiben", "required": "Darf nicht leer bleiben",
"noDigitsAllowed": "Zahlen sind nicht erlaubt", "noDigitsAllowed": "Zahlen sind nicht erlaubt",
"notEnoughChars": "Nicht wenige Zeichen", "notEnoughChars": "Nicht genug Zeichen",
"tooMuchChars": "Zu viele Zeichen", "tooMuchChars": "Zu viele Zeichen",
"onlyDigitsAllowed": "Nur Zahlen erlaubt", "onlyDigitsAllowed": "Nur Zahlen erlaubt",
"digitsAtStartNeeded": "Muss mit einer Zahl beginnen" "digitsAtStartNeeded": "Muss mit einer Zahl beginnen"
@@ -236,7 +248,8 @@
"connectToServer": "Server", "connectToServer": "Server",
"database": "Datenbank", "database": "Datenbank",
"exercises": "Aufgaben", "exercises": "Aufgaben",
"userData": "Persönliche Daten" "userData": "Persönliche Daten",
"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", "user": "Angaben zur Person",
"registrationNumber": "Matrikelnummer", "registrationNumber": "Matrikelnummer",
@@ -244,7 +257,14 @@
"chooseFile": "Datei auswählen", "chooseFile": "Datei auswählen",
"chooseDestinationFolder": "Zielordner auswählen", "chooseDestinationFolder": "Zielordner auswählen",
"upload": "Hochladen", "upload": "Hochladen",
"fulfillYourPersonalDataFirst": "Gehe zu den Einstellungen und fülle deinen Namen und deine Matrikelnummer aus" "fulfillYourPersonalDataFirst": "Gehe zu den Einstellungen und fülle deinen Namen und deine Matrikelnummer aus",
"testEnvironment": "Testumgebung",
"search": {
"empty": {
"headline": "So leer hier..."
},
"searchterm": "Suchbegriff"
}
}, },
"genre": { "genre": {
"withoutBand": "ohne Band" "withoutBand": "ohne Band"

View File

@@ -74,7 +74,7 @@
"emailIsNotValid": "E-Mail not valid", "emailIsNotValid": "E-Mail not valid",
"emailRequired": "E-Mail required", "emailRequired": "E-Mail required",
"accountManagement": "Manage Account", "accountManagement": "Manage Account",
"accountManagementDescription": "Personal data, addresses, payments", "accountManagementDescription": "Personal data, delete account",
"login": { "login": {
"pleaseLoginToOrder": "Please login to order", "pleaseLoginToOrder": "Please login to order",
"backToLogin": "Back to Login", "backToLogin": "Back to Login",
@@ -109,7 +109,16 @@
"addNewAccount": "Add new account", "addNewAccount": "Add new account",
"accountRole": "Account Role", "accountRole": "Account Role",
"noRealPaymentsNeeded": "No real payment data required!", "noRealPaymentsNeeded": "No real payment data required!",
"administrator": "Administrator" "administrator": "Administrator",
"managePaymentsDescription": "Add, change, remove payments",
"paymentsManagement": "Manage payments",
"payments": {
"editPayment": "Edit Payment",
"editAddress": "Edit address"
},
"addressManagementDetails": "Add, change, remove addresses",
"addressManagement": "Manage addresses",
"sessionTime": "Session time"
}, },
"order": { "order": {
"oclock": "o'clock", "oclock": "o'clock",
@@ -173,7 +182,6 @@
"exerciseProgressResetSuccessful": "Exercise progress successfully resetted!", "exerciseProgressResetSuccessful": "Exercise progress successfully resetted!",
"registerSuccessful": "Account successfully created!", "registerSuccessful": "Account successfully created!",
"registerError": "Error on register account", "registerError": "Error on register account",
"usernameInUse": "The username is already in use!",
"accountUpdated": "Account successfully updated", "accountUpdated": "Account successfully updated",
"logoutSuccessful": "Logout successfull", "logoutSuccessful": "Logout successfull",
"orderPlaceSuccessfull": "Order successfully placed", "orderPlaceSuccessfull": "Order successfully placed",
@@ -187,7 +195,11 @@
"genreDeleteError": "Error on deleting Genre", "genreDeleteError": "Error on deleting Genre",
"genreDeleteSuccessful": "Genre successfully deleted", "genreDeleteSuccessful": "Genre successfully deleted",
"genreSavedError": "Error on saving genre", "genreSavedError": "Error on saving genre",
"genreSavedSuccessful": "Genre successfully saved" "genreSavedSuccessful": "Genre successfully saved",
"accountPasswordTooShort": "Password too short",
"accountUsernameTooShort": "Username too short",
"accountMailAddressUnvalid": "Mail-Address unvalid",
"usernameOrMailInUse": "The username and/or the mail address are already in use!"
}, },
"misc": { "misc": {
"404": { "404": {
@@ -236,7 +248,8 @@
"connectToServer": "Server", "connectToServer": "Server",
"database": "Database", "database": "Database",
"exercises": "Exercises", "exercises": "Exercises",
"userData": "User data" "userData": "User data",
"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", "user": "About person",
"registrationNumber": "Matrikel number", "registrationNumber": "Matrikel number",
@@ -244,7 +257,14 @@
"chooseFile": "Choose file", "chooseFile": "Choose file",
"chooseDestinationFolder": "Choose destination folder", "chooseDestinationFolder": "Choose destination folder",
"upload": "Upload", "upload": "Upload",
"fulfillYourPersonalDataFirst": "Go to settings and enter your name and the right register number" "fulfillYourPersonalDataFirst": "Go to settings and enter your name and the right register number",
"testEnvironment": "Test Environment",
"search": {
"empty": {
"headline": "So empty here..."
},
"searchterm": "Search term"
}
}, },
"genre": { "genre": {
"withoutBand": "without Band" "withoutBand": "without Band"

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import { getIbanRules, getNumberStartRules, getPostalRules, getStringRules } from '@/scripts/validationRules';
import { useAccountStore } from '@/stores/account.store';
import cardViewOneLine from '@/components/basics/cardViewOneLine.vue';
import { ref } from 'vue';
const valid = ref(false)
const accountStore = useAccountStore()
</script>
<template>
<action-dialog
v-model="accountStore.showEditDialog"
max-width="800"
:title="$t('account.payments.editAddress')"
>
<v-container>
<v-form v-model="valid">
<v-row class="pt-5">
<v-col>
<v-text-field
:label="$t('account.userData.street')"
v-model="accountStore.address.street"
:rules="getStringRules()"
variant="outlined"
clearable
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.userData.houseNumber')"
v-model="accountStore.address.houseNumber"
:rules="getNumberStartRules()"
variant="outlined"
clearable
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('account.userData.postalCode')"
v-model="accountStore.address.postalCode"
:rules="getPostalRules()"
variant="outlined"
clearable
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.userData.placeOfResidence')"
v-model="accountStore.address.city"
:rules="getStringRules()"
variant="outlined"
clearable
/>
</v-col>
</v-row>
</v-form>
</v-container>
<template #actions>
<outlined-button
color="success"
prepend-icon="mdi-content-save"
:disabled="!valid"
:loading="accountStore.fetchInProgress"
@click="accountStore.saveAddress"
>
{{ $t('misc.actions.save') }}
</outlined-button>
</template>
</action-dialog>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import dataLayout from '@/layouts/dataLayout.vue';
import { useAccountStore } from '@/stores/account.store';
import { useFeedbackStore } from '@/stores/feedback.store';
import addressEditDialog from './addressEditDialog.vue';
const accountStore = useAccountStore()
const feedbackStore = useFeedbackStore()
const headers = [
{ title: feedbackStore.i18n.t('account.userData.street'), value: "street" },
{ 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: "Aktionen", value: "actions", width: 130 }
]
accountStore.refreshAccount()
</script>
<template>
<data-layout
:add-button-string="$t('misc.actions.add')"
:fetch-in-progress="accountStore.fetchInProgress"
:on-add-click="() => { accountStore.newAddress() }"
>
<v-data-table
:headers="headers"
:items="accountStore.userAccount.addresses"
:loading="accountStore.fetchInProgress"
>
<template #item.actions="{ item }">
<v-btn
icon="mdi-pencil"
variant="plain"
color="orange"
@click="accountStore.editAddress(item)"
/>
<v-btn
icon="mdi-delete"
variant="plain"
color="red"
@click="accountStore.removeAddress(item)"
/>
</template>
</v-data-table>
</data-layout>
<address-edit-dialog />
</template>

View File

@@ -81,7 +81,6 @@ const stringRules = [
v-model="accountStore.userAccount.firstName" v-model="accountStore.userAccount.firstName"
variant="outlined" variant="outlined"
:rules="stringRules" :rules="stringRules"
hide-details
/> />
</v-col> </v-col>
<v-col> <v-col>
@@ -90,7 +89,6 @@ const stringRules = [
v-model="accountStore.userAccount.lastName" v-model="accountStore.userAccount.lastName"
variant="outlined" variant="outlined"
:rules="stringRules" :rules="stringRules"
hide-details
/> />
</v-col> </v-col>
</v-row> </v-row>

View File

@@ -18,7 +18,7 @@ const accountStore = useAccountStore()
<v-col class="d-flex justify-center align-center"> <v-col class="d-flex justify-center align-center">
<outlined-button <outlined-button
prepend-icon="mdi-delete" prepend-icon="mdi-delete"
color="red" color="error"
:loading="accountStore.fetchInProgress" :loading="accountStore.fetchInProgress"
@click="showConfirmDialog = true" @click="showConfirmDialog = true"
> >
@@ -29,7 +29,7 @@ const accountStore = useAccountStore()
<v-col class="d-flex justify-center align-center"> <v-col class="d-flex justify-center align-center">
<outlined-button <outlined-button
prepend-icon="mdi-content-save" prepend-icon="mdi-content-save"
color="green" color="success"
:loading="accountStore.fetchInProgress" :loading="accountStore.fetchInProgress"
@click="accountStore.updateAccount()" @click="accountStore.updateAccount()"
> >

View File

@@ -1,111 +0,0 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import { useAccountStore } from '@/stores/account.store';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import { AddressModel } from '@/data/models/user/addressModel';
import { getNumberStartRules, getPostalRules, getStringRules } from '@/scripts/validationRules';
const accountStore = useAccountStore()
</script>
<template>
<card-view
icon="mdi-home"
:title="$t('account.userData.address', 2)"
>
<v-expansion-panels v-if="accountStore.userAccount.addresses.length > 0">
<v-expansion-panel
v-for="address in accountStore.userAccount.addresses"
color="primary"
>
<template #title>
<div v-if="address.street != undefined">
{{ address.street }}
</div>
&nbsp;
<div v-if="address.houseNumber != undefined">
{{ address.houseNumber }}
</div>
</template>
<template #text>
<v-row class="pt-5">
<v-col>
<v-text-field
:label="$t('account.userData.street')"
v-model="address.street"
:rules="getStringRules()"
variant="outlined"
clearable
hide-details
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.userData.houseNumber')"
v-model="address.houseNumber"
:rules="getNumberStartRules()"
variant="outlined"
clearable
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('account.userData.postalCode')"
v-model="address.postalCode"
:rules="getPostalRules()"
variant="outlined"
clearable
hide-details
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.userData.placeOfResidence')"
v-model="address.city"
:rules="getStringRules()"
variant="outlined"
clearable
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-center align-center">
<outlined-button
@click="accountStore.removeAddress(address)"
color="red"
prepend-icon="mdi-delete"
>
{{ $t('misc.actions.remove') }}
</outlined-button>
</v-col>
</v-row>
</template>
</v-expansion-panel>
</v-expansion-panels>
<v-empty-state
v-else
:title="$t('account.noAddresses')"
icon="mdi-home-off"
/>
<template #actions>
<outlined-button
@click="accountStore.userAccount.addresses.push(new AddressModel())"
prepend-icon="mdi-plus"
color="green"
>
{{ $t('misc.actions.add') }}
</outlined-button>
</template>
</card-view>
</template>

View File

@@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import accountDataCard from './accountDataCard.vue'; import accountDataCard from './accountDataCard.vue';
import accountManagingCard from './accountManagingCard.vue'; import accountManagingCard from './accountManagingCard.vue';
import addressesCard from './addressesCard.vue';
import paymentsCard from './paymentsCard.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import accountSubPageLayout from '@/layouts/accountSubPageLayout.vue'; import accountSubPageLayout from '@/layouts/accountSubPageLayout.vue';
@@ -17,18 +15,6 @@ const router = useRouter()
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col>
<addresses-card />
</v-col>
</v-row>
<v-row>
<v-col>
<payments-card />
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<account-managing-card /> <account-managing-card />

View File

@@ -1,97 +0,0 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import { useAccountStore } from '@/stores/account.store';
import outlinedButton from '@/components/basics/outlinedButton.vue';
import { PaymentModel } from '@/data/models/user/paymentModel';
import { getIbanRules, getStringRules } from '@/scripts/validationRules';
import cardViewOneLine from '@/components/basics/cardViewOneLine.vue';
const accountStore = useAccountStore()
</script>
<template>
<card-view
icon="mdi-currency-usd"
:title="$t('account.userData.payment', 2)"
>
<v-row>
<v-col>
<card-view-one-line
color="amber"
prepend-icon="mdi-alert"
:title="$t('account.noRealPaymentsNeeded')"
/>
</v-col>
</v-row>
<v-row v-if="accountStore.userAccount.payments.length > 0">
<v-col>
<v-expansion-panels>
<v-expansion-panel
v-for="payment in accountStore.userAccount.payments"
color="primary"
>
<template #title>
{{ payment.bankName }}
</template>
<template #text>
<v-row class="pt-5">
<v-col>
<v-text-field
:label="$t('account.userData.bankName')"
v-model="payment.bankName"
:rules="getStringRules()"
variant="outlined"
hide-details
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.userData.iban')"
v-model="payment.iban"
:rules="getIbanRules()"
variant="outlined"
hide-details
/>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-center align-center">
<outlined-button
@click="accountStore.removePayment(payment)"
color="red"
prepend-icon="mdi-delete"
>
{{ $t('misc.actions.remove') }}
</outlined-button>
</v-col>
</v-row>
</template>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
<v-row v-else>
<v-col>
<v-empty-state
:title="$t('account.noPayments')"
icon="mdi-currency-usd-off"
/>
</v-col>
</v-row>
<template #actions>
<outlined-button
@click="accountStore.userAccount.payments.push(new PaymentModel())"
prepend-icon="mdi-plus"
color="green"
>
{{ $t('misc.actions.add') }}
</outlined-button>
</template>
</card-view>
</template>

View File

@@ -1,60 +1,76 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccountStore } from '@/stores/account.store'; import { useAccountStore } from '@/stores/account.store';
import cardView from '@/components/basics/cardView.vue'; import dashboardCard from '@/components/pageParts/dashboardCard.vue';
import { useOrderStore } from '@/stores/order.store';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import moment from 'moment';
import { millisecondsToHumanReadableString } from '@/scripts/dateTimeScripts';
const accountStore = useAccountStore() const accountStore = useAccountStore()
const orderStore = useOrderStore()
const router = useRouter() const router = useRouter()
orderStore.getOrdersOfAccount(accountStore.userAccount, accountStore.userAccountToken)
accountStore.refreshAccount()
</script> </script>
<template> <template>
<v-container max-width="1000"> <v-container>
<v-row> <v-row>
<v-col> <dashboard-card
<card-view :title="$t('order.order', 2)"
:title="$t('misc.greeting', { msg: accountStore.userAccount.username })" icon="mdi-basket-check"
icon="mdi-hand-wave" :first-line="orderStore.orders.length + ' ' + $t('order.order', 2)"
> :second-line="$t('order.ordersDescription')"
<v-container> button-route="/account/orders"
<v-row> :loading="orderStore.fetchInProgress"
<v-col> />
<card-view
:title="$t('order.order', 2)"
icon="mdi-basket-check"
@click="router.push('/account/orders')"
>
{{ $t('order.ordersDescription') }}
</card-view>
</v-col>
</v-row>
<v-row> <dashboard-card
<v-col> :title="$t('account.accountManagement')"
<card-view icon="mdi-account"
:title="$t('account.accountManagement')" :first-line="accountStore.userAccount.username"
icon="mdi-account" :second-line="$t('account.accountManagementDescription')"
@click="router.push('/account/data')" :loading="accountStore.fetchInProgress"
> button-route="/account/data"
{{ $t('account.accountManagementDescription') }} />
</card-view>
</v-col>
</v-row>
<v-row> <dashboard-card
<v-col> :title="$t('account.addressManagement')"
<card-view icon="mdi-city"
:title="$t('account.logout.logout')" :first-line="accountStore.userAccount.addresses?.length + ' ' +
icon="mdi-logout" $t('account.userData.address', accountStore.userAccount.addresses?.length)"
@click="accountStore.logout(); router.push('/account/login')" :second-line="$t('account.addressManagementDetails')"
> :loading="accountStore.fetchInProgress"
{{ $t('account.logout.logoutDescription') }} button-route="/account/addresses"
</card-view> />
</v-col>
</v-row> <dashboard-card
</v-container> :title="$t('account.paymentsManagement', 2)"
</card-view> icon="mdi-currency-eur"
:first-line="accountStore.userAccount.payments?.length + ' ' +
</v-col> $t('account.userData.payment', accountStore.userAccount.payments?.length)"
:second-line="$t('account.managePaymentsDescription')"
:loading="accountStore.fetchInProgress"
button-route="/account/payments"
/>
<dashboard-card
:title="$t('account.logout.logout')"
:first-line="millisecondsToHumanReadableString(moment().diff(moment(accountStore.loggedInTimeStamp))) + ' h ' + $t('account.sessionTime')"
:second-line="$t('account.logout.logoutDescription')"
icon="mdi-logout"
>
<template #actions>
<outlined-button
color="error"
@click="accountStore.logout(); router.push('/account/login')"
>
{{ $t('account.logout.logout') }}
</outlined-button>
</template>
</dashboard-card>
</v-row> </v-row>
</v-container> </v-container>
</template> </template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import dataLayout from '@/layouts/dataLayout.vue';
import { useAccountStore } from '@/stores/account.store';
import { useFeedbackStore } from '@/stores/feedback.store';
import PaymentEditDialog from './paymentEditDialog.vue';
const accountStore = useAccountStore()
const feedbackStore = useFeedbackStore()
const headers = [
{ title: feedbackStore.i18n.t('account.userData.bankName'), value: "bankName" },
{ title: feedbackStore.i18n.t('account.userData.iban'), value: "iban" },
{ title: "Aktionen", value: "actions", width: 130 }
]
accountStore.refreshAccount()
</script>
<template>
<data-layout
:add-button-string="$t('misc.actions.add')"
:fetch-in-progress="accountStore.fetchInProgress"
:on-add-click="() => { accountStore.newPayment() }"
>
<v-data-table
:headers="headers"
:items="accountStore.userAccount.payments"
:loading="accountStore.fetchInProgress"
>
<template #item.actions="{ item }">
<v-btn
icon="mdi-pencil"
variant="plain"
color="orange"
@click="accountStore.editPayment(item)"
/>
<v-btn
icon="mdi-delete"
variant="plain"
color="red"
@click="accountStore.removePayment(item)"
/>
</template>
</v-data-table>
</data-layout>
<payment-edit-dialog />
</template>

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import actionDialog from '@/components/basics/actionDialog.vue';
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import { getIbanRules, getStringRules } from '@/scripts/validationRules';
import { useAccountStore } from '@/stores/account.store';
import cardViewOneLine from '@/components/basics/cardViewOneLine.vue';
import { ref } from 'vue';
const valid = ref(false)
const accountStore = useAccountStore()
</script>
<template>
<action-dialog
v-model="accountStore.showEditDialog"
max-width="800"
:title="$t('account.payments.editPayment')"
>
<v-container>
<v-row>
<v-col>
<card-view-one-line
color="warning"
prepend-icon="mdi-alert"
:title="$t('account.noRealPaymentsNeeded')"
/>
</v-col>
</v-row>
<v-form v-model="valid">
<v-row>
<v-col>
<v-text-field
:label="$t('account.userData.bankName')"
v-model="accountStore.payment.bankName"
:rules="getStringRules(8)"
variant="outlined"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('account.userData.iban')"
v-model="accountStore.payment.iban"
:rules="getIbanRules()"
variant="outlined"
/>
</v-col>
</v-row>
</v-form>
</v-container>
<template #actions>
<outlined-button
color="success"
prepend-icon="mdi-content-save"
:disabled="!valid"
:loading="accountStore.fetchInProgress"
@click="accountStore.savePayment"
>
{{ $t('misc.actions.save') }}
</outlined-button>
</template>
</action-dialog>
</template>

View File

@@ -33,6 +33,7 @@ async function startLogin() {
v-model="accountStore.loginData.username" v-model="accountStore.loginData.username"
variant="outlined" variant="outlined"
clearable clearable
hide-details
@keyup.enter="startLogin" @keyup.enter="startLogin"
id="txt-username" id="txt-username"
/> />
@@ -48,6 +49,7 @@ async function startLogin() {
variant="outlined" variant="outlined"
v-model="accountStore.loginData.password" v-model="accountStore.loginData.password"
clearable clearable
hide-details
@keyup.enter="startLogin" @keyup.enter="startLogin"
id="txt-password" id="txt-password"
/> />
@@ -67,7 +69,7 @@ async function startLogin() {
append-icon="mdi-arrow-right" append-icon="mdi-arrow-right"
@click="startLogin" @click="startLogin"
:loading="accountStore.fetchInProgress" :loading="accountStore.fetchInProgress"
color="green" color="success"
id="btn-login" id="btn-login"
> >
{{ $t('account.login.login') }} {{ $t('account.login.login') }}

View File

@@ -3,15 +3,14 @@ import cardView from '@/components/basics/cardView.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue'; import outlinedButton from '@/components/basics/outlinedButton.vue';
import { useAccountStore } from '@/stores/account.store'; import { useAccountStore } from '@/stores/account.store';
import { getEmailRules, getPasswordRules, getStringRules } from '@/scripts/validationRules'; import { getEmailRules, getPasswordRules, getStringRules } from '@/scripts/validationRules';
import { useRouter } from 'vue-router';
const showRegisterCard = defineModel("showRegisterCard", { type: Boolean, default: false }) const showRegisterCard = defineModel("showRegisterCard", { type: Boolean, default: false })
const accountStore = useAccountStore() const accountStore = useAccountStore()
const router = useRouter()
async function registerAccount() { async function registerAccount() {
accountStore.registerAccount() accountStore.registerAccount()
.then(result => { .then(result => {
console.log(result)
if (result) { if (result) {
showRegisterCard.value = false showRegisterCard.value = false
} }
@@ -31,7 +30,6 @@ async function registerAccount() {
prepend-icon="mdi-account" prepend-icon="mdi-account"
v-model="accountStore.registerData.username" v-model="accountStore.registerData.username"
clearable clearable
hide-details
variant="outlined" variant="outlined"
:rules="getStringRules()" :rules="getStringRules()"
/> />
@@ -46,7 +44,6 @@ async function registerAccount() {
type="password" type="password"
v-model="accountStore.registerData.password" v-model="accountStore.registerData.password"
clearable clearable
hide-details
variant="outlined" variant="outlined"
:rules="getPasswordRules()" :rules="getPasswordRules()"
/> />
@@ -61,7 +58,6 @@ async function registerAccount() {
v-model="accountStore.registerData.email" v-model="accountStore.registerData.email"
:rules="getEmailRules()" :rules="getEmailRules()"
variant="outlined" variant="outlined"
hide-details
clearable clearable
/> />
</v-col> </v-col>
@@ -80,6 +76,7 @@ async function registerAccount() {
prepend-icon="mdi-account-plus" prepend-icon="mdi-account-plus"
@click="registerAccount" @click="registerAccount"
:loading="accountStore.fetchInProgress" :loading="accountStore.fetchInProgress"
color="success"
> >
{{ $t('account.register') }} {{ $t('account.register') }}
</outlined-button> </outlined-button>

View File

@@ -8,7 +8,7 @@ import { useOrderStore } from '@/stores/order.store';
const accountStore = useAccountStore() const accountStore = useAccountStore()
const orderStore = useOrderStore() const orderStore = useOrderStore()
orderStore.getOrdersOfAccount(accountStore.userAccount) orderStore.getOrdersOfAccount(accountStore.userAccount, accountStore.userAccountToken)
</script> </script>
<template> <template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccountStore } from '@/stores/account.store'; import { useAccountStore } from '@/stores/account.store';
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import { useFeedbackStore } from '@/stores/feedback.store'; import { useFeedbackStore } from '@/stores/feedback.store';
const accountStore = useAccountStore() const accountStore = useAccountStore()
@@ -19,7 +19,7 @@ accountStore.getAllAccounts()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:add-button-string="$t('account.addNewAccount')" :add-button-string="$t('account.addNewAccount')"
:fetch-in-progress="accountStore.fetchInProgress" :fetch-in-progress="accountStore.fetchInProgress"
> >
@@ -44,5 +44,5 @@ accountStore.getAllAccounts()
/> --> /> -->
</template> </template>
</v-data-table> </v-data-table>
</admin-data-layout> </data-layout>
</template> </template>

View File

@@ -93,7 +93,7 @@ function itemProps(item: GenreModel) {
<template #actions> <template #actions>
<outlined-button <outlined-button
color="green" color="success"
@click="bandStore.saveBand" @click="bandStore.saveBand"
:loading="bandStore.fetchInProgress" :loading="bandStore.fetchInProgress"
> >

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useBandStore } from '@/stores/band.store'; import { useBandStore } from '@/stores/band.store';
import bandEditDialog from './bandEditDialog.vue'; import bandEditDialog from './bandEditDialog.vue';
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import { useFeedbackStore } from '@/stores/feedback.store'; import { useFeedbackStore } from '@/stores/feedback.store';
const bandStore = useBandStore() const bandStore = useBandStore()
@@ -22,7 +22,7 @@ bandStore.getBands()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:add-button-string="$t('band.addNewBand')" :add-button-string="$t('band.addNewBand')"
:fetch-in-progress="bandStore.fetchInProgress" :fetch-in-progress="bandStore.fetchInProgress"
:on-add-click="() => bandStore.newBand()" :on-add-click="() => bandStore.newBand()"
@@ -72,7 +72,7 @@ bandStore.getBands()
/> --> /> -->
</template> </template>
</v-data-table> </v-data-table>
</admin-data-layout> </data-layout>
<band-edit-dialog /> <band-edit-dialog />
</template> </template>

View File

@@ -2,7 +2,7 @@
import { useBandStore } from '@/stores/band.store'; import { useBandStore } from '@/stores/band.store';
import { useConcertStore } from '@/stores/concert.store'; import { useConcertStore } from '@/stores/concert.store';
import { useFeedbackStore } from '@/stores/feedback.store'; import { useFeedbackStore } from '@/stores/feedback.store';
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import moment from 'moment'; import moment from 'moment';
const concertStore = useConcertStore() const concertStore = useConcertStore()
@@ -25,7 +25,7 @@ concertStore.getConcerts()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:add-button-string="$t('concert.addNewConcert')" :add-button-string="$t('concert.addNewConcert')"
:fetch-in-progress="concertStore.fetchInProgress" :fetch-in-progress="concertStore.fetchInProgress"
:on-add-click="() => concertStore.newConcert()" :on-add-click="() => concertStore.newConcert()"
@@ -73,5 +73,5 @@ concertStore.getConcerts()
/> --> /> -->
</template> </template>
</v-data-table> </v-data-table>
</admin-data-layout> </data-layout>
</template> </template>

View File

@@ -5,7 +5,7 @@ import { useAccountStore } from '@/stores/account.store';
import { useLocationStore } from '@/stores/location.store'; import { useLocationStore } from '@/stores/location.store';
import { useGenreStore } from '@/stores/genre.store'; import { useGenreStore } from '@/stores/genre.store';
import { usePreferencesStore } from '@/stores/preferences.store'; import { usePreferencesStore } from '@/stores/preferences.store';
import dashboardCard from './dashboardCard.vue'; import dashboardCard from '../../../components/pageParts/dashboardCard.vue';
import { useOrderStore } from '@/stores/order.store'; import { useOrderStore } from '@/stores/order.store';
import { useFilesStore } from '@/stores/files.store'; import { useFilesStore } from '@/stores/files.store';

View File

@@ -53,7 +53,7 @@ const test = ref()
<outlined-button <outlined-button
@click="filesStore.uploadFile" @click="filesStore.uploadFile"
prepend-icon="mdi-file-upload" prepend-icon="mdi-file-upload"
color="green" color="success"
:disabled="filesStore.fileUploadDir.length == 0 || filesStore.fileUpload == undefined" :disabled="filesStore.fileUploadDir.length == 0 || filesStore.fileUpload == undefined"
:loading="filesStore.fetchInProgress" :loading="filesStore.fetchInProgress"
> >

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import FileUploadDialog from './fileUploadDialog.vue'; import FileUploadDialog from './fileUploadDialog.vue';
import { useFilesStore } from '@/stores/files.store'; import { useFilesStore } from '@/stores/files.store';
@@ -12,13 +12,14 @@ filesStore.getStaticFolders()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:add-button-string="$t('misc.uploadFile')" :add-button-string="$t('misc.uploadFile')"
:fetch-in-progress="filesStore.fetchInProgress" :fetch-in-progress="filesStore.fetchInProgress"
:on-add-click="() => { filesStore.showFileUploadDialog = true }" :on-add-click="() => { filesStore.showFileUploadDialog = true }"
:hide-add-button="true" :hide-add-button="true"
> >
<v-row > <v-row >
<!-- Column folder -->
<v-col cols="2" class="border"> <v-col cols="2" class="border">
<v-list> <v-list>
<v-list-item <v-list-item
@@ -27,10 +28,13 @@ filesStore.getStaticFolders()
:value="folder" :value="folder"
:title="folder.name + '/'" :title="folder.name + '/'"
@click="filesStore.selectedFolder = folder; filesStore.getStaticFiles()" @click="filesStore.selectedFolder = folder; filesStore.getStaticFiles()"
prepend-icon="mdi-folder"
/> />
</v-list> </v-list>
</v-col> </v-col>
<!-- Column files in folder -->
<v-col cols="4" class="border"> <v-col cols="4" class="border">
<v-skeleton-loader <v-skeleton-loader
:loading="filesStore.fetchInProgress" :loading="filesStore.fetchInProgress"
@@ -41,39 +45,74 @@ filesStore.getStaticFolders()
v-for="file of filesStore.staticFiles" v-for="file of filesStore.staticFiles"
:title="file.name" :title="file.name"
:value="file.name" :value="file.name"
:subtitle="Math.round(file.size / 1024) + ' KB'"
@click="() => { filesStore.selectedFile = file }" @click="() => { filesStore.selectedFile = file }"
/> >
<template #prepend>
<v-icon
:icon="file.name.endsWith('js') ? 'mdi-file' : 'mdi-image'"
:color="file.copyright != undefined ? 'grey' : 'red'"
/>
</template>
</v-list-item>
</v-list> </v-list>
</v-skeleton-loader> </v-skeleton-loader>
</v-col> </v-col>
<!-- File detail viewer -->
<v-col class="border"> <v-col class="border">
<v-row> <v-row>
<v-col> <v-col v-if="filesStore.selectedFile != undefined">
{{ filesStore.selectedFile.url }}
</v-col>
</v-row>
<v-row>
<v-col>
<v-textarea <v-textarea
v-if="filesStore.selectedFile != undefined && filesStore.selectedFile.name.endsWith('html')" v-if="filesStore.selectedFile != undefined && filesStore.selectedFile.name.endsWith('js')"
:model-value="filesStore.selectedFile.content" :model-value="filesStore.selectedFile.content"
variant="outlined" variant="outlined"
label="Content" label="Content"
height="300" height="300"
rows="30" rows="30"
/> />
<v-img <v-img
v-else-if="filesStore.selectedFile != undefined" v-else-if="filesStore.selectedFile != undefined"
:src="filesStore.selectedFile.url" max-height="400" :src="filesStore.selectedFile.url" max-height="400"
/> />
</v-col> </v-col>
</v-row> </v-row>
<!-- File details -->
<v-row>
<v-col v-if="filesStore.selectedFile != undefined">
<v-list>
<v-list-item prepend-icon="mdi-server">
{{ filesStore.selectedFile.url }}
</v-list-item>
<v-list-item prepend-icon="mdi-package">
{{ Math.round(filesStore.selectedFile.size / 1024) + ' KB' }}
</v-list-item>
<template v-if="filesStore.selectedFile['copyright'] != undefined">
<v-list-item prepend-icon="mdi-copyright">
{{ filesStore.selectedFile.copyright.license }}
</v-list-item>
<v-list-item prepend-icon="mdi-account">
{{ filesStore.selectedFile.copyright.creator }}
</v-list-item>
<v-list-item
prepend-icon="mdi-web"
v-if="filesStore.selectedFile.copyright.url.length > 0"
>
<a :href="filesStore.selectedFile.copyright.url" target="_blank" >Quelle</a>
</v-list-item>
</template>
</v-list>
</v-col>
</v-row>
</v-col> </v-col>
</v-row> </v-row>
</admin-data-layout> </data-layout>
<file-preview-dialog <file-preview-dialog
v-model:show-dialog="showPreviewDialog" v-model:show-dialog="showPreviewDialog"

View File

@@ -35,7 +35,7 @@ const genreStore = useGenreStore()
<template #actions> <template #actions>
<outlined-button <outlined-button
color="green" color="success"
@click="genreStore.saveGenre" @click="genreStore.saveGenre"
:disabled="!valid" :disabled="!valid"
:loading="genreStore.fetchInProgress" :loading="genreStore.fetchInProgress"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import genreEditDialog from './genreEditDialog.vue'; import genreEditDialog from './genreEditDialog.vue';
import { useGenreStore } from '@/stores/genre.store'; import { useGenreStore } from '@/stores/genre.store';
@@ -15,7 +15,7 @@ genreStore.getGenres()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:add-button-string="$t('band.addNewGenre')" :add-button-string="$t('band.addNewGenre')"
:fetch-in-progress="genreStore.fetchInProgress" :fetch-in-progress="genreStore.fetchInProgress"
:on-add-click="() => { genreStore.newGenre() }" :on-add-click="() => { genreStore.newGenre() }"
@@ -48,7 +48,7 @@ genreStore.getGenres()
/> --> /> -->
</template> </template>
</v-data-table> </v-data-table>
</admin-data-layout> </data-layout>
<genre-edit-dialog /> <genre-edit-dialog />
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import { useFeedbackStore } from '@/stores/feedback.store'; import { useFeedbackStore } from '@/stores/feedback.store';
import { useLocationStore } from '@/stores/location.store'; import { useLocationStore } from '@/stores/location.store';
@@ -22,7 +22,7 @@ locationStore.getLocations()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:fetch-in-progress="locationStore.fetchInProgress" :fetch-in-progress="locationStore.fetchInProgress"
:add-button-string="$t('location.addLocation')" :add-button-string="$t('location.addLocation')"
:on-add-click="() => { locationStore.newLocation() }" :on-add-click="() => { locationStore.newLocation() }"
@@ -66,5 +66,5 @@ locationStore.getLocations()
/> --> /> -->
</template> </template>
</v-data-table> </v-data-table>
</admin-data-layout> </data-layout>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import adminDataLayout from '@/layouts/adminDataLayout.vue'; import dataLayout from '@/layouts/dataLayout.vue';
import { useOrderStore } from '@/stores/order.store'; import { useOrderStore } from '@/stores/order.store';
import moment from 'moment'; import moment from 'moment';
import OrderDetailDialog from './orderDetailDialog.vue'; import OrderDetailDialog from './orderDetailDialog.vue';
@@ -13,19 +13,21 @@ const headers = [
{ title: "Adresse", value: "street" }, { title: "Adresse", value: "street" },
{ title: "Stadt", value: "city" }, { title: "Stadt", value: "city" },
{ title: "Versendet", value: "shipped" }, { title: "Versendet", value: "shipped" },
{ title: "", value: "edit", width: 130 } { title: "Aktionen", value: "edit", width: 130 }
] ]
orderStore.getAllOrders() orderStore.getAllOrders()
</script> </script>
<template> <template>
<admin-data-layout <data-layout
:hide-add-button="true" :hide-add-button="true"
> >
<v-data-table <v-data-table
:headers="headers" :headers="headers"
:items="orderStore.orders" :items="orderStore.orders"
:loading="orderStore.fetchInProgress"
:items-per-page="100"
> >
<template #item.account="{ item }"> <template #item.account="{ item }">
{{ item.account.firstName }} {{ item.account.lastName }} {{ item.account.firstName }} {{ item.account.lastName }}
@@ -46,27 +48,27 @@ orderStore.getAllOrders()
<template #item.shipped="{ item }"> <template #item.shipped="{ item }">
<v-icon <v-icon
:icon="item.shipped ? 'mdi-check' : 'mdi-close'" :icon="item.shipped ? 'mdi-check' : 'mdi-close'"
:color="item.shipped ? 'green' : 'red'" :color="item.shipped ? 'success' : 'error'"
/> />
</template> </template>
<template #item.edit="{ item }"> <template #item.edit="{ item }">
<!-- todo <v-btn <v-btn
icon="mdi-eye" icon="mdi-eye"
variant="plain" variant="plain"
@click="orderStore.openDetails(item)" @click="orderStore.openDetails(item)"
/> --> />
<!-- todo <v-btn <v-btn
icon="mdi-delete" :icon="item.shipped ? 'mdi-close-circle-outline' : 'mdi-check-circle-outline'"
variant="plain" variant="plain"
color="red" :color="item.shipped ? 'error' : 'success'"
@click="orderStore.deleteOrder(item)" @click="orderStore.changeOrderShippedState(item, !item.shipped)"
/> --> />
</template> </template>
</v-data-table> </v-data-table>
</admin-data-layout> </data-layout>
<order-detail-dialog /> <order-detail-dialog />
</template> </template>

View File

@@ -12,15 +12,67 @@ const orderStore = useOrderStore()
v-model="orderStore.showDetailDialog" v-model="orderStore.showDetailDialog"
:title="$t('order.order')" :title="$t('order.order')"
icon="mdi-basket" icon="mdi-basket"
max-width="800"
> >
<v-list> <v-list>
<v-list-subheader> <v-list-subheader>
{{ $t('ticket.ticket', 2) }} {{ $t('account.account') }}
</v-list-subheader> </v-list-subheader>
<v-list-item v-for="ticket of orderStore.order.tickets"> <v-list-item prepend-icon="mdi-account">
{{ moment(ticket.concert.date).format("DD.MM.YYYY") }} - {{ orderStore.order.account.username }}
{{ ticket.concert.band.name }} - {{ ticket.concert.name }} </v-list-item>
<v-list-item prepend-icon="mdi-card-account-details">
{{ orderStore.order.account.firstName }} {{ orderStore.order.account.lastName }}
</v-list-item>
<v-list-item prepend-icon="mdi-home">
{{ orderStore.order.address.street }} {{ orderStore.order.address.houseNumber }}
</v-list-item>
<v-list-item prepend-icon="mdi-city">
{{ orderStore.order.address.postalCode }} {{ orderStore.order.address.city }}
</v-list-item>
<v-list-subheader>
{{ $t('order.order') }}
</v-list-subheader>
<v-list-item prepend-icon="mdi-calendar">
{{ moment(orderStore.order.orderedAt).format("DD.MM.YYYY, HH:mm:ss") }}
</v-list-item>
<v-list-item prepend-icon="mdi-truck">
{{ orderStore.order.shipped ? 'Versendet' : 'Nicht versendet' }}
</v-list-item>
<v-list-item>
<v-table>
<thead>
<tr>
<th>{{ $t('concert.date') }}</th>
<th>{{ $t('concert.name') }}</th>
<th>{{ $t('band.name') }}</th>
<th>{{ $t('location.name') }}</th>
<th>{{ $t('location.seat.seatGroup') }}</th>
<th>{{ $t('location.seat.seatRow') }}</th>
<th>{{ $t('location.seat.seat') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="ticket of orderStore.order.tickets">
<td>{{ moment(ticket.concert.date).format("DD.MM.YYYY") }}</td>
<td>{{ ticket.concert.name }}</td>
<td>{{ ticket.concert.band.name }}</td>
<td>{{ ticket.concert.location.name }}</td>
<td>{{ ticket.seat.seatRow.seatGroup.name }}</td>
<td>{{ ticket.seat.seatRow.row }}</td>
<td>{{ ticket.seat.seatNr }}</td>
</tr>
</tbody>
</v-table>
</v-list-item> </v-list-item>
</v-list> </v-list>
</action-dialog> </action-dialog>

View File

@@ -46,8 +46,10 @@ defineProps({
</v-col> </v-col>
<v-col> <v-col>
<v-list> <v-list style="background-color: transparent;">
<v-list-item v-for="ratingValue in ratings"> <v-list-item
v-for="ratingValue in ratings"
>
<template v-slot:prepend> <template v-slot:prepend>
<span>{{ ratingValue.value }}</span> <span>{{ ratingValue.value }}</span>
<v-icon class="ml-3 mr-n3" icon="mdi-star" /> <v-icon class="ml-3 mr-n3" icon="mdi-star" />

View File

@@ -94,7 +94,7 @@ watch(() => router.currentRoute.value.params.date, () => {
<v-row> <v-row>
<v-col> <v-col>
<v-list > <v-list style="background-color: transparent;">
<v-list-item v-for="seat in basketStore.selectedSeats" > <v-list-item v-for="seat in basketStore.selectedSeats" >
<ticket-list-item <ticket-list-item
:concert="concertStore.concert" :concert="concertStore.concert"

View File

@@ -18,7 +18,6 @@ const showOrderingDialog = ref()
<v-col> <v-col>
<v-alert <v-alert
color="info" color="info"
closable
> >
{{ $t('account.login.pleaseLoginToOrder') }} {{ $t('account.login.pleaseLoginToOrder') }}
</v-alert> </v-alert>
@@ -53,7 +52,7 @@ const showOrderingDialog = ref()
prepend-icon="mdi-basket-check" prepend-icon="mdi-basket-check"
:disabled="basketStore.itemsInBasket.length == 0 || accountStore.userAccount.id == null" :disabled="basketStore.itemsInBasket.length == 0 || accountStore.userAccount.id == null"
variant="outlined" variant="outlined"
color="green" color="success"
@click="showOrderingDialog = true" @click="showOrderingDialog = true"
> >
{{ $t('order.takeOrder') }} {{ $t('order.takeOrder') }}

View File

@@ -100,7 +100,7 @@ function paymentItemProps(item: PaymentModel) {
@click="doOrder" @click="doOrder"
:loading="orderingInProgress" :loading="orderingInProgress"
prepend-icon="mdi-send" prepend-icon="mdi-send"
color="green" color="success"
> >
{{ $t('order.takeOrder') }} {{ $t('order.takeOrder') }}
</outlined-button> </outlined-button>

View File

@@ -2,6 +2,7 @@
import { useBasketStore } from '@/stores/basket.store'; import { useBasketStore } from '@/stores/basket.store';
import { BasketItemModel } from '@/data/models/ordering/basketItemModel'; import { BasketItemModel } from '@/data/models/ordering/basketItemModel';
import { calcPrice } from '@/scripts/concertScripts'; import { calcPrice } from '@/scripts/concertScripts';
import moment from 'moment';
const basketStore = useBasketStore() const basketStore = useBasketStore()
@@ -11,9 +12,10 @@ function removeFromBasket(basketItem: BasketItemModel) {
</script> </script>
<template> <template>
<v-table> <v-table style="background-color: transparent;">
<thead> <thead>
<tr> <tr>
<th>{{ $t('concert.date') }}</th>
<th>{{ $t('band.band') }}</th> <th>{{ $t('band.band') }}</th>
<th>{{ $t('concert.concert') }}</th> <th>{{ $t('concert.concert') }}</th>
<th class="text-center">{{ $t('misc.quantity') }}</th> <th class="text-center">{{ $t('misc.quantity') }}</th>
@@ -25,6 +27,11 @@ function removeFromBasket(basketItem: BasketItemModel) {
<tbody> <tbody>
<tr v-for="basketItem in basketStore.itemsInBasket"> <tr v-for="basketItem in basketStore.itemsInBasket">
<!-- Concert date -->
<td>
{{ moment(basketItem.concert.date).format("DD.MM.YYYY") }}
</td>
<!-- Band name --> <!-- Band name -->
<td> <td>
{{ basketItem.band.name }} {{ basketItem.band.name }}
@@ -43,7 +50,7 @@ function removeFromBasket(basketItem: BasketItemModel) {
<!-- Price per event --> <!-- Price per event -->
<td class="text-right"> <td class="text-right">
<div v-if="basketItem.seats"> <div v-if="basketItem.seats">
{{ basketItem.price }} {{ basketItem.price.toFixed(2) }}
</div> </div>
</td> </td>
@@ -56,7 +63,7 @@ function removeFromBasket(basketItem: BasketItemModel) {
<v-btn <v-btn
icon="mdi-delete" icon="mdi-delete"
@click="removeFromBasket(basketItem)" @click="removeFromBasket(basketItem)"
color="red" color="error"
variant="text" variant="text"
flat flat
/> />

View File

@@ -19,6 +19,22 @@ function getDotColor(exerciseGroupNr: number) {
case 3: return "pink" case 3: return "pink"
} }
} }
function generateExerciseKey() {
try {
let code = ""
for (let i = 0; i < 13; i++) {
if (exerciseStore.exercises[i].solved) {
code += "3"
} else {
code += "0"
}
}
return (Number(code) + Number(preferencesStore.registrationNumber)) * 237
} catch(e) {}
}
</script> </script>
<template> <template>
@@ -26,20 +42,7 @@ function getDotColor(exerciseGroupNr: number) {
<v-row> <v-row>
<v-spacer /> <v-spacer />
<v-col
v-if="preferencesStore.studentName.length < 3 || preferencesStore.registrationNumber.length < 7"
cols="auto"
>
<card-view variant="outlined" >
{{ $t('misc.fulfillYourPersonalDataFirst') }}
</card-view>
</v-col>
<v-col cols="auto"> <v-col cols="auto">
<v-tooltip :text="$t('misc.fulfillYourPersonalDataFirst')">
<template #activator="{ props }"></template>
</v-tooltip>
<outlined-button <outlined-button
prepend-icon="mdi-file-pdf-box" prepend-icon="mdi-file-pdf-box"
@click="generateResultsPdf()" @click="generateResultsPdf()"
@@ -50,6 +53,17 @@ function getDotColor(exerciseGroupNr: number) {
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col class="text-h5 text-center">
<div>
Persönlicher Lösungsschlüssel:
</div>
<div>
{{ generateExerciseKey() }}
</div>
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<card-view <card-view

View File

@@ -2,6 +2,7 @@
import actionDialog from '@/components/basics/actionDialog.vue'; import actionDialog from '@/components/basics/actionDialog.vue';
import outlinedButton from '@/components/basics/outlinedButton.vue'; import outlinedButton from '@/components/basics/outlinedButton.vue';
import ServerStateText from '@/components/pageParts/serverStateText.vue'; import ServerStateText from '@/components/pageParts/serverStateText.vue';
import { getRegisterNumberRules, getStringRules } from '@/scripts/validationRules';
import { useFeedbackStore } from '@/stores/feedback.store'; import { useFeedbackStore } from '@/stores/feedback.store';
import { usePreferencesStore } from '@/stores/preferences.store'; import { usePreferencesStore } from '@/stores/preferences.store';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
@@ -63,7 +64,7 @@ watch(() => currentStep.value, () => {
:title="step" :title="step"
:value="n + 1" :value="n + 1"
complete-icon="mdi-check" complete-icon="mdi-check"
color="green" color="success"
/> />
<v-divider v-if="n < steps.length - 1" /> <v-divider v-if="n < steps.length - 1" />
@@ -73,6 +74,7 @@ watch(() => currentStep.value, () => {
<!-- Content --> <!-- Content -->
<v-stepper-window> <v-stepper-window>
<!-- Step 1: Check connection to backend server -->
<v-stepper-window-item <v-stepper-window-item
:value="1" :value="1"
class="text-h4 text-center" class="text-h4 text-center"
@@ -84,6 +86,7 @@ watch(() => currentStep.value, () => {
<server-state-text /> <server-state-text />
</v-stepper-window-item> </v-stepper-window-item>
<!-- Step 2: Reset the database -->
<v-stepper-window-item <v-stepper-window-item
:value="2" :value="2"
> >
@@ -100,7 +103,7 @@ watch(() => currentStep.value, () => {
</div> </div>
</v-stepper-window-item> </v-stepper-window-item>
<!-- Step 3: Create exercises -->
<v-stepper-window-item <v-stepper-window-item
:value="3" :value="3"
> >
@@ -117,18 +120,25 @@ watch(() => currentStep.value, () => {
</div> </div>
</v-stepper-window-item> </v-stepper-window-item>
<!-- Step 4: Personal data -->
<v-stepper-window-item <v-stepper-window-item
:value="4" :value="4"
> >
<v-container class="px-0 py-2"> <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-row>
<v-col> <v-col>
<v-text-field <v-text-field
variant="outlined" variant="outlined"
hide-details
:label="$t('misc.yourFullName')" :label="$t('misc.yourFullName')"
v-model="preferencesStore.studentName" v-model="preferencesStore.studentName"
:rules="getStringRules(4)"
/> />
</v-col> </v-col>
</v-row> </v-row>
@@ -137,9 +147,9 @@ watch(() => currentStep.value, () => {
<v-col> <v-col>
<v-text-field <v-text-field
variant="outlined" variant="outlined"
hide-details
:label="$t('misc.registrationNumber')" :label="$t('misc.registrationNumber')"
v-model="preferencesStore.registrationNumber" v-model="preferencesStore.registrationNumber"
:rules="getRegisterNumberRules()"
/> />
</v-col> </v-col>
</v-row> </v-row>
@@ -168,10 +178,10 @@ watch(() => currentStep.value, () => {
<outlined-button <outlined-button
v-else v-else
@click="showDialog = false; preferencesStore.firstStartup = false" @click="showDialog = false; preferencesStore.firstStartup = false"
:disabled="preferencesStore.studentName.length == 0 || :disabled="preferencesStore.studentName.length < 5 ||
preferencesStore.registrationNumber.length == 0" preferencesStore.registrationNumber.length < 8"
prepend-icon="mdi-check" prepend-icon="mdi-check"
color="green" color="success"
> >
{{ $t('misc.firstStartup.complete') }} {{ $t('misc.firstStartup.complete') }}
</outlined-button> </outlined-button>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import { useFilesStore } from '@/stores/files.store';
const filesStore = useFilesStore()
filesStore.getStaticFiles()
</script>
<template>
<v-img v-for="image of filesStore.staticFiles" :src="image.url"/>
</template>

View File

@@ -2,7 +2,6 @@
import pageSetup from './pageSetupSection.vue'; import pageSetup from './pageSetupSection.vue';
import systemSetup from './systemSetupSection.vue'; import systemSetup from './systemSetupSection.vue';
import aboutSection from './aboutSection.vue'; import aboutSection from './aboutSection.vue';
import userSection from './userSection.vue';
</script> </script>
<template> <template>
@@ -13,12 +12,6 @@ import userSection from './userSection.vue';
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col>
<user-section />
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<system-setup /> <system-setup />

View File

@@ -28,7 +28,7 @@ preferenceStore.getServerState()
<outlined-button <outlined-button
@click="preferenceStore.showDeleteDbDialog = true" @click="preferenceStore.showDeleteDbDialog = true"
prepend-icon="mdi-database-refresh" prepend-icon="mdi-database-refresh"
color="red" color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress" :disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress"
> >
{{ $t('preferences.resetDatabase.resetDatabase') }} {{ $t('preferences.resetDatabase.resetDatabase') }}
@@ -39,7 +39,7 @@ preferenceStore.getServerState()
<outlined-button <outlined-button
@click="preferenceStore.showDeleteExerciseDialog = true" @click="preferenceStore.showDeleteExerciseDialog = true"
prepend-icon="mdi-progress-close" prepend-icon="mdi-progress-close"
color="red" color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress" :disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress"
> >
{{ $t('preferences.resetExerciseProgress.resetExerciseProgress') }} {{ $t('preferences.resetExerciseProgress.resetExerciseProgress') }}
@@ -50,7 +50,7 @@ preferenceStore.getServerState()
<outlined-button <outlined-button
@click="preferenceStore.showFactoryResetDialog = true" @click="preferenceStore.showFactoryResetDialog = true"
prepend-icon="mdi-factory" prepend-icon="mdi-factory"
color="red" color="warning"
:disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress" :disabled="preferenceStore.serverState != ServerStateEnum.ONLINE || preferenceStore.fetchInProgress"
> >
{{ $t('preferences.factoryReset.factoryReset') }} {{ $t('preferences.factoryReset.factoryReset') }}
@@ -67,6 +67,7 @@ preferenceStore.getServerState()
v-model="preferenceStore.showDeleteDbDialog" v-model="preferenceStore.showDeleteDbDialog"
:onConfirm="preferenceStore.resetDb" :onConfirm="preferenceStore.resetDb"
:loading="preferenceStore.fetchInProgress" :loading="preferenceStore.fetchInProgress"
icon="mdi-database-refresh"
/> />
<!-- Confirm delete exercise progress --> <!-- Confirm delete exercise progress -->
@@ -76,12 +77,14 @@ preferenceStore.getServerState()
v-model="preferenceStore.showDeleteExerciseDialog" v-model="preferenceStore.showDeleteExerciseDialog"
:onConfirm="preferenceStore.resetExerciseProg" :onConfirm="preferenceStore.resetExerciseProg"
:loading="preferenceStore.fetchInProgress" :loading="preferenceStore.fetchInProgress"
icon="mdi-progress-close"
/> />
<confirm-dialog <confirm-dialog
:title="$t('preferences.factoryReset.dialog.title')" :title="$t('preferences.factoryReset.dialog.title')"
:description="$t('preferences.factoryReset.dialog.description')" :description="$t('preferences.factoryReset.dialog.description')"
v-model="preferenceStore.showFactoryResetDialog" v-model="preferenceStore.showFactoryResetDialog"
icon="mdi-factory"
:onConfirm="() => { :onConfirm="() => {
preferenceStore.resetToFactorySettings() preferenceStore.resetToFactorySettings()
router.push('/') router.push('/')

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import { usePreferencesStore } from '@/stores/preferences.store';
const preferencesStore = usePreferencesStore()
</script>
<template>
<card-view icon="mdi-account-school" :title="$t('misc.user')">
<v-row>
<v-col>
<v-text-field
variant="outlined"
hide-details
:label="$t('misc.yourFullName')"
v-model="preferencesStore.studentName"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="outlined"
hide-details
:label="$t('misc.registrationNumber')"
v-model="preferencesStore.registrationNumber"
/>
</v-col>
</v-row>
</card-view>
</template>

View File

@@ -23,7 +23,17 @@ const searchStore = useSearchStore()
</v-col> </v-col>
</v-row> </v-row>
<div v-if="searchStore.alreadySearched"> <v-row v-if="!searchStore.alreadySearched">
<v-col>
<v-empty-state
icon="mdi-magnify"
:headline="$t('misc.search.empty.headline')"
:title="$t('misc.enterSomeKeywords')"
/>
</v-col>
</v-row>
<div v-else>
<v-row> <v-row>
<v-col> <v-col>
<section-divider :title="$t('band.band', 2)" /> <section-divider :title="$t('band.band', 2)" />
@@ -41,7 +51,8 @@ const searchStore = useSearchStore()
<v-row <v-row
v-else-if="searchStore.bands.length > 0" v-else-if="searchStore.bands.length > 0"
v-for="band in searchStore.bands"> v-for="band in searchStore.bands"
>
<v-col> <v-col>
<band-list-item <band-list-item
:band="band" :band="band"

View File

@@ -11,7 +11,7 @@ const searchStore = useSearchStore()
variant="outlined" variant="outlined"
hide-details hide-details
v-model="searchStore.searchTerm" v-model="searchStore.searchTerm"
:placeholder="$t('misc.enterSomeKeywords')" :label="$t('misc.search.searchterm')"
@keyup.enter="searchStore.startSearch" @keyup.enter="searchStore.startSearch"
> >
<template #append-inner> <template #append-inner>

View File

@@ -18,7 +18,11 @@ const vuetify = createVuetify({
colors: { colors: {
primary: colors.blue.darken4, primary: colors.blue.darken4,
secondary: colors.yellow.darken3, secondary: colors.yellow.darken3,
sheet: colors.grey.darken4 sheet: colors.grey.darken4,
warning: colors.amber.darken3,
success: colors.green.darken2,
error: colors.red.darken2,
exerciseSolved: colors.purple.darken2
} }
}, },
light: { light: {
@@ -26,7 +30,11 @@ const vuetify = createVuetify({
colors: { colors: {
primary: colors.blue.darken4, primary: colors.blue.darken4,
secondary: colors.yellow.darken3, secondary: colors.yellow.darken3,
sheet: colors.grey.lighten3 sheet: colors.grey.lighten4,
warning: colors.amber.darken4,
success: colors.green.darken2,
error: colors.red.darken4,
exerciseSolved: colors.purple.darken2
} }
}, },
} }

View File

@@ -15,6 +15,9 @@ import LoginPage from "@/pages/account/loginPage/index.vue"
import PreferencesPage from "@/pages/misc/preferencesPage/index.vue"; import PreferencesPage from "@/pages/misc/preferencesPage/index.vue";
import HelpPage from "@/pages/misc/helpPage/index.vue" import HelpPage from "@/pages/misc/helpPage/index.vue"
import ErrorPage from "@/pages/misc/errorPage/index.vue" import ErrorPage from "@/pages/misc/errorPage/index.vue"
import ImageLicensePage from "@/pages/misc/imageLicensePage/index.vue"
import AccountPaymentsPage from "@/pages/account/accountPaymentsPage/index.vue"
import AccountAddressesPage from "@/pages/account/accountAddressesPage/index.vue"
const routes = [ const routes = [
// Main page // Main page
@@ -31,6 +34,8 @@ const routes = [
{ path: '/account/orders', component: OrdersPage }, { path: '/account/orders', component: OrdersPage },
{ path: '/account/data', component: AccountDataPage }, { path: '/account/data', component: AccountDataPage },
{ path: '/account/login', component: LoginPage }, { path: '/account/login', component: LoginPage },
{ path: '/account/payments', component: AccountPaymentsPage },
{ path: '/account/addresses', component: AccountAddressesPage },
// Admin // Admin
...adminRoutes, ...adminRoutes,
@@ -52,6 +57,7 @@ const routes = [
{ path: '/basket', component: BasketPage }, { path: '/basket', component: BasketPage },
{ path: '/preferences', component: PreferencesPage }, { path: '/preferences', component: PreferencesPage },
{ path: '/help', component: HelpPage }, { path: '/help', component: HelpPage },
{ path: '/license', component: ImageLicensePage },
// Error Page // Error Page
{ path: "/404", component: ErrorPage } { path: "/404", component: ErrorPage }

View File

@@ -53,9 +53,12 @@ export function createDateRangeString(concerts: Array<ConcertModel>) {
export function lowestTicketPrice(concerts: Array<ConcertModel>): string { export function lowestTicketPrice(concerts: Array<ConcertModel>): string {
const priceArray : Array<number> = [] const priceArray : Array<number> = []
for (let concert of concerts) { try {
priceArray.push(concert.price) for (let concert of concerts) {
} priceArray.push(concert.price)
}
} catch (e) {}
priceArray.sort() priceArray.sort()

View File

@@ -20,4 +20,16 @@ export function dateToHumanReadableString(date: Date) {
*/ */
export function dateStringToHumanReadableString(string: string) { export function dateStringToHumanReadableString(string: string) {
return dateToHumanReadableString(new Date(string)) return dateToHumanReadableString(new Date(string))
}
/**
* Format milliseconds to a readable format
*
* @param milliseconds Milliseconds to format
*
* @returns h:mm format
*/
export function millisecondsToHumanReadableString(milliseconds: number): string {
return Math.floor(milliseconds / 1000 / 60 / 60) + ':' +
String(Math.floor(milliseconds / 60000)).padStart(2, "0") + ''
} }

Some files were not shown because too many files have changed in this diff Show More