Compare commits
5 Commits
main
...
falukant-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d74f7b852b | ||
|
|
92d6b15c3f | ||
|
|
91f59062f5 | ||
|
|
1674086c73 | ||
|
|
5ddb099f5a |
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
description: C++-Worker unter src/ sind obsolet — nicht erweitern oder als Quelle für Spiellogik nutzen
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Legacy C++ (`src/`)
|
|
||||||
|
|
||||||
- Verzeichnis **`src/`** (C++-Worker, WebSocket-Server): **obsolet**. Keine neuen Features, keine fachlichen Fixes dort planen oder umsetzen, sofern der Nutzer nicht ausdrücklich etwas anderes verlangt.
|
|
||||||
- Falukant-Hintergrundlogik: **Backend** (`backend/`), **externer Daemon**, **Frontend** — siehe `docs/LEGACY_CPP_WORKERS.md`.
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
name: Deploy to production
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Detect vocab course changes
|
|
||||||
id: vocab_course_changes
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BASE="${{ gitea.event.before }}"
|
|
||||||
HEAD="${{ gitea.sha }}"
|
|
||||||
|
|
||||||
if [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
|
|
||||||
BASE="HEAD~1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git diff --name-only "$BASE" "$HEAD" > changed-files.txt
|
|
||||||
cat changed-files.txt
|
|
||||||
COMMIT_MESSAGE="$(git log -1 --pretty=%B "$HEAD" || true)"
|
|
||||||
|
|
||||||
if echo "$COMMIT_MESSAGE" | grep -qi '\[force-deploy\]'; then
|
|
||||||
echo "force_deploy=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "force_deploy=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -E '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))' changed-files.txt; then
|
|
||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -E '^frontend/' changed-files.txt >/dev/null; then
|
|
||||||
echo "frontend_changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "frontend_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -E '^backend/' changed-files.txt \
|
|
||||||
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab)$' >/dev/null; then
|
|
||||||
echo "backend_app_changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "backend_app_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# App-Code-Änderungen, die einen echten Deploy benötigen
|
|
||||||
# (Frontend oder Backend außerhalb reiner Kurs-/Dokument-Sync-Dateien)
|
|
||||||
if grep -E '^(frontend/|backend/)' changed-files.txt \
|
|
||||||
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))'; then
|
|
||||||
echo "app_changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "app_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Prepare SSH
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
printf "%s" "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan -p "${{ secrets.PROD_PORT }}" "${{ secrets.PROD_HOST }}" >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
- name: Test SSH connection
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/id_ed25519 \
|
|
||||||
-o StrictHostKeyChecking=no \
|
|
||||||
-o BatchMode=yes \
|
|
||||||
-p "${{ secrets.PROD_PORT }}" \
|
|
||||||
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
|
||||||
"echo SSH OK"
|
|
||||||
|
|
||||||
- name: Run deployment script
|
|
||||||
if: steps.vocab_course_changes.outputs.app_changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
|
|
||||||
run: |
|
|
||||||
DEPLOY_FLAGS=""
|
|
||||||
if [ "${{ steps.vocab_course_changes.outputs.force_deploy }}" = "true" ]; then
|
|
||||||
DEPLOY_FLAGS=""
|
|
||||||
elif [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" != "true" ]; then
|
|
||||||
DEPLOY_FLAGS="--skip-frontend"
|
|
||||||
elif [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" != "true" ]; then
|
|
||||||
DEPLOY_FLAGS="--skip-backend"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_TARGET="${{ secrets.PROD_DEPLOY_TARGET }}"
|
|
||||||
if [ -z "$DEPLOY_TARGET" ]; then
|
|
||||||
DEPLOY_TARGET="/opt/yourpart-green"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Deploy-Flags: ${DEPLOY_FLAGS:-<none>}"
|
|
||||||
echo "Deploy-Target: $DEPLOY_TARGET"
|
|
||||||
|
|
||||||
ssh -i ~/.ssh/id_ed25519 \
|
|
||||||
-p "${{ secrets.PROD_PORT }}" \
|
|
||||||
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
|
||||||
"/home/tsschulz/deploy-yourpart-bluegreen.sh ${DEPLOY_TARGET} ${DEPLOY_FLAGS}"
|
|
||||||
|
|
||||||
- name: Skip full deployment (no app changes)
|
|
||||||
if: steps.vocab_course_changes.outputs.app_changed != 'true' && steps.vocab_course_changes.outputs.force_deploy != 'true'
|
|
||||||
run: |
|
|
||||||
echo "Kein Full-Deploy: Es wurden keine Frontend/Backend-App-Dateien geändert."
|
|
||||||
|
|
||||||
- name: Sync vocab course content
|
|
||||||
if: steps.vocab_course_changes.outputs.changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/id_ed25519 \
|
|
||||||
-p "${{ secrets.PROD_PORT }}" \
|
|
||||||
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
|
||||||
"cd /opt/yourpart && npm --prefix backend run sync:vocab-courses"
|
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -5,9 +5,7 @@
|
|||||||
.depbe.sh
|
.depbe.sh
|
||||||
node_modules
|
node_modules
|
||||||
node_modules/*
|
node_modules/*
|
||||||
# package-lock.json wird versioniert (npm ci im Deploy braucht konsistente Locks zu package.json)
|
|
||||||
backend/.env
|
backend/.env
|
||||||
backend/.env.local
|
|
||||||
backend/images
|
backend/images
|
||||||
backend/images/*
|
backend/images/*
|
||||||
backend/node_modules
|
backend/node_modules
|
||||||
@@ -17,14 +15,5 @@ frontend/node_modules
|
|||||||
frontend/node_modules/*
|
frontend/node_modules/*
|
||||||
frontend/dist
|
frontend/dist
|
||||||
frontend/dist/*
|
frontend/dist/*
|
||||||
frontend/scripts/.i18n-de-fr-cache.json
|
|
||||||
frontend/scripts/.falukant-fr-smooth-cache.json
|
|
||||||
frontend/ceb-locale-audit-report.json
|
|
||||||
frontedtree.txt
|
frontedtree.txt
|
||||||
backend/dist/
|
backend/dist/
|
||||||
backend/data/model-cache
|
|
||||||
build
|
|
||||||
build/*
|
|
||||||
.vscode
|
|
||||||
.vscode/*
|
|
||||||
.clang-format
|
|
||||||
|
|||||||
156
CHURCH_MODELS.md
156
CHURCH_MODELS.md
@@ -1,156 +0,0 @@
|
|||||||
# Church Models - Übersicht für Daemon-Entwicklung
|
|
||||||
|
|
||||||
## 1. ChurchOfficeType (falukant_type.church_office_type)
|
|
||||||
|
|
||||||
**Schema:** `falukant_type`
|
|
||||||
**Tabelle:** `church_office_type`
|
|
||||||
**Zweck:** Definiert die verschiedenen Kirchenämter-Typen
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
id: INTEGER (PK, auto-increment)
|
|
||||||
name: STRING (z.B. "pope", "cardinal", "lay-preacher")
|
|
||||||
seatsPerRegion: INTEGER (Anzahl verfügbarer Plätze pro Region)
|
|
||||||
regionType: STRING (z.B. "country", "duchy", "city")
|
|
||||||
hierarchyLevel: INTEGER (0-8, höhere Zahl = höhere Position)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Beziehungen:**
|
|
||||||
- `hasMany` ChurchOffice (als `offices`)
|
|
||||||
- `hasMany` ChurchApplication (als `applications`)
|
|
||||||
- `hasMany` ChurchOfficeRequirement (als `requirements`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. ChurchOfficeRequirement (falukant_predefine.church_office_requirement)
|
|
||||||
|
|
||||||
**Schema:** `falukant_predefine`
|
|
||||||
**Tabelle:** `church_office_requirement`
|
|
||||||
**Zweck:** Definiert Voraussetzungen für Kirchenämter
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
id: INTEGER (PK, auto-increment)
|
|
||||||
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
|
||||||
prerequisiteOfficeTypeId: INTEGER (FK -> ChurchOfficeType.id, nullable)
|
|
||||||
minTitleLevel: INTEGER (nullable, optional)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Beziehungen:**
|
|
||||||
- `belongsTo` ChurchOfficeType (als `officeType`)
|
|
||||||
- `belongsTo` ChurchOfficeType (als `prerequisiteOfficeType`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ChurchOffice (falukant_data.church_office)
|
|
||||||
|
|
||||||
**Schema:** `falukant_data`
|
|
||||||
**Tabelle:** `church_office`
|
|
||||||
**Zweck:** Speichert tatsächlich besetzte Kirchenämter
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
id: INTEGER (PK, auto-increment)
|
|
||||||
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
|
||||||
characterId: INTEGER (FK -> FalukantCharacter.id)
|
|
||||||
regionId: INTEGER (FK -> RegionData.id)
|
|
||||||
supervisorId: INTEGER (FK -> FalukantCharacter.id, nullable)
|
|
||||||
createdAt: DATE
|
|
||||||
updatedAt: DATE
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Beziehungen:**
|
|
||||||
- `belongsTo` ChurchOfficeType (als `type`)
|
|
||||||
- `belongsTo` FalukantCharacter (als `holder`)
|
|
||||||
- `belongsTo` FalukantCharacter (als `supervisor`)
|
|
||||||
- `belongsTo` RegionData (als `region`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ChurchApplication (falukant_data.church_application)
|
|
||||||
|
|
||||||
**Schema:** `falukant_data`
|
|
||||||
**Tabelle:** `church_application`
|
|
||||||
**Zweck:** Speichert Bewerbungen für Kirchenämter
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
id: INTEGER (PK, auto-increment)
|
|
||||||
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
|
||||||
characterId: INTEGER (FK -> FalukantCharacter.id)
|
|
||||||
regionId: INTEGER (FK -> RegionData.id)
|
|
||||||
supervisorId: INTEGER (FK -> FalukantCharacter.id)
|
|
||||||
status: ENUM('pending', 'approved', 'rejected')
|
|
||||||
decisionDate: DATE (nullable)
|
|
||||||
createdAt: DATE
|
|
||||||
updatedAt: DATE
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Beziehungen:**
|
|
||||||
- `belongsTo` ChurchOfficeType (als `officeType`)
|
|
||||||
- `belongsTo` FalukantCharacter (als `applicant`)
|
|
||||||
- `belongsTo` FalukantCharacter (als `supervisor`)
|
|
||||||
- `belongsTo` RegionData (als `region`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Zusätzlich benötigte Models (für Daemon)
|
|
||||||
|
|
||||||
### RegionData (falukant_data.region)
|
|
||||||
- Wird für `regionId` in ChurchOffice und ChurchApplication benötigt
|
|
||||||
- Enthält `regionType` (country, duchy, markgravate, shire, county, city)
|
|
||||||
- Enthält `parentId` für Hierarchie
|
|
||||||
|
|
||||||
### FalukantCharacter (falukant_data.character)
|
|
||||||
- Wird für `characterId` (Inhaber/Bewerber) benötigt
|
|
||||||
- Wird für `supervisorId` benötigt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Wichtige Queries für Daemon
|
|
||||||
|
|
||||||
### Verfügbare Positionen finden
|
|
||||||
```sql
|
|
||||||
SELECT cot.*, COUNT(co.id) as occupied_seats
|
|
||||||
FROM falukant_type.church_office_type cot
|
|
||||||
LEFT JOIN falukant_data.church_office co
|
|
||||||
ON cot.id = co.office_type_id
|
|
||||||
AND co.region_id = ?
|
|
||||||
WHERE cot.region_type = ?
|
|
||||||
GROUP BY cot.id
|
|
||||||
HAVING COUNT(co.id) < cot.seats_per_region
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supervisor finden
|
|
||||||
```sql
|
|
||||||
SELECT co.*
|
|
||||||
FROM falukant_data.church_office co
|
|
||||||
JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id
|
|
||||||
WHERE co.region_id = ?
|
|
||||||
AND cot.hierarchy_level > (
|
|
||||||
SELECT hierarchy_level
|
|
||||||
FROM falukant_type.church_office_type
|
|
||||||
WHERE id = ?
|
|
||||||
)
|
|
||||||
ORDER BY cot.hierarchy_level ASC
|
|
||||||
LIMIT 1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Voraussetzungen prüfen
|
|
||||||
```sql
|
|
||||||
SELECT cor.*
|
|
||||||
FROM falukant_predefine.church_office_requirement cor
|
|
||||||
WHERE cor.office_type_id = ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bewerbungen für Supervisor
|
|
||||||
```sql
|
|
||||||
SELECT ca.*
|
|
||||||
FROM falukant_data.church_application ca
|
|
||||||
WHERE ca.supervisor_id = ?
|
|
||||||
AND ca.status = 'pending'
|
|
||||||
```
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# Kirchenämter - Hierarchie und Verfügbarkeit
|
|
||||||
|
|
||||||
## Regionstypen
|
|
||||||
- **country** (Land): Falukant
|
|
||||||
- **duchy** (Herzogtum): Hessen
|
|
||||||
- **markgravate** (Markgrafschaft): Groß-Benbach
|
|
||||||
- **shire** (Grafschaft): Siebenbachen
|
|
||||||
- **county** (Kreis): Bad Homburg, Maintal
|
|
||||||
- **city** (Stadt): Frankfurt, Oberursel, Offenbach, Königstein
|
|
||||||
|
|
||||||
## Kirchenämter (von höchstem zu niedrigstem Rang)
|
|
||||||
|
|
||||||
| Amt | Translation Key | Hierarchie-Level | Regionstyp | Plätze pro Region | Beschreibung |
|
|
||||||
|-----|----------------|-------------------|------------|-------------------|--------------|
|
|
||||||
| **Papst** | `pope` | 8 | country | 1 | Höchstes Amt, nur einer im ganzen Land |
|
|
||||||
| **Kardinal** | `cardinal` | 7 | country | 3 | Höchste Kardinäle, mehrere pro Land möglich |
|
|
||||||
| **Erzbischof** | `archbishop` | 6 | duchy | 1 | Pro Herzogtum ein Erzbischof |
|
|
||||||
| **Bischof** | `bishop` | 5 | markgravate | 1 | Pro Markgrafschaft ein Bischof |
|
|
||||||
| **Erzdiakon** | `archdeacon` | 4 | shire | 1 | Pro Grafschaft ein Erzdiakon |
|
|
||||||
| **Dekan** | `dean` | 3 | county | 1 | Pro Kreis ein Dekan |
|
|
||||||
| **Pfarrer** | `parish-priest` | 2 | city | 1 | Pro Stadt ein Pfarrer |
|
|
||||||
| **Dorfgeistlicher** | `village-priest` | 1 | city | 1 | Pro Stadt ein Dorfgeistlicher (Einstiegsposition) |
|
|
||||||
| **Laienprediger** | `lay-preacher` | 0 | city | 3 | Pro Stadt mehrere Laienprediger (niedrigste Position) |
|
|
||||||
|
|
||||||
## Verfügbare Positionen pro Regionstyp
|
|
||||||
|
|
||||||
### country (Land: Falukant)
|
|
||||||
- **Papst**: 1 Platz
|
|
||||||
- **Kardinal**: 3 Plätze
|
|
||||||
- **Gesamt**: 4 Plätze
|
|
||||||
|
|
||||||
### duchy (Herzogtum: Hessen)
|
|
||||||
- **Erzbischof**: 1 Platz
|
|
||||||
- **Gesamt**: 1 Platz
|
|
||||||
|
|
||||||
### markgravate (Markgrafschaft: Groß-Benbach)
|
|
||||||
- **Bischof**: 1 Platz
|
|
||||||
- **Gesamt**: 1 Platz
|
|
||||||
|
|
||||||
### shire (Grafschaft: Siebenbachen)
|
|
||||||
- **Erzdiakon**: 1 Platz
|
|
||||||
- **Gesamt**: 1 Platz
|
|
||||||
|
|
||||||
### county (Kreis: Bad Homburg, Maintal)
|
|
||||||
- **Dekan**: 1 Platz pro Kreis
|
|
||||||
- **Gesamt**: 1 Platz pro Kreis
|
|
||||||
|
|
||||||
### city (Stadt: Frankfurt, Oberursel, Offenbach, Königstein)
|
|
||||||
- **Pfarrer**: 1 Platz pro Stadt
|
|
||||||
- **Dorfgeistlicher**: 1 Platz pro Stadt
|
|
||||||
- **Laienprediger**: 3 Plätze pro Stadt
|
|
||||||
- **Gesamt**: 5 Plätze pro Stadt
|
|
||||||
|
|
||||||
## Hierarchie und Beförderungsweg
|
|
||||||
|
|
||||||
1. **Laienprediger** (lay-preacher) - Einstiegsposition, keine Voraussetzung
|
|
||||||
2. **Dorfgeistlicher** (village-priest) - Voraussetzung: Laienprediger
|
|
||||||
3. **Pfarrer** (parish-priest) - Voraussetzung: Dorfgeistlicher
|
|
||||||
4. **Dekan** (dean) - Voraussetzung: Pfarrer
|
|
||||||
5. **Erzdiakon** (archdeacon) - Voraussetzung: Dekan
|
|
||||||
6. **Bischof** (bishop) - Voraussetzung: Erzdiakon
|
|
||||||
7. **Erzbischof** (archbishop) - Voraussetzung: Bischof
|
|
||||||
8. **Kardinal** (cardinal) - Voraussetzung: Erzbischof
|
|
||||||
9. **Papst** (pope) - Voraussetzung: Kardinal
|
|
||||||
|
|
||||||
## Gesamtübersicht verfügbarer Positionen
|
|
||||||
|
|
||||||
- **Papst**: 1 Position (Land)
|
|
||||||
- **Kardinal**: 3 Positionen (Land)
|
|
||||||
- **Erzbischof**: 1 Position (Herzogtum)
|
|
||||||
- **Bischof**: 1 Position (Markgrafschaft)
|
|
||||||
- **Erzdiakon**: 1 Position (Grafschaft)
|
|
||||||
- **Dekan**: 2 Positionen (2 Kreise)
|
|
||||||
- **Pfarrer**: 4 Positionen (4 Städte)
|
|
||||||
- **Dorfgeistlicher**: 4 Positionen (4 Städte)
|
|
||||||
- **Laienprediger**: 12 Positionen (4 Städte × 3)
|
|
||||||
|
|
||||||
**Gesamt**: 30 Positionen im System
|
|
||||||
119
CMakeLists.txt
119
CMakeLists.txt
@@ -1,119 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
|
||||||
project(YourPartDaemon VERSION 1.0 LANGUAGES CXX)
|
|
||||||
|
|
||||||
# C++ Standard and Compiler Settings
|
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
||||||
# Use best available GCC for C++23 support (OpenSUSE Tumbleweed)
|
|
||||||
# Try GCC 15 first (best C++23 support), then GCC 13, then system default
|
|
||||||
find_program(GCC15_CC gcc-15)
|
|
||||||
find_program(GCC15_CXX g++-15)
|
|
||||||
find_program(GCC13_CC gcc-13)
|
|
||||||
find_program(GCC13_CXX g++-13)
|
|
||||||
|
|
||||||
if(GCC15_CC AND GCC15_CXX)
|
|
||||||
set(CMAKE_C_COMPILER ${GCC15_CC})
|
|
||||||
set(CMAKE_CXX_COMPILER ${GCC15_CXX})
|
|
||||||
message(STATUS "Using GCC 15 for best C++23 support")
|
|
||||||
elseif(GCC13_CC AND GCC13_CXX)
|
|
||||||
set(CMAKE_C_COMPILER ${GCC13_CC})
|
|
||||||
set(CMAKE_CXX_COMPILER ${GCC13_CXX})
|
|
||||||
message(STATUS "Using GCC 13 for C++23 support")
|
|
||||||
else()
|
|
||||||
message(STATUS "Using system default compiler")
|
|
||||||
endif()
|
|
||||||
# Optimize for GCC 13 with C++23
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto=auto -O3 -march=native -mtune=native")
|
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g -DDEBUG")
|
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native")
|
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
|
|
||||||
set(CMAKE_BUILD_TYPE Release)
|
|
||||||
|
|
||||||
# Include /usr/local if needed
|
|
||||||
list(APPEND CMAKE_PREFIX_PATH /usr/local)
|
|
||||||
|
|
||||||
# Find libwebsockets via pkg-config
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(LWS REQUIRED libwebsockets)
|
|
||||||
|
|
||||||
# Find other dependencies
|
|
||||||
find_package(PostgreSQL REQUIRED)
|
|
||||||
find_package(Threads REQUIRED)
|
|
||||||
find_package(nlohmann_json CONFIG REQUIRED)
|
|
||||||
|
|
||||||
# PostgreSQL C++ libpqxx
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(LIBPQXX REQUIRED libpqxx)
|
|
||||||
|
|
||||||
# Project sources and headers
|
|
||||||
set(SOURCES
|
|
||||||
src/main.cpp
|
|
||||||
src/config.cpp
|
|
||||||
src/connection_pool.cpp
|
|
||||||
src/database.cpp
|
|
||||||
src/character_creation_worker.cpp
|
|
||||||
src/produce_worker.cpp
|
|
||||||
src/message_broker.cpp
|
|
||||||
src/websocket_server.cpp
|
|
||||||
src/stockagemanager.cpp
|
|
||||||
src/director_worker.cpp
|
|
||||||
src/valuerecalculationworker.cpp
|
|
||||||
src/usercharacterworker.cpp
|
|
||||||
src/houseworker.cpp
|
|
||||||
src/politics_worker.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
set(HEADERS
|
|
||||||
src/config.h
|
|
||||||
src/database.h
|
|
||||||
src/connection_pool.h
|
|
||||||
src/worker.h
|
|
||||||
src/character_creation_worker.h
|
|
||||||
src/produce_worker.h
|
|
||||||
src/message_broker.h
|
|
||||||
src/websocket_server.h
|
|
||||||
src/stockagemanager.h
|
|
||||||
src/director_worker.h
|
|
||||||
src/valuerecalculationworker.h
|
|
||||||
src/usercharacterworker.h
|
|
||||||
src/houseworker.h
|
|
||||||
src/politics_worker.h
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define executable target
|
|
||||||
add_executable(yourpart-daemon ${SOURCES} ${HEADERS}
|
|
||||||
src/utils.h src/utils.cpp
|
|
||||||
src/underground_worker.h src/underground_worker.cpp)
|
|
||||||
|
|
||||||
# Include directories
|
|
||||||
target_include_directories(yourpart-daemon PRIVATE
|
|
||||||
${PostgreSQL_INCLUDE_DIRS}
|
|
||||||
${LIBPQXX_INCLUDE_DIRS}
|
|
||||||
${LWS_INCLUDE_DIRS}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Find systemd
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(SYSTEMD REQUIRED libsystemd)
|
|
||||||
|
|
||||||
# Link libraries
|
|
||||||
target_link_libraries(yourpart-daemon PRIVATE
|
|
||||||
${PostgreSQL_LIBRARIES}
|
|
||||||
Threads::Threads
|
|
||||||
z ssl crypto
|
|
||||||
${LIBPQXX_LIBRARIES}
|
|
||||||
${LWS_LIBRARIES}
|
|
||||||
nlohmann_json::nlohmann_json
|
|
||||||
${SYSTEMD_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Installation rules
|
|
||||||
install(TARGETS yourpart-daemon DESTINATION /usr/local/bin)
|
|
||||||
|
|
||||||
# Installiere Template als Referenz ZUERST (wird vom install-Skript benötigt)
|
|
||||||
install(FILES daemon.conf DESTINATION /etc/yourpart/ RENAME daemon.conf.example)
|
|
||||||
|
|
||||||
# Intelligente Konfigurationsdatei-Installation
|
|
||||||
# Verwendet ein CMake-Skript, das nur fehlende Keys hinzufügt, ohne bestehende zu überschreiben
|
|
||||||
# Das Skript liest das Template aus /etc/yourpart/daemon.conf.example oder dem Source-Verzeichnis
|
|
||||||
install(SCRIPT cmake/install-config.cmake)
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE QtCreatorProject>
|
|
||||||
<!-- Written by QtCreator 17.0.0, 2025-08-16T22:07:06. -->
|
|
||||||
<qtcreator>
|
|
||||||
<data>
|
|
||||||
<variable>EnvironmentId</variable>
|
|
||||||
<value type="QByteArray">{551ef6b3-a39b-43e2-9ee3-ad56e19ff4f4}</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.ActiveTarget</variable>
|
|
||||||
<value type="qlonglong">0</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.EditorSettings</variable>
|
|
||||||
<valuemap type="QVariantMap">
|
|
||||||
<value type="bool" key="EditorConfiguration.AutoDetect">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
|
|
||||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
|
|
||||||
<value type="QString" key="language">Cpp</value>
|
|
||||||
<valuemap type="QVariantMap" key="value">
|
|
||||||
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
|
|
||||||
</valuemap>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
|
|
||||||
<value type="QString" key="language">QmlJS</value>
|
|
||||||
<valuemap type="QVariantMap" key="value">
|
|
||||||
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
|
|
||||||
</valuemap>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
|
|
||||||
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.IndentSize">4</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.LineEndingBehavior">0</value>
|
|
||||||
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
|
|
||||||
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
|
|
||||||
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">2</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
|
|
||||||
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
|
|
||||||
<value type="int" key="EditorConfiguration.TabSize">8</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
|
|
||||||
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
|
|
||||||
</valuemap>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.PluginSettings</variable>
|
|
||||||
<valuemap type="QVariantMap">
|
|
||||||
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
|
|
||||||
<value type="bool" key="AutoTest.Framework.Boost">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.CTest">false</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.Catch">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.GTest">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="bool" key="AutoTest.ApplyFilter">false</value>
|
|
||||||
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
|
|
||||||
<valuelist type="QVariantList" key="AutoTest.PathFilters"/>
|
|
||||||
<value type="int" key="AutoTest.RunAfterBuild">0</value>
|
|
||||||
<value type="bool" key="AutoTest.UseGlobal">true</value>
|
|
||||||
<valuemap type="QVariantMap" key="ClangTools">
|
|
||||||
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
|
|
||||||
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
|
|
||||||
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
|
|
||||||
<value type="int" key="ClangTools.ParallelJobs">8</value>
|
|
||||||
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
|
|
||||||
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
|
|
||||||
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
|
|
||||||
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
|
|
||||||
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
|
|
||||||
</valuemap>
|
|
||||||
</valuemap>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.Target.0</variable>
|
|
||||||
<valuemap type="QVariantMap">
|
|
||||||
<value type="QString" key="DeviceType">Desktop</value>
|
|
||||||
<value type="bool" key="HasPerBcDcs">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{78ff90a3-f672-45c2-ad08-343b0923896f}</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
|
||||||
<value type="QString" key="CMake.Build.Type">Debug</value>
|
|
||||||
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
|
||||||
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
|
||||||
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
|
|
||||||
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
|
|
||||||
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
|
||||||
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
|
||||||
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
|
||||||
-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
|
||||||
-DCMAKE_BUILD_TYPE:STRING=Release
|
|
||||||
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build/</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">all</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">clean</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Release</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString"></value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
|
||||||
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
|
||||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
|
||||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
|
||||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
|
||||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
|
||||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
|
||||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
|
||||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
|
||||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
|
||||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
|
||||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.1">
|
|
||||||
<value type="QString" key="CMake.Build.Type">Debug</value>
|
|
||||||
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
|
||||||
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
|
||||||
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
|
|
||||||
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
|
|
||||||
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
|
||||||
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
|
||||||
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
|
||||||
-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
|
||||||
-DCMAKE_BUILD_TYPE:STRING=Debug
|
|
||||||
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
|
|
||||||
<value type="QString" key="CMake.Source.Directory">/mnt/share/torsten/Programs/yourpart-daemon</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">all</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">clean</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Debug (importiert)</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">-1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">install</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
|
||||||
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
|
||||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
|
||||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">0</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">2</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString"></value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
|
||||||
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
|
||||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
|
||||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
|
||||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
|
||||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
|
||||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
|
||||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
|
||||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
|
||||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
|
||||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
|
||||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
|
||||||
</valuemap>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.TargetCount</variable>
|
|
||||||
<value type="qlonglong">1</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
|
|
||||||
<value type="int">22</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>Version</variable>
|
|
||||||
<value type="int">22</value>
|
|
||||||
</data>
|
|
||||||
</qtcreator>
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE QtCreatorProject>
|
|
||||||
<!-- Written by QtCreator 12.0.2, 2025-07-18T07:45:58. -->
|
|
||||||
<qtcreator>
|
|
||||||
<data>
|
|
||||||
<variable>EnvironmentId</variable>
|
|
||||||
<value type="QByteArray">{d36652ff-969b-426b-a63f-1edd325096c5}</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.ActiveTarget</variable>
|
|
||||||
<value type="qlonglong">0</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.EditorSettings</variable>
|
|
||||||
<valuemap type="QVariantMap">
|
|
||||||
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.AutoSpacesForTabs">false</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
|
|
||||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
|
|
||||||
<value type="QString" key="language">Cpp</value>
|
|
||||||
<valuemap type="QVariantMap" key="value">
|
|
||||||
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
|
|
||||||
</valuemap>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
|
|
||||||
<value type="QString" key="language">QmlJS</value>
|
|
||||||
<valuemap type="QVariantMap" key="value">
|
|
||||||
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
|
|
||||||
</valuemap>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
|
|
||||||
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.IndentSize">4</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
|
|
||||||
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
|
|
||||||
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">0</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
|
|
||||||
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
|
|
||||||
<value type="int" key="EditorConfiguration.TabSize">8</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
|
|
||||||
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
|
|
||||||
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
|
|
||||||
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
|
|
||||||
</valuemap>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.PluginSettings</variable>
|
|
||||||
<valuemap type="QVariantMap">
|
|
||||||
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
|
|
||||||
<value type="bool" key="AutoTest.Framework.Boost">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.CTest">false</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.Catch">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.GTest">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
|
|
||||||
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
|
|
||||||
<value type="int" key="AutoTest.RunAfterBuild">0</value>
|
|
||||||
<value type="bool" key="AutoTest.UseGlobal">true</value>
|
|
||||||
<valuemap type="QVariantMap" key="ClangTools">
|
|
||||||
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
|
|
||||||
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
|
|
||||||
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
|
|
||||||
<value type="int" key="ClangTools.ParallelJobs">8</value>
|
|
||||||
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
|
|
||||||
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
|
|
||||||
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
|
|
||||||
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
|
|
||||||
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="CppEditor.QuickFix">
|
|
||||||
<value type="bool" key="UseGlobalSettings">true</value>
|
|
||||||
</valuemap>
|
|
||||||
</valuemap>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.Target.0</variable>
|
|
||||||
<valuemap type="QVariantMap">
|
|
||||||
<value type="QString" key="DeviceType">Desktop</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{3c6cfc13-714d-4db1-bd45-b9794643cc67}</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
|
||||||
<value type="QString" key="CMake.Build.Type">Debug</value>
|
|
||||||
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
|
||||||
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
|
||||||
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
|
||||||
-DCMAKE_BUILD_TYPE:STRING=Build
|
|
||||||
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
|
||||||
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}
|
|
||||||
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
|
||||||
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
|
||||||
-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}</value>
|
|
||||||
<value type="QString" key="CMake.Source.Directory">/home/torsten/Programs/yourpart-daemon</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">all</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
|
||||||
</valuemap>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
|
||||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
|
||||||
<value type="QString">clean</value>
|
|
||||||
</valuelist>
|
|
||||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
|
||||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
|
||||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">1</value>
|
|
||||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
|
||||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
|
||||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
|
||||||
<value type="QString" key="Analyzer.Valgrind.ValgrindExecutable">/usr/bin/valgrind</value>
|
|
||||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
|
||||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
|
||||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
|
||||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.yourpart-daemon</value>
|
|
||||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
|
||||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
|
||||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
|
||||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
|
||||||
</valuemap>
|
|
||||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
|
||||||
</valuemap>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.TargetCount</variable>
|
|
||||||
<value type="qlonglong">1</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
|
|
||||||
<value type="int">22</value>
|
|
||||||
</data>
|
|
||||||
<data>
|
|
||||||
<variable>Version</variable>
|
|
||||||
<value type="int">22</value>
|
|
||||||
</data>
|
|
||||||
</qtcreator>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
## zum testen des push
|
|
||||||
|
|
||||||
Hinweis: Das Verzeichnis **`src/`** (C++-Worker) ist veraltet; siehe [`docs/LEGACY_CPP_WORKERS.md`](docs/LEGACY_CPP_WORKERS.md).
|
|
||||||
168
SSL-SETUP.md
168
SSL-SETUP.md
@@ -1,168 +0,0 @@
|
|||||||
# SSL/TLS Setup für YourPart Daemon
|
|
||||||
|
|
||||||
Dieses Dokument beschreibt, wie Sie SSL/TLS-Zertifikate für den YourPart Daemon einrichten können.
|
|
||||||
|
|
||||||
## 🚀 Schnellstart
|
|
||||||
|
|
||||||
### 1. Self-Signed Certificate (Entwicklung/Testing)
|
|
||||||
```bash
|
|
||||||
./setup-ssl.sh
|
|
||||||
# Wählen Sie Option 1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Let's Encrypt Certificate (Produktion)
|
|
||||||
```bash
|
|
||||||
./setup-ssl.sh
|
|
||||||
# Wählen Sie Option 2
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Apache2-Zertifikate verwenden (empfohlen für Ubuntu)
|
|
||||||
```bash
|
|
||||||
./setup-ssl.sh
|
|
||||||
# Wählen Sie Option 4
|
|
||||||
# Verwendet bereits vorhandene Apache2-Zertifikate
|
|
||||||
# ⚠️ Warnung bei Snakeoil-Zertifikaten (nur für localhost)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. DNS-01 Challenge (für komplexe Setups)
|
|
||||||
```bash
|
|
||||||
./setup-ssl-dns.sh
|
|
||||||
# Für Cloudflare, Route53, etc.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Voraussetzungen
|
|
||||||
|
|
||||||
### Für Apache2-Zertifikate:
|
|
||||||
- Apache2 installiert oder Zertifikate in Standard-Pfaden
|
|
||||||
- Unterstützte Pfade (priorisiert nach Qualität):
|
|
||||||
- `/etc/letsencrypt/live/your-part.de/fullchain.pem` (Let's Encrypt - empfohlen)
|
|
||||||
- `/etc/letsencrypt/live/$(hostname)/fullchain.pem` (Let's Encrypt)
|
|
||||||
- `/etc/apache2/ssl/apache.crt` (Custom Apache2)
|
|
||||||
- `/etc/ssl/certs/ssl-cert-snakeoil.pem` (Ubuntu Standard - nur localhost)
|
|
||||||
|
|
||||||
### Für Let's Encrypt (HTTP-01 Challenge):
|
|
||||||
- Port 80 muss verfügbar sein
|
|
||||||
- Domain `your-part.de` muss auf den Server zeigen
|
|
||||||
- Kein anderer Service auf Port 80
|
|
||||||
|
|
||||||
### Für DNS-01 Challenge:
|
|
||||||
- DNS-Provider Account (Cloudflare, Route53, etc.)
|
|
||||||
- API-Credentials für DNS-Management
|
|
||||||
|
|
||||||
## 🔧 Konfiguration
|
|
||||||
|
|
||||||
Nach der Zertifikats-Erstellung:
|
|
||||||
|
|
||||||
1. **SSL in der Konfiguration aktivieren:**
|
|
||||||
```ini
|
|
||||||
# /etc/yourpart/daemon.conf
|
|
||||||
WEBSOCKET_SSL_ENABLED=true
|
|
||||||
WEBSOCKET_SSL_CERT_PATH=/etc/yourpart/server.crt
|
|
||||||
WEBSOCKET_SSL_KEY_PATH=/etc/yourpart/server.key
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Daemon neu starten:**
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart yourpart-daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Verbindung testen:**
|
|
||||||
```bash
|
|
||||||
# WebSocket Secure
|
|
||||||
wss://your-part.de:4551
|
|
||||||
|
|
||||||
# Oder ohne SSL
|
|
||||||
ws://your-part.de:4551
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Automatische Erneuerung
|
|
||||||
|
|
||||||
### Let's Encrypt-Zertifikate:
|
|
||||||
- **Cron Job:** Täglich um 2:30 Uhr
|
|
||||||
- **Script:** `/etc/yourpart/renew-ssl.sh`
|
|
||||||
- **Log:** `/var/log/yourpart/ssl-renewal.log`
|
|
||||||
|
|
||||||
### Apache2-Zertifikate:
|
|
||||||
- **Ubuntu Snakeoil:** Automatisch von Apache2 verwaltet
|
|
||||||
- **Let's Encrypt:** Automatische Erneuerung wenn erkannt
|
|
||||||
- **Custom:** Manuelle Verwaltung erforderlich
|
|
||||||
|
|
||||||
## 📁 Dateistruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
/etc/yourpart/
|
|
||||||
├── server.crt # Zertifikat (Symlink zu Let's Encrypt)
|
|
||||||
├── server.key # Private Key (Symlink zu Let's Encrypt)
|
|
||||||
├── renew-ssl.sh # Auto-Renewal Script
|
|
||||||
└── cloudflare.ini # Cloudflare Credentials (falls verwendet)
|
|
||||||
|
|
||||||
/etc/letsencrypt/live/your-part.de/
|
|
||||||
├── fullchain.pem # Vollständige Zertifikatskette
|
|
||||||
├── privkey.pem # Private Key
|
|
||||||
├── cert.pem # Zertifikat
|
|
||||||
└── chain.pem # Intermediate Certificate
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Troubleshooting
|
|
||||||
|
|
||||||
### Zertifikat wird nicht akzeptiert
|
|
||||||
```bash
|
|
||||||
# Prüfe Zertifikats-Gültigkeit
|
|
||||||
openssl x509 -in /etc/yourpart/server.crt -text -noout
|
|
||||||
|
|
||||||
# Prüfe Berechtigungen
|
|
||||||
ls -la /etc/yourpart/server.*
|
|
||||||
```
|
|
||||||
|
|
||||||
### Let's Encrypt Challenge fehlgeschlagen
|
|
||||||
```bash
|
|
||||||
# Prüfe Port 80
|
|
||||||
sudo netstat -tlnp | grep :80
|
|
||||||
|
|
||||||
# Prüfe DNS
|
|
||||||
nslookup your-part.de
|
|
||||||
|
|
||||||
# Prüfe Firewall
|
|
||||||
sudo ufw status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Auto-Renewal funktioniert nicht
|
|
||||||
```bash
|
|
||||||
# Prüfe Cron Jobs
|
|
||||||
sudo crontab -l
|
|
||||||
|
|
||||||
# Teste Renewal Script
|
|
||||||
sudo /etc/yourpart/renew-ssl.sh
|
|
||||||
|
|
||||||
# Prüfe Logs
|
|
||||||
tail -f /var/log/yourpart/ssl-renewal.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Sicherheit
|
|
||||||
|
|
||||||
### Berechtigungen
|
|
||||||
- **Zertifikat:** `644` (readable by all, writable by owner)
|
|
||||||
- **Private Key:** `600` (readable/writable by owner only)
|
|
||||||
- **Owner:** `yourpart:yourpart`
|
|
||||||
|
|
||||||
### Firewall
|
|
||||||
```bash
|
|
||||||
# Öffne Port 80 für Let's Encrypt Challenge
|
|
||||||
sudo ufw allow 80/tcp
|
|
||||||
|
|
||||||
# Öffne Port 4551 für WebSocket
|
|
||||||
sudo ufw allow 4551/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 Weitere Informationen
|
|
||||||
|
|
||||||
- [Let's Encrypt Dokumentation](https://letsencrypt.org/docs/)
|
|
||||||
- [Certbot Dokumentation](https://certbot.eff.org/docs/)
|
|
||||||
- [libwebsockets SSL](https://libwebsockets.org/lws-api-doc-master/html/group__ssl.html)
|
|
||||||
|
|
||||||
## 🆘 Support
|
|
||||||
|
|
||||||
Bei Problemen:
|
|
||||||
1. Prüfen Sie die Logs: `sudo journalctl -u yourpart-daemon -f`
|
|
||||||
2. Testen Sie die Zertifikate: `openssl s_client -connect your-part.de:4551`
|
|
||||||
3. Prüfen Sie die Firewall: `sudo ufw status`
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
This project now supports a per-region sales tax (`tax_percent`) for Falukant.
|
This project now supports a per-region sales tax (`tax_percent`) for Falukant.
|
||||||
|
|
||||||
Migration
|
Migration
|
||||||
- A SQL migration was added: `backend/migrations-archive/20260101000000-add-tax-percent-to-region.cjs`.
|
- A SQL migration was added: `backend/migrations/20260101000000-add-tax-percent-to-region.cjs`.
|
||||||
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
|
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
|
||||||
|
|
||||||
Runtime configuration
|
Runtime configuration
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script zur Analyse und Empfehlung von Indizes
|
|
||||||
*
|
|
||||||
* Analysiert:
|
|
||||||
* - Tabellen mit vielen Sequential Scans
|
|
||||||
* - Fehlende Composite Indizes für häufige JOINs
|
|
||||||
* - Ungenutzte Indizes
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './config/loadEnv.js';
|
|
||||||
import { sequelize } from './utils/sequelize.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Index-Analyse und Empfehlungen\n');
|
|
||||||
console.log('='.repeat(60) + '\n');
|
|
||||||
|
|
||||||
// 1. Tabellen mit vielen Sequential Scans
|
|
||||||
await analyzeSequentialScans();
|
|
||||||
|
|
||||||
// 2. Prüfe häufige JOIN-Patterns
|
|
||||||
await analyzeJoinPatterns();
|
|
||||||
|
|
||||||
// 3. Ungenutzte Indizes
|
|
||||||
await analyzeUnusedIndexes();
|
|
||||||
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
console.log('✅ Analyse abgeschlossen\n');
|
|
||||||
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler:', error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function analyzeSequentialScans() {
|
|
||||||
console.log('📊 1. Tabellen mit vielen Sequential Scans\n');
|
|
||||||
|
|
||||||
const [tables] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || relname as table_name,
|
|
||||||
seq_scan,
|
|
||||||
seq_tup_read,
|
|
||||||
idx_scan,
|
|
||||||
seq_tup_read / NULLIF(seq_scan, 0) as avg_rows_per_scan,
|
|
||||||
CASE
|
|
||||||
WHEN seq_scan + idx_scan > 0
|
|
||||||
THEN round((seq_scan::numeric / (seq_scan + idx_scan)) * 100, 2)
|
|
||||||
ELSE 0
|
|
||||||
END as seq_scan_percent
|
|
||||||
FROM pg_stat_user_tables
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
AND seq_scan > 1000
|
|
||||||
ORDER BY seq_tup_read DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (tables.length > 0) {
|
|
||||||
console.log(' ⚠️ Tabellen mit vielen Sequential Scans:');
|
|
||||||
tables.forEach(t => {
|
|
||||||
console.log(`\n ${t.table_name}:`);
|
|
||||||
console.log(` Sequential Scans: ${parseInt(t.seq_scan).toLocaleString()}`);
|
|
||||||
console.log(` Zeilen gelesen: ${parseInt(t.seq_tup_read).toLocaleString()}`);
|
|
||||||
console.log(` Index Scans: ${parseInt(t.idx_scan).toLocaleString()}`);
|
|
||||||
console.log(` Seq Scan Anteil: ${t.seq_scan_percent}%`);
|
|
||||||
console.log(` Ø Zeilen pro Scan: ${parseInt(t.avg_rows_per_scan).toLocaleString()}`);
|
|
||||||
|
|
||||||
if (t.seq_scan_percent > 50) {
|
|
||||||
console.log(` ⚠️ KRITISCH: Mehr als 50% Sequential Scans!`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function analyzeJoinPatterns() {
|
|
||||||
console.log('🔗 2. Analyse häufiger JOIN-Patterns\n');
|
|
||||||
|
|
||||||
// Prüfe welche Indizes auf knowledge existieren
|
|
||||||
const [knowledgeIndexes] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
indexname,
|
|
||||||
indexdef
|
|
||||||
FROM pg_indexes
|
|
||||||
WHERE schemaname = 'falukant_data'
|
|
||||||
AND tablename = 'knowledge'
|
|
||||||
ORDER BY indexname;
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log(' Indizes auf falukant_data.knowledge:');
|
|
||||||
if (knowledgeIndexes.length > 0) {
|
|
||||||
knowledgeIndexes.forEach(idx => {
|
|
||||||
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' Keine Indizes gefunden');
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Empfehlung: Composite Index auf (character_id, product_id)
|
|
||||||
const [knowledgeUsage] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
idx_scan,
|
|
||||||
idx_tup_read,
|
|
||||||
idx_tup_fetch
|
|
||||||
FROM pg_stat_user_indexes
|
|
||||||
WHERE schemaname = 'falukant_data'
|
|
||||||
AND relname = 'knowledge'
|
|
||||||
AND indexrelname = 'idx_knowledge_character_id';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (knowledgeUsage.length > 0) {
|
|
||||||
const usage = knowledgeUsage[0];
|
|
||||||
console.log(' Aktuelle Nutzung von idx_knowledge_character_id:');
|
|
||||||
console.log(` Scans: ${parseInt(usage.idx_scan).toLocaleString()}`);
|
|
||||||
console.log(` Zeilen gelesen: ${parseInt(usage.idx_tup_read).toLocaleString()}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
console.log(' 💡 Empfehlung:');
|
|
||||||
console.log(' CREATE INDEX IF NOT EXISTS idx_knowledge_character_product');
|
|
||||||
console.log(' ON falukant_data.knowledge(character_id, product_id);');
|
|
||||||
console.log(' → Wird häufig für JOINs mit character_id UND product_id verwendet\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe character Indizes
|
|
||||||
const [characterIndexes] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
indexname,
|
|
||||||
indexdef
|
|
||||||
FROM pg_indexes
|
|
||||||
WHERE schemaname = 'falukant_data'
|
|
||||||
AND tablename = 'character'
|
|
||||||
ORDER BY indexname;
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log(' Indizes auf falukant_data.character:');
|
|
||||||
if (characterIndexes.length > 0) {
|
|
||||||
characterIndexes.forEach(idx => {
|
|
||||||
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function analyzeUnusedIndexes() {
|
|
||||||
console.log('🗑️ 3. Ungenutzte Indizes\n');
|
|
||||||
|
|
||||||
const [unused] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || indexrelname as index_name,
|
|
||||||
schemaname || '.' || relname as table_name,
|
|
||||||
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
|
||||||
idx_scan as scans,
|
|
||||||
pg_relation_size(indexrelid) as size_bytes
|
|
||||||
FROM pg_stat_user_indexes
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
AND idx_scan = 0
|
|
||||||
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
|
|
||||||
ORDER BY pg_relation_size(indexrelid) DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (unused.length > 0) {
|
|
||||||
console.log(' ⚠️ Ungenutzte Indizes (> 1MB):');
|
|
||||||
unused.forEach(idx => {
|
|
||||||
console.log(` ${idx.index_name} auf ${idx.table_name}`);
|
|
||||||
console.log(` Größe: ${idx.index_size}, Scans: ${idx.scans}`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
console.log(' 💡 Überlege, ob diese Indizes gelöscht werden können:');
|
|
||||||
console.log(' DROP INDEX IF EXISTS <index_name>;');
|
|
||||||
console.log('');
|
|
||||||
} else {
|
|
||||||
console.log(' ✅ Keine großen ungenutzten Indizes gefunden\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -12,7 +12,6 @@ import socialnetworkRouter from './routers/socialnetworkRouter.js';
|
|||||||
import forumRouter from './routers/forumRouter.js';
|
import forumRouter from './routers/forumRouter.js';
|
||||||
import falukantRouter from './routers/falukantRouter.js';
|
import falukantRouter from './routers/falukantRouter.js';
|
||||||
import friendshipRouter from './routers/friendshipRouter.js';
|
import friendshipRouter from './routers/friendshipRouter.js';
|
||||||
import modelsProxyRouter from './routers/modelsProxyRouter.js';
|
|
||||||
import blogRouter from './routers/blogRouter.js';
|
import blogRouter from './routers/blogRouter.js';
|
||||||
import match3Router from './routers/match3Router.js';
|
import match3Router from './routers/match3Router.js';
|
||||||
import taxiRouter from './routers/taxiRouter.js';
|
import taxiRouter from './routers/taxiRouter.js';
|
||||||
@@ -20,10 +19,6 @@ import taxiMapRouter from './routers/taxiMapRouter.js';
|
|||||||
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
|
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
|
||||||
import termineRouter from './routers/termineRouter.js';
|
import termineRouter from './routers/termineRouter.js';
|
||||||
import vocabRouter from './routers/vocabRouter.js';
|
import vocabRouter from './routers/vocabRouter.js';
|
||||||
import dashboardRouter from './routers/dashboardRouter.js';
|
|
||||||
import newsRouter from './routers/newsRouter.js';
|
|
||||||
import calendarRouter from './routers/calendarRouter.js';
|
|
||||||
import moderationRouter from './routers/moderationRouter.js';
|
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import './jobs/sessionCleanup.js';
|
import './jobs/sessionCleanup.js';
|
||||||
|
|
||||||
@@ -37,19 +32,6 @@ const app = express();
|
|||||||
// - LOG_ALL_REQ=1: Logge alle Requests
|
// - LOG_ALL_REQ=1: Logge alle Requests
|
||||||
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
|
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
|
||||||
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
|
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
|
||||||
const defaultCorsOrigins = [
|
|
||||||
'http://localhost:3000',
|
|
||||||
'http://localhost:5173',
|
|
||||||
'http://127.0.0.1:3000',
|
|
||||||
'http://127.0.0.1:5173'
|
|
||||||
];
|
|
||||||
const corsOrigins = (process.env.CORS_ORIGINS || process.env.FRONTEND_URL || '')
|
|
||||||
.split(',')
|
|
||||||
.map((origin) => origin.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
const effectiveCorsOrigins = corsOrigins.length > 0 ? corsOrigins : defaultCorsOrigins;
|
|
||||||
const corsAllowAll = process.env.CORS_ALLOW_ALL === '1';
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
|
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
|
||||||
req.reqId = reqId;
|
req.reqId = reqId;
|
||||||
@@ -65,31 +47,15 @@ app.use((req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin(origin, callback) {
|
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
|
||||||
if (!origin) {
|
|
||||||
return callback(null, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (corsAllowAll || effectiveCorsOrigins.includes(origin)) {
|
|
||||||
return callback(null, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback(null, false);
|
|
||||||
},
|
|
||||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'userid', 'authcode', 'userId', 'authCode'],
|
allowedHeaders: ['Content-Type', 'Authorization', 'userId', 'authCode'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
preflightContinue: false,
|
preflightContinue: false,
|
||||||
optionsSuccessStatus: 204
|
optionsSuccessStatus: 204
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
return cors(corsOptions)(req, res, next);
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
app.use(express.json()); // To handle JSON request bodies
|
app.use(express.json()); // To handle JSON request bodies
|
||||||
|
|
||||||
app.use('/api/chat', chatRouter);
|
app.use('/api/chat', chatRouter);
|
||||||
@@ -106,36 +72,19 @@ app.use('/api/contact', contactRouter);
|
|||||||
app.use('/api/socialnetwork', socialnetworkRouter);
|
app.use('/api/socialnetwork', socialnetworkRouter);
|
||||||
app.use('/api/vocab', vocabRouter);
|
app.use('/api/vocab', vocabRouter);
|
||||||
app.use('/api/forum', forumRouter);
|
app.use('/api/forum', forumRouter);
|
||||||
app.use('/api/moderation', moderationRouter);
|
|
||||||
app.use('/api/falukant', falukantRouter);
|
app.use('/api/falukant', falukantRouter);
|
||||||
app.use('/api/friendships', friendshipRouter);
|
app.use('/api/friendships', friendshipRouter);
|
||||||
app.use('/api/models', modelsProxyRouter);
|
|
||||||
app.use('/api/blog', blogRouter);
|
app.use('/api/blog', blogRouter);
|
||||||
app.use('/api/termine', termineRouter);
|
app.use('/api/termine', termineRouter);
|
||||||
app.use('/api/dashboard', dashboardRouter);
|
|
||||||
app.use('/api/news', newsRouter);
|
|
||||||
app.use('/api/calendar', calendarRouter);
|
|
||||||
|
|
||||||
// Serve frontend SPA for non-API routes to support history mode clean URLs
|
// Serve frontend SPA for non-API routes to support history mode clean URLs
|
||||||
// /models/* nicht statisch ausliefern – nur über /api/models (Proxy mit Komprimierung)
|
|
||||||
const frontendDir = path.join(__dirname, '../frontend');
|
const frontendDir = path.join(__dirname, '../frontend');
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (req.path.startsWith('/models/')) {
|
|
||||||
return res.status(404).send('Use /api/models/ for 3D models (optimized).');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
app.use(express.static(path.join(frontendDir, 'dist')));
|
app.use(express.static(path.join(frontendDir, 'dist')));
|
||||||
app.get(/^\/(?!api\/).*/, (req, res) => {
|
app.get(/^\/(?!api\/).*/, (req, res) => {
|
||||||
res.sendFile(path.join(frontendDir, 'dist', 'index.html'));
|
res.sendFile(path.join(frontendDir, 'dist', 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback 404 for unknown API routes
|
// Fallback 404 for unknown API routes
|
||||||
app.use((req, res, next) => {
|
app.use('/api/*', (req, res) => res.status(404).send('404 Not Found'));
|
||||||
if (req.path.startsWith('/api/')) {
|
|
||||||
return res.status(404).send('404 Not Found');
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script zum Prüfen und Bereinigen von PostgreSQL-Verbindungen
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './config/loadEnv.js';
|
|
||||||
import { sequelize } from './utils/sequelize.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Prüfe PostgreSQL-Verbindungen...\n');
|
|
||||||
|
|
||||||
// Prüfe aktive Verbindungen
|
|
||||||
const [connections] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
count(*) as total,
|
|
||||||
count(*) FILTER (WHERE state = 'active') as active,
|
|
||||||
count(*) FILTER (WHERE state = 'idle') as idle,
|
|
||||||
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
|
||||||
count(*) FILTER (WHERE usename = current_user) as my_connections
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE datname = current_database();
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('📊 Verbindungsstatistik:');
|
|
||||||
console.log(` Gesamt: ${connections[0].total}`);
|
|
||||||
console.log(` Aktiv: ${connections[0].active}`);
|
|
||||||
console.log(` Idle: ${connections[0].idle}`);
|
|
||||||
console.log(` Idle in Transaction: ${connections[0].idle_in_transaction}`);
|
|
||||||
console.log(` Meine Verbindungen: ${connections[0].my_connections}\n`);
|
|
||||||
|
|
||||||
// Prüfe max_connections Limit
|
|
||||||
const [maxConn] = await sequelize.query(`
|
|
||||||
SELECT setting::int as max_connections
|
|
||||||
FROM pg_settings
|
|
||||||
WHERE name = 'max_connections';
|
|
||||||
`);
|
|
||||||
console.log(`📈 Max Connections Limit: ${maxConn[0].max_connections}`);
|
|
||||||
console.log(`📉 Verfügbare Connections: ${maxConn[0].max_connections - connections[0].total}\n`);
|
|
||||||
|
|
||||||
// Zeige alte idle Verbindungen
|
|
||||||
const [oldConnections] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
pid,
|
|
||||||
usename,
|
|
||||||
application_name,
|
|
||||||
state,
|
|
||||||
state_change,
|
|
||||||
now() - state_change as idle_duration,
|
|
||||||
query
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE datname = current_database()
|
|
||||||
AND state = 'idle'
|
|
||||||
AND state_change < now() - interval '1 minute'
|
|
||||||
ORDER BY state_change ASC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (oldConnections.length > 0) {
|
|
||||||
console.log(`⚠️ Gefunden ${oldConnections.length} alte idle Verbindungen (> 1 Minute):`);
|
|
||||||
oldConnections.forEach(conn => {
|
|
||||||
console.log(` PID: ${conn.pid}, User: ${conn.usename}, Idle seit: ${conn.idle_duration}`);
|
|
||||||
});
|
|
||||||
console.log('\n💡 Tipp: Du kannst alte Verbindungen beenden mit:');
|
|
||||||
console.log(' SELECT pg_terminate_backend(pid) FROM pg_stat_activity');
|
|
||||||
console.log(' WHERE datname = current_database() AND state = \'idle\' AND state_change < now() - interval \'5 minutes\';\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe ob wir nahe am Limit sind
|
|
||||||
const usagePercent = (connections[0].total / maxConn[0].max_connections) * 100;
|
|
||||||
if (usagePercent > 80) {
|
|
||||||
console.log(`⚠️ WARNUNG: ${usagePercent.toFixed(1)}% der verfügbaren Verbindungen werden verwendet!`);
|
|
||||||
console.log(' Es könnte sein, dass nicht genug Verbindungen verfügbar sind.\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script zur Analyse des knowledge_pkey Problems
|
|
||||||
*
|
|
||||||
* Prüft warum knowledge_pkey nicht verwendet wird
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './config/loadEnv.js';
|
|
||||||
import { sequelize } from './utils/sequelize.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Analyse knowledge_pkey Problem\n');
|
|
||||||
console.log('='.repeat(60) + '\n');
|
|
||||||
|
|
||||||
// Prüfe ob knowledge einen Primary Key hat
|
|
||||||
const [pkInfo] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
a.attname as column_name,
|
|
||||||
t.conname as constraint_name,
|
|
||||||
t.contype as constraint_type
|
|
||||||
FROM pg_constraint t
|
|
||||||
JOIN pg_class c ON c.oid = t.conrelid
|
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
||||||
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(t.conkey)
|
|
||||||
WHERE n.nspname = 'falukant_data'
|
|
||||||
AND c.relname = 'knowledge'
|
|
||||||
AND t.contype = 'p';
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('📋 Primary Key Information:');
|
|
||||||
if (pkInfo.length > 0) {
|
|
||||||
pkInfo.forEach(pk => {
|
|
||||||
console.log(` Constraint: ${pk.constraint_name}`);
|
|
||||||
console.log(` Spalte: ${pk.column_name}`);
|
|
||||||
console.log(` Typ: ${pk.constraint_type === 'p' ? 'PRIMARY KEY' : pk.constraint_type}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ⚠️ Kein Primary Key gefunden!');
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Prüfe alle Indizes auf knowledge
|
|
||||||
const [allIndexes] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
indexname,
|
|
||||||
indexdef,
|
|
||||||
idx_scan,
|
|
||||||
idx_tup_read,
|
|
||||||
idx_tup_fetch
|
|
||||||
FROM pg_indexes
|
|
||||||
LEFT JOIN pg_stat_user_indexes
|
|
||||||
ON pg_stat_user_indexes.indexrelname = pg_indexes.indexname
|
|
||||||
AND pg_stat_user_indexes.schemaname = pg_indexes.schemaname
|
|
||||||
WHERE pg_indexes.schemaname = 'falukant_data'
|
|
||||||
AND pg_indexes.tablename = 'knowledge'
|
|
||||||
ORDER BY indexname;
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('📊 Alle Indizes auf knowledge:');
|
|
||||||
allIndexes.forEach(idx => {
|
|
||||||
console.log(`\n ${idx.indexname}:`);
|
|
||||||
console.log(` Definition: ${idx.indexdef}`);
|
|
||||||
console.log(` Scans: ${idx.idx_scan ? parseInt(idx.idx_scan).toLocaleString() : 'N/A'}`);
|
|
||||||
console.log(` Zeilen gelesen: ${idx.idx_tup_read ? parseInt(idx.idx_tup_read).toLocaleString() : 'N/A'}`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Prüfe Tabellenstruktur
|
|
||||||
const [tableStructure] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
column_name,
|
|
||||||
data_type,
|
|
||||||
is_nullable,
|
|
||||||
column_default
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_data'
|
|
||||||
AND table_name = 'knowledge'
|
|
||||||
ORDER BY ordinal_position;
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('📋 Tabellenstruktur:');
|
|
||||||
tableStructure.forEach(col => {
|
|
||||||
console.log(` ${col.column_name}: ${col.data_type} ${col.is_nullable === 'NO' ? 'NOT NULL' : 'NULL'}`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Erklärung: Warum knowledge_pkey ungenutzt ist
|
|
||||||
const pkUnused = allIndexes.find(i => i.indexname === 'knowledge_pkey' && (i.idx_scan == null || parseInt(i.idx_scan) === 0));
|
|
||||||
if (pkUnused) {
|
|
||||||
console.log('💡 Warum knowledge_pkey (0 Scans) ungenutzt ist:');
|
|
||||||
console.log(' Alle Zugriffe filtern nach (character_id, product_id), nie nach id.');
|
|
||||||
console.log(' Der PK-Index wird nur für Eindeutigkeit/Referenzen genutzt, nicht für Lookups.');
|
|
||||||
console.log(' idx_knowledge_character_product deckt die tatsächlichen Queries ab.\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prüfe ob Queries mit id (Primary Key) gemacht werden
|
|
||||||
let idUsage = [];
|
|
||||||
try {
|
|
||||||
const [rows] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
query,
|
|
||||||
calls,
|
|
||||||
total_exec_time,
|
|
||||||
mean_exec_time
|
|
||||||
FROM pg_stat_statements
|
|
||||||
WHERE query LIKE '%knowledge%'
|
|
||||||
AND (query LIKE '%knowledge.id%' OR query LIKE '%knowledge%id%')
|
|
||||||
ORDER BY calls DESC
|
|
||||||
LIMIT 5;
|
|
||||||
`);
|
|
||||||
idUsage = rows;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ℹ️ pg_stat_statements nicht verfügbar – keine Query-Statistik.\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idUsage.length > 0) {
|
|
||||||
console.log('🔍 Queries die knowledge.id verwenden:');
|
|
||||||
idUsage.forEach(q => {
|
|
||||||
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}`);
|
|
||||||
console.log(` Query: ${q.query.substring(0, 150)}...`);
|
|
||||||
console.log('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message.includes('pg_stat_statements')) {
|
|
||||||
console.log(' ⚠️ pg_stat_statements ist nicht aktiviert oder nicht verfügbar\n');
|
|
||||||
} else {
|
|
||||||
console.error('❌ Fehler:', error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script zum Bereinigen von alten/idle PostgreSQL-Verbindungen
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './config/loadEnv.js';
|
|
||||||
import { sequelize } from './utils/sequelize.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log('🧹 Bereinige alte PostgreSQL-Verbindungen...\n');
|
|
||||||
|
|
||||||
// Beende idle Verbindungen, die älter als 5 Minuten sind (außer unserer eigenen)
|
|
||||||
const [result] = await sequelize.query(`
|
|
||||||
SELECT pg_terminate_backend(pid) as terminated
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE datname = current_database()
|
|
||||||
AND pid <> pg_backend_pid()
|
|
||||||
AND state = 'idle'
|
|
||||||
AND state_change < now() - interval '5 minutes';
|
|
||||||
`);
|
|
||||||
|
|
||||||
const terminated = result.filter(r => r.terminated).length;
|
|
||||||
console.log(`✅ ${terminated} alte idle Verbindungen wurden beendet\n`);
|
|
||||||
|
|
||||||
// Zeige verbleibende Verbindungen
|
|
||||||
const [connections] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
count(*) as total,
|
|
||||||
count(*) FILTER (WHERE state = 'active') as active,
|
|
||||||
count(*) FILTER (WHERE state = 'idle') as idle
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE datname = current_database();
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('📊 Verbleibende Verbindungen:');
|
|
||||||
console.log(` Gesamt: ${connections[0].total}`);
|
|
||||||
console.log(` Aktiv: ${connections[0].active}`);
|
|
||||||
console.log(` Idle: ${connections[0].idle}\n`);
|
|
||||||
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler:', error.message);
|
|
||||||
if (error.message.includes('SUPERUSER')) {
|
|
||||||
console.error('\n💡 Tipp: Du benötigst Superuser-Rechte oder musst warten, bis Verbindungen freigegeben werden.');
|
|
||||||
console.error(' Versuche es in ein paar Minuten erneut.');
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 1236
|
"port": 1235
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,90 +7,40 @@ import fs from 'fs';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const quietEnv = process.env.QUIET_ENV_LOGS === '1';
|
|
||||||
const dotenvQuiet = quietEnv || process.env.DOTENV_CONFIG_QUIET === '1';
|
|
||||||
|
|
||||||
function log(...args) {
|
|
||||||
if (!quietEnv) console.log(...args);
|
|
||||||
}
|
|
||||||
function warn(...args) {
|
|
||||||
console.warn(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Versuche zuerst Produktions-.env, dann lokale .env
|
// Versuche zuerst Produktions-.env, dann lokale .env
|
||||||
const productionEnvPath = '/opt/yourpart/backend/.env';
|
const productionEnvPath = '/opt/yourpart/backend/.env';
|
||||||
const localEnvPath = path.resolve(__dirname, '../.env');
|
const localEnvPath = path.resolve(__dirname, '../.env');
|
||||||
|
|
||||||
let envPath = localEnvPath; // Fallback
|
let envPath = localEnvPath; // Fallback
|
||||||
let usingProduction = false;
|
|
||||||
if (fs.existsSync(productionEnvPath)) {
|
if (fs.existsSync(productionEnvPath)) {
|
||||||
// Prüfe Lesbarkeit bevor wir versuchen, sie zu laden
|
|
||||||
try {
|
|
||||||
fs.accessSync(productionEnvPath, fs.constants.R_OK);
|
|
||||||
envPath = productionEnvPath;
|
envPath = productionEnvPath;
|
||||||
usingProduction = true;
|
console.log('[env] Lade Produktions-.env:', productionEnvPath);
|
||||||
log('[env] Produktions-.env gefunden und lesbar:', productionEnvPath);
|
|
||||||
} catch (err) {
|
|
||||||
if (!quietEnv) {
|
|
||||||
warn('[env] Produktions-.env vorhanden, aber nicht lesbar - verwende lokale .env stattdessen:', productionEnvPath);
|
|
||||||
warn('[env] Fehler:', err && err.message);
|
|
||||||
}
|
|
||||||
envPath = localEnvPath;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log('[env] Produktions-.env nicht gefunden, lade lokale .env:', localEnvPath);
|
console.log('[env] Lade lokale .env:', localEnvPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lade .env-Datei (robust gegen Fehler)
|
// Lade .env-Datei
|
||||||
log('[env] Versuche .env zu laden von:', envPath);
|
console.log('[env] Versuche .env zu laden von:', envPath);
|
||||||
log('[env] Datei existiert:', fs.existsSync(envPath));
|
console.log('[env] Datei existiert:', fs.existsSync(envPath));
|
||||||
let result;
|
console.log('[env] Datei lesbar:', fs.accessSync ? (() => { try { fs.accessSync(envPath, fs.constants.R_OK); return true; } catch { return false; } })() : 'unbekannt');
|
||||||
try {
|
|
||||||
result = dotenv.config({ path: envPath, quiet: dotenvQuiet });
|
const result = dotenv.config({ path: envPath });
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
warn('[env] Konnte .env nicht laden:', result.error.message);
|
console.warn('[env] Konnte .env nicht laden:', result.error.message);
|
||||||
warn('[env] Fehler-Details:', result.error);
|
console.warn('[env] Fehler-Details:', result.error);
|
||||||
} else {
|
} else {
|
||||||
log('[env] .env erfolgreich geladen von:', envPath, usingProduction ? '(production)' : '(local)');
|
console.log('[env] .env erfolgreich geladen von:', envPath);
|
||||||
log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
|
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Sollte nicht passieren, aber falls dotenv intern eine Exception wirft (z.B. EACCES), fange sie ab
|
|
||||||
warn('[env] Unerwarteter Fehler beim Laden der .env:', err && err.message);
|
|
||||||
warn('[env] Stack:', err && err.stack);
|
|
||||||
if (envPath !== localEnvPath && fs.existsSync(localEnvPath)) {
|
|
||||||
log('[env] Versuche stattdessen lokale .env:', localEnvPath);
|
|
||||||
try {
|
|
||||||
result = dotenv.config({ path: localEnvPath, quiet: dotenvQuiet });
|
|
||||||
if (!result.error) {
|
|
||||||
log('[env] Lokale .env erfolgreich geladen von:', localEnvPath);
|
|
||||||
}
|
|
||||||
} catch (err2) {
|
|
||||||
warn('[env] Konnte lokale .env auch nicht laden:', err2 && err2.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lokale Überschreibungen (nicht committen): z. B. SSH-Tunnel DB_HOST=127.0.0.1 DB_PORT=60000
|
// Debug: Zeige Redis-Konfiguration
|
||||||
const localOverridePath = path.resolve(__dirname, '../.env.local');
|
|
||||||
if (fs.existsSync(localOverridePath)) {
|
|
||||||
const overrideResult = dotenv.config({ path: localOverridePath, override: true, quiet: dotenvQuiet });
|
|
||||||
if (!overrideResult.error) {
|
|
||||||
log('[env] .env.local geladen (überschreibt Werte, z. B. SSH-Tunnel)');
|
|
||||||
} else {
|
|
||||||
warn('[env] .env.local vorhanden, aber Laden fehlgeschlagen:', overrideResult.error?.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quietEnv) {
|
|
||||||
console.log('[env] Redis-Konfiguration:');
|
console.log('[env] Redis-Konfiguration:');
|
||||||
console.log('[env] REDIS_HOST:', process.env.REDIS_HOST);
|
console.log('[env] REDIS_HOST:', process.env.REDIS_HOST);
|
||||||
console.log('[env] REDIS_PORT:', process.env.REDIS_PORT);
|
console.log('[env] REDIS_PORT:', process.env.REDIS_PORT);
|
||||||
console.log('[env] REDIS_PASSWORD:', process.env.REDIS_PASSWORD ? '***gesetzt***' : 'NICHT GESETZT');
|
console.log('[env] REDIS_PASSWORD:', process.env.REDIS_PASSWORD ? '***gesetzt***' : 'NICHT GESETZT');
|
||||||
console.log('[env] REDIS_URL:', process.env.REDIS_URL);
|
console.log('[env] REDIS_URL:', process.env.REDIS_URL);
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.SECRET_KEY) {
|
if (!process.env.SECRET_KEY) {
|
||||||
warn('[env] SECRET_KEY nicht gesetzt in .env');
|
console.warn('[env] SECRET_KEY nicht gesetzt in .env');
|
||||||
}
|
}
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const dotenv = require('dotenv');
|
|
||||||
|
|
||||||
const envPath = process.env.SEQUELIZE_ENV_FILE
|
|
||||||
? path.resolve(process.cwd(), process.env.SEQUELIZE_ENV_FILE)
|
|
||||||
: path.resolve(process.cwd(), '.env');
|
|
||||||
|
|
||||||
dotenv.config({ path: envPath });
|
|
||||||
|
|
||||||
const dialectOptions = {};
|
|
||||||
if (process.env.DB_SSL === '1' || process.env.PGSSLMODE === 'require') {
|
|
||||||
dialectOptions.ssl = process.env.DB_SSL_REJECT_UNAUTHORIZED === '0'
|
|
||||||
? { rejectUnauthorized: false }
|
|
||||||
: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pg/SCRAM: password muss ein String sein; bei fehlender .env sonst undefined-Fallen vermeiden
|
|
||||||
const shared = {
|
|
||||||
username: process.env.DB_USER != null ? String(process.env.DB_USER) : undefined,
|
|
||||||
password: process.env.DB_PASS != null ? String(process.env.DB_PASS) : '',
|
|
||||||
database: process.env.DB_NAME != null ? String(process.env.DB_NAME) : undefined,
|
|
||||||
host: process.env.DB_HOST || '127.0.0.1',
|
|
||||||
port: Number.parseInt(process.env.DB_PORT || '5432', 10),
|
|
||||||
dialect: 'postgres',
|
|
||||||
logging: false,
|
|
||||||
dialectOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
development: shared,
|
|
||||||
test: shared,
|
|
||||||
production: shared
|
|
||||||
};
|
|
||||||
@@ -13,11 +13,6 @@ class AdminController {
|
|||||||
this.searchUser = this.searchUser.bind(this);
|
this.searchUser = this.searchUser.bind(this);
|
||||||
this.getFalukantUserById = this.getFalukantUserById.bind(this);
|
this.getFalukantUserById = this.getFalukantUserById.bind(this);
|
||||||
this.changeFalukantUser = this.changeFalukantUser.bind(this);
|
this.changeFalukantUser = this.changeFalukantUser.bind(this);
|
||||||
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
|
|
||||||
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
|
|
||||||
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
|
|
||||||
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
|
|
||||||
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
|
|
||||||
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
|
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
|
||||||
this.updateFalukantStock = this.updateFalukantStock.bind(this);
|
this.updateFalukantStock = this.updateFalukantStock.bind(this);
|
||||||
this.addFalukantStock = this.addFalukantStock.bind(this);
|
this.addFalukantStock = this.addFalukantStock.bind(this);
|
||||||
@@ -34,16 +29,6 @@ class AdminController {
|
|||||||
this.getUser = this.getUser.bind(this);
|
this.getUser = this.getUser.bind(this);
|
||||||
this.getUsers = this.getUsers.bind(this);
|
this.getUsers = this.getUsers.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
|
|
||||||
this.markUserVocabLessonsCompleteThrough = this.markUserVocabLessonsCompleteThrough.bind(this);
|
|
||||||
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
|
|
||||||
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
|
|
||||||
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
|
||||||
this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this);
|
|
||||||
this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this);
|
|
||||||
this.getEroticModerationReports = this.getEroticModerationReports.bind(this);
|
|
||||||
this.applyEroticModerationAction = this.applyEroticModerationAction.bind(this);
|
|
||||||
this.getEroticModerationPreview = this.getEroticModerationPreview.bind(this);
|
|
||||||
|
|
||||||
// Rights
|
// Rights
|
||||||
this.listRightTypes = this.listRightTypes.bind(this);
|
this.listRightTypes = this.listRightTypes.bind(this);
|
||||||
@@ -54,9 +39,6 @@ class AdminController {
|
|||||||
// Statistics
|
// Statistics
|
||||||
this.getUserStatistics = this.getUserStatistics.bind(this);
|
this.getUserStatistics = this.getUserStatistics.bind(this);
|
||||||
this.getFalukantRegions = this.getFalukantRegions.bind(this);
|
this.getFalukantRegions = this.getFalukantRegions.bind(this);
|
||||||
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
|
|
||||||
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
|
|
||||||
this.createFalukantRegion = this.createFalukantRegion.bind(this);
|
|
||||||
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
||||||
this.getRegionDistances = this.getRegionDistances.bind(this);
|
this.getRegionDistances = this.getRegionDistances.bind(this);
|
||||||
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
||||||
@@ -137,168 +119,6 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetUserVocabLessonProgress(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
lessonId: Joi.number().integer().positive().required()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await AdminService.adminResetUserVocabLessonProgress(requester, id, value.lessonId);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (err.message === 'lessonnotfound' ? 404 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async markUserVocabLessonsCompleteThrough(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
courseId: Joi.number().integer().positive().required(),
|
|
||||||
throughLessonNumber: Joi.number().integer().positive().required()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await AdminService.adminMarkUserVocabLessonsCompleteThrough(
|
|
||||||
requester,
|
|
||||||
id,
|
|
||||||
value.courseId,
|
|
||||||
value.throughLessonNumber
|
|
||||||
);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
let status = 500;
|
|
||||||
if (err.message === 'noaccess') status = 403;
|
|
||||||
else if (err.message === 'notenrolled') status = 403;
|
|
||||||
else if (err.message === 'badrequest') status = 400;
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserVocabCourses(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await AdminService.adminListUserEnrolledVocabCourses(requester, id);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (err.message === 'notfound' ? 404 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getVocabCourseForAdmin(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { courseId } = req.params;
|
|
||||||
const result = await AdminService.adminGetVocabCourseWithLessons(requester, courseId);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (err.message === 'coursenotfound' ? 404 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAdultVerificationRequests(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { status = 'pending' } = req.query;
|
|
||||||
const result = await AdminService.getAdultVerificationRequests(requester, status);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (error) {
|
|
||||||
const status = error.message === 'noaccess' ? 403 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAdultVerificationStatus(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
status: Joi.string().valid('approved', 'rejected', 'pending').required()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await AdminService.setAdultVerificationStatus(requester, id, value.status);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'notadult', 'wrongstatus', 'missingparamtype'].includes(err.message) ? 400 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAdultVerificationDocument(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await AdminService.getAdultVerificationDocument(requester, id);
|
|
||||||
res.setHeader('Content-Type', result.mimeType);
|
|
||||||
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(result.originalName)}"`);
|
|
||||||
res.sendFile(result.filePath);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'norequest', 'nofile'].includes(err.message) ? 404 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEroticModerationReports(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { status = 'open' } = req.query;
|
|
||||||
const result = await AdminService.getEroticModerationReports(requester, status);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (error) {
|
|
||||||
const status = error.message === 'noaccess' ? 403 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyEroticModerationAction(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
action: Joi.string().valid('dismiss', 'hide_content', 'restore_content', 'delete_content', 'block_uploads', 'revoke_access').required(),
|
|
||||||
note: Joi.string().allow('', null).max(2000).optional()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await AdminService.applyEroticModerationAction(requester, Number(id), value.action, value.note || null);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'targetnotfound', 'wrongaction'].includes(err.message) ? 400 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEroticModerationPreview(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: requester } = req.headers;
|
|
||||||
const { type, targetId } = req.params;
|
|
||||||
const result = await AdminService.getEroticModerationPreview(requester, type, Number(targetId));
|
|
||||||
res.setHeader('Content-Type', result.mimeType);
|
|
||||||
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(result.originalName)}"`);
|
|
||||||
res.sendFile(result.filePath);
|
|
||||||
} catch (err) {
|
|
||||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'nofile', 'wrongtype'].includes(err.message) ? 404 : 500);
|
|
||||||
res.status(status).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Rights ---
|
// --- Rights ---
|
||||||
async listRightTypes(req, res) {
|
async listRightTypes(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -455,77 +275,6 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async adminForceFalukantPregnancy(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const { characterId, fatherCharacterId, dueInDays } = req.body;
|
|
||||||
const response = await AdminService.adminForceFalukantPregnancy(userId, characterId, {
|
|
||||||
fatherCharacterId,
|
|
||||||
dueInDays,
|
|
||||||
});
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async adminGetPotentialFathersForCharacter(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const { characterId } = req.params;
|
|
||||||
const response = await AdminService.adminGetPotentialFathersForCharacter(userId, characterId);
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async adminClearFalukantPregnancy(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const { characterId } = req.body;
|
|
||||||
const response = await AdminService.adminClearFalukantPregnancy(userId, characterId);
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async adminForceFalukantBirth(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const { motherCharacterId, fatherCharacterId, birthContext, legitimacy, gender } = req.body;
|
|
||||||
const response = await AdminService.adminForceFalukantBirth(userId, motherCharacterId, {
|
|
||||||
fatherCharacterId,
|
|
||||||
birthContext,
|
|
||||||
legitimacy,
|
|
||||||
gender,
|
|
||||||
});
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async adminCleanupCharacterDeathArtifacts(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const { characterId } = req.params;
|
|
||||||
const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId);
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
const status = error.message === 'noaccess'
|
|
||||||
? 403
|
|
||||||
: (['invalidCharacter', 'notfound', 'targetnotdead'].includes(error.message) ? 400 : 500);
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFalukantUserBranches(req, res) {
|
async getFalukantUserBranches(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
const { userid: userId } = req.headers;
|
||||||
@@ -586,42 +335,6 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFalukantAllRegions(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const regions = await AdminService.getFalukantAllRegions(userId);
|
|
||||||
res.status(200).json(regions);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
const status = error.message === 'noaccess' ? 403 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFalukantRegionTypes(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const types = await AdminService.getFalukantRegionTypes(userId);
|
|
||||||
res.status(200).json(types);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
const status = error.message === 'noaccess' ? 403 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createFalukantRegion(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const created = await AdminService.createFalukantRegion(userId, req.body || {});
|
|
||||||
res.status(200).json(created);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
const status = error.message === 'noaccess' ? 403 : 400;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFalukantRegionMap(req, res) {
|
async updateFalukantRegionMap(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
const { userid: userId } = req.headers;
|
||||||
@@ -810,7 +523,6 @@ class AdminController {
|
|||||||
title: Joi.string().min(1).max(255).required(),
|
title: Joi.string().min(1).max(255).required(),
|
||||||
roomTypeId: Joi.number().integer().required(),
|
roomTypeId: Joi.number().integer().required(),
|
||||||
isPublic: Joi.boolean().required(),
|
isPublic: Joi.boolean().required(),
|
||||||
isAdultOnly: Joi.boolean().allow(null),
|
|
||||||
genderRestrictionId: Joi.number().integer().allow(null),
|
genderRestrictionId: Joi.number().integer().allow(null),
|
||||||
minAge: Joi.number().integer().min(0).allow(null),
|
minAge: Joi.number().integer().min(0).allow(null),
|
||||||
maxAge: Joi.number().integer().min(0).allow(null),
|
maxAge: Joi.number().integer().min(0).allow(null),
|
||||||
@@ -822,7 +534,7 @@ class AdminController {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
}
|
}
|
||||||
const room = await AdminService.updateRoom(userId, req.params.id, value);
|
const room = await AdminService.updateRoom(req.params.id, value);
|
||||||
res.status(200).json(room);
|
res.status(200).json(room);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -841,7 +553,6 @@ class AdminController {
|
|||||||
title: Joi.string().min(1).max(255).required(),
|
title: Joi.string().min(1).max(255).required(),
|
||||||
roomTypeId: Joi.number().integer().required(),
|
roomTypeId: Joi.number().integer().required(),
|
||||||
isPublic: Joi.boolean().required(),
|
isPublic: Joi.boolean().required(),
|
||||||
isAdultOnly: Joi.boolean().allow(null),
|
|
||||||
genderRestrictionId: Joi.number().integer().allow(null),
|
genderRestrictionId: Joi.number().integer().allow(null),
|
||||||
minAge: Joi.number().integer().min(0).allow(null),
|
minAge: Joi.number().integer().min(0).allow(null),
|
||||||
maxAge: Joi.number().integer().min(0).allow(null),
|
maxAge: Joi.number().integer().min(0).allow(null),
|
||||||
@@ -868,7 +579,7 @@ class AdminController {
|
|||||||
if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) {
|
if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) {
|
||||||
return res.status(403).json({ error: 'Keine Berechtigung.' });
|
return res.status(403).json({ error: 'Keine Berechtigung.' });
|
||||||
}
|
}
|
||||||
await AdminService.deleteRoom(userId, req.params.id);
|
await AdminService.deleteRoom(req.params.id);
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ class AuthController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'credentialsinvalid') {
|
if (error.message === 'credentialsinvalid') {
|
||||||
res.status(404).json({ error: error.message });
|
res.status(404).json({ error: error.message });
|
||||||
} else if (error.message === 'userblocked') {
|
|
||||||
res.status(403).json({ error: error.message });
|
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
import calendarService from '../services/calendarService.js';
|
|
||||||
|
|
||||||
function getHashedUserId(req) {
|
|
||||||
return req.headers?.userid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
/**
|
|
||||||
* GET /api/calendar/events
|
|
||||||
* Get all events for the authenticated user
|
|
||||||
* Query params: startDate, endDate (optional)
|
|
||||||
*/
|
|
||||||
async getEvents(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { startDate, endDate } = req.query;
|
|
||||||
const events = await calendarService.getEvents(hashedUserId, { startDate, endDate });
|
|
||||||
res.json(events);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar getEvents:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/calendar/events/:id
|
|
||||||
* Get a single event by ID
|
|
||||||
*/
|
|
||||||
async getEvent(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = await calendarService.getEvent(hashedUserId, req.params.id);
|
|
||||||
res.json(event);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar getEvent:', error);
|
|
||||||
if (error.message === 'Event not found') {
|
|
||||||
return res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/calendar/events
|
|
||||||
* Create a new event
|
|
||||||
*/
|
|
||||||
async createEvent(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const eventData = req.body;
|
|
||||||
if (!eventData.title || !eventData.startDate) {
|
|
||||||
return res.status(400).json({ error: 'Title and startDate are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = await calendarService.createEvent(hashedUserId, eventData);
|
|
||||||
res.status(201).json(event);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar createEvent:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/calendar/events/:id
|
|
||||||
* Update an existing event
|
|
||||||
*/
|
|
||||||
async updateEvent(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const eventData = req.body;
|
|
||||||
if (!eventData.title || !eventData.startDate) {
|
|
||||||
return res.status(400).json({ error: 'Title and startDate are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = await calendarService.updateEvent(hashedUserId, req.params.id, eventData);
|
|
||||||
res.json(event);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar updateEvent:', error);
|
|
||||||
if (error.message === 'Event not found') {
|
|
||||||
return res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/calendar/events/:id
|
|
||||||
* Delete an event
|
|
||||||
*/
|
|
||||||
async deleteEvent(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await calendarService.deleteEvent(hashedUserId, req.params.id);
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar deleteEvent:', error);
|
|
||||||
if (error.message === 'Event not found') {
|
|
||||||
return res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/calendar/birthdays
|
|
||||||
* Get friends' birthdays for a given year
|
|
||||||
* Query params: year (required)
|
|
||||||
*/
|
|
||||||
async getFriendsBirthdays(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const year = parseInt(req.query.year) || new Date().getFullYear();
|
|
||||||
const birthdays = await calendarService.getFriendsBirthdays(hashedUserId, year);
|
|
||||||
res.json(birthdays);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar getFriendsBirthdays:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/calendar/widget/birthdays
|
|
||||||
* Get upcoming birthdays for widget display
|
|
||||||
*/
|
|
||||||
async getWidgetBirthdays(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const limit = parseInt(req.query.limit) || 10;
|
|
||||||
const birthdays = await calendarService.getUpcomingBirthdays(hashedUserId, limit);
|
|
||||||
res.json(birthdays);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar getWidgetBirthdays:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/calendar/widget/upcoming
|
|
||||||
* Get upcoming events for widget display
|
|
||||||
*/
|
|
||||||
async getWidgetUpcoming(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const limit = parseInt(req.query.limit) || 10;
|
|
||||||
const events = await calendarService.getUpcomingEvents(hashedUserId, limit);
|
|
||||||
res.json(events);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar getWidgetUpcoming:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/calendar/widget/mini
|
|
||||||
* Get mini calendar data for widget display
|
|
||||||
*/
|
|
||||||
async getWidgetMiniCalendar(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await calendarService.getMiniCalendarData(hashedUserId);
|
|
||||||
res.json(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar getWidgetMiniCalendar:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -13,10 +13,6 @@ class ChatController {
|
|||||||
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
|
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
|
||||||
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
|
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
|
||||||
this.getRoomList = this.getRoomList.bind(this);
|
this.getRoomList = this.getRoomList.bind(this);
|
||||||
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
|
|
||||||
this.getOwnRooms = this.getOwnRooms.bind(this);
|
|
||||||
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
|
|
||||||
this.reportChatIncident = this.reportChatIncident.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMessages(req, res) {
|
async getMessages(req, res) {
|
||||||
@@ -173,75 +169,12 @@ class ChatController {
|
|||||||
async getRoomList(req, res) {
|
async getRoomList(req, res) {
|
||||||
// Öffentliche Räume für Chat-Frontend
|
// Öffentliche Räume für Chat-Frontend
|
||||||
try {
|
try {
|
||||||
const { userid: hashedUserId } = req.headers;
|
const rooms = await chatService.getRoomList();
|
||||||
const adultOnly = String(req.query.adultOnly || '').toLowerCase() === 'true';
|
|
||||||
const rooms = await chatService.getRoomList(hashedUserId, { adultOnly });
|
|
||||||
res.status(200).json(rooms);
|
res.status(200).json(rooms);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRoomCreateOptions(req, res) {
|
|
||||||
try {
|
|
||||||
const options = await chatService.getRoomCreateOptions();
|
|
||||||
res.status(200).json(options);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOwnRooms(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: hashedUserId } = req.headers;
|
|
||||||
const rooms = await chatService.getOwnRooms(hashedUserId);
|
|
||||||
res.status(200).json(rooms);
|
|
||||||
} catch (error) {
|
|
||||||
const status = error.message === 'user_not_found' ? 404 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteOwnRoom(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: hashedUserId } = req.headers;
|
|
||||||
const roomId = Number.parseInt(req.params.id, 10);
|
|
||||||
if (!Number.isInteger(roomId) || roomId <= 0) {
|
|
||||||
return res.status(400).json({ error: 'invalid_room_id' });
|
|
||||||
}
|
|
||||||
await chatService.deleteOwnRoom(hashedUserId, roomId);
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
const status = error.message === 'room_not_found_or_not_owner' || error.message === 'user_not_found' ? 404 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reportChatIncident(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
context: Joi.string().valid('random_chat', 'multi_chat', 'one_to_one').required(),
|
|
||||||
reporterHashedId: Joi.string().allow('', null),
|
|
||||||
reporterRandomId: Joi.string().allow('', null),
|
|
||||||
reporterUsername: Joi.string().allow('', null),
|
|
||||||
offenderHashedId: Joi.string().allow('', null),
|
|
||||||
offenderRandomId: Joi.string().allow('', null),
|
|
||||||
offenderUsername: Joi.string().allow('', null),
|
|
||||||
incidentAt: Joi.date().iso().required(),
|
|
||||||
chatHistory: Joi.array().min(1).required(),
|
|
||||||
metadata: Joi.object().unknown(true).optional()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await chatService.reportChatIncident(value);
|
|
||||||
return res.status(201).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in reportChatIncident:', err);
|
|
||||||
return res.status(400).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatController;
|
export default ChatController;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import dashboardService from '../services/dashboardService.js';
|
|
||||||
|
|
||||||
function getHashedUserId(req) {
|
|
||||||
return req.headers?.userid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
/** Liste der möglichen Widget-Typen (öffentlich, keine Auth nötig wenn gewünscht – aktuell mit Auth). */
|
|
||||||
async getAvailableWidgets(req, res) {
|
|
||||||
try {
|
|
||||||
const list = await dashboardService.getAvailableWidgets();
|
|
||||||
res.json(list);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Dashboard getAvailableWidgets:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getConfig(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = await dashboardService.getConfig(hashedUserId);
|
|
||||||
res.json(config);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Dashboard getConfig:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async setConfig(req, res) {
|
|
||||||
const hashedUserId = getHashedUserId(req);
|
|
||||||
if (!hashedUserId) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
const config = req.body;
|
|
||||||
if (!config || typeof config !== 'object') {
|
|
||||||
return res.status(400).json({ error: 'Invalid config' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await dashboardService.setConfig(hashedUserId, config);
|
|
||||||
res.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Dashboard setConfig:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import FalukantService from '../services/falukantService.js';
|
import FalukantService from '../services/falukantService.js';
|
||||||
import politicalPowersService from '../services/falukantPoliticalPowersService.js';
|
|
||||||
|
|
||||||
function extractHashedUserId(req) {
|
function extractHashedUserId(req) {
|
||||||
return req.headers?.userid;
|
return req.headers?.userid;
|
||||||
@@ -27,51 +26,45 @@ class FalukantController {
|
|||||||
}, { successStatus: 201 });
|
}, { successStatus: 201 });
|
||||||
|
|
||||||
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
|
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
|
||||||
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
|
|
||||||
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
|
||||||
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
||||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId), { blockInDebtorsPrison: true });
|
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
||||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||||
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
||||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId), { blockInDebtorsPrison: true });
|
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
|
||||||
this.createProduction = this._wrapWithUser((userId, req) => {
|
this.createProduction = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, productId, quantity } = req.body;
|
const { branchId, productId, quantity } = req.body;
|
||||||
return this.service.createProduction(userId, branchId, productId, quantity);
|
return this.service.createProduction(userId, branchId, productId, quantity);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
this.getProduction = this._wrapWithUser((userId, req) => this.service.getProduction(userId, req.params.branchId));
|
this.getProduction = this._wrapWithUser((userId, req) => this.service.getProduction(userId, req.params.branchId));
|
||||||
this.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
|
this.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
|
||||||
this.createStock = this._wrapWithUser((userId, req) => {
|
this.createStock = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, stockTypeId, stockSize } = req.body;
|
const { branchId, stockTypeId, stockSize } = req.body;
|
||||||
return this.service.createStock(userId, branchId, stockTypeId, stockSize);
|
return this.service.createStock(userId, branchId, stockTypeId, stockSize);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
|
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
|
||||||
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
|
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
|
||||||
this.sellProduct = this._wrapWithUser((userId, req) => {
|
this.sellProduct = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, productId, quality, quantity } = req.body;
|
const { branchId, productId, quality, quantity } = req.body;
|
||||||
return this.service.sellProduct(userId, branchId, productId, quality, quantity);
|
return this.service.sellProduct(userId, branchId, productId, quality, quantity);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
this.sellAllProducts = this._wrapWithUser((userId, req) => {
|
this.sellAllProducts = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId } = req.body;
|
const { branchId } = req.body;
|
||||||
return this.service.sellAllProducts(userId, branchId);
|
return this.service.sellAllProducts(userId, branchId);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
this.moneyHistory = this._wrapWithUser((userId, req) => {
|
this.moneyHistory = this._wrapWithUser((userId, req) => {
|
||||||
let { page, filter } = req.body;
|
let { page, filter } = req.body;
|
||||||
if (!page) page = 1;
|
if (!page) page = 1;
|
||||||
return this.service.moneyHistory(userId, page, filter);
|
return this.service.moneyHistory(userId, page, filter);
|
||||||
});
|
});
|
||||||
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
|
|
||||||
const { range } = req.body || {};
|
|
||||||
return this.service.moneyHistoryGraph(userId, range || '24h');
|
|
||||||
});
|
|
||||||
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
|
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
|
||||||
this.buyStorage = this._wrapWithUser((userId, req) => {
|
this.buyStorage = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, amount, stockTypeId } = req.body;
|
const { branchId, amount, stockTypeId } = req.body;
|
||||||
return this.service.buyStorage(userId, branchId, amount, stockTypeId);
|
return this.service.buyStorage(userId, branchId, amount, stockTypeId);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
this.sellStorage = this._wrapWithUser((userId, req) => {
|
this.sellStorage = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, amount, stockTypeId } = req.body;
|
const { branchId, amount, stockTypeId } = req.body;
|
||||||
return this.service.sellStorage(userId, branchId, amount, stockTypeId);
|
return this.service.sellStorage(userId, branchId, amount, stockTypeId);
|
||||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
}, { successStatus: 202 });
|
||||||
|
|
||||||
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
|
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
|
||||||
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
|
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
|
||||||
@@ -81,157 +74,83 @@ class FalukantController {
|
|||||||
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
|
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
|
||||||
return this.service.getDirectorProposals(userId, req.body.branchId);
|
return this.service.getDirectorProposals(userId, req.body.branchId);
|
||||||
});
|
});
|
||||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId));
|
||||||
this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId));
|
this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId));
|
||||||
this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
|
this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
|
||||||
this.updateDirector = this._wrapWithUser((userId, req) => {
|
this.updateDirector = this._wrapWithUser((userId, req) => {
|
||||||
const { directorId, income } = req.body;
|
const { directorId, income } = req.body;
|
||||||
return this.service.updateDirector(userId, directorId, income);
|
return this.service.updateDirector(userId, directorId, income);
|
||||||
}, { blockInDebtorsPrison: true });
|
});
|
||||||
|
|
||||||
this.setSetting = this._wrapWithUser((userId, req) => {
|
this.setSetting = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, directorId, settingKey, value } = req.body;
|
const { branchId, directorId, settingKey, value } = req.body;
|
||||||
return this.service.setSetting(userId, branchId, directorId, settingKey, value);
|
return this.service.setSetting(userId, branchId, directorId, settingKey, value);
|
||||||
}, { blockInDebtorsPrison: true });
|
});
|
||||||
|
|
||||||
this.getFamily = this._wrapWithUser(async (userId) => {
|
this.getFamily = this._wrapWithUser(async (userId) => {
|
||||||
const result = await this.service.getFamily(userId);
|
const result = await this.service.getFamily(userId);
|
||||||
if (!result) throw { status: 404, message: 'No family data found' };
|
if (!result) throw { status: 404, message: 'No family data found' };
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
|
||||||
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
||||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
|
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
|
||||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
|
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
|
||||||
this.cancelWooing = this._wrapWithUser(async (userId) => {
|
|
||||||
try {
|
|
||||||
return await this.service.cancelWooing(userId);
|
|
||||||
} catch (e) {
|
|
||||||
if (e && e.name === 'PreconditionError' && e.message === 'cancelTooSoon') {
|
|
||||||
throw { status: 412, message: 'cancelTooSoon', retryAt: e.meta?.retryAt };
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
|
||||||
this.getGifts = this._wrapWithUser((userId) => {
|
this.getGifts = this._wrapWithUser((userId) => {
|
||||||
console.log('🔍 getGifts called with userId:', userId);
|
console.log('🔍 getGifts called with userId:', userId);
|
||||||
return this.service.getGifts(userId);
|
return this.service.getGifts(userId);
|
||||||
});
|
});
|
||||||
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
|
|
||||||
this.improveLoverAffection = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.improveLoverAffection(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
|
||||||
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
|
|
||||||
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
|
|
||||||
this.service.spendTimeWithSpouse(userId), { blockInDebtorsPrison: true });
|
|
||||||
this.giftToSpouse = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.giftToSpouse(userId, req.body?.giftLevel), { blockInDebtorsPrison: true });
|
|
||||||
this.reconcileMarriage = this._wrapWithUser((userId) =>
|
|
||||||
this.service.reconcileMarriage(userId), { blockInDebtorsPrison: true });
|
|
||||||
this.acknowledgeLover = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.acknowledgeLover(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
|
||||||
this.endLoverRelationship = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.endLoverRelationship(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
|
||||||
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
||||||
this.sendGift = this._wrapWithUser(async (userId, req) => {
|
this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId));
|
||||||
try {
|
|
||||||
return await this.service.sendGift(userId, req.body.giftId);
|
|
||||||
} catch (e) {
|
|
||||||
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
|
|
||||||
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}, { blockInDebtorsPrison: true });
|
|
||||||
|
|
||||||
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
||||||
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
|
|
||||||
this.executeReputationAction = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201, blockInDebtorsPrison: true });
|
|
||||||
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
||||||
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
||||||
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
||||||
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
||||||
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId));
|
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId));
|
||||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201, blockInDebtorsPrison: true });
|
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 });
|
||||||
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201, blockInDebtorsPrison: true });
|
|
||||||
this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount), { blockInDebtorsPrison: true });
|
|
||||||
this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel), { blockInDebtorsPrison: true });
|
|
||||||
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId), { blockInDebtorsPrison: true });
|
|
||||||
|
|
||||||
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
||||||
this.createParty = this._wrapWithUser((userId, req) => {
|
this.createParty = this._wrapWithUser((userId, req) => {
|
||||||
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
|
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
|
||||||
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
|
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
||||||
|
|
||||||
|
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
|
||||||
|
this.executeReputationAction = this._wrapWithUser((userId, req) => {
|
||||||
|
const { actionTypeId } = req.body;
|
||||||
|
return this.service.executeReputationAction(userId, actionTypeId);
|
||||||
|
}, { successStatus: 201 });
|
||||||
|
|
||||||
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
|
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
|
||||||
this.baptise = this._wrapWithUser((userId, req) => {
|
this.baptise = this._wrapWithUser((userId, req) => {
|
||||||
const { characterId: childId, firstName } = req.body;
|
const { characterId: childId, firstName } = req.body;
|
||||||
return this.service.baptise(userId, childId, firstName);
|
return this.service.baptise(userId, childId, firstName);
|
||||||
}, { blockInDebtorsPrison: true });
|
});
|
||||||
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
|
|
||||||
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
|
|
||||||
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
|
|
||||||
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
|
|
||||||
const { officeTypeId, regionId } = req.body;
|
|
||||||
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
|
|
||||||
}, { blockInDebtorsPrison: true });
|
|
||||||
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
|
|
||||||
const { applicationId, decision } = req.body;
|
|
||||||
return this.service.decideOnChurchApplication(userId, applicationId, decision);
|
|
||||||
}, { blockInDebtorsPrison: true });
|
|
||||||
|
|
||||||
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
||||||
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
||||||
const { item, student, studentId } = req.body;
|
const { item, student, studentId } = req.body;
|
||||||
return this.service.sendToSchool(userId, item, student, studentId);
|
return this.service.sendToSchool(userId, item, student, studentId);
|
||||||
}, { blockInDebtorsPrison: true });
|
});
|
||||||
|
|
||||||
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
|
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
|
||||||
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
|
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
|
||||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height), { blockInDebtorsPrison: true });
|
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height));
|
||||||
|
|
||||||
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
|
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
|
||||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId), { blockInDebtorsPrison: true });
|
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
||||||
|
|
||||||
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
||||||
this.healthActivity = this._wrapWithUser(async (userId, req) => {
|
this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
|
||||||
try {
|
|
||||||
return await this.service.healthActivity(userId, req.body.measureTr);
|
|
||||||
} catch (e) {
|
|
||||||
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
|
|
||||||
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}, { blockInDebtorsPrison: true });
|
|
||||||
|
|
||||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||||
this.getPoliticalOfficeCatalog = this._wrapWithUser((userId) => this.service.getPoliticalOfficeCatalog(userId));
|
|
||||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||||
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
||||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
|
||||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
|
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||||
|
|
||||||
this.getPoliticalMyPowers = this._wrapWithUser((userId) => politicalPowersService.getMyPowers(userId));
|
|
||||||
this.getPoliticalTaxJurisdiction = this._wrapWithUser((userId) => politicalPowersService.getTaxJurisdiction(userId));
|
|
||||||
this.setPoliticalRegionTax = this._wrapWithUser((userId, req) =>
|
|
||||||
politicalPowersService.setRegionTax(userId, parseInt(req.params.regionId, 10), req.body?.percent), { blockInDebtorsPrison: true });
|
|
||||||
this.getPoliticalRegionTaxHistory = this._wrapWithUser((userId, req) =>
|
|
||||||
politicalPowersService.getRegionTaxHistory(userId, parseInt(req.params.regionId, 10), parseInt(req.query.limit || '5', 10)));
|
|
||||||
this.getPoliticalAppointableOffices = this._wrapWithUser((userId) => politicalPowersService.getAppointableOffices(userId));
|
|
||||||
this.createPoliticalAppointment = this._wrapWithUser(
|
|
||||||
(userId, req) =>
|
|
||||||
politicalPowersService.createAppointment(userId, {
|
|
||||||
targetCharacterId: req.body?.targetCharacterId,
|
|
||||||
officeTypeId: req.body?.officeTypeId,
|
|
||||||
regionId: req.body?.regionId
|
|
||||||
}),
|
|
||||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||||
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||||
@@ -243,15 +162,6 @@ class FalukantController {
|
|||||||
}
|
}
|
||||||
return this.service.getProductPriceInRegion(userId, productId, regionId);
|
return this.service.getProductPriceInRegion(userId, productId, regionId);
|
||||||
});
|
});
|
||||||
this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
|
|
||||||
const regionId = parseInt(req.query.regionId, 10);
|
|
||||||
if (Number.isNaN(regionId)) {
|
|
||||||
throw new Error('regionId is required');
|
|
||||||
}
|
|
||||||
const networkWorth = req.query.networkWorth === '1' || req.query.networkWorth === 'true';
|
|
||||||
const branchId = req.query.branchId != null ? parseInt(req.query.branchId, 10) : null;
|
|
||||||
return this.service.getAllProductPricesInRegion(userId, regionId, { networkWorth, branchId });
|
|
||||||
});
|
|
||||||
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||||
const productId = parseInt(req.query.productId, 10);
|
const productId = parseInt(req.query.productId, 10);
|
||||||
const currentPrice = parseFloat(req.query.currentPrice);
|
const currentPrice = parseFloat(req.query.currentPrice);
|
||||||
@@ -261,32 +171,13 @@ class FalukantController {
|
|||||||
}
|
}
|
||||||
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
|
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
|
||||||
});
|
});
|
||||||
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
|
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||||
const body = req.body || {};
|
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||||
const items = Array.isArray(body.items) ? body.items : [];
|
|
||||||
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
|
|
||||||
const includeTransportCosts = body.includeTransportCosts === true || body.includeTransportCosts === 'true';
|
|
||||||
const valid = items.map(i => ({
|
|
||||||
productId: parseInt(i.productId, 10),
|
|
||||||
currentPrice: parseFloat(i.currentPrice)
|
|
||||||
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
|
||||||
return this.service.getProductPricesInCitiesBatch(
|
|
||||||
userId,
|
|
||||||
valid,
|
|
||||||
Number.isNaN(currentRegionId) ? null : currentRegionId,
|
|
||||||
{ includeTransportCosts }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
|
|
||||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
|
|
||||||
|
|
||||||
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
|
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
|
||||||
this.getRaidTransportRegions = this._wrapWithUser((userId) => this.service.getRaidTransportRegions(userId));
|
|
||||||
this.getUndergroundActivities = this._wrapWithUser((userId) => this.service.getUndergroundActivities(userId));
|
|
||||||
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
||||||
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
||||||
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
|
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
|
||||||
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
|
||||||
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
|
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
|
||||||
|
|
||||||
this.searchUsers = this._wrapWithUser((userId, req) => {
|
this.searchUsers = this._wrapWithUser((userId, req) => {
|
||||||
@@ -307,7 +198,7 @@ class FalukantController {
|
|||||||
throw { status: 400, message: 'goal is required for corrupt_politician' };
|
throw { status: 400, message: 'goal is required for corrupt_politician' };
|
||||||
}
|
}
|
||||||
return this.service.createUndergroundActivity(userId, payload);
|
return this.service.createUndergroundActivity(userId, payload);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201 });
|
||||||
|
|
||||||
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
|
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
|
||||||
const direction = (req.query.direction || '').toLowerCase();
|
const direction = (req.query.direction || '').toLowerCase();
|
||||||
@@ -321,14 +212,14 @@ class FalukantController {
|
|||||||
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
||||||
this.buyVehicles = this._wrapWithUser(
|
this.buyVehicles = this._wrapWithUser(
|
||||||
(userId, req) => this.service.buyVehicles(userId, req.body),
|
(userId, req) => this.service.buyVehicles(userId, req.body),
|
||||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
{ successStatus: 201 }
|
||||||
);
|
);
|
||||||
this.getVehicles = this._wrapWithUser(
|
this.getVehicles = this._wrapWithUser(
|
||||||
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
||||||
);
|
);
|
||||||
this.createTransport = this._wrapWithUser(
|
this.createTransport = this._wrapWithUser(
|
||||||
(userId, req) => this.service.createTransport(userId, req.body),
|
(userId, req) => this.service.createTransport(userId, req.body),
|
||||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
{ successStatus: 201 }
|
||||||
);
|
);
|
||||||
this.getTransportRoute = this._wrapWithUser(
|
this.getTransportRoute = this._wrapWithUser(
|
||||||
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
||||||
@@ -338,40 +229,31 @@ class FalukantController {
|
|||||||
);
|
);
|
||||||
this.repairVehicle = this._wrapWithUser(
|
this.repairVehicle = this._wrapWithUser(
|
||||||
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
||||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
{ successStatus: 200 }
|
||||||
);
|
);
|
||||||
this.repairAllVehicles = this._wrapWithUser(
|
this.repairAllVehicles = this._wrapWithUser(
|
||||||
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
||||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
{ successStatus: 200 }
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_wrapWithUser(fn, { successStatus = 200, postProcess, blockInDebtorsPrison = false } = {}) {
|
_wrapWithUser(fn, { successStatus = 200, postProcess } = {}) {
|
||||||
return async (req, res) => {
|
return async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const hashedUserId = extractHashedUserId(req);
|
const hashedUserId = extractHashedUserId(req);
|
||||||
if (!hashedUserId) {
|
if (!hashedUserId) {
|
||||||
return res.status(400).json({ error: 'Missing user identifier' });
|
return res.status(400).json({ error: 'Missing user identifier' });
|
||||||
}
|
}
|
||||||
if (blockInDebtorsPrison) {
|
|
||||||
await this.service.assertActionAllowedOutsideDebtorsPrison(hashedUserId);
|
|
||||||
}
|
|
||||||
const result = await fn(hashedUserId, req, res);
|
const result = await fn(hashedUserId, req, res);
|
||||||
const toSend = postProcess ? postProcess(result) : result;
|
const toSend = postProcess ? postProcess(result) : result;
|
||||||
res.status(successStatus).json(toSend);
|
res.status(successStatus).json(toSend);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Controller error:', error);
|
console.error('Controller error:', error);
|
||||||
const status = error.status && typeof error.status === 'number' ? error.status : 500;
|
const status = error.status && typeof error.status === 'number' ? error.status : 500;
|
||||||
// Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen
|
|
||||||
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
|
|
||||||
const { status: errorStatus, ...errorData } = error;
|
|
||||||
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
|
|
||||||
} else {
|
|
||||||
res.status(status).json({ error: error.message || 'Internal error' });
|
res.status(status).json({ error: error.message || 'Internal error' });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import Joi from 'joi';
|
|
||||||
import moderationService from '../services/moderationService.js';
|
|
||||||
|
|
||||||
const moderationController = {
|
|
||||||
async createReport(req, res) {
|
|
||||||
const allowedTargetTypes = [
|
|
||||||
'forum_message',
|
|
||||||
'gallery_image',
|
|
||||||
'guestbook_entry',
|
|
||||||
'one_to_one_message',
|
|
||||||
'diary_entry',
|
|
||||||
'user_profile',
|
|
||||||
'blog',
|
|
||||||
'blog_post'
|
|
||||||
];
|
|
||||||
const schema = Joi.object({
|
|
||||||
targetType: Joi.string().valid(...allowedTargetTypes).required(),
|
|
||||||
targetId: Joi.number().integer().min(1).optional(),
|
|
||||||
targetRef: Joi.string().trim().max(255).allow('').optional(),
|
|
||||||
reason: Joi.string().trim().min(3).max(120).required(),
|
|
||||||
details: Joi.string().allow('').max(2000).optional()
|
|
||||||
}).or('targetId', 'targetRef');
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const result = await moderationService.createReport(userId, value);
|
|
||||||
return res.status(201).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in createReport:', err);
|
|
||||||
return res.status(400).json({ error: err.message });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async listReports(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const result = await moderationService.listReports(userId, req.query || {});
|
|
||||||
return res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in listReports:', err);
|
|
||||||
return res.status(400).json({ error: err.message });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateReportStatus(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
status: Joi.string().valid('open', 'in_review', 'resolved', 'rejected').required(),
|
|
||||||
reviewerNote: Joi.string().allow('').max(2000).optional()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const result = await moderationService.updateReportStatus(userId, req.params.reportId, value);
|
|
||||||
return res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in updateReportStatus:', err);
|
|
||||||
return res.status(400).json({ error: err.message });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getOpenReportCount(req, res) {
|
|
||||||
try {
|
|
||||||
const { userid: userId } = req.headers;
|
|
||||||
const result = await moderationService.getOpenReportCount(userId);
|
|
||||||
return res.status(200).json(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in getOpenReportCount:', err);
|
|
||||||
return res.status(400).json({ error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default moderationController;
|
|
||||||
@@ -50,6 +50,11 @@ const menuStructure = {
|
|||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
path: "/socialnetwork/gallery"
|
path: "/socialnetwork/gallery"
|
||||||
},
|
},
|
||||||
|
vocabtrainer: {
|
||||||
|
visible: ["all"],
|
||||||
|
path: "/socialnetwork/vocab",
|
||||||
|
children: {}
|
||||||
|
},
|
||||||
blockedUsers: {
|
blockedUsers: {
|
||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
path: "/socialnetwork/blocked"
|
path: "/socialnetwork/blocked"
|
||||||
@@ -96,7 +101,9 @@ const menuStructure = {
|
|||||||
},
|
},
|
||||||
eroticChat: {
|
eroticChat: {
|
||||||
visible: ["over18"],
|
visible: ["over18"],
|
||||||
action: "openEroticChat"
|
action: "openEroticChat",
|
||||||
|
view: "window",
|
||||||
|
class: "eroticChatWindow"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -176,30 +183,6 @@ const menuStructure = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
personal: {
|
|
||||||
visible: ["all"],
|
|
||||||
icon: "profile16.png",
|
|
||||||
children: {
|
|
||||||
sprachenlernen: {
|
|
||||||
visible: ["all"],
|
|
||||||
children: {
|
|
||||||
vocabtrainer: {
|
|
||||||
visible: ["all"],
|
|
||||||
path: "/socialnetwork/vocab",
|
|
||||||
showVocabLanguages: 1 // Flag für dynamische Sprachen-Liste
|
|
||||||
},
|
|
||||||
sprachkurse: {
|
|
||||||
visible: ["all"],
|
|
||||||
path: "/socialnetwork/vocab/courses"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calendar: {
|
|
||||||
visible: ["all"],
|
|
||||||
path: "/personal/calendar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
icon: "settings16.png",
|
icon: "settings16.png",
|
||||||
@@ -212,10 +195,6 @@ const menuStructure = {
|
|||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
path: "/settings/account"
|
path: "/settings/account"
|
||||||
},
|
},
|
||||||
languageAssistant: {
|
|
||||||
visible: ["all"],
|
|
||||||
path: "/settings/language-assistant"
|
|
||||||
},
|
|
||||||
personal: {
|
personal: {
|
||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
path: "/settings/personal"
|
path: "/settings/personal"
|
||||||
@@ -256,14 +235,6 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "useradministration"],
|
visible: ["mainadmin", "useradministration"],
|
||||||
path: "/admin/users"
|
path: "/admin/users"
|
||||||
},
|
},
|
||||||
adultverification: {
|
|
||||||
visible: ["mainadmin", "useradministration"],
|
|
||||||
path: "/admin/users/adult-verification"
|
|
||||||
},
|
|
||||||
eroticmoderation: {
|
|
||||||
visible: ["mainadmin", "useradministration"],
|
|
||||||
path: "/admin/users/erotic-moderation"
|
|
||||||
},
|
|
||||||
userstatistics: {
|
userstatistics: {
|
||||||
visible: ["mainadmin"],
|
visible: ["mainadmin"],
|
||||||
path: "/admin/users/statistics"
|
path: "/admin/users/statistics"
|
||||||
@@ -278,10 +249,6 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "forum"],
|
visible: ["mainadmin", "forum"],
|
||||||
path: "/admin/forum"
|
path: "/admin/forum"
|
||||||
},
|
},
|
||||||
moderationReports: {
|
|
||||||
visible: ["mainadmin", "forum"],
|
|
||||||
path: "/admin/moderation/reports"
|
|
||||||
},
|
|
||||||
chatrooms: {
|
chatrooms: {
|
||||||
visible: ["mainadmin", "chatrooms"],
|
visible: ["mainadmin", "chatrooms"],
|
||||||
path: "/admin/chatrooms"
|
path: "/admin/chatrooms"
|
||||||
@@ -295,7 +262,7 @@ const menuStructure = {
|
|||||||
path: "/admin/interests"
|
path: "/admin/interests"
|
||||||
},
|
},
|
||||||
falukant: {
|
falukant: {
|
||||||
visible: ["mainadmin", "falukant", "worker_schedule_read"],
|
visible: ["mainadmin", "falukant"],
|
||||||
children: {
|
children: {
|
||||||
logentries: {
|
logentries: {
|
||||||
visible: ["mainadmin", "falukant"],
|
visible: ["mainadmin", "falukant"],
|
||||||
@@ -317,10 +284,6 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "falukant"],
|
visible: ["mainadmin", "falukant"],
|
||||||
path: "/admin/falukant/create-npc"
|
path: "/admin/falukant/create-npc"
|
||||||
},
|
},
|
||||||
workerSchedules: {
|
|
||||||
visible: ["mainadmin", "worker_schedule_read"],
|
|
||||||
path: "/admin/falukant/worker-schedules"
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
minigames: {
|
minigames: {
|
||||||
@@ -357,14 +320,7 @@ class NavigationController {
|
|||||||
return age;
|
return age;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeAdultVerificationStatus(value) {
|
async filterMenu(menu, rights, age, userId) {
|
||||||
if (['pending', 'approved', 'rejected'].includes(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterMenu(menu, rights, age, userId, adultVerificationStatus = 'none') {
|
|
||||||
const filteredMenu = {};
|
const filteredMenu = {};
|
||||||
try {
|
try {
|
||||||
const hasFalukantAccount = await this.hasFalukantAccount(userId);
|
const hasFalukantAccount = await this.hasFalukantAccount(userId);
|
||||||
@@ -378,17 +334,8 @@ class NavigationController {
|
|||||||
|| (value.visible.includes('hasfalukantaccount') && hasFalukantAccount)) {
|
|| (value.visible.includes('hasfalukantaccount') && hasFalukantAccount)) {
|
||||||
const { visible, ...itemWithoutVisible } = value;
|
const { visible, ...itemWithoutVisible } = value;
|
||||||
filteredMenu[key] = { ...itemWithoutVisible };
|
filteredMenu[key] = { ...itemWithoutVisible };
|
||||||
if (
|
|
||||||
value.visible.includes("over18")
|
|
||||||
&& age >= 18
|
|
||||||
&& adultVerificationStatus !== 'approved'
|
|
||||||
&& (value.path || value.action || value.view)
|
|
||||||
) {
|
|
||||||
filteredMenu[key].disabled = true;
|
|
||||||
filteredMenu[key].disabledReasonKey = 'socialnetwork.erotic.lockedShort';
|
|
||||||
}
|
|
||||||
if (value.children) {
|
if (value.children) {
|
||||||
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId, adultVerificationStatus);
|
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,33 +362,37 @@ class NavigationController {
|
|||||||
required: false
|
required: false
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
const userParams = await UserParam.findAll({
|
const userBirthdateParams = await UserParam.findAll({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: UserParamType,
|
model: UserParamType,
|
||||||
as: 'paramType',
|
as: 'paramType',
|
||||||
where: { description: ['birthdate', 'adult_verification_status'] }
|
where: { description: 'birthdate' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
let birthDate = (new Date()).toDateString();
|
const birthDate = userBirthdateParams.length > 0 ? userBirthdateParams[0].value : (new Date()).toDateString();
|
||||||
let adultVerificationStatus = 'none';
|
|
||||||
for (const param of userParams) {
|
|
||||||
if (param.paramType?.description === 'birthdate' && param.value) {
|
|
||||||
birthDate = param.value;
|
|
||||||
}
|
|
||||||
if (param.paramType?.description === 'adult_verification_status') {
|
|
||||||
adultVerificationStatus = this.normalizeAdultVerificationStatus(param.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const age = this.calculateAge(birthDate);
|
const age = this.calculateAge(birthDate);
|
||||||
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
||||||
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id, adultVerificationStatus);
|
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
|
||||||
|
|
||||||
// Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum)
|
// Dynamisches Submenü: Treffpunkt → Vokabeltrainer → (Neue Sprache + abonnierte/angelegte)
|
||||||
// Keine children mehr, da das Menü nur 2 Ebenen unterstützt
|
// Wichtig: "Neue Sprache" soll IMMER sichtbar sein – auch wenn die DB-Abfrage (noch) fehlschlägt.
|
||||||
// Das Frontend lädt die Sprachen separat und zeigt sie als submenu2 an
|
if (filteredMenu?.socialnetwork?.children?.vocabtrainer) {
|
||||||
|
const children = {
|
||||||
|
newLanguage: { path: '/socialnetwork/vocab/new' },
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const langs = await this.vocabService.listLanguagesForMenu(user.id);
|
||||||
|
for (const l of langs) {
|
||||||
|
children[`lang_${l.id}`] = { path: `/socialnetwork/vocab/${l.id}`, label: l.name };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[menu] Konnte Vokabeltrainer-Sprachen nicht laden:', e?.message || e);
|
||||||
|
}
|
||||||
|
filteredMenu.socialnetwork.children.vocabtrainer.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json(filteredMenu);
|
res.status(200).json(filteredMenu);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import newsService from '../services/newsService.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/news?counter=0&language=de&category=top
|
|
||||||
* counter = wievieltes News-Widget aufgerufen wird (0, 1, 2, …), damit keine doppelten Artikel.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
async getNews(req, res) {
|
|
||||||
const counter = Math.max(0, parseInt(req.query.counter, 10) || 0);
|
|
||||||
const language = (req.query.language || 'de').slice(0, 10);
|
|
||||||
const category = (req.query.category || 'top').slice(0, 50);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { results, nextPage } = await newsService.getNews({ counter, language, category });
|
|
||||||
res.json({ results, nextPage });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('News getNews:', error);
|
|
||||||
res.status(500).json({ error: error.message || 'News konnten nicht geladen werden.' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -185,57 +185,6 @@ class SettingsController {
|
|||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLlmSettings(req, res) {
|
|
||||||
try {
|
|
||||||
const hashedUserId = req.headers.userid;
|
|
||||||
const data = await settingsService.getLlmSettings(hashedUserId);
|
|
||||||
res.status(200).json(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error retrieving LLM settings:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveLlmSettings(req, res) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
baseUrl: Joi.string().allow('').optional(),
|
|
||||||
model: Joi.string().allow('').optional(),
|
|
||||||
enabled: Joi.boolean().optional(),
|
|
||||||
apiKey: Joi.string().allow('').optional(),
|
|
||||||
clearKey: Joi.boolean().optional()
|
|
||||||
});
|
|
||||||
const { error, value } = schema.validate(req.body || {});
|
|
||||||
if (error) {
|
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await settingsService.saveLlmSettings(req.headers.userid, value);
|
|
||||||
res.status(200).json({ success: true });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error saving LLM settings:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitAdultVerificationRequest(req, res) {
|
|
||||||
try {
|
|
||||||
const hashedUserId = req.headers.userid;
|
|
||||||
const note = req.body?.note || '';
|
|
||||||
const file = req.file || null;
|
|
||||||
const result = await settingsService.submitAdultVerificationRequest(hashedUserId, { note }, file);
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting adult verification request:', error);
|
|
||||||
const status = [
|
|
||||||
'User not found',
|
|
||||||
'Adult verification can only be requested by adult users',
|
|
||||||
'No verification document provided',
|
|
||||||
'Unsupported verification document type'
|
|
||||||
].includes(error.message) ? 400 : 500;
|
|
||||||
res.status(status).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsController;
|
export default SettingsController;
|
||||||
|
|||||||
@@ -15,19 +15,6 @@ class SocialNetworkController {
|
|||||||
this.changeImage = this.changeImage.bind(this);
|
this.changeImage = this.changeImage.bind(this);
|
||||||
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
||||||
this.deleteFolder = this.deleteFolder.bind(this);
|
this.deleteFolder = this.deleteFolder.bind(this);
|
||||||
this.getAdultFolders = this.getAdultFolders.bind(this);
|
|
||||||
this.getAdultFoldersByUsername = this.getAdultFoldersByUsername.bind(this);
|
|
||||||
this.createAdultFolder = this.createAdultFolder.bind(this);
|
|
||||||
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
|
|
||||||
this.uploadAdultImage = this.uploadAdultImage.bind(this);
|
|
||||||
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
|
|
||||||
this.changeAdultImage = this.changeAdultImage.bind(this);
|
|
||||||
this.listEroticVideos = this.listEroticVideos.bind(this);
|
|
||||||
this.getEroticVideosByUsername = this.getEroticVideosByUsername.bind(this);
|
|
||||||
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
|
|
||||||
this.changeEroticVideo = this.changeEroticVideo.bind(this);
|
|
||||||
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
|
|
||||||
this.reportEroticContent = this.reportEroticContent.bind(this);
|
|
||||||
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
||||||
this.getGuestbookEntries = this.getGuestbookEntries.bind(this);
|
this.getGuestbookEntries = this.getGuestbookEntries.bind(this);
|
||||||
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
|
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
|
||||||
@@ -160,8 +147,8 @@ class SocialNetworkController {
|
|||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
const { imageId } = req.params;
|
const { imageId } = req.params;
|
||||||
const { title, visibilities, selectedUsers } = req.body;
|
const { title, visibilities } = req.body;
|
||||||
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers);
|
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities);
|
||||||
console.log('--->', folderId);
|
console.log('--->', folderId);
|
||||||
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
|
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -200,177 +187,6 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAdultFolders(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const folders = await this.socialNetworkService.getAdultFolders(userId);
|
|
||||||
res.status(200).json(folders);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getAdultFolders:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAdultFoldersByUsername(req, res) {
|
|
||||||
try {
|
|
||||||
const requestingUserId = req.headers.userid;
|
|
||||||
const { username } = req.params;
|
|
||||||
const folders = await this.socialNetworkService.getAdultFoldersByUsername(username, requestingUserId);
|
|
||||||
if (!folders) {
|
|
||||||
return res.status(404).json({ error: 'No folders found or access denied.' });
|
|
||||||
}
|
|
||||||
res.status(200).json(folders);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getAdultFoldersByUsername:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createAdultFolder(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const folderData = req.body;
|
|
||||||
const { folderId } = req.params;
|
|
||||||
const folder = await this.socialNetworkService.createAdultFolder(userId, folderData, folderId);
|
|
||||||
res.status(201).json(folder);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in createAdultFolder:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAdultFolderImageList(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const { folderId } = req.params;
|
|
||||||
const images = await this.socialNetworkService.getAdultFolderImageList(userId, folderId);
|
|
||||||
res.status(200).json(images);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getAdultFolderImageList:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadAdultImage(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const file = req.file;
|
|
||||||
const formData = req.body;
|
|
||||||
const image = await this.socialNetworkService.uploadAdultImage(userId, file, formData);
|
|
||||||
res.status(201).json(image);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in uploadAdultImage:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAdultImageByHash(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const { hash } = req.params;
|
|
||||||
const filePath = await this.socialNetworkService.getAdultImageFilePath(userId, hash);
|
|
||||||
res.sendFile(filePath, err => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error sending adult file:', err);
|
|
||||||
res.status(500).json({ error: 'Error sending file' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getAdultImageByHash:', error);
|
|
||||||
res.status(error.status || 403).json({ error: error.message || 'Access denied or image not found' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeAdultImage(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const { imageId } = req.params;
|
|
||||||
const { title, visibilities, selectedUsers } = req.body;
|
|
||||||
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities, selectedUsers);
|
|
||||||
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in changeAdultImage:', error);
|
|
||||||
res.status(error.status || 403).json({ error: error.message || 'Access denied or image not found' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async listEroticVideos(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const videos = await this.socialNetworkService.listEroticVideos(userId);
|
|
||||||
res.status(200).json(videos);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in listEroticVideos:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEroticVideosByUsername(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const { username } = req.params;
|
|
||||||
const videos = await this.socialNetworkService.getEroticVideosByUsername(username, userId);
|
|
||||||
res.status(200).json(videos);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getEroticVideosByUsername:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadEroticVideo(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const file = req.file;
|
|
||||||
const formData = req.body;
|
|
||||||
const video = await this.socialNetworkService.uploadEroticVideo(userId, file, formData);
|
|
||||||
res.status(201).json(video);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in uploadEroticVideo:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeEroticVideo(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const { videoId } = req.params;
|
|
||||||
const updatedVideo = await this.socialNetworkService.changeEroticVideo(userId, videoId, req.body);
|
|
||||||
res.status(200).json(updatedVideo);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in changeEroticVideo:', error);
|
|
||||||
res.status(error.status || 500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEroticVideoByHash(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const { hash } = req.params;
|
|
||||||
const { filePath, mimeType } = await this.socialNetworkService.getEroticVideoFilePath(userId, hash);
|
|
||||||
res.type(mimeType);
|
|
||||||
res.sendFile(filePath, err => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error sending adult video:', err);
|
|
||||||
res.status(500).json({ error: 'Error sending file' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getEroticVideoByHash:', error);
|
|
||||||
res.status(error.status || 403).json({ error: error.message || 'Access denied or video not found' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reportEroticContent(req, res) {
|
|
||||||
try {
|
|
||||||
const userId = req.headers.userid;
|
|
||||||
const result = await this.socialNetworkService.createEroticContentReport(userId, req.body || {});
|
|
||||||
res.status(201).json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in reportEroticContent:', error);
|
|
||||||
res.status(error.status || 400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createGuestbookEntry(req, res) {
|
async createGuestbookEntry(req, res) {
|
||||||
try {
|
try {
|
||||||
const { htmlContent, recipientName } = req.body;
|
const { htmlContent, recipientName } = req.body;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ class VocabController {
|
|||||||
this.service = new VocabService();
|
this.service = new VocabService();
|
||||||
|
|
||||||
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
|
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
|
||||||
this.listAllLanguages = this._wrapWithUser(() => this.service.listAllLanguages());
|
|
||||||
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
|
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
|
||||||
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
|
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
|
||||||
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
|
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
|
||||||
@@ -18,66 +17,10 @@ class VocabController {
|
|||||||
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
|
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
|
||||||
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
|
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
|
||||||
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
|
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
|
||||||
this.getLanguageDictionary = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.getLanguageDictionary(userId, req.params.languageId, req.query)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
|
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
|
||||||
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
|
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
|
||||||
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
|
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
|
||||||
this.getLessonVocabPool = this._wrapWithUser((userId, req) => this.service.getLessonVocabPool(userId, req.params.lessonId));
|
|
||||||
|
|
||||||
// Courses
|
|
||||||
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
|
|
||||||
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
|
|
||||||
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
|
|
||||||
this.getCompletedLessonVocabPool = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.getCompletedLessonVocabPool(userId, req.params.courseId, req.query.untilLessonId)
|
|
||||||
);
|
|
||||||
this.getCourseDictionary = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.getCourseDictionary(userId, req.params.courseId, req.query)
|
|
||||||
);
|
|
||||||
this.getVocabDistractorPool = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.getVocabDistractorPool(userId, req.params.courseId, req.query.beforeLessonId)
|
|
||||||
);
|
|
||||||
this.getCourseSrsDue = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.getCourseSrsDue(userId, req.params.courseId, req.query)
|
|
||||||
);
|
|
||||||
this.reviewSrsItem = this._wrapWithUser((userId, req) =>
|
|
||||||
this.service.reviewSrsItem(userId, req.body),
|
|
||||||
{ successStatus: 201 }
|
|
||||||
);
|
|
||||||
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
|
|
||||||
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
|
|
||||||
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
|
|
||||||
|
|
||||||
// Lessons
|
|
||||||
this.getLesson = this._wrapWithUser((userId, req) => this.service.getLesson(userId, req.params.lessonId));
|
|
||||||
this.addLessonToCourse = this._wrapWithUser((userId, req) => this.service.addLessonToCourse(userId, req.params.courseId, req.body), { successStatus: 201 });
|
|
||||||
this.updateLesson = this._wrapWithUser((userId, req) => this.service.updateLesson(userId, req.params.lessonId, req.body));
|
|
||||||
this.deleteLesson = this._wrapWithUser((userId, req) => this.service.deleteLesson(userId, req.params.lessonId));
|
|
||||||
|
|
||||||
// Enrollment
|
|
||||||
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
|
|
||||||
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
|
|
||||||
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
|
|
||||||
this.getDashboardLearningSummary = this._wrapWithUser((userId) => this.service.getDashboardLearningSummary(userId));
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
|
|
||||||
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
|
|
||||||
this.resetLessonProgress = this._wrapWithUser((userId, req) => this.service.resetMyLessonProgress(userId, req.params.lessonId));
|
|
||||||
|
|
||||||
// Grammar Exercises
|
|
||||||
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
|
|
||||||
this.createGrammarExercise = this._wrapWithUser((userId, req) => this.service.createGrammarExercise(userId, req.params.lessonId, req.body), { successStatus: 201 });
|
|
||||||
this.getGrammarExercisesForLesson = this._wrapWithUser((userId, req) => this.service.getGrammarExercisesForLesson(userId, req.params.lessonId));
|
|
||||||
this.getGrammarExercise = this._wrapWithUser((userId, req) => this.service.getGrammarExercise(userId, req.params.exerciseId));
|
|
||||||
this.checkGrammarExerciseAnswer = this._wrapWithUser((userId, req) => this.service.checkGrammarExerciseAnswer(userId, req.params.exerciseId, req.body.answer));
|
|
||||||
this.getGrammarExerciseProgress = this._wrapWithUser((userId, req) => this.service.getGrammarExerciseProgress(userId, req.params.lessonId));
|
|
||||||
this.updateGrammarExercise = this._wrapWithUser((userId, req) => this.service.updateGrammarExercise(userId, req.params.exerciseId, req.body));
|
|
||||||
this.deleteGrammarExercise = this._wrapWithUser((userId, req) => this.service.deleteGrammarExercise(userId, req.params.exerciseId));
|
|
||||||
this.sendLessonAssistantMessage = this._wrapWithUser((userId, req) => this.service.sendLessonAssistantMessage(userId, req.params.lessonId, req.body), { successStatus: 201 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_wrapWithUser(fn, { successStatus = 200 } = {}) {
|
_wrapWithUser(fn, { successStatus = 200 } = {}) {
|
||||||
@@ -99,3 +42,5 @@ class VocabController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default VocabController;
|
export default VocabController;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script zum Erstellen von Performance-Indizes
|
|
||||||
*
|
|
||||||
* Erstellt Indizes basierend auf der Analyse häufiger Queries:
|
|
||||||
* - inventory: stock_id
|
|
||||||
* - stock: branch_id
|
|
||||||
* - production: branch_id
|
|
||||||
* - director: employer_user_id
|
|
||||||
* - knowledge: (character_id, product_id) composite
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './config/loadEnv.js';
|
|
||||||
import { sequelize } from './utils/sequelize.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log('🔧 Erstelle Performance-Indizes\n');
|
|
||||||
console.log('='.repeat(60) + '\n');
|
|
||||||
|
|
||||||
const indexes = [
|
|
||||||
{
|
|
||||||
name: 'idx_knowledge_character_product',
|
|
||||||
table: 'falukant_data.knowledge',
|
|
||||||
columns: '(character_id, product_id)',
|
|
||||||
description: 'Composite Index für JOINs mit character_id UND product_id',
|
|
||||||
critical: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'idx_inventory_stock_id',
|
|
||||||
table: 'falukant_data.inventory',
|
|
||||||
columns: '(stock_id)',
|
|
||||||
description: 'Index für WHERE inventory.stock_id = ...',
|
|
||||||
critical: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'idx_stock_branch_id',
|
|
||||||
table: 'falukant_data.stock',
|
|
||||||
columns: '(branch_id)',
|
|
||||||
description: 'Index für WHERE stock.branch_id = ...',
|
|
||||||
critical: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'idx_production_branch_id',
|
|
||||||
table: 'falukant_data.production',
|
|
||||||
columns: '(branch_id)',
|
|
||||||
description: 'Index für WHERE production.branch_id = ...',
|
|
||||||
critical: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'idx_director_employer_user_id',
|
|
||||||
table: 'falukant_data.director',
|
|
||||||
columns: '(employer_user_id)',
|
|
||||||
description: 'Index für WHERE director.employer_user_id = ...',
|
|
||||||
critical: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'idx_production_start_timestamp',
|
|
||||||
table: 'falukant_data.production',
|
|
||||||
columns: '(start_timestamp)',
|
|
||||||
description: 'Index für WHERE production.start_timestamp <= ...',
|
|
||||||
critical: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'idx_director_last_salary_payout',
|
|
||||||
table: 'falukant_data.director',
|
|
||||||
columns: '(last_salary_payout)',
|
|
||||||
description: 'Index für WHERE director.last_salary_payout < ...',
|
|
||||||
critical: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log(`📋 ${indexes.length} Indizes werden erstellt:\n`);
|
|
||||||
|
|
||||||
let created = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
let errors = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < indexes.length; i++) {
|
|
||||||
const idx = indexes[i];
|
|
||||||
const criticalMark = idx.critical ? ' ⚠️ KRITISCH' : '';
|
|
||||||
|
|
||||||
console.log(`[${i + 1}/${indexes.length}] ${idx.name}${criticalMark}`);
|
|
||||||
console.log(` Tabelle: ${idx.table}`);
|
|
||||||
console.log(` Spalten: ${idx.columns}`);
|
|
||||||
console.log(` Beschreibung: ${idx.description}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Prüfe ob Index bereits existiert
|
|
||||||
const [existing] = await sequelize.query(`
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM pg_indexes
|
|
||||||
WHERE schemaname || '.' || tablename = '${idx.table}'
|
|
||||||
AND indexname = '${idx.name}'
|
|
||||||
) as exists;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (existing[0].exists) {
|
|
||||||
console.log(` ⏭️ Index existiert bereits, überspringe\n`);
|
|
||||||
skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erstelle Index
|
|
||||||
const startTime = Date.now();
|
|
||||||
await sequelize.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS ${idx.name}
|
|
||||||
ON ${idx.table} USING btree ${idx.columns};
|
|
||||||
`);
|
|
||||||
|
|
||||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
||||||
console.log(` ✅ Erstellt in ${duration}s\n`);
|
|
||||||
created++;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(` ❌ Fehler: ${error.message}\n`);
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
console.log(`✅ Zusammenfassung:`);
|
|
||||||
console.log(` Erstellt: ${created}`);
|
|
||||||
console.log(` Übersprungen: ${skipped}`);
|
|
||||||
console.log(` Fehler: ${errors}\n`);
|
|
||||||
|
|
||||||
// ANALYZE ausführen, damit PostgreSQL die neuen Indizes berücksichtigt
|
|
||||||
const tablesToAnalyze = [
|
|
||||||
'falukant_data.knowledge',
|
|
||||||
'falukant_data.inventory',
|
|
||||||
'falukant_data.stock',
|
|
||||||
'falukant_data.production',
|
|
||||||
'falukant_data.director'
|
|
||||||
];
|
|
||||||
if (created > 0) {
|
|
||||||
console.log('📊 Führe ANALYZE auf betroffenen Tabellen aus...\n');
|
|
||||||
for (const table of tablesToAnalyze) {
|
|
||||||
try {
|
|
||||||
await sequelize.query(`ANALYZE ${table};`);
|
|
||||||
console.log(` ✅ ANALYZE ${table};`);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ⚠️ ${table}: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler:', error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -25,13 +25,11 @@ function createServer() {
|
|||||||
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
||||||
});
|
});
|
||||||
wss = new WebSocketServer({ server: httpsServer });
|
wss = new WebSocketServer({ server: httpsServer });
|
||||||
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
|
||||||
httpsServer.listen(PORT, '0.0.0.0', () => {
|
httpsServer.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
|
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
wss = new WebSocketServer({ port: PORT });
|
||||||
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
|
|
||||||
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
|
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,479 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Umfassendes Diagnose-Script für Datenbank-Performance
|
|
||||||
*
|
|
||||||
* Untersucht:
|
|
||||||
* - Verbindungsstatistiken
|
|
||||||
* - Langsame Queries
|
|
||||||
* - Tabellengrößen und Bloat
|
|
||||||
* - Indizes (fehlende/ungenutzte)
|
|
||||||
* - Vacuum/Analyze Status
|
|
||||||
* - Locking/Blocking
|
|
||||||
* - Query-Statistiken
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './config/loadEnv.js';
|
|
||||||
import { sequelize } from './utils/sequelize.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Datenbank-Performance-Diagnose\n');
|
|
||||||
console.log('='.repeat(60) + '\n');
|
|
||||||
|
|
||||||
// 1. Verbindungsstatistiken
|
|
||||||
await checkConnections();
|
|
||||||
|
|
||||||
// 2. Langsame Queries (wenn pg_stat_statements aktiviert ist)
|
|
||||||
await checkSlowQueries();
|
|
||||||
|
|
||||||
// 3. Tabellengrößen und Bloat
|
|
||||||
await checkTableSizes();
|
|
||||||
|
|
||||||
// 4. Indizes prüfen
|
|
||||||
await checkIndexes();
|
|
||||||
|
|
||||||
// 5. Vacuum/Analyze Status
|
|
||||||
await checkVacuumStatus();
|
|
||||||
|
|
||||||
// 6. Locking/Blocking
|
|
||||||
await checkLocks();
|
|
||||||
|
|
||||||
// 7. Query-Statistiken (wenn pg_stat_statements aktiviert ist)
|
|
||||||
await checkQueryStats();
|
|
||||||
|
|
||||||
// 8. Connection Pool Status
|
|
||||||
await checkConnectionPool();
|
|
||||||
|
|
||||||
console.log('\n' + '='.repeat(60));
|
|
||||||
console.log('✅ Diagnose abgeschlossen\n');
|
|
||||||
|
|
||||||
await sequelize.close();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Fehler:', error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkConnections() {
|
|
||||||
console.log('📊 1. Verbindungsstatistiken\n');
|
|
||||||
|
|
||||||
const [connections] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
count(*) as total,
|
|
||||||
count(*) FILTER (WHERE state = 'active') as active,
|
|
||||||
count(*) FILTER (WHERE state = 'idle') as idle,
|
|
||||||
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
|
||||||
count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE datname = current_database();
|
|
||||||
`);
|
|
||||||
|
|
||||||
const conn = connections[0];
|
|
||||||
console.log(` Gesamt: ${conn.total}`);
|
|
||||||
console.log(` Aktiv: ${conn.active}`);
|
|
||||||
console.log(` Idle: ${conn.idle}`);
|
|
||||||
console.log(` Idle in Transaction: ${conn.idle_in_transaction}`);
|
|
||||||
console.log(` Wartend: ${conn.waiting}\n`);
|
|
||||||
|
|
||||||
const [maxConn] = await sequelize.query(`
|
|
||||||
SELECT setting::int as max_connections
|
|
||||||
FROM pg_settings
|
|
||||||
WHERE name = 'max_connections';
|
|
||||||
`);
|
|
||||||
|
|
||||||
const usagePercent = (conn.total / maxConn[0].max_connections) * 100;
|
|
||||||
console.log(` Max Connections: ${maxConn[0].max_connections}`);
|
|
||||||
console.log(` Auslastung: ${usagePercent.toFixed(1)}%\n`);
|
|
||||||
|
|
||||||
if (usagePercent > 80) {
|
|
||||||
console.log(' ⚠️ WARNUNG: Hohe Verbindungsauslastung!\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeige lange laufende Queries
|
|
||||||
const [longRunning] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
pid,
|
|
||||||
usename,
|
|
||||||
application_name,
|
|
||||||
state,
|
|
||||||
now() - query_start as duration,
|
|
||||||
wait_event_type,
|
|
||||||
wait_event,
|
|
||||||
left(query, 100) as query_preview
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE datname = current_database()
|
|
||||||
AND state != 'idle'
|
|
||||||
AND now() - query_start > interval '5 seconds'
|
|
||||||
ORDER BY query_start ASC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (longRunning.length > 0) {
|
|
||||||
console.log(' ⚠️ Lange laufende Queries (> 5 Sekunden):');
|
|
||||||
longRunning.forEach(q => {
|
|
||||||
const duration = Math.round(q.duration.total_seconds);
|
|
||||||
console.log(` PID ${q.pid}: ${duration}s - ${q.query_preview}...`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkSlowQueries() {
|
|
||||||
console.log('🐌 2. Langsame Queries (pg_stat_statements)\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Prüfe ob pg_stat_statements aktiviert ist
|
|
||||||
const [extension] = await sequelize.query(`
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
|
||||||
) as exists;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!extension[0].exists) {
|
|
||||||
console.log(' ℹ️ pg_stat_statements ist nicht aktiviert.');
|
|
||||||
console.log(' 💡 Aktivieren mit: CREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [slowQueries] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
left(query, 100) as query_preview,
|
|
||||||
calls,
|
|
||||||
total_exec_time,
|
|
||||||
mean_exec_time,
|
|
||||||
max_exec_time,
|
|
||||||
(total_exec_time / sum(total_exec_time) OVER ()) * 100 as percent_total
|
|
||||||
FROM pg_stat_statements
|
|
||||||
WHERE mean_exec_time > 100 -- Queries mit > 100ms Durchschnitt
|
|
||||||
ORDER BY total_exec_time DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (slowQueries.length > 0) {
|
|
||||||
console.log(' Top 10 langsamste Queries (nach Gesamtzeit):');
|
|
||||||
slowQueries.forEach((q, i) => {
|
|
||||||
console.log(` ${i + 1}. ${q.query_preview}...`);
|
|
||||||
console.log(` Aufrufe: ${q.calls}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms, Max: ${q.max_exec_time.toFixed(2)}ms`);
|
|
||||||
console.log(` Gesamtzeit: ${q.total_exec_time.toFixed(2)}ms (${q.percent_total.toFixed(1)}%)\n`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✅ Keine sehr langsamen Queries gefunden (> 100ms Durchschnitt)\n');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ⚠️ Fehler beim Abrufen der Query-Statistiken: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkTableSizes() {
|
|
||||||
console.log('📦 3. Tabellengrößen und Bloat\n');
|
|
||||||
|
|
||||||
const [tableSizes] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || relname as full_table_name,
|
|
||||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size,
|
|
||||||
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) as table_size,
|
|
||||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) as indexes_size,
|
|
||||||
n_live_tup as row_count,
|
|
||||||
n_dead_tup as dead_rows,
|
|
||||||
CASE
|
|
||||||
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
|
||||||
ELSE 0
|
|
||||||
END as dead_row_percent,
|
|
||||||
last_vacuum,
|
|
||||||
last_autovacuum,
|
|
||||||
last_analyze,
|
|
||||||
last_autoanalyze
|
|
||||||
FROM pg_stat_user_tables
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC
|
|
||||||
LIMIT 20;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (tableSizes.length > 0) {
|
|
||||||
console.log(' Top 20 größte Tabellen:');
|
|
||||||
tableSizes.forEach((t, i) => {
|
|
||||||
console.log(` ${i + 1}. ${t.full_table_name}`);
|
|
||||||
console.log(` Größe: ${t.total_size} (Tabelle: ${t.table_size}, Indizes: ${t.indexes_size})`);
|
|
||||||
console.log(` Zeilen: ${parseInt(t.row_count).toLocaleString()}, Tote Zeilen: ${parseInt(t.dead_rows).toLocaleString()} (${t.dead_row_percent}%)`);
|
|
||||||
|
|
||||||
if (parseFloat(t.dead_row_percent) > 20) {
|
|
||||||
console.log(` ⚠️ Hoher Bloat-Anteil! Vacuum empfohlen.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.last_vacuum || t.last_autovacuum) {
|
|
||||||
const lastVacuum = t.last_vacuum || t.last_autovacuum;
|
|
||||||
const daysSinceVacuum = Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24));
|
|
||||||
if (daysSinceVacuum > 7) {
|
|
||||||
console.log(` ⚠️ Letztes Vacuum: ${daysSinceVacuum} Tage her`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIndexes() {
|
|
||||||
console.log('🔍 4. Indizes-Analyse\n');
|
|
||||||
|
|
||||||
// Fehlende Indizes (basierend auf pg_stat_user_tables)
|
|
||||||
const [missingIndexes] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || relname as table_name,
|
|
||||||
seq_scan,
|
|
||||||
seq_tup_read,
|
|
||||||
idx_scan,
|
|
||||||
seq_tup_read / NULLIF(seq_scan, 0) as avg_seq_read
|
|
||||||
FROM pg_stat_user_tables
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
AND seq_scan > 1000
|
|
||||||
AND seq_tup_read / NULLIF(seq_scan, 0) > 1000
|
|
||||||
ORDER BY seq_tup_read DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (missingIndexes.length > 0) {
|
|
||||||
console.log(' ⚠️ Tabellen mit vielen Sequential Scans (möglicherweise fehlende Indizes):');
|
|
||||||
missingIndexes.forEach(t => {
|
|
||||||
console.log(` ${t.table_name}: ${t.seq_scan} seq scans, ${parseInt(t.seq_tup_read).toLocaleString()} Zeilen gelesen`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ungenutzte Indizes
|
|
||||||
const [unusedIndexes] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || indexrelname as index_name,
|
|
||||||
schemaname || '.' || relname as table_name,
|
|
||||||
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
|
||||||
idx_scan as scans
|
|
||||||
FROM pg_stat_user_indexes
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
AND idx_scan = 0
|
|
||||||
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
|
|
||||||
ORDER BY pg_relation_size(indexrelid) DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (unusedIndexes.length > 0) {
|
|
||||||
console.log(' ⚠️ Ungenutzte Indizes (> 1MB, nie verwendet):');
|
|
||||||
unusedIndexes.forEach(idx => {
|
|
||||||
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (0 Scans)`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index Bloat
|
|
||||||
const [indexBloat] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || indexrelname as index_name,
|
|
||||||
schemaname || '.' || relname as table_name,
|
|
||||||
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
|
||||||
idx_scan as scans,
|
|
||||||
idx_tup_read as tuples_read,
|
|
||||||
idx_tup_fetch as tuples_fetched
|
|
||||||
FROM pg_stat_user_indexes
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
AND pg_relation_size(indexrelid) > 10 * 1024 * 1024 -- Größer als 10MB
|
|
||||||
ORDER BY pg_relation_size(indexrelid) DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (indexBloat.length > 0) {
|
|
||||||
console.log(' Top 10 größte Indizes:');
|
|
||||||
indexBloat.forEach(idx => {
|
|
||||||
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (${idx.scans} Scans)`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkVacuumStatus() {
|
|
||||||
console.log('🧹 5. Vacuum/Analyze Status\n');
|
|
||||||
|
|
||||||
const [vacuumStats] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
schemaname || '.' || relname as table_name,
|
|
||||||
last_vacuum,
|
|
||||||
last_autovacuum,
|
|
||||||
last_analyze,
|
|
||||||
last_autoanalyze,
|
|
||||||
n_dead_tup,
|
|
||||||
n_live_tup,
|
|
||||||
CASE
|
|
||||||
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
|
||||||
ELSE 0
|
|
||||||
END as dead_percent
|
|
||||||
FROM pg_stat_user_tables
|
|
||||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
|
||||||
AND (
|
|
||||||
(last_vacuum IS NULL AND last_autovacuum IS NULL)
|
|
||||||
OR (last_vacuum < now() - interval '7 days' AND last_autovacuum < now() - interval '7 days')
|
|
||||||
OR n_dead_tup > 10000
|
|
||||||
)
|
|
||||||
ORDER BY n_dead_tup DESC
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (vacuumStats.length > 0) {
|
|
||||||
console.log(' ⚠️ Tabellen, die Vacuum benötigen könnten:');
|
|
||||||
vacuumStats.forEach(t => {
|
|
||||||
const lastVacuum = t.last_vacuum || t.last_autovacuum || 'Nie';
|
|
||||||
const daysSince = lastVacuum !== 'Nie'
|
|
||||||
? Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24))
|
|
||||||
: '∞';
|
|
||||||
console.log(` ${t.table_name}:`);
|
|
||||||
console.log(` Tote Zeilen: ${parseInt(t.n_dead_tup).toLocaleString()} (${t.dead_percent}%)`);
|
|
||||||
console.log(` Letztes Vacuum: ${lastVacuum} (${daysSince} Tage)`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
} else {
|
|
||||||
console.log(' ✅ Alle Tabellen sind aktuell gevacuumt\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkLocks() {
|
|
||||||
console.log('🔒 6. Locking/Blocking\n');
|
|
||||||
|
|
||||||
const [locks] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
blocked_locks.pid AS blocked_pid,
|
|
||||||
blocked_activity.usename AS blocked_user,
|
|
||||||
blocking_locks.pid AS blocking_pid,
|
|
||||||
blocking_activity.usename AS blocking_user,
|
|
||||||
blocked_activity.query AS blocked_statement,
|
|
||||||
blocking_activity.query AS blocking_statement,
|
|
||||||
blocked_activity.application_name AS blocked_app,
|
|
||||||
blocking_activity.application_name AS blocking_app
|
|
||||||
FROM pg_catalog.pg_locks blocked_locks
|
|
||||||
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
|
|
||||||
JOIN pg_catalog.pg_locks blocking_locks
|
|
||||||
ON blocking_locks.locktype = blocked_locks.locktype
|
|
||||||
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
|
|
||||||
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
|
|
||||||
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
|
|
||||||
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
|
|
||||||
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
|
|
||||||
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
|
|
||||||
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
|
|
||||||
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
|
|
||||||
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
|
|
||||||
AND blocking_locks.pid != blocked_locks.pid
|
|
||||||
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
|
|
||||||
WHERE NOT blocked_locks.granted;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (locks.length > 0) {
|
|
||||||
console.log(' ⚠️ Blockierte Queries gefunden:');
|
|
||||||
locks.forEach(lock => {
|
|
||||||
console.log(` Blockiert: PID ${lock.blocked_pid} (${lock.blocked_user})`);
|
|
||||||
console.log(` Blockiert von: PID ${lock.blocking_pid} (${lock.blocking_user})`);
|
|
||||||
console.log(` Blockierte Query: ${lock.blocked_statement.substring(0, 100)}...`);
|
|
||||||
console.log(` Blockierende Query: ${lock.blocking_statement.substring(0, 100)}...\n`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✅ Keine blockierten Queries gefunden\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeige alle aktiven Locks
|
|
||||||
const [allLocks] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
locktype,
|
|
||||||
relation::regclass as relation,
|
|
||||||
mode,
|
|
||||||
granted,
|
|
||||||
pid
|
|
||||||
FROM pg_locks
|
|
||||||
WHERE relation IS NOT NULL
|
|
||||||
AND NOT granted
|
|
||||||
LIMIT 10;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (allLocks.length > 0) {
|
|
||||||
console.log(' ⚠️ Wartende Locks:');
|
|
||||||
allLocks.forEach(lock => {
|
|
||||||
console.log(` ${lock.locktype} auf ${lock.relation}: ${lock.mode} (PID ${lock.pid})`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkQueryStats() {
|
|
||||||
console.log('📈 7. Query-Statistiken\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [extension] = await sequelize.query(`
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
|
||||||
) as exists;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!extension[0].exists) {
|
|
||||||
console.log(' ℹ️ pg_stat_statements ist nicht aktiviert.\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [topQueries] = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
left(query, 80) as query_preview,
|
|
||||||
calls,
|
|
||||||
total_exec_time,
|
|
||||||
mean_exec_time,
|
|
||||||
(100 * total_exec_time / sum(total_exec_time) OVER ()) as percent_total
|
|
||||||
FROM pg_stat_statements
|
|
||||||
WHERE query NOT LIKE '%pg_stat_statements%'
|
|
||||||
ORDER BY calls DESC
|
|
||||||
LIMIT 5;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (topQueries.length > 0) {
|
|
||||||
console.log(' Top 5 häufigste Queries:');
|
|
||||||
topQueries.forEach((q, i) => {
|
|
||||||
console.log(` ${i + 1}. ${q.query_preview}...`);
|
|
||||||
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms`);
|
|
||||||
});
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ⚠️ Fehler: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkConnectionPool() {
|
|
||||||
console.log('🏊 8. Connection Pool Status\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Hole Pool-Konfiguration aus Sequelize Config
|
|
||||||
const config = sequelize.config;
|
|
||||||
const poolConfig = config.pool || {};
|
|
||||||
|
|
||||||
console.log(` Pool-Konfiguration:`);
|
|
||||||
console.log(` Max: ${poolConfig.max || 'N/A'}`);
|
|
||||||
console.log(` Min: ${poolConfig.min || 'N/A'}`);
|
|
||||||
console.log(` Acquire Timeout: ${poolConfig.acquire || 'N/A'}ms`);
|
|
||||||
console.log(` Idle Timeout: ${poolConfig.idle || 'N/A'}ms`);
|
|
||||||
console.log(` Evict Interval: ${poolConfig.evict || 'N/A'}ms\n`);
|
|
||||||
|
|
||||||
// Versuche Pool-Status zu bekommen
|
|
||||||
const pool = sequelize.connectionManager.pool;
|
|
||||||
if (pool) {
|
|
||||||
const poolSize = pool.size || 0;
|
|
||||||
const poolUsed = pool.used || 0;
|
|
||||||
const poolPending = pool.pending || 0;
|
|
||||||
|
|
||||||
console.log(` Pool-Status:`);
|
|
||||||
console.log(` Größe: ${poolSize}`);
|
|
||||||
console.log(` Verwendet: ${poolUsed}`);
|
|
||||||
console.log(` Wartend: ${poolPending}\n`);
|
|
||||||
} else {
|
|
||||||
console.log(` ℹ️ Pool-Objekt nicht verfügbar\n`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ⚠️ Fehler beim Abrufen der Pool-Informationen: ${error.message}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Kopie nach `backend/.env` — nicht committen.
|
|
||||||
#
|
|
||||||
# Produktion / direkter DB-Host steht typischerweise in `.env`.
|
|
||||||
# Für Entwicklung mit SSH-Tunnel: Datei `backend/.env.local` anlegen (wird nach `.env`
|
|
||||||
# geladen und überschreibt). So bleibt `.env` mit echtem Host, Tunnel nur lokal.
|
|
||||||
#
|
|
||||||
# Beispiel backend/.env.local:
|
|
||||||
# DB_HOST=127.0.0.1
|
|
||||||
# DB_PORT=60000
|
|
||||||
# # DB_SSL=0
|
|
||||||
# (Tunnel z. B.: ssh -L 60000:127.0.0.1:5432 user@server)
|
|
||||||
#
|
|
||||||
DB_NAME=
|
|
||||||
DB_USER=
|
|
||||||
DB_PASS=
|
|
||||||
# DB_HOST=
|
|
||||||
# DB_PORT=5432
|
|
||||||
# DB_SSL=0
|
|
||||||
#
|
|
||||||
# Optional (Defaults siehe utils/sequelize.js)
|
|
||||||
# DB_CONNECT_TIMEOUT_MS=30000
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Kopie nach backend/.env.local (liegt neben .env, wird nicht committet).
|
|
||||||
# Überschreibt nur bei dir lokal z. B. SSH-Tunnel — .env kann weiter DB_HOST=tsschulz.de haben.
|
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=60000
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/**
|
|
||||||
* Periodischer Job: reputation_periodic für politische Amtsinhaber.
|
|
||||||
* Aufruf: systemd-Timer oder FALUKANT_POLITICAL_REPUTATION_JOB=1 (siehe server.js).
|
|
||||||
*/
|
|
||||||
import PoliticalOffice from '../models/falukant/data/political_office.js';
|
|
||||||
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
|
|
||||||
import PoliticalOfficeBenefit from '../models/falukant/predefine/political_office_benefit.js';
|
|
||||||
import PoliticalOfficeBenefitType from '../models/falukant/type/political_office_benefit_type.js';
|
|
||||||
import PoliticalBenefitLastTick from '../models/falukant/data/political_benefit_last_tick.js';
|
|
||||||
import FalukantCharacter from '../models/falukant/data/character.js';
|
|
||||||
import FalukantUser from '../models/falukant/data/user.js';
|
|
||||||
import User from '../models/community/user.js';
|
|
||||||
import { sequelize } from '../utils/sequelize.js';
|
|
||||||
import { notifyUser } from '../utils/socket.js';
|
|
||||||
|
|
||||||
export async function runPoliticalReputationTicks() {
|
|
||||||
const offices = await PoliticalOffice.findAll({
|
|
||||||
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['id', 'name'] }]
|
|
||||||
});
|
|
||||||
const toNotify = new Set();
|
|
||||||
let ticks = 0;
|
|
||||||
|
|
||||||
for (const po of offices) {
|
|
||||||
const characterId = po.characterId;
|
|
||||||
const benefitRows = await PoliticalOfficeBenefit.findAll({
|
|
||||||
where: { officeTypeId: po.officeTypeId },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: PoliticalOfficeBenefitType,
|
|
||||||
as: 'benefitDefinition',
|
|
||||||
attributes: ['tr'],
|
|
||||||
required: true,
|
|
||||||
where: { tr: 'reputation_periodic' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const br of benefitRows) {
|
|
||||||
const v = br.value && typeof br.value === 'object' ? br.value : {};
|
|
||||||
const intervalDays = Math.max(1, Number(v.intervalDays ?? v.everyDays ?? 7));
|
|
||||||
const gain = Math.max(1, Number(v.gain ?? 1));
|
|
||||||
|
|
||||||
const [tickRow, created] = await PoliticalBenefitLastTick.findOrCreate({
|
|
||||||
where: {
|
|
||||||
characterId,
|
|
||||||
politicalOfficeBenefitId: br.id
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
characterId,
|
|
||||||
politicalOfficeBenefitId: br.id,
|
|
||||||
lastTickAt: new Date(po.createdAt),
|
|
||||||
ticksCount: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseMs = new Date(tickRow.lastTickAt).getTime();
|
|
||||||
const daysSince = Math.floor((Date.now() - baseMs) / 86400000);
|
|
||||||
if (daysSince < intervalDays) continue;
|
|
||||||
|
|
||||||
await sequelize.transaction(async (t) => {
|
|
||||||
const ch = await FalukantCharacter.findByPk(characterId, { transaction: t });
|
|
||||||
if (!ch) return;
|
|
||||||
const nextRep = Math.min(100, (ch.reputation ?? 0) + gain);
|
|
||||||
await ch.update({ reputation: nextRep }, { transaction: t });
|
|
||||||
await PoliticalBenefitLastTick.update(
|
|
||||||
{
|
|
||||||
lastTickAt: new Date(),
|
|
||||||
ticksCount: (tickRow.ticksCount || 0) + 1
|
|
||||||
},
|
|
||||||
{ where: { id: tickRow.id }, transaction: t }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ticks += 1;
|
|
||||||
toNotify.add(characterId);
|
|
||||||
console.info(
|
|
||||||
`[PoliticalBenefits] reputation_tick characterId=${characterId} benefitId=${br.id} gain=${gain}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const characterId of toNotify) {
|
|
||||||
const ch = await FalukantCharacter.findByPk(characterId, { attributes: ['userId'] });
|
|
||||||
if (!ch?.userId) continue;
|
|
||||||
const fu = await FalukantUser.findOne({
|
|
||||||
where: { id: ch.userId },
|
|
||||||
include: [{ model: User, as: 'user', attributes: ['hashedId'] }]
|
|
||||||
});
|
|
||||||
const hid = fu?.user?.hashedId;
|
|
||||||
if (hid) notifyUser(hid, 'falukantUpdateStatus', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { processedOffices: offices.length, ticksApplied: ticks };
|
|
||||||
}
|
|
||||||
@@ -10,9 +10,6 @@ export const authenticate = async (req, res, next) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
||||||
}
|
}
|
||||||
if (!user.active) {
|
|
||||||
return res.status(403).json({ error: 'Unauthorized: User blocked' });
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await updateUserTimestamp(user.id);
|
await updateUserTimestamp(user.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface, Sequelize) => {
|
|
||||||
const table = { tableName: 'contact_message', schema: 'service' };
|
|
||||||
const columns = await queryInterface.describeTable(table);
|
|
||||||
|
|
||||||
if (!columns.answer) {
|
|
||||||
await queryInterface.addColumn(table, 'answer', {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!columns.answered_at) {
|
|
||||||
await queryInterface.addColumn(table, 'answered_at', {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!columns.is_answered) {
|
|
||||||
await queryInterface.addColumn(table, 'is_answered', {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (queryInterface, Sequelize) => {
|
|
||||||
const table = { tableName: 'contact_message', schema: 'service' };
|
|
||||||
const columns = await queryInterface.describeTable(table);
|
|
||||||
|
|
||||||
if (columns.answer) {
|
|
||||||
await queryInterface.removeColumn(table, 'answer');
|
|
||||||
}
|
|
||||||
if (columns.answered_at) {
|
|
||||||
await queryInterface.removeColumn(table, 'answered_at');
|
|
||||||
}
|
|
||||||
if (columns.is_answered) {
|
|
||||||
await queryInterface.removeColumn(table, 'is_answered');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
// Kurs-Tabelle
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_course (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
owner_user_id INTEGER NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
language_id INTEGER NOT NULL,
|
|
||||||
difficulty_level INTEGER DEFAULT 1,
|
|
||||||
is_public BOOLEAN DEFAULT false,
|
|
||||||
share_code TEXT,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT vocab_course_owner_fk
|
|
||||||
FOREIGN KEY (owner_user_id)
|
|
||||||
REFERENCES community."user"(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_language_fk
|
|
||||||
FOREIGN KEY (language_id)
|
|
||||||
REFERENCES community.vocab_language(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Lektionen innerhalb eines Kurses
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
course_id INTEGER NOT NULL,
|
|
||||||
chapter_id INTEGER NOT NULL,
|
|
||||||
lesson_number INTEGER NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT vocab_course_lesson_course_fk
|
|
||||||
FOREIGN KEY (course_id)
|
|
||||||
REFERENCES community.vocab_course(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_lesson_chapter_fk
|
|
||||||
FOREIGN KEY (chapter_id)
|
|
||||||
REFERENCES community.vocab_chapter(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Einschreibungen in Kurse
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
course_id INTEGER NOT NULL,
|
|
||||||
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT vocab_course_enrollment_user_fk
|
|
||||||
FOREIGN KEY (user_id)
|
|
||||||
REFERENCES community."user"(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_enrollment_course_fk
|
|
||||||
FOREIGN KEY (course_id)
|
|
||||||
REFERENCES community.vocab_course(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Fortschritt pro User und Lektion
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
course_id INTEGER NOT NULL,
|
|
||||||
lesson_id INTEGER NOT NULL,
|
|
||||||
completed BOOLEAN DEFAULT false,
|
|
||||||
score INTEGER DEFAULT 0,
|
|
||||||
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
CONSTRAINT vocab_course_progress_user_fk
|
|
||||||
FOREIGN KEY (user_id)
|
|
||||||
REFERENCES community."user"(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_progress_course_fk
|
|
||||||
FOREIGN KEY (course_id)
|
|
||||||
REFERENCES community.vocab_course(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_progress_lesson_fk
|
|
||||||
FOREIGN KEY (lesson_id)
|
|
||||||
REFERENCES community.vocab_course_lesson(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Indizes
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
|
|
||||||
ON community.vocab_course(owner_user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
|
||||||
ON community.vocab_course(language_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
|
||||||
ON community.vocab_course(is_public);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
|
||||||
ON community.vocab_course_lesson(course_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
|
|
||||||
ON community.vocab_course_lesson(chapter_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
|
|
||||||
ON community.vocab_course_enrollment(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
|
|
||||||
ON community.vocab_course_enrollment(course_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
|
|
||||||
ON community.vocab_course_progress(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
|
|
||||||
ON community.vocab_course_progress(course_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
|
|
||||||
ON community.vocab_course_progress(lesson_id);
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TABLE IF EXISTS community.vocab_course_progress CASCADE;
|
|
||||||
DROP TABLE IF EXISTS community.vocab_course_enrollment CASCADE;
|
|
||||||
DROP TABLE IF EXISTS community.vocab_course_lesson CASCADE;
|
|
||||||
DROP TABLE IF EXISTS community.vocab_course CASCADE;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
// Grammatik-Übungstypen (z.B. "gap_fill", "multiple_choice", "sentence_building", "transformation")
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Grammatik-Übungen (verknüpft mit Lektionen)
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
lesson_id INTEGER NOT NULL,
|
|
||||||
exercise_type_id INTEGER NOT NULL,
|
|
||||||
exercise_number INTEGER NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
instruction TEXT,
|
|
||||||
question_data JSONB NOT NULL,
|
|
||||||
answer_data JSONB NOT NULL,
|
|
||||||
explanation TEXT,
|
|
||||||
created_by_user_id INTEGER NOT NULL,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT vocab_grammar_exercise_lesson_fk
|
|
||||||
FOREIGN KEY (lesson_id)
|
|
||||||
REFERENCES community.vocab_course_lesson(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_grammar_exercise_type_fk
|
|
||||||
FOREIGN KEY (exercise_type_id)
|
|
||||||
REFERENCES community.vocab_grammar_exercise_type(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_grammar_exercise_creator_fk
|
|
||||||
FOREIGN KEY (created_by_user_id)
|
|
||||||
REFERENCES community."user"(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Fortschritt für Grammatik-Übungen
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
exercise_id INTEGER NOT NULL,
|
|
||||||
attempts INTEGER DEFAULT 0,
|
|
||||||
correct_attempts INTEGER DEFAULT 0,
|
|
||||||
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
completed BOOLEAN DEFAULT false,
|
|
||||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
CONSTRAINT vocab_grammar_exercise_progress_user_fk
|
|
||||||
FOREIGN KEY (user_id)
|
|
||||||
REFERENCES community."user"(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
|
|
||||||
FOREIGN KEY (exercise_id)
|
|
||||||
REFERENCES community.vocab_grammar_exercise(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Indizes
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
|
|
||||||
ON community.vocab_grammar_exercise(lesson_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
|
|
||||||
ON community.vocab_grammar_exercise(exercise_type_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
|
|
||||||
ON community.vocab_grammar_exercise_progress(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
|
|
||||||
ON community.vocab_grammar_exercise_progress(exercise_id);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Standard-Übungstypen einfügen
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
|
||||||
('gap_fill', 'Lückentext-Übung'),
|
|
||||||
('multiple_choice', 'Multiple-Choice-Fragen'),
|
|
||||||
('sentence_building', 'Satzbau-Übung'),
|
|
||||||
('transformation', 'Satzumformung'),
|
|
||||||
('conjugation', 'Konjugations-Übung'),
|
|
||||||
('declension', 'Deklinations-Übung')
|
|
||||||
ON CONFLICT (name) DO NOTHING;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TABLE IF EXISTS community.vocab_grammar_exercise_progress CASCADE;
|
|
||||||
DROP TABLE IF EXISTS community.vocab_grammar_exercise CASCADE;
|
|
||||||
DROP TABLE IF EXISTS community.vocab_grammar_exercise_type CASCADE;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
// chapter_id optional machen (nicht alle Lektionen brauchen ein Kapitel)
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
ALTER COLUMN chapter_id DROP NOT NULL;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Kurs-Wochen/Module hinzufügen
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
ADD COLUMN IF NOT EXISTS week_number INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS day_number INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
|
|
||||||
ADD COLUMN IF NOT EXISTS audio_url TEXT,
|
|
||||||
ADD COLUMN IF NOT EXISTS cultural_notes TEXT;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Indizes für Wochen/Tage
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
|
|
||||||
ON community.vocab_course_lesson(course_id, week_number);
|
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
|
|
||||||
ON community.vocab_course_lesson(lesson_type);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Kommentar hinzufügen für lesson_type
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
|
|
||||||
'Type: vocab, grammar, conversation, culture, review';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
DROP COLUMN IF EXISTS week_number,
|
|
||||||
DROP COLUMN IF EXISTS day_number,
|
|
||||||
DROP COLUMN IF EXISTS lesson_type,
|
|
||||||
DROP COLUMN IF EXISTS audio_url,
|
|
||||||
DROP COLUMN IF EXISTS cultural_notes;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
// Lernziele für Lektionen
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
|
|
||||||
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Kommentare hinzufügen
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
|
|
||||||
'Zielzeit in Minuten für diese Lektion';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
|
|
||||||
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
|
|
||||||
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
DROP COLUMN IF EXISTS target_minutes,
|
|
||||||
DROP COLUMN IF EXISTS target_score_percent,
|
|
||||||
DROP COLUMN IF EXISTS requires_review;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS falukant_data.relationship_state (
|
|
||||||
id serial PRIMARY KEY,
|
|
||||||
relationship_id integer NOT NULL UNIQUE,
|
|
||||||
marriage_satisfaction integer NOT NULL DEFAULT 55 CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100),
|
|
||||||
marriage_public_stability integer NOT NULL DEFAULT 55 CHECK (marriage_public_stability >= 0 AND marriage_public_stability <= 100),
|
|
||||||
lover_role text NULL CHECK (lover_role IN ('secret_affair', 'lover', 'mistress_or_favorite')),
|
|
||||||
affection integer NOT NULL DEFAULT 50 CHECK (affection >= 0 AND affection <= 100),
|
|
||||||
visibility integer NOT NULL DEFAULT 15 CHECK (visibility >= 0 AND visibility <= 100),
|
|
||||||
discretion integer NOT NULL DEFAULT 50 CHECK (discretion >= 0 AND discretion <= 100),
|
|
||||||
maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100),
|
|
||||||
status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2),
|
|
||||||
monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0),
|
|
||||||
months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0),
|
|
||||||
active boolean NOT NULL DEFAULT true,
|
|
||||||
acknowledged boolean NOT NULL DEFAULT false,
|
|
||||||
exclusive_flag boolean NOT NULL DEFAULT false,
|
|
||||||
last_monthly_processed_at timestamp with time zone NULL,
|
|
||||||
last_daily_processed_at timestamp with time zone NULL,
|
|
||||||
notes_json jsonb NULL,
|
|
||||||
flags_json jsonb NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT relationship_state_relationship_fk
|
|
||||||
FOREIGN KEY (relationship_id)
|
|
||||||
REFERENCES falukant_data.relationship(id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS relationship_state_active_idx
|
|
||||||
ON falukant_data.relationship_state (active);
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS relationship_state_lover_role_idx
|
|
||||||
ON falukant_data.relationship_state (lover_role);
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
ADD COLUMN IF NOT EXISTS legitimacy text NOT NULL DEFAULT 'legitimate';
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
ADD COLUMN IF NOT EXISTS birth_context text NOT NULL DEFAULT 'marriage';
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_constraint
|
|
||||||
WHERE conname = 'child_relation_legitimacy_chk'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
ADD CONSTRAINT child_relation_legitimacy_chk
|
|
||||||
CHECK (legitimacy IN ('legitimate', 'acknowledged_bastard', 'hidden_bastard'));
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
$$;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_constraint
|
|
||||||
WHERE conname = 'child_relation_birth_context_chk'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
ADD CONSTRAINT child_relation_birth_context_chk
|
|
||||||
CHECK (birth_context IN ('marriage', 'lover'));
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
$$;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
DROP CONSTRAINT IF EXISTS child_relation_birth_context_chk;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
DROP CONSTRAINT IF EXISTS child_relation_legitimacy_chk;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
DROP COLUMN IF EXISTS public_known;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
DROP COLUMN IF EXISTS birth_context;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.child_relation
|
|
||||||
DROP COLUMN IF EXISTS legitimacy;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TABLE IF EXISTS falukant_data.relationship_state;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO falukant_data.relationship_state (
|
|
||||||
relationship_id,
|
|
||||||
marriage_satisfaction,
|
|
||||||
marriage_public_stability,
|
|
||||||
lover_role,
|
|
||||||
affection,
|
|
||||||
visibility,
|
|
||||||
discretion,
|
|
||||||
maintenance_level,
|
|
||||||
status_fit,
|
|
||||||
monthly_base_cost,
|
|
||||||
months_underfunded,
|
|
||||||
active,
|
|
||||||
acknowledged,
|
|
||||||
exclusive_flag,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
r.id,
|
|
||||||
55,
|
|
||||||
55,
|
|
||||||
CASE WHEN rt.tr = 'lover' THEN 'lover' ELSE NULL END,
|
|
||||||
50,
|
|
||||||
CASE WHEN rt.tr = 'lover' THEN 20 ELSE 15 END,
|
|
||||||
CASE WHEN rt.tr = 'lover' THEN 45 ELSE 50 END,
|
|
||||||
50,
|
|
||||||
0,
|
|
||||||
CASE WHEN rt.tr = 'lover' THEN 30 ELSE 0 END,
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
CURRENT_TIMESTAMP,
|
|
||||||
CURRENT_TIMESTAMP
|
|
||||||
FROM falukant_data.relationship r
|
|
||||||
INNER JOIN falukant_type.relationship rt
|
|
||||||
ON rt.id = r.relationship_type_id
|
|
||||||
LEFT JOIN falukant_data.relationship_state rs
|
|
||||||
ON rs.relationship_id = r.id
|
|
||||||
WHERE rs.id IS NULL
|
|
||||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DELETE FROM falukant_data.relationship_state rs
|
|
||||||
USING falukant_data.relationship r
|
|
||||||
INNER JOIN falukant_type.relationship rt
|
|
||||||
ON rt.id = r.relationship_type_id
|
|
||||||
WHERE rs.relationship_id = r.id
|
|
||||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'servant_count',
|
|
||||||
{
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'servant_quality',
|
|
||||||
{
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 50
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'servant_pay_level',
|
|
||||||
{
|
|
||||||
type: Sequelize.STRING(20),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'normal'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'household_order',
|
|
||||||
{
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 55
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'household_order');
|
|
||||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_pay_level');
|
|
||||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_quality');
|
|
||||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_count');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'household_tension_score',
|
|
||||||
{
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 10
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'household_tension_reasons_json',
|
|
||||||
{
|
|
||||||
type: Sequelize.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.removeColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'household_tension_reasons_json'
|
|
||||||
);
|
|
||||||
await queryInterface.removeColumn(
|
|
||||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
|
||||||
'household_tension_score'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'status', {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'delinquent'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'entered_at', {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'released_at', {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'debt_at_entry', {
|
|
||||||
type: Sequelize.DECIMAL(14, 2),
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'remaining_debt', {
|
|
||||||
type: Sequelize.DECIMAL(14, 2),
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'days_overdue', {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'reason', {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'creditworthiness_penalty', {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'next_forced_action', {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'assets_seized_json', {
|
|
||||||
type: Sequelize.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(table, 'public_known', {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
|
||||||
|
|
||||||
await queryInterface.removeColumn(table, 'public_known');
|
|
||||||
await queryInterface.removeColumn(table, 'assets_seized_json');
|
|
||||||
await queryInterface.removeColumn(table, 'next_forced_action');
|
|
||||||
await queryInterface.removeColumn(table, 'creditworthiness_penalty');
|
|
||||||
await queryInterface.removeColumn(table, 'reason');
|
|
||||||
await queryInterface.removeColumn(table, 'days_overdue');
|
|
||||||
await queryInterface.removeColumn(table, 'remaining_debt');
|
|
||||||
await queryInterface.removeColumn(table, 'debt_at_entry');
|
|
||||||
await queryInterface.removeColumn(table, 'released_at');
|
|
||||||
await queryInterface.removeColumn(table, 'entered_at');
|
|
||||||
await queryInterface.removeColumn(table, 'status');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.transport
|
|
||||||
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.underground
|
|
||||||
ALTER COLUMN victim_id DROP NOT NULL;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.transport
|
|
||||||
DROP COLUMN IF EXISTS guard_count;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
ADD COLUMN IF NOT EXISTS learning_goals JSONB,
|
|
||||||
ADD COLUMN IF NOT EXISTS core_patterns JSONB,
|
|
||||||
ADD COLUMN IF NOT EXISTS grammar_focus JSONB,
|
|
||||||
ADD COLUMN IF NOT EXISTS speaking_prompts JSONB,
|
|
||||||
ADD COLUMN IF NOT EXISTS practical_tasks JSONB;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
|
||||||
('dialog_completion', 'Dialogergänzung'),
|
|
||||||
('situational_response', 'Situative Antwort'),
|
|
||||||
('pattern_drill', 'Muster-Drill'),
|
|
||||||
('reading_aloud', 'Lautlese-Übung'),
|
|
||||||
('speaking_from_memory', 'Freies Sprechen')
|
|
||||||
ON CONFLICT (name) DO NOTHING;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
DROP COLUMN IF EXISTS practical_tasks,
|
|
||||||
DROP COLUMN IF EXISTS speaking_prompts,
|
|
||||||
DROP COLUMN IF EXISTS grammar_focus,
|
|
||||||
DROP COLUMN IF EXISTS core_patterns,
|
|
||||||
DROP COLUMN IF EXISTS learning_goals;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.user_param
|
|
||||||
ALTER COLUMN value TYPE TEXT;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.user_param
|
|
||||||
ALTER COLUMN value TYPE VARCHAR(255);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'community', tableName: 'folder' },
|
|
||||||
'is_adult_area',
|
|
||||||
{
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'community', tableName: 'image' },
|
|
||||||
'is_adult_content',
|
|
||||||
{
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.removeColumn({ schema: 'community', tableName: 'image' }, 'is_adult_content');
|
|
||||||
await queryInterface.removeColumn({ schema: 'community', tableName: 'folder' }, 'is_adult_area');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.createTable(
|
|
||||||
{ schema: 'community', tableName: 'erotic_video' },
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
original_file_name: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
hash: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
mime_type: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
user_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: { schema: 'community', tableName: 'user' },
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video' });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'chat', tableName: 'room' },
|
|
||||||
'is_adult_only',
|
|
||||||
{
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.removeColumn({ schema: 'chat', tableName: 'room' }, 'is_adult_only');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'community', tableName: 'image' },
|
|
||||||
'is_moderated_hidden',
|
|
||||||
{
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
).catch(() => {});
|
|
||||||
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
{ schema: 'community', tableName: 'erotic_video' },
|
|
||||||
'is_moderated_hidden',
|
|
||||||
{
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
).catch(() => {});
|
|
||||||
|
|
||||||
await queryInterface.createTable(
|
|
||||||
{ schema: 'community', tableName: 'erotic_content_report' },
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
reporter_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: { model: { schema: 'community', tableName: 'user' }, key: 'id' },
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
target_type: {
|
|
||||||
type: Sequelize.STRING(20),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
target_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
reason: {
|
|
||||||
type: Sequelize.STRING(80),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
note: {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: Sequelize.STRING(20),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'open'
|
|
||||||
},
|
|
||||||
action_taken: {
|
|
||||||
type: Sequelize.STRING(40),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
handled_by: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
references: { model: { schema: 'community', tableName: 'user' }, key: 'id' },
|
|
||||||
onDelete: 'SET NULL'
|
|
||||||
},
|
|
||||||
handled_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: Sequelize.literal('NOW()')
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: Sequelize.literal('NOW()')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).catch(() => {});
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_content_report' }).catch(() => {});
|
|
||||||
await queryInterface.removeColumn({ schema: 'community', tableName: 'erotic_video' }, 'is_moderated_hidden').catch(() => {});
|
|
||||||
await queryInterface.removeColumn({ schema: 'community', tableName: 'image' }, 'is_moderated_hidden').catch(() => {});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.createTable(
|
|
||||||
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
erotic_video_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: { schema: 'community', tableName: 'erotic_video' },
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
},
|
|
||||||
visibility_type_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: { schema: 'type', tableName: 'image_visibility' },
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.createTable(
|
|
||||||
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
erotic_video_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: { schema: 'community', tableName: 'erotic_video' },
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
},
|
|
||||||
user_id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: { schema: 'community', tableName: 'user' },
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
|
|
||||||
SELECT ev.id, iv.id
|
|
||||||
FROM community.erotic_video ev
|
|
||||||
CROSS JOIN type.image_visibility iv
|
|
||||||
WHERE iv.description = 'adults'
|
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM community.erotic_video_image_visibility eviv
|
|
||||||
WHERE eviv.erotic_video_id = ev.id
|
|
||||||
AND eviv.visibility_type_id = iv.id
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
|
|
||||||
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_data."character"
|
|
||||||
ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_data."character"
|
|
||||||
ADD COLUMN pregnancy_father_character_id INTEGER NULL
|
|
||||||
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;
|
|
||||||
END IF;
|
|
||||||
END$$;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id;
|
|
||||||
`);
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
ADD COLUMN IF NOT EXISTS didactic_mode TEXT,
|
|
||||||
ADD COLUMN IF NOT EXISTS phase_label TEXT,
|
|
||||||
ADD COLUMN IF NOT EXISTS block_number INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS difficulty_weight INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS new_unit_target INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS review_weight INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS is_intensive_review BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.didactic_mode IS
|
|
||||||
'Didaktischer Modus der Lektion, z.B. core_input, guided_dialogue, intensive_review oder checkpoint.';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.phase_label IS
|
|
||||||
'Übergeordnete Lernphase, z.B. quickstart, daily_life oder stabilization.';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.block_number IS
|
|
||||||
'Inhaltlicher Block für Konsolidierungs- und Wiederholungswellen.';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.difficulty_weight IS
|
|
||||||
'Grobe relative Schwierigkeit der Lektion von leicht bis schwer.';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.new_unit_target IS
|
|
||||||
'Empfohlene Zahl neuer Spracheinheiten in dieser Lektion.';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.review_weight IS
|
|
||||||
'Wie stark Wiederholung in dieser Lektion dominieren soll, typischerweise 0 bis 100.';
|
|
||||||
COMMENT ON COLUMN community.vocab_course_lesson.is_intensive_review IS
|
|
||||||
'Markiert Lektionen, die als intensive Wiederholungsphase gedacht sind.';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_lesson
|
|
||||||
DROP COLUMN IF EXISTS is_intensive_review,
|
|
||||||
DROP COLUMN IF EXISTS review_weight,
|
|
||||||
DROP COLUMN IF EXISTS new_unit_target,
|
|
||||||
DROP COLUMN IF EXISTS difficulty_weight,
|
|
||||||
DROP COLUMN IF EXISTS block_number,
|
|
||||||
DROP COLUMN IF EXISTS phase_label,
|
|
||||||
DROP COLUMN IF EXISTS didactic_mode;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_progress
|
|
||||||
ADD COLUMN IF NOT EXISTS lesson_state JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN community.vocab_course_progress.lesson_state IS
|
|
||||||
'Persistierter UI- und Lernzustand pro Nutzer und Lektion fuer Resume im Sprachkurs.';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE community.vocab_course_progress
|
|
||||||
DROP COLUMN IF EXISTS lesson_state;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.falukant_user
|
|
||||||
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_predefine'
|
|
||||||
AND table_name = 'political_office_benefit'
|
|
||||||
AND column_name = 'political_office_id'
|
|
||||||
) AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_predefine'
|
|
||||||
AND table_name = 'political_office_benefit'
|
|
||||||
AND column_name = 'office_type_id'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_predefine.political_office_benefit
|
|
||||||
RENAME COLUMN political_office_id TO office_type_id;
|
|
||||||
ELSIF EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_predefine'
|
|
||||||
AND table_name = 'political_office_benefit'
|
|
||||||
AND column_name = 'political_office_id'
|
|
||||||
) AND EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_predefine'
|
|
||||||
AND table_name = 'political_office_benefit'
|
|
||||||
AND column_name = 'office_type_id'
|
|
||||||
) THEN
|
|
||||||
UPDATE falukant_predefine.political_office_benefit
|
|
||||||
SET office_type_id = COALESCE(office_type_id, political_office_id);
|
|
||||||
ALTER TABLE falukant_predefine.political_office_benefit
|
|
||||||
DROP COLUMN political_office_id;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.falukant_user
|
|
||||||
DROP COLUMN IF EXISTS last_political_daily_salary_on;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_predefine'
|
|
||||||
AND table_name = 'political_office_benefit'
|
|
||||||
AND column_name = 'office_type_id'
|
|
||||||
) AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'falukant_predefine'
|
|
||||||
AND table_name = 'political_office_benefit'
|
|
||||||
AND column_name = 'political_office_id'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_predefine.political_office_benefit
|
|
||||||
RENAME COLUMN office_type_id TO political_office_id;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO type.user_param_value (user_param_type_id, value, order_id)
|
|
||||||
SELECT upt.id, 'fr', COALESCE(
|
|
||||||
(SELECT MAX(v.order_id) FROM type.user_param_value v WHERE v.user_param_type_id = upt.id),
|
|
||||||
0
|
|
||||||
) + 1
|
|
||||||
FROM type.user_param upt
|
|
||||||
WHERE upt.description = 'language'
|
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM type.user_param_value x
|
|
||||||
WHERE x.user_param_type_id = upt.id AND x.value = 'fr'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DELETE FROM type.user_param_value v
|
|
||||||
USING type.user_param upt
|
|
||||||
WHERE v.user_param_type_id = upt.id
|
|
||||||
AND upt.description = 'language'
|
|
||||||
AND v.value = 'fr';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.falukant_user
|
|
||||||
ADD COLUMN IF NOT EXISTS certificate_productions_count_since TIMESTAMPTZ;
|
|
||||||
`);
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
COMMENT ON COLUMN falukant_data.falukant_user.certificate_productions_count_since IS
|
|
||||||
'Daemon/UI: Zählt nur falukant_log.production-Zeilen mit COALESCE(production_timestamp, production_date::timestamp) >= diesem Wert; bei Stufenänderung (Aufstieg/Bankrott/Erbfolge) auf NOW() (YpDaemon QUERY_UPDATE_FALUKANT_USER_CERTIFICATE). NULL = alle passenden Log-Zeilen bis zur ersten Stufenänderung nach Migration. Kein Löschen der Logs zum Reset.';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.falukant_user
|
|
||||||
DROP COLUMN IF EXISTS certificate_productions_count_since;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @param {import('sequelize').QueryInterface} queryInterface */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS falukant_data.political_benefit_last_tick (
|
|
||||||
id serial PRIMARY KEY,
|
|
||||||
character_id integer NOT NULL
|
|
||||||
REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
|
||||||
political_office_benefit_id integer NOT NULL
|
|
||||||
REFERENCES falukant_predefine.political_office_benefit(id) ON DELETE CASCADE,
|
|
||||||
last_tick_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ticks_count integer NOT NULL DEFAULT 0,
|
|
||||||
CONSTRAINT political_benefit_last_tick_unique UNIQUE (character_id, political_office_benefit_id)
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS political_benefit_last_tick_character_idx
|
|
||||||
ON falukant_data.political_benefit_last_tick (character_id);
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS falukant_data.region_tax_history (
|
|
||||||
id serial PRIMARY KEY,
|
|
||||||
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
|
|
||||||
old_tax_percent numeric(12,4) NOT NULL,
|
|
||||||
new_tax_percent numeric(12,4) NOT NULL,
|
|
||||||
setter_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
|
||||||
political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL,
|
|
||||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS region_tax_history_region_idx
|
|
||||||
ON falukant_data.region_tax_history (region_id, created_at DESC);
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS falukant_data.political_appointment (
|
|
||||||
id serial PRIMARY KEY,
|
|
||||||
appointer_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
|
||||||
target_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
|
||||||
office_type_id integer NOT NULL REFERENCES falukant_type.political_office_type(id) ON DELETE CASCADE,
|
|
||||||
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
|
|
||||||
status varchar(32) NOT NULL DEFAULT 'completed',
|
|
||||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
expires_at timestamptz NULL,
|
|
||||||
completed_political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS political_appointment_appointer_idx
|
|
||||||
ON falukant_data.political_appointment (appointer_character_id, created_at DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS political_appointment_target_idx
|
|
||||||
ON falukant_data.political_appointment (target_character_id);
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TABLE IF EXISTS falukant_data.political_appointment;
|
|
||||||
DROP TABLE IF EXISTS falukant_data.region_tax_history;
|
|
||||||
DROP TABLE IF EXISTS falukant_data.political_benefit_last_tick;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** Stufe pro politischem Amt (Tageshonorar: base + perRank × hierarchy_level). */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_type.political_office_type
|
|
||||||
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
UPDATE falukant_type.political_office_type AS pot
|
|
||||||
SET hierarchy_level = sub.lvl
|
|
||||||
FROM (VALUES
|
|
||||||
('assessor', 1),
|
|
||||||
('councillor', 1),
|
|
||||||
('council', 2),
|
|
||||||
('beadle', 2),
|
|
||||||
('town-clerk', 2),
|
|
||||||
('mayor', 3),
|
|
||||||
('master-builder', 2),
|
|
||||||
('village-major', 2),
|
|
||||||
('judge', 3),
|
|
||||||
('bailif', 3),
|
|
||||||
('taxman', 2),
|
|
||||||
('sheriff', 3),
|
|
||||||
('consultant', 3),
|
|
||||||
('treasurer', 4),
|
|
||||||
('hangman', 2),
|
|
||||||
('territorial-council', 3),
|
|
||||||
('territorial-council-speaker', 4),
|
|
||||||
('ruler-consultant', 4),
|
|
||||||
('state-administrator', 4),
|
|
||||||
('super-state-administrator', 5),
|
|
||||||
('governor', 5),
|
|
||||||
('ministry-helper', 4),
|
|
||||||
('minister', 5),
|
|
||||||
('chancellor', 6)
|
|
||||||
) AS sub(name, lvl)
|
|
||||||
WHERE pot.name = sub.name;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_type.political_office_type
|
|
||||||
DROP COLUMN IF EXISTS hierarchy_level;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
ADD COLUMN IF NOT EXISTS scandal_extra_daily_pct double precision NOT NULL DEFAULT 0;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
ADD CONSTRAINT relationship_state_scandal_extra_daily_pct_chk
|
|
||||||
CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100);
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
DROP COLUMN IF EXISTS scandal_extra_daily_pct;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
ADD COLUMN IF NOT EXISTS marriage_satisfaction integer;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
UPDATE falukant_data.relationship_state
|
|
||||||
SET marriage_satisfaction = 55
|
|
||||||
WHERE marriage_satisfaction IS NULL;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
ALTER COLUMN marriage_satisfaction SET DEFAULT 55;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
ALTER COLUMN marriage_satisfaction SET NOT NULL;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_constraint
|
|
||||||
WHERE conname = 'relationship_state_marriage_satisfaction_check'
|
|
||||||
AND connamespace = 'falukant_data'::regnamespace
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
ADD CONSTRAINT relationship_state_marriage_satisfaction_check
|
|
||||||
CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100);
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
DROP CONSTRAINT IF EXISTS relationship_state_marriage_satisfaction_check;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE falukant_data.relationship_state
|
|
||||||
DROP COLUMN IF EXISTS marriage_satisfaction;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS community.vocab_srs_item (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES community."user"(id) ON DELETE CASCADE,
|
|
||||||
course_id INTEGER NOT NULL REFERENCES community.vocab_course(id) ON DELETE CASCADE,
|
|
||||||
lesson_id INTEGER NULL REFERENCES community.vocab_course_lesson(id) ON DELETE SET NULL,
|
|
||||||
item_key VARCHAR(80) NOT NULL,
|
|
||||||
learning TEXT NOT NULL,
|
|
||||||
reference TEXT NOT NULL,
|
|
||||||
direction VARCHAR(8) NOT NULL DEFAULT 'BOTH',
|
|
||||||
stage INTEGER NOT NULL DEFAULT 0,
|
|
||||||
interval_days INTEGER NOT NULL DEFAULT 0,
|
|
||||||
last_reviewed_at TIMESTAMP WITH TIME ZONE NULL,
|
|
||||||
next_due_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
correct_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
wrong_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
lapse_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT vocab_srs_item_user_key_unique UNIQUE (user_id, item_key)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_due
|
|
||||||
ON community.vocab_srs_item (user_id, course_id, next_due_at);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_lesson
|
|
||||||
ON community.vocab_srs_item (user_id, course_id, lesson_id);
|
|
||||||
|
|
||||||
COMMENT ON TABLE community.vocab_srs_item IS
|
|
||||||
'Nutzerbezogener SRS-Fortschritt pro Vokabel/Phrase aus Sprachkursen.';
|
|
||||||
COMMENT ON COLUMN community.vocab_srs_item.item_key IS
|
|
||||||
'Stabiler deterministischer Schlüssel aus Kurs, Lektion und normalisiertem Begriffspaar.';
|
|
||||||
COMMENT ON COLUMN community.vocab_srs_item.stage IS
|
|
||||||
'SRS-Stufe. Höhere Stufen bedeuten längere Wiederholungsintervalle.';
|
|
||||||
COMMENT ON COLUMN community.vocab_srs_item.next_due_at IS
|
|
||||||
'Zeitpunkt, zu dem das Item wieder fällig ist.';
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TABLE IF EXISTS community.vocab_srs_item;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn({
|
||||||
|
tableName: 'contact_message',
|
||||||
|
schema: 'service'
|
||||||
|
}, 'answer', {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn({
|
||||||
|
tableName: 'contact_message',
|
||||||
|
schema: 'service'
|
||||||
|
}, 'answered_at', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn({
|
||||||
|
tableName: 'contact_message',
|
||||||
|
schema: 'service'
|
||||||
|
}, 'is_answered', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn({
|
||||||
|
tableName: 'contact_message',
|
||||||
|
schema: 'service'
|
||||||
|
}, 'answer');
|
||||||
|
await queryInterface.removeColumn({
|
||||||
|
tableName: 'contact_message',
|
||||||
|
schema: 'service'
|
||||||
|
}, 'answered_at');
|
||||||
|
await queryInterface.removeColumn({
|
||||||
|
tableName: 'contact_message',
|
||||||
|
schema: 'service'
|
||||||
|
}, 'is_answered');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- Rollback: Remove indexes for director proposals and character queries
|
|
||||||
-- Created: 2026-01-12
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_character_region_user_created;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_character_region_user;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_character_user_id;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_director_proposal_employer_character;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_director_character_id;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_director_employer_user_id;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_knowledge_character_id;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_relationship_character1_id;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_child_relation_father_id;
|
|
||||||
DROP INDEX IF EXISTS falukant_data.idx_child_relation_mother_id;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
-- Migration: Add indexes for director proposals and character queries
|
|
||||||
-- Created: 2026-01-12
|
|
||||||
|
|
||||||
-- Index für schnelle Suche nach NPCs in einer Region (mit Altersbeschränkung)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_character_region_user_created
|
|
||||||
ON falukant_data.character (region_id, user_id, created_at)
|
|
||||||
WHERE user_id IS NULL;
|
|
||||||
|
|
||||||
-- Index für schnelle Suche nach NPCs ohne Altersbeschränkung
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_character_region_user
|
|
||||||
ON falukant_data.character (region_id, user_id)
|
|
||||||
WHERE user_id IS NULL;
|
|
||||||
|
|
||||||
-- Index für Character-Suche nach user_id (wichtig für getFamily, getDirectorForBranch)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_character_user_id
|
|
||||||
ON falukant_data.character (user_id);
|
|
||||||
|
|
||||||
-- Index für Director-Proposals
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_director_proposal_employer_character
|
|
||||||
ON falukant_data.director_proposal (employer_user_id, director_character_id);
|
|
||||||
|
|
||||||
-- Index für aktive Direktoren
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_director_character_id
|
|
||||||
ON falukant_data.director (director_character_id);
|
|
||||||
|
|
||||||
-- Index für Director-Suche nach employer_user_id
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_director_employer_user_id
|
|
||||||
ON falukant_data.director (employer_user_id);
|
|
||||||
|
|
||||||
-- Index für Knowledge-Berechnung
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_knowledge_character_id
|
|
||||||
ON falukant_data.knowledge (character_id);
|
|
||||||
|
|
||||||
-- Index für Relationships (getFamily)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_relationship_character1_id
|
|
||||||
ON falukant_data.relationship (character1_id);
|
|
||||||
|
|
||||||
-- Index für ChildRelations (getFamily)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_child_relation_father_id
|
|
||||||
ON falukant_data.child_relation (father_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_child_relation_mother_id
|
|
||||||
ON falukant_data.child_relation (mother_id);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Backend-Migrationen (Sequelize)
|
|
||||||
|
|
||||||
## Aktive Migrationen
|
|
||||||
|
|
||||||
Neue Schema-Änderungen: nur noch Dateien unter **`migrations-active/`**. Ausführung z. B.:
|
|
||||||
|
|
||||||
`npm run db:migrate` (siehe `backend/package.json`, Pfad `migrations-active`).
|
|
||||||
|
|
||||||
## Archiv
|
|
||||||
|
|
||||||
Ältere, bereits auf den Umgebungen ausgerollte Migrationen liegen unter **`migrations-archive/`** und werden vom Sequelize-CLI **nicht** mehr ausgeführt.
|
|
||||||
|
|
||||||
Vor dem ersten Deploy nach dieser Aufteilung: fehlende Einträge in `"SequelizeMeta"` für die archivierten Dateinamen per SQL ergänzen, siehe **`sql/baseline-sequelize-meta-migrations-archive.sql`** (idempotent).
|
|
||||||
|
|
||||||
## Sonstiges in diesem Ordner
|
|
||||||
|
|
||||||
SQL-Hilfsdateien und ältere Notizen verbleiben hier (werden nicht vom CLI geladen).
|
|
||||||
|
|
||||||
## Falukant: Zertifikat und Produktionszählung
|
|
||||||
|
|
||||||
| Datei (Archiv) | Inhalt |
|
|
||||||
|--------|--------|
|
|
||||||
| `migrations-archive/20260402140000-add-certificate-productions-count-since.cjs` | Spalte `falukant_data.falukant_user.certificate_productions_count_since` (`TIMESTAMPTZ`, nullable) inkl. Kommentar. Setzt die DB-Grundlage dafür, dass Daemon, Backend und UI dieselbe Periode für „abgeschlossene Produktionen“ nutzen (Filter mit `COALESCE(production_timestamp, production_date::timestamp)` ab diesem Zeitpunkt; `NULL` = bisherige Historie). |
|
|
||||||
|
|
||||||
Eine parallele SQL-Migration im Daemon-Repository (z. B. `014_falukant_certificate_productions_count_since.sql`) kann dieselbe Spalte anlegen, wenn das Deployment dort getrennt ist – Schema doppelt anlegen vermeiden.
|
|
||||||
|
|
||||||
Details zur Zähl- und Retention-Logik: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md`.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
ALTER TABLE chat.room
|
|
||||||
ADD COLUMN IF NOT EXISTS gender_restriction_id INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS min_age INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS max_age INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS password VARCHAR(255),
|
|
||||||
ADD COLUMN IF NOT EXISTS friends_of_owner_only BOOLEAN DEFAULT FALSE,
|
|
||||||
ADD COLUMN IF NOT EXISTS required_user_right_id INTEGER;
|
|
||||||
|
|
||||||
UPDATE chat.room
|
|
||||||
SET friends_of_owner_only = FALSE
|
|
||||||
WHERE friends_of_owner_only IS NULL;
|
|
||||||
|
|
||||||
ALTER TABLE chat.room
|
|
||||||
ALTER COLUMN friends_of_owner_only SET DEFAULT FALSE,
|
|
||||||
ALTER COLUMN friends_of_owner_only SET NOT NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -5,7 +5,6 @@ import ChatUser from './chat/user.js';
|
|||||||
import Room from './chat/room.js';
|
import Room from './chat/room.js';
|
||||||
import User from './community/user.js';
|
import User from './community/user.js';
|
||||||
import UserParam from './community/user_param.js';
|
import UserParam from './community/user_param.js';
|
||||||
import UserDashboard from './community/user_dashboard.js';
|
|
||||||
import UserParamType from './type/user_param.js';
|
import UserParamType from './type/user_param.js';
|
||||||
import UserRightType from './type/user_right.js';
|
import UserRightType from './type/user_right.js';
|
||||||
import UserRight from './community/user_right.js';
|
import UserRight from './community/user_right.js';
|
||||||
@@ -18,15 +17,11 @@ import UserParamVisibilityType from './type/user_param_visibility.js';
|
|||||||
import UserParamVisibility from './community/user_param_visibility.js';
|
import UserParamVisibility from './community/user_param_visibility.js';
|
||||||
import Folder from './community/folder.js';
|
import Folder from './community/folder.js';
|
||||||
import Image from './community/image.js';
|
import Image from './community/image.js';
|
||||||
import EroticVideo from './community/erotic_video.js';
|
|
||||||
import EroticContentReport from './community/erotic_content_report.js';
|
|
||||||
import ImageVisibilityType from './type/image_visibility.js';
|
import ImageVisibilityType from './type/image_visibility.js';
|
||||||
import ImageVisibilityUser from './community/image_visibility_user.js';
|
import ImageVisibilityUser from './community/image_visibility_user.js';
|
||||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||||
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
|
|
||||||
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
|
|
||||||
import GuestbookEntry from './community/guestbook.js';
|
import GuestbookEntry from './community/guestbook.js';
|
||||||
import Forum from './forum/forum.js';
|
import Forum from './forum/forum.js';
|
||||||
import Title from './forum/title.js';
|
import Title from './forum/title.js';
|
||||||
@@ -49,7 +44,6 @@ import FalukantStockType from './falukant/type/stock.js';
|
|||||||
import Knowledge from './falukant/data/product_knowledge.js';
|
import Knowledge from './falukant/data/product_knowledge.js';
|
||||||
import ProductType from './falukant/type/product.js';
|
import ProductType from './falukant/type/product.js';
|
||||||
import TitleOfNobility from './falukant/type/title_of_nobility.js';
|
import TitleOfNobility from './falukant/type/title_of_nobility.js';
|
||||||
import TitleBenefit from './falukant/type/title_benefit.js';
|
|
||||||
import TitleRequirement from './falukant/type/title_requirement.js';
|
import TitleRequirement from './falukant/type/title_requirement.js';
|
||||||
import Branch from './falukant/data/branch.js';
|
import Branch from './falukant/data/branch.js';
|
||||||
import BranchType from './falukant/type/branch.js';
|
import BranchType from './falukant/type/branch.js';
|
||||||
@@ -72,7 +66,6 @@ import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift
|
|||||||
import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js';
|
import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js';
|
||||||
import RelationshipType from './falukant/type/relationship.js';
|
import RelationshipType from './falukant/type/relationship.js';
|
||||||
import Relationship from './falukant/data/relationship.js';
|
import Relationship from './falukant/data/relationship.js';
|
||||||
import RelationshipState from './falukant/data/relationship_state.js';
|
|
||||||
import PromotionalGiftLog from './falukant/log/promotional_gift.js';
|
import PromotionalGiftLog from './falukant/log/promotional_gift.js';
|
||||||
import HouseType from './falukant/type/house.js';
|
import HouseType from './falukant/type/house.js';
|
||||||
import BuyableHouse from './falukant/data/buyable_house.js';
|
import BuyableHouse from './falukant/data/buyable_house.js';
|
||||||
@@ -94,19 +87,12 @@ import Candidate from './falukant/data/candidate.js';
|
|||||||
import Vote from './falukant/data/vote.js';
|
import Vote from './falukant/data/vote.js';
|
||||||
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
||||||
import PoliticalOffice from './falukant/data/political_office.js';
|
import PoliticalOffice from './falukant/data/political_office.js';
|
||||||
import PoliticalBenefitLastTick from './falukant/data/political_benefit_last_tick.js';
|
|
||||||
import RegionTaxHistory from './falukant/data/region_tax_history.js';
|
|
||||||
import PoliticalAppointment from './falukant/data/political_appointment.js';
|
|
||||||
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
||||||
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
||||||
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
||||||
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
||||||
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||||
import ElectionHistory from './falukant/log/election_history.js';
|
import ElectionHistory from './falukant/log/election_history.js';
|
||||||
import ChurchOfficeType from './falukant/type/church_office_type.js';
|
|
||||||
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
|
|
||||||
import ChurchOffice from './falukant/data/church_office.js';
|
|
||||||
import ChurchApplication from './falukant/data/church_application.js';
|
|
||||||
import Underground from './falukant/data/underground.js';
|
import Underground from './falukant/data/underground.js';
|
||||||
import UndergroundType from './falukant/type/underground.js';
|
import UndergroundType from './falukant/type/underground.js';
|
||||||
import VehicleType from './falukant/type/vehicle.js';
|
import VehicleType from './falukant/type/vehicle.js';
|
||||||
@@ -116,18 +102,8 @@ import RegionDistance from './falukant/data/region_distance.js';
|
|||||||
import WeatherType from './falukant/type/weather.js';
|
import WeatherType from './falukant/type/weather.js';
|
||||||
import Weather from './falukant/data/weather.js';
|
import Weather from './falukant/data/weather.js';
|
||||||
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
|
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
|
||||||
import ProductPriceHistory from './falukant/log/product_price_history.js';
|
|
||||||
import Blog from './community/blog.js';
|
import Blog from './community/blog.js';
|
||||||
import BlogPost from './community/blog_post.js';
|
import BlogPost from './community/blog_post.js';
|
||||||
import VocabCourse from './community/vocab_course.js';
|
|
||||||
import VocabCourseLesson from './community/vocab_course_lesson.js';
|
|
||||||
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
|
|
||||||
import VocabCourseProgress from './community/vocab_course_progress.js';
|
|
||||||
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
|
||||||
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
|
||||||
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
|
||||||
import VocabSrsItem from './community/vocab_srs_item.js';
|
|
||||||
import CalendarEvent from './community/calendar_event.js';
|
|
||||||
import Campaign from './match3/campaign.js';
|
import Campaign from './match3/campaign.js';
|
||||||
import Match3Level from './match3/level.js';
|
import Match3Level from './match3/level.js';
|
||||||
import Objective from './match3/objective.js';
|
import Objective from './match3/objective.js';
|
||||||
@@ -179,9 +155,6 @@ export default function setupAssociations() {
|
|||||||
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
|
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
|
||||||
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
|
||||||
User.hasOne(UserDashboard, { foreignKey: 'userId', as: 'dashboard' });
|
|
||||||
UserDashboard.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
|
||||||
|
|
||||||
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
|
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
|
||||||
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
|
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
|
||||||
|
|
||||||
@@ -217,14 +190,6 @@ export default function setupAssociations() {
|
|||||||
Image.belongsTo(User, { foreignKey: 'userId' });
|
Image.belongsTo(User, { foreignKey: 'userId' });
|
||||||
User.hasMany(Image, { foreignKey: 'userId' });
|
User.hasMany(Image, { foreignKey: 'userId' });
|
||||||
|
|
||||||
EroticVideo.belongsTo(User, { foreignKey: 'userId', as: 'owner' });
|
|
||||||
User.hasMany(EroticVideo, { foreignKey: 'userId', as: 'eroticVideos' });
|
|
||||||
|
|
||||||
EroticContentReport.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter' });
|
|
||||||
User.hasMany(EroticContentReport, { foreignKey: 'reporterId', as: 'eroticContentReports' });
|
|
||||||
EroticContentReport.belongsTo(User, { foreignKey: 'handledBy', as: 'moderator' });
|
|
||||||
User.hasMany(EroticContentReport, { foreignKey: 'handledBy', as: 'handledEroticContentReports' });
|
|
||||||
|
|
||||||
// Image visibility associations
|
// Image visibility associations
|
||||||
Folder.belongsToMany(ImageVisibilityType, {
|
Folder.belongsToMany(ImageVisibilityType, {
|
||||||
through: FolderImageVisibility,
|
through: FolderImageVisibility,
|
||||||
@@ -248,17 +213,6 @@ export default function setupAssociations() {
|
|||||||
otherKey: 'imageId'
|
otherKey: 'imageId'
|
||||||
});
|
});
|
||||||
|
|
||||||
EroticVideo.belongsToMany(ImageVisibilityType, {
|
|
||||||
through: EroticVideoImageVisibility,
|
|
||||||
foreignKey: 'eroticVideoId',
|
|
||||||
otherKey: 'visibilityTypeId'
|
|
||||||
});
|
|
||||||
ImageVisibilityType.belongsToMany(EroticVideo, {
|
|
||||||
through: EroticVideoImageVisibility,
|
|
||||||
foreignKey: 'visibilityTypeId',
|
|
||||||
otherKey: 'eroticVideoId'
|
|
||||||
});
|
|
||||||
|
|
||||||
Folder.belongsToMany(ImageVisibilityUser, {
|
Folder.belongsToMany(ImageVisibilityUser, {
|
||||||
through: FolderVisibilityUser,
|
through: FolderVisibilityUser,
|
||||||
foreignKey: 'folderId',
|
foreignKey: 'folderId',
|
||||||
@@ -270,19 +224,6 @@ export default function setupAssociations() {
|
|||||||
otherKey: 'folderId'
|
otherKey: 'folderId'
|
||||||
});
|
});
|
||||||
|
|
||||||
EroticVideo.belongsToMany(User, {
|
|
||||||
through: EroticVideoVisibilityUser,
|
|
||||||
foreignKey: 'eroticVideoId',
|
|
||||||
otherKey: 'userId',
|
|
||||||
as: 'selectedVisibilityUsers'
|
|
||||||
});
|
|
||||||
User.belongsToMany(EroticVideo, {
|
|
||||||
through: EroticVideoVisibilityUser,
|
|
||||||
foreignKey: 'userId',
|
|
||||||
otherKey: 'eroticVideoId',
|
|
||||||
as: 'visibleEroticVideos'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Guestbook related associations
|
// Guestbook related associations
|
||||||
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
|
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
|
||||||
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
|
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
|
||||||
@@ -383,8 +324,6 @@ export default function setupAssociations() {
|
|||||||
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
|
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
|
||||||
|
|
||||||
FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' });
|
|
||||||
|
|
||||||
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
|
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
|
||||||
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
|
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
|
||||||
|
|
||||||
@@ -396,8 +335,6 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
||||||
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
|
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
|
||||||
TitleOfNobility.hasMany(TitleBenefit, { foreignKey: 'titleId', as: 'benefits' });
|
|
||||||
TitleBenefit.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
|
||||||
|
|
||||||
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
|
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
|
||||||
@@ -468,13 +405,6 @@ export default function setupAssociations() {
|
|||||||
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
|
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
|
||||||
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
|
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
|
||||||
|
|
||||||
// Produkt-Preishistorie (Zeitreihe für Preiskurven)
|
|
||||||
ProductPriceHistory.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
|
|
||||||
ProductType.hasMany(ProductPriceHistory, { foreignKey: 'productId', as: 'priceHistory' });
|
|
||||||
|
|
||||||
ProductPriceHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
RegionData.hasMany(ProductPriceHistory, { foreignKey: 'regionId', as: 'productPriceHistory' });
|
|
||||||
|
|
||||||
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
|
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
|
||||||
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
|
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
|
||||||
|
|
||||||
@@ -503,8 +433,6 @@ export default function setupAssociations() {
|
|||||||
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
|
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
|
||||||
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
|
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
|
||||||
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
|
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
|
||||||
Relationship.hasOne(RelationshipState, { foreignKey: 'relationshipId', as: 'state' });
|
|
||||||
RelationshipState.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'relationship' });
|
|
||||||
|
|
||||||
PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
|
PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
|
||||||
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
|
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
|
||||||
@@ -630,14 +558,14 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
Party.belongsToMany(TitleOfNobility, {
|
Party.belongsToMany(TitleOfNobility, {
|
||||||
through: PartyInvitedNobility,
|
through: PartyInvitedNobility,
|
||||||
foreignKey: 'partyId',
|
foreignKey: 'party_id',
|
||||||
otherKey: 'titleOfNobilityId',
|
otherKey: 'title_of_nobility_id',
|
||||||
as: 'invitedNobilities',
|
as: 'invitedNobilities',
|
||||||
});
|
});
|
||||||
TitleOfNobility.belongsToMany(Party, {
|
TitleOfNobility.belongsToMany(Party, {
|
||||||
through: PartyInvitedNobility,
|
through: PartyInvitedNobility,
|
||||||
foreignKey: 'titleOfNobilityId',
|
foreignKey: 'title_of_nobility_id',
|
||||||
otherKey: 'partyId',
|
otherKey: 'party_id',
|
||||||
as: 'partiesInvitedTo',
|
as: 'partiesInvitedTo',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -796,48 +724,6 @@ export default function setupAssociations() {
|
|||||||
as: 'heldOffice'
|
as: 'heldOffice'
|
||||||
});
|
});
|
||||||
|
|
||||||
PoliticalBenefitLastTick.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'characterId',
|
|
||||||
as: 'character'
|
|
||||||
});
|
|
||||||
FalukantCharacter.hasMany(PoliticalBenefitLastTick, {
|
|
||||||
foreignKey: 'characterId',
|
|
||||||
as: 'politicalBenefitTicks'
|
|
||||||
});
|
|
||||||
PoliticalBenefitLastTick.belongsTo(PoliticalOfficeBenefit, {
|
|
||||||
foreignKey: 'politicalOfficeBenefitId',
|
|
||||||
as: 'officeBenefit'
|
|
||||||
});
|
|
||||||
|
|
||||||
RegionTaxHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
RegionData.hasMany(RegionTaxHistory, { foreignKey: 'regionId', as: 'taxHistory' });
|
|
||||||
RegionTaxHistory.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'setterCharacterId',
|
|
||||||
as: 'setterCharacter'
|
|
||||||
});
|
|
||||||
RegionTaxHistory.belongsTo(PoliticalOffice, {
|
|
||||||
foreignKey: 'politicalOfficeId',
|
|
||||||
as: 'sourceOffice'
|
|
||||||
});
|
|
||||||
|
|
||||||
PoliticalAppointment.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'appointerCharacterId',
|
|
||||||
as: 'appointer'
|
|
||||||
});
|
|
||||||
PoliticalAppointment.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'targetCharacterId',
|
|
||||||
as: 'targetCharacter'
|
|
||||||
});
|
|
||||||
PoliticalAppointment.belongsTo(PoliticalOfficeType, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'officeType'
|
|
||||||
});
|
|
||||||
PoliticalAppointment.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
PoliticalAppointment.belongsTo(PoliticalOffice, {
|
|
||||||
foreignKey: 'completedPoliticalOfficeId',
|
|
||||||
as: 'completedOffice'
|
|
||||||
});
|
|
||||||
|
|
||||||
// elections
|
// elections
|
||||||
Election.belongsTo(PoliticalOfficeType, {
|
Election.belongsTo(PoliticalOfficeType, {
|
||||||
foreignKey: 'officeTypeId',
|
foreignKey: 'officeTypeId',
|
||||||
@@ -973,96 +859,6 @@ export default function setupAssociations() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// — Church Offices —
|
|
||||||
|
|
||||||
// Requirements for church office
|
|
||||||
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'officeType'
|
|
||||||
});
|
|
||||||
ChurchOfficeType.hasMany(ChurchOfficeRequirement, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'requirements'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prerequisite office type
|
|
||||||
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
|
||||||
foreignKey: 'prerequisiteOfficeTypeId',
|
|
||||||
as: 'prerequisiteOfficeType'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Actual church office holdings
|
|
||||||
ChurchOffice.belongsTo(ChurchOfficeType, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'type'
|
|
||||||
});
|
|
||||||
ChurchOfficeType.hasMany(ChurchOffice, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'offices'
|
|
||||||
});
|
|
||||||
|
|
||||||
ChurchOffice.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'characterId',
|
|
||||||
as: 'holder'
|
|
||||||
});
|
|
||||||
FalukantCharacter.hasOne(ChurchOffice, {
|
|
||||||
foreignKey: 'characterId',
|
|
||||||
as: 'heldChurchOffice'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Supervisor relationship
|
|
||||||
ChurchOffice.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'supervisorId',
|
|
||||||
as: 'supervisor'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Region relationship
|
|
||||||
ChurchOffice.belongsTo(RegionData, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
RegionData.hasMany(ChurchOffice, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'churchOffices'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Applications for church office
|
|
||||||
ChurchApplication.belongsTo(ChurchOfficeType, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'officeType'
|
|
||||||
});
|
|
||||||
ChurchOfficeType.hasMany(ChurchApplication, {
|
|
||||||
foreignKey: 'officeTypeId',
|
|
||||||
as: 'applications'
|
|
||||||
});
|
|
||||||
|
|
||||||
ChurchApplication.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'characterId',
|
|
||||||
as: 'applicant'
|
|
||||||
});
|
|
||||||
FalukantCharacter.hasMany(ChurchApplication, {
|
|
||||||
foreignKey: 'characterId',
|
|
||||||
as: 'churchApplications'
|
|
||||||
});
|
|
||||||
|
|
||||||
ChurchApplication.belongsTo(FalukantCharacter, {
|
|
||||||
foreignKey: 'supervisorId',
|
|
||||||
as: 'supervisor'
|
|
||||||
});
|
|
||||||
FalukantCharacter.hasMany(ChurchApplication, {
|
|
||||||
foreignKey: 'supervisorId',
|
|
||||||
as: 'supervisedApplications'
|
|
||||||
});
|
|
||||||
|
|
||||||
ChurchApplication.belongsTo(RegionData, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
RegionData.hasMany(ChurchApplication, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'churchApplications'
|
|
||||||
});
|
|
||||||
|
|
||||||
Underground.belongsTo(UndergroundType, {
|
Underground.belongsTo(UndergroundType, {
|
||||||
foreignKey: 'undergroundTypeId',
|
foreignKey: 'undergroundTypeId',
|
||||||
as: 'undergroundType'
|
as: 'undergroundType'
|
||||||
@@ -1145,47 +941,5 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
|
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
|
||||||
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
|
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
|
||||||
|
|
||||||
// Vocab Course associations
|
|
||||||
VocabCourse.belongsTo(User, { foreignKey: 'ownerUserId', as: 'owner' });
|
|
||||||
User.hasMany(VocabCourse, { foreignKey: 'ownerUserId', as: 'ownedCourses' });
|
|
||||||
|
|
||||||
VocabCourseLesson.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
|
||||||
VocabCourse.hasMany(VocabCourseLesson, { foreignKey: 'courseId', as: 'lessons' });
|
|
||||||
|
|
||||||
VocabCourseEnrollment.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
|
||||||
User.hasMany(VocabCourseEnrollment, { foreignKey: 'userId', as: 'courseEnrollments' });
|
|
||||||
VocabCourseEnrollment.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
|
||||||
VocabCourse.hasMany(VocabCourseEnrollment, { foreignKey: 'courseId', as: 'enrollments' });
|
|
||||||
|
|
||||||
VocabCourseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
|
||||||
User.hasMany(VocabCourseProgress, { foreignKey: 'userId', as: 'courseProgress' });
|
|
||||||
VocabCourseProgress.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
|
||||||
VocabCourse.hasMany(VocabCourseProgress, { foreignKey: 'courseId', as: 'progress' });
|
|
||||||
VocabCourseProgress.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
|
|
||||||
VocabCourseLesson.hasMany(VocabCourseProgress, { foreignKey: 'lessonId', as: 'progress' });
|
|
||||||
|
|
||||||
// Grammar Exercise associations
|
|
||||||
VocabGrammarExercise.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
|
|
||||||
VocabCourseLesson.hasMany(VocabGrammarExercise, { foreignKey: 'lessonId', as: 'grammarExercises' });
|
|
||||||
VocabGrammarExercise.belongsTo(VocabGrammarExerciseType, { foreignKey: 'exerciseTypeId', as: 'exerciseType' });
|
|
||||||
VocabGrammarExerciseType.hasMany(VocabGrammarExercise, { foreignKey: 'exerciseTypeId', as: 'exercises' });
|
|
||||||
VocabGrammarExercise.belongsTo(User, { foreignKey: 'createdByUserId', as: 'creator' });
|
|
||||||
User.hasMany(VocabGrammarExercise, { foreignKey: 'createdByUserId', as: 'createdGrammarExercises' });
|
|
||||||
|
|
||||||
VocabGrammarExerciseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
|
||||||
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
|
|
||||||
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
|
|
||||||
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
|
|
||||||
|
|
||||||
VocabSrsItem.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
|
||||||
User.hasMany(VocabSrsItem, { foreignKey: 'userId', as: 'vocabSrsItems' });
|
|
||||||
VocabSrsItem.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
|
||||||
VocabCourse.hasMany(VocabSrsItem, { foreignKey: 'courseId', as: 'srsItems' });
|
|
||||||
VocabSrsItem.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
|
|
||||||
VocabCourseLesson.hasMany(VocabSrsItem, { foreignKey: 'lessonId', as: 'srsItems' });
|
|
||||||
|
|
||||||
// Calendar associations
|
|
||||||
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
|
||||||
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ const Room = sequelize.define('Room', {
|
|||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true},
|
defaultValue: true},
|
||||||
isAdultOnly: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false},
|
|
||||||
genderRestrictionId: {
|
genderRestrictionId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true},
|
allowNull: true},
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { Model, DataTypes } from 'sequelize';
|
|
||||||
import { sequelize } from '../../utils/sequelize.js';
|
|
||||||
|
|
||||||
class CalendarEvent extends Model { }
|
|
||||||
|
|
||||||
CalendarEvent.init({
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'user',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
categoryId: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'personal',
|
|
||||||
comment: 'Category key: personal, work, family, health, birthday, holiday, reminder, other'
|
|
||||||
},
|
|
||||||
startDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
endDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'End date for multi-day events, null means same as startDate'
|
|
||||||
},
|
|
||||||
startTime: {
|
|
||||||
type: DataTypes.TIME,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Start time, null for all-day events'
|
|
||||||
},
|
|
||||||
endTime: {
|
|
||||||
type: DataTypes.TIME,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'End time, null for all-day events'
|
|
||||||
},
|
|
||||||
allDay: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
sequelize,
|
|
||||||
modelName: 'CalendarEvent',
|
|
||||||
tableName: 'calendar_event',
|
|
||||||
schema: 'community',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['user_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['user_id', 'start_date']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['user_id', 'start_date', 'end_date']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
export default CalendarEvent;
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Model, DataTypes } from 'sequelize';
|
|
||||||
import { sequelize } from '../../utils/sequelize.js';
|
|
||||||
|
|
||||||
class EroticContentReport extends Model {}
|
|
||||||
|
|
||||||
EroticContentReport.init({
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true
|
|
||||||
},
|
|
||||||
reporterId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'reporter_id'
|
|
||||||
},
|
|
||||||
targetType: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'target_type'
|
|
||||||
},
|
|
||||||
targetId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'target_id'
|
|
||||||
},
|
|
||||||
reason: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
note: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'open'
|
|
||||||
},
|
|
||||||
actionTaken: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'action_taken'
|
|
||||||
},
|
|
||||||
handledBy: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'handled_by'
|
|
||||||
},
|
|
||||||
handledAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'handled_at'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
sequelize,
|
|
||||||
modelName: 'EroticContentReport',
|
|
||||||
tableName: 'erotic_content_report',
|
|
||||||
schema: 'community',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EroticContentReport;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Model, DataTypes } from 'sequelize';
|
|
||||||
import { sequelize } from '../../utils/sequelize.js';
|
|
||||||
|
|
||||||
class EroticVideo extends Model {}
|
|
||||||
|
|
||||||
EroticVideo.init({
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
originalFileName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'original_file_name'
|
|
||||||
},
|
|
||||||
hash: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
mimeType: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'mime_type'
|
|
||||||
},
|
|
||||||
isModeratedHidden: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_moderated_hidden'
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'user_id'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
sequelize,
|
|
||||||
modelName: 'EroticVideo',
|
|
||||||
tableName: 'erotic_video',
|
|
||||||
schema: 'community',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EroticVideo;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { sequelize } from '../../utils/sequelize.js';
|
|
||||||
import { DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
const EroticVideoImageVisibility = sequelize.define('erotic_video_image_visibility', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
autoIncrement: true,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
eroticVideoId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
visibilityTypeId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'erotic_video_image_visibility',
|
|
||||||
timestamps: false,
|
|
||||||
underscored: true,
|
|
||||||
schema: 'community'
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EroticVideoImageVisibility;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { sequelize } from '../../utils/sequelize.js';
|
|
||||||
import { DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
const EroticVideoVisibilityUser = sequelize.define('erotic_video_visibility_user', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
autoIncrement: true,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
eroticVideoId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'erotic_video_visibility_user',
|
|
||||||
timestamps: false,
|
|
||||||
underscored: true,
|
|
||||||
schema: 'community'
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EroticVideoVisibilityUser;
|
|
||||||
@@ -6,11 +6,6 @@ const Folder = sequelize.define('folder', {
|
|||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false},
|
allowNull: false},
|
||||||
isAdultArea: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
parentId: {
|
parentId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
|||||||
@@ -6,16 +6,6 @@ const Image = sequelize.define('image', {
|
|||||||
title: {
|
title: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false},
|
allowNull: false},
|
||||||
isAdultContent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
isModeratedHidden: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true},
|
allowNull: true},
|
||||||
|
|||||||
@@ -3,66 +3,6 @@ import { DataTypes } from 'sequelize';
|
|||||||
import { encrypt, decrypt } from '../../utils/encryption.js';
|
import { encrypt, decrypt } from '../../utils/encryption.js';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
function encodeEncryptedValueToBlob(value) {
|
|
||||||
const encrypted = encrypt(value);
|
|
||||||
return Buffer.from(encrypted, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Nur echte Adressen zurückgeben — verhindert Anzeige von Base64-/Key-artigem Müll bei fehlender Entschlüsselung. */
|
|
||||||
function looksLikePlausibleEmail(s) {
|
|
||||||
if (typeof s !== 'string') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const t = s.trim();
|
|
||||||
if (!t || t.length > 254) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeEmailCandidate(s) {
|
|
||||||
if (!s || typeof s !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const t = s.trim();
|
|
||||||
return looksLikePlausibleEmail(t) ? t : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeEncryptedBlob(value) {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encryptedUtf8 = value.toString('utf8');
|
|
||||||
const decryptedUtf8 = decrypt(encryptedUtf8);
|
|
||||||
const fromUtf8 = normalizeEmailCandidate(decryptedUtf8);
|
|
||||||
if (fromUtf8) {
|
|
||||||
return fromUtf8;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encryptedHex = value.toString('hex');
|
|
||||||
const decryptedHex = decrypt(encryptedHex);
|
|
||||||
const fromHex = normalizeEmailCandidate(decryptedHex);
|
|
||||||
if (fromHex) {
|
|
||||||
return fromHex;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Email legacy hex decryption failed:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return normalizeEmailCandidate(value.toString('utf8'));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Email could not be read as plain text:', error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const User = sequelize.define('user', {
|
const User = sequelize.define('user', {
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.BLOB,
|
type: DataTypes.BLOB,
|
||||||
@@ -70,12 +10,35 @@ const User = sequelize.define('user', {
|
|||||||
unique: true,
|
unique: true,
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.setDataValue('email', encodeEncryptedValueToBlob(value));
|
const encrypted = encrypt(value);
|
||||||
|
// Konvertiere Hex-String zu Buffer für die Speicherung
|
||||||
|
const buffer = Buffer.from(encrypted, 'hex');
|
||||||
|
this.setDataValue('email', buffer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
const encrypted = this.getDataValue('email');
|
const encrypted = this.getDataValue('email');
|
||||||
return decodeEncryptedBlob(encrypted);
|
if (encrypted) {
|
||||||
|
try {
|
||||||
|
// Konvertiere Buffer zu String für die Entschlüsselung
|
||||||
|
const encryptedString = encrypted.toString('hex');
|
||||||
|
const decrypted = decrypt(encryptedString);
|
||||||
|
if (decrypted) {
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Email decryption failed, treating as plain text:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Versuche es als Klartext zu lesen
|
||||||
|
try {
|
||||||
|
return encrypted.toString('utf8');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Email could not be read as plain text:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
salt: {
|
salt: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user