1088 lines
55 KiB
Markdown
1088 lines
55 KiB
Markdown
|
|
# Bamort — Detailed Project Documentation
|
|||
|
|
|
|||
|
|
> Version: 0.2.4 (backend) / 0.2.3 (frontend)
|
|||
|
|
> Generated: 2026-03-13
|
|||
|
|
> Purpose: Comprehensive architecture, component, and business-logic reference
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Table of Contents
|
|||
|
|
|
|||
|
|
1. [Project Purpose and Domain](#1-project-purpose-and-domain)
|
|||
|
|
2. [High-Level Architecture](#2-high-level-architecture)
|
|||
|
|
3. [Technology Stack](#3-technology-stack)
|
|||
|
|
4. [Backend Architecture](#4-backend-architecture)
|
|||
|
|
- [Entry Point and Startup Sequence](#41-entry-point-and-startup-sequence)
|
|||
|
|
- [Router and Middleware](#42-router-and-middleware)
|
|||
|
|
- [Authentication System](#43-authentication-system)
|
|||
|
|
- [Module Breakdown (Functional View)](#44-module-breakdown-functional-view)
|
|||
|
|
5. [Frontend Architecture](#5-frontend-architecture)
|
|||
|
|
- [Application Bootstrap](#51-application-bootstrap)
|
|||
|
|
- [Router and Navigation Guards](#52-router-and-navigation-guards)
|
|||
|
|
- [State Management (Pinia Stores)](#53-state-management-pinia-stores)
|
|||
|
|
- [Views and Components (Functional View)](#54-views-and-components-functional-view)
|
|||
|
|
- [API Communication Layer](#55-api-communication-layer)
|
|||
|
|
6. [Core Business Logic](#6-core-business-logic)
|
|||
|
|
- [Character Data Model](#61-character-data-model)
|
|||
|
|
- [Derived Values Calculation](#62-derived-values-calculation)
|
|||
|
|
- [Skill and Spell Learning System](#63-skill-and-spell-learning-system)
|
|||
|
|
- [Character Creation Wizard](#64-character-creation-wizard)
|
|||
|
|
- [PDF Export Pipeline](#65-pdf-export-pipeline)
|
|||
|
|
- [Audit Log](#66-audit-log)
|
|||
|
|
7. [Data Access and Persistence](#7-data-access-and-persistence)
|
|||
|
|
8. [Module Interaction Map](#8-module-interaction-map)
|
|||
|
|
9. [Complete API Route Reference](#9-complete-api-route-reference)
|
|||
|
|
10. [Infrastructure and Deployment](#10-infrastructure-and-deployment)
|
|||
|
|
11. [User Roles and Access Control](#11-user-roles-and-access-control)
|
|||
|
|
12. [Internationalisation (i18n)](#12-internationalisation-i18n)
|
|||
|
|
13. [Known Limitations and Security Notes](#13-known-limitations-and-security-notes)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Project Purpose and Domain
|
|||
|
|
|
|||
|
|
**Bamort** (BaMoRT = *Ba*sic Mo*R*phing *T*ool) is a web-based character management system for the **Midgard** tabletop role-playing game (M5 system). It is designed as a replacement for the older MOAM desktop application.
|
|||
|
|
|
|||
|
|
**Core user-facing features:**
|
|||
|
|
- Create, manage, and view RPG characters (stats, skills, spells, equipment)
|
|||
|
|
- Guide new users through a multi-step character creation wizard following Midgard M5 rules
|
|||
|
|
- Manage skill and spell learning progression with rule-enforced costs (EP, TE, LE, gold)
|
|||
|
|
- Track experience points (EP), practice points (PP), and wealth (gold/silver/copper)
|
|||
|
|
- Export characters as PDF character sheets via HTML templates
|
|||
|
|
- Import/export characters as JSON or CSV for interoperability with other tools
|
|||
|
|
- Share characters between users (read-only or read/write)
|
|||
|
|
- Maintain game-system master data (canonical skill, spell, and equipment tables)
|
|||
|
|
|
|||
|
|
**Business domain vocabulary:**
|
|||
|
|
| Term | Meaning |
|
|||
|
|
|------|---------|
|
|||
|
|
| Char / Character | A player or NPC character in the game |
|
|||
|
|
| Grad | Character level/grade |
|
|||
|
|
| EP | Experience points — spent to improve skills and spells |
|
|||
|
|
| TE | Training entries — attempts needed to improve a skill |
|
|||
|
|
| LE | Learning entries — prerequisite step counts for spells |
|
|||
|
|
| PP | Practice points — earned in combat/use, reduce learning costs |
|
|||
|
|
| Fertigkeit | Skill (e.g., Schwimmen, Klettern) |
|
|||
|
|
| Waffenfertigkeit | Weapon skill (subtype of Fertigkeit) |
|
|||
|
|
| Zauber | Spell (magical ability) |
|
|||
|
|
| Eigenschaft | Attribute (St=Strength, Gs=Dexterity, Gw=Agility, Ko=Constitution, In=Intelligence, Zt=Magical Talent, Au=Appearance, Wk=Willpower, Pa=Parry) |
|
|||
|
|
| Rasse | Race (Mensch, Zwerg, Elf, etc.) |
|
|||
|
|
| Typ | Character class code (Kr=Knight, Ma=Mage, Hx=Witch, etc.) |
|
|||
|
|
| Lp / Ap / B | Life Points / Action Points / Load capacity |
|
|||
|
|
| Bennies (Gg/Gp/Sg) | Luck tokens used in gameplay |
|
|||
|
|
| Vermoegen | Wealth in gold/silver/copper pieces |
|
|||
|
|
| Abwehr | Derived defense value |
|
|||
|
|
| GSMaster | Game-system master data (canonical tables for skills, spells, equipment) |
|
|||
|
|
| M5 | Midgard 5th edition rule set (the game system code used) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. High-Level Architecture
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ User (Browser) │
|
|||
|
|
└──────────────────────────┬──────────────────────────────────────┘
|
|||
|
|
│ HTTP / HTTPS
|
|||
|
|
┌──────────────────────────▼──────────────────────────────────────┐
|
|||
|
|
│ Vue 3 SPA (bamort-frontend) │
|
|||
|
|
│ Vite dev server (port 5173) / Nginx (port 80) │
|
|||
|
|
│ - Vue Router (client-side navigation) │
|
|||
|
|
│ - Pinia (state management) │
|
|||
|
|
│ - vue-i18n (DE/EN translations) │
|
|||
|
|
│ - Axios (API calls to backend) │
|
|||
|
|
└──────────────────────────┬──────────────────────────────────────┘
|
|||
|
|
│ REST API calls to VITE_API_URL
|
|||
|
|
│ Bearer token in Authorization header
|
|||
|
|
┌──────────────────────────▼──────────────────────────────────────┐
|
|||
|
|
│ Go / Gin REST API (bamort-backend) │
|
|||
|
|
│ Port 8180 │
|
|||
|
|
│ - Public routes: /register /login /password-reset/* │
|
|||
|
|
│ - Protected routes: /api/* (auth middleware enforced) │
|
|||
|
|
│ - Public info routes: /api/public/* │
|
|||
|
|
└──────────────────────────┬──────────────────────────────────────┘
|
|||
|
|
│ GORM
|
|||
|
|
┌──────────────────────────▼──────────────────────────────────────┐
|
|||
|
|
│ MariaDB 11.4 │
|
|||
|
|
│ (bamort-mariadb, port 3306) │
|
|||
|
|
│ Production: not exposed to host network │
|
|||
|
|
│ Dev: accessible at localhost:3306 + phpMyAdmin at :8081 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The frontend communicates **directly** with the backend API via the `VITE_API_URL` environment variable — there is no Nginx reverse-proxy layer between them in production (TLS termination is the responsibility of an external proxy like Traefik).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Technology Stack
|
|||
|
|
|
|||
|
|
| Layer | Technology | Version | Purpose |
|
|||
|
|
|-------|-----------|---------|---------|
|
|||
|
|
| Backend language | Go | 1.25 | API server, business logic |
|
|||
|
|
| Backend framework | Gin | latest | HTTP routing, middleware |
|
|||
|
|
| ORM | GORM | latest | Database access, migrations |
|
|||
|
|
| Database | MariaDB | 11.4 | Primary persistent storage |
|
|||
|
|
| Test DB | SQLite | (via CGO) | Isolated per-test snapshot |
|
|||
|
|
| PDF rendering | chromedp + Chromium | latest | HTML→PDF via headless browser |
|
|||
|
|
| PDF merging | pdfcpu | latest | Multi-page PDF assembly |
|
|||
|
|
| Live reload (dev) | Air | latest | Backend hot-reload during development |
|
|||
|
|
| Frontend framework | Vue 3 | ^3.5.13 | UI (Options API) |
|
|||
|
|
| Frontend build | Vite | ^6.0.1 | Dev server + production bundler |
|
|||
|
|
| State management | Pinia | ^2.3.0 | Reactive global stores |
|
|||
|
|
| HTTP client | Axios | ^1.7.9 | API communication with auth interceptors |
|
|||
|
|
| i18n | vue-i18n | ^11.0.0 | German/English UI localisation |
|
|||
|
|
| Frontend server | Nginx (prod) / Vite (dev) | alpine / 6.x | Static file serving + SPA routing |
|
|||
|
|
| Container runtime | Docker + Docker Compose | — | Isolated service deployment |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Backend Architecture
|
|||
|
|
|
|||
|
|
### 4.1 Entry Point and Startup Sequence
|
|||
|
|
|
|||
|
|
**File:** `backend/cmd/main.go`
|
|||
|
|
|
|||
|
|
The `main()` function orchestrates startup in this order:
|
|||
|
|
|
|||
|
|
1. **Load configuration** — reads `.env` / `.env.local` then environment variable overrides via `config.Cfg` (auto-loaded in `config.init()`)
|
|||
|
|
2. **Configure logger** — sets log level (`DEBUG`, `INFO`, `WARN`, `ERROR`) and debug mode based on config
|
|||
|
|
3. **Set Gin mode** — `gin.ReleaseMode` in production, `gin.DebugMode` otherwise
|
|||
|
|
4. **Connect database** — calls `database.ConnectDatabase()` which selects MySQL or SQLite based on `DATABASE_TYPE` and `ENVIRONMENT`
|
|||
|
|
5. **Initialize PDF templates** — copies default templates from `/app/default_templates` to `cfg.TemplatesDir` if needed via `pdfrender.InitializeTemplates()`
|
|||
|
|
6. **Set up Gin engine** — calls `router.SetupGin(r)` for CORS config
|
|||
|
|
7. **Register routes** — each module calls `RegisterRoutes(protected)` on the protected `/api` group
|
|||
|
|
8. **Register public routes** — pdfrender and appsystem register unauthenticated endpoints
|
|||
|
|
9. **Start HTTP server** — `r.Run(cfg.GetServerAddress())`
|
|||
|
|
|
|||
|
|
### 4.2 Router and Middleware
|
|||
|
|
|
|||
|
|
**File:** `backend/router/routes.go` and `backend/router/setup.go`
|
|||
|
|
|
|||
|
|
**Route structure:**
|
|||
|
|
```
|
|||
|
|
POST /register → user.RegisterUser
|
|||
|
|
POST /login → user.LoginUser
|
|||
|
|
POST /password-reset/request → user.RequestPasswordReset
|
|||
|
|
GET /password-reset/validate/:token → user.ValidateResetToken
|
|||
|
|
POST /password-reset/reset → user.ResetPassword
|
|||
|
|
|
|||
|
|
/api/* ← ALL require valid Bearer token via user.AuthMiddleware()
|
|||
|
|
├── /api/public/version → appsystem (no auth check inside group, separate route)
|
|||
|
|
├── /api/public/systeminfo → appsystem
|
|||
|
|
├── /api/user/* → user module
|
|||
|
|
├── /api/users/* → user module (admin-only subset)
|
|||
|
|
├── /api/characters/* → character module
|
|||
|
|
├── /api/maintenance/* → gsmaster (read) + maintenance (maintainer-only write)
|
|||
|
|
├── /api/importer/* → importer module
|
|||
|
|
├── /api/pdf/* → pdfrender module
|
|||
|
|
├── /api/transfer/* → transfer module
|
|||
|
|
└── /api/appsystem/* → appsystem module
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**CORS:** Configured in `router/setup.go` with allowed origins including `localhost:5173` (dev), `localhost:8180`, and configurable `BASE_URL` (production frontend).
|
|||
|
|
|
|||
|
|
### 4.3 Authentication System
|
|||
|
|
|
|||
|
|
**Token generation** (`user/handlers.go` → `GenerateToken`):
|
|||
|
|
- Hashes `username + createdAt` with MD5
|
|||
|
|
- Embeds the user's ID in an predictable position within the hex string
|
|||
|
|
- Token is stored client-side in `localStorage`
|
|||
|
|
|
|||
|
|
**Token validation** (`user/handlers.go` → `CheckToken`):
|
|||
|
|
- Extracts the Base64/hex-encoded user ID from position `len("Bearer ")` onward in the `Authorization` header
|
|||
|
|
- Loads the user from the database by that ID
|
|||
|
|
- **No cryptographic signature verification is performed** — this is a design limitation (see Section 13)
|
|||
|
|
|
|||
|
|
**AuthMiddleware:**
|
|||
|
|
- Calls `CheckToken`, sets `userID`, `username`, `user` in the Gin context
|
|||
|
|
- Returns `401 Unauthorized` if token is absent or user not found
|
|||
|
|
|
|||
|
|
**Password hashing:** MD5 (`crypto/md5`). The bcrypt implementation is present but commented out.
|
|||
|
|
|
|||
|
|
### 4.4 Module Breakdown (Functional View)
|
|||
|
|
|
|||
|
|
Each module follows the same structure: `handlers.go` (business logic + HTTP responses), `routes.go` (route registration), and `*_test.go` (tests).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `user/` — User Account Management
|
|||
|
|
|
|||
|
|
**Business function:** Registration, login, profile management, password reset via email, and role-based access control.
|
|||
|
|
|
|||
|
|
**Key responsibilities:**
|
|||
|
|
- `RegisterUser` — creates a new user with MD5-hashed password and default `standard` role
|
|||
|
|
- `LoginUser` — validates credentials, returns a token
|
|||
|
|
- `AuthMiddleware` — protects all `/api/*` routes; injects user context
|
|||
|
|
- `RequireAdmin()` / `RequireMaintainer()` — role-check middleware used on specific sub-routes
|
|||
|
|
- Profile endpoints: `GET/PUT /api/user/profile`, display name, email, preferred language, password change
|
|||
|
|
- Admin user management: `GET/PUT/DELETE /api/users/:id`
|
|||
|
|
- Password reset flow: request → email with token → validate token → set new password (uses `mail/` module for SMTP)
|
|||
|
|
|
|||
|
|
**Roles:**
|
|||
|
|
| Role | Permissions |
|
|||
|
|
|------|------------|
|
|||
|
|
| `standard` | Own characters + shared characters |
|
|||
|
|
| `maintainer` | + Write access to GSMaster data and maintenance endpoints |
|
|||
|
|
| `admin` | + User management (list, change role, delete, reset password) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `character/` — Character Management (Core Module)
|
|||
|
|
|
|||
|
|
**Business function:** The heart of the application. Full CRUD for characters plus all RPG game-logic operations.
|
|||
|
|
|
|||
|
|
**Sub-responsibilities:**
|
|||
|
|
|
|||
|
|
| Sub-area | Files | Business purpose |
|
|||
|
|
|----------|-------|-----------------|
|
|||
|
|
| CRUD | `handlers.go`, `database.go` | Create / read / update / delete characters and their sub-entities (attributes, skills, spells, equipment) |
|
|||
|
|
| Derived values | `derived_values_calculator.go` | Calculate secondary stats (Abwehr, Zaubern, Raufen, body/mind resistance) from primary attributes using M5 formulas |
|
|||
|
|
| Derived values (dice) | `derived_values_calculator.go` | Calculate dice-dependent stats during character creation (Lp_max, Ap_max, B_max, PA, Wk) requiring die-roll input from frontend |
|
|||
|
|
| Learning cost engine | `lerncost_handler.go` | Compute EP/TE/LE/gold cost to learn a new skill/spell or improve an existing one, considering class, category, difficulty, practice points, and reward bonuses |
|
|||
|
|
| Skill actions | `handlers.go` | Add/remove/edit skills and weapon skills inline |
|
|||
|
|
| Learn skill | Separate handler | Learn a new skill (deducts resources, creates audit log entries) |
|
|||
|
|
| Improve skill | `ImproveSkill` handler | Increase an existing skill value (validates costs, writes audit trail) |
|
|||
|
|
| Learn spell | `LearnSpell` handler | Add a new spell to the character |
|
|||
|
|
| Practice points | `practice_points_handler.go` | Track PP per skill category; PP reduce TE costs in learning |
|
|||
|
|
| Experience & wealth | `handlers.go` | Update EP/gold separately from full character update |
|
|||
|
|
| Character sharing | `share_handlers.go` | Grant read or write access to specific other users |
|
|||
|
|
| Character creation wizard | Multiple handlers | Multi-step session-based creation: basic info → attributes → derived rolling → skills → finalize |
|
|||
|
|
| Creation rules | `creation_rules.go` | M5-specific tables for special abilities (W100 roll → bonus/malus), starting attribute bonuses by race/class |
|
|||
|
|
| Audit log | `audit_log.go`, `audit_log_handlers.go` | Immutable log of EP/gold/stat changes with reason codes |
|
|||
|
|
| Image | `image_handler.go` | Upload and store Base64 character portrait |
|
|||
|
|
| Ownership guard | `ownership_guard_test.go` | Ensures only the owner (or share-granted user) can modify |
|
|||
|
|
| System info | `system_information_handlers.go` | Returns available skill categories for UI dropdowns |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `gsmaster/` — Game-System Master Data
|
|||
|
|
|
|||
|
|
**Business function:** Canonical reference tables for the M5 game system. Skills, weapon skills, spells, equipment, weapons, and containers as defined by the Midgard rules. Users with the `maintainer` role can edit these tables.
|
|||
|
|
|
|||
|
|
**Read (all authenticated users):**
|
|||
|
|
- `GET /api/maintenance/skills` — list all skills
|
|||
|
|
- `GET /api/maintenance/spells` — list all spells
|
|||
|
|
- `GET /api/maintenance/weapons` — list all weapons
|
|||
|
|
- `GET /api/maintenance/equipment` — general equipment
|
|||
|
|
- `GET /api/maintenance/*-enhanced` — enhanced views with additional metadata (learning costs, categories, etc.)
|
|||
|
|
|
|||
|
|
**Write (maintainer only):**
|
|||
|
|
- POST/PUT/DELETE on all the above categories
|
|||
|
|
|
|||
|
|
**Learning cost integration:** `gsmaster` package provides lookup helpers used by the `character` module to calculate learning costs. Skills carry `Category`, `Difficulty`, and `InnateSkill` properties that drive cost lookup logic.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `equipment/` — Character Equipment Management
|
|||
|
|
|
|||
|
|
**Business function:** CRUD operations specifically for the equipment attached to a character — weapons (`EqWaffe`), containers/bags (`EqContainer`), transportation (`EqContainer` with `IsTransportation=true`), and general gear (`EqAusruestung`).
|
|||
|
|
|
|||
|
|
**Why separate from `character/`?** Equipment has its own handlers/routes to keep the character module focused on core stats and skills. Equipment can also reference master data items from `gsmaster/`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `pdfrender/` — PDF Character Sheet Export
|
|||
|
|
|
|||
|
|
**Business function:** Generate printable PDF character sheets by rendering HTML templates with character data using a Chromium headless browser, then merging the resulting per-page PDFs into a single file.
|
|||
|
|
|
|||
|
|
**Flow:**
|
|||
|
|
1. `ExportCharacterToPDF` receives a character ID and template name
|
|||
|
|
2. Loads the character from the database (fully preloaded)
|
|||
|
|
3. For each template page (page1_stats.html, page2_play.html, etc.):
|
|||
|
|
- Injects character data into the HTML template using Go template engine
|
|||
|
|
- Renders it to PDF via `chromedp` (headless Chromium)
|
|||
|
|
- Checks `<!-- MaxItems: N -->` comment in template to detect overflow
|
|||
|
|
- Generates continuation pages (page1.2_stats.html) for overflow
|
|||
|
|
4. Merges all page PDFs with `pdfcpu` into one file
|
|||
|
|
5. Saves to `cfg.ExportTempDir` and returns the filename
|
|||
|
|
6. Frontend opens a download URL in a new window
|
|||
|
|
|
|||
|
|
**Templates:** Located in `backend/templates/Default_A4_Quer/`. The `InitializeTemplates` function copies the default template to the runtime directory on first run.
|
|||
|
|
|
|||
|
|
**Routes:**
|
|||
|
|
- `GET /api/pdf/export/:id` — export, returns `{"filename": "..."}`
|
|||
|
|
- `GET /api/pdf/file/:filename` — download the generated PDF (public route, no auth required, filename-based security)
|
|||
|
|
- `GET /api/pdf/templates` — list available templates
|
|||
|
|
- `POST /api/pdf/cleanup` — clean up old generated PDF files
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `importer/` — VTT / CSV Import and Export
|
|||
|
|
|
|||
|
|
**Business function:** Import characters from Virtual Tabletop (VTT) JSON format or CSV files. Export characters to VTT JSON or CSV. Export/import spell lists via CSV.
|
|||
|
|
|
|||
|
|
**Routes:**
|
|||
|
|
```
|
|||
|
|
POST /api/importer/upload — Upload VTT JSON + optional CSV
|
|||
|
|
POST /api/importer/spells/csv — Import spell data from CSV
|
|||
|
|
GET /api/importer/export/vtt/:id — Export character as VTT JSON (API response)
|
|||
|
|
GET /api/importer/export/vtt/:id/file — Export character as VTT JSON (file download)
|
|||
|
|
GET /api/importer/export/csv/:id — Export character as CSV
|
|||
|
|
GET /api/importer/export/spells/csv — Export all spells as CSV
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `transfer/` — JSON Backup and Restore
|
|||
|
|
|
|||
|
|
**Business function:** Portable character backup/restore in a Bamort-native JSON format. Also supports full-database export/import (admin/migration use case).
|
|||
|
|
|
|||
|
|
**Routes:**
|
|||
|
|
```
|
|||
|
|
GET /api/transfer/export/:id — Export character as JSON
|
|||
|
|
GET /api/transfer/download/:id — Download character JSON as file
|
|||
|
|
POST /api/transfer/import — Import character from JSON
|
|||
|
|
POST /api/transfer/database/export — Export entire database as JSON
|
|||
|
|
POST /api/transfer/database/import — Import full database from JSON
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `maintenance/` — Admin and Database Operations
|
|||
|
|
|
|||
|
|
**Business function:** Maintainer-only admin operations: database consistency checks, re-connect/reload env, data migration (SQLite→MariaDB), and managing auxiliary master data not covered by `gsmaster/`.
|
|||
|
|
|
|||
|
|
**Key operations:**
|
|||
|
|
- `GET /api/maintenance/setupcheck` — validate database schema integrity
|
|||
|
|
- `GET /api/maintenance/setupcheck-dev` — extended dev-mode diagnostics
|
|||
|
|
- `GET /api/maintenance/reconndb` — reconnect database (useful after connection interruption)
|
|||
|
|
- `GET /api/maintenance/reloadenv` — reload environment variables at runtime
|
|||
|
|
- `POST /api/maintenance/transfer-sqlite-to-mariadb` — one-time data migration
|
|||
|
|
- CRUD for beliefs (`gsm-believes`), game systems, literature sources, misc lookups, skill improvement cost tables
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `appsystem/` — System Information
|
|||
|
|
|
|||
|
|
**Business function:** Exposes version and system health information.
|
|||
|
|
|
|||
|
|
**Routes:**
|
|||
|
|
```
|
|||
|
|
GET /api/public/version — { version, gitCommit }
|
|||
|
|
GET /api/public/systeminfo — { version, gitCommit, userCount, charCount, dbVersion }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
These are reachable without authentication (used by `LandingView` and `SystemInfoView` to display status without requiring login).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `gamesystem/` — Game System Records
|
|||
|
|
|
|||
|
|
**Business function:** Manages `GameSystem` records in the database (e.g., M5 = Midgard 5th edition). Used when creating characters and as a FK reference in master data. Initial seed data creates the M5 game system if it doesn't exist.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `logger/` — Application Logging
|
|||
|
|
|
|||
|
|
**Business function:** Custom structured logger with levels (DEBUG/INFO/WARN/ERROR). Used throughout all packages. Debug mode controlled by config.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### `mail/` — Email Delivery
|
|||
|
|
|
|||
|
|
**Business function:** SMTP client used exclusively for password-reset emails. Supports TLS (port 465) and STARTTLS (port 587). Configured via `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM` env vars.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Frontend Architecture
|
|||
|
|
|
|||
|
|
### 5.1 Application Bootstrap
|
|||
|
|
|
|||
|
|
**`src/main.js`** registers plugins in order:
|
|||
|
|
|
|||
|
|
1. **Pinia** — state store (must be first)
|
|||
|
|
2. **Vue Router** — client-side routing
|
|||
|
|
3. **vue-i18n** — i18n instance created and exported from `stores/languageStore.js`
|
|||
|
|
4. **UtilsPlugin** — installs global utilities on all components: `$formatDate`, `$formatDateTime`, `$formatRelativeDate`, `$safeValue`, `$capitalize`, `$rollDie`, `$rollDice`, `$rollDiceWithSum`, `$rollNotation`, `$randomBetween`, `$randomChoice`, `$shuffleArray`
|
|||
|
|
|
|||
|
|
**`App.vue`** — root component:
|
|||
|
|
- Shows `<Menu />` only when `localStorage.getItem('token')` is truthy
|
|||
|
|
- Applies `.full-width` CSS class on `<main>` for non-logged-in pages (centers login/register forms)
|
|||
|
|
- Reacts to `storage` and `auth-changed` window events to update `loggedIn` state reactively
|
|||
|
|
|
|||
|
|
### 5.2 Router and Navigation Guards
|
|||
|
|
|
|||
|
|
**File:** `src/router/index.js`
|
|||
|
|
|
|||
|
|
Uses `createWebHistory` (HTML5 pushstate — no hash URLs). Unauthenticated landing/auth pages are statically imported; all other views are lazy-loaded (code-split) for performance.
|
|||
|
|
|
|||
|
|
**Navigation guard logic (beforeEach):**
|
|||
|
|
1. If route requires auth and user is not logged in → redirect to `/login`
|
|||
|
|
2. If route requires admin role → lazy-load `userStore`, fetch profile if not loaded, check `isAdmin` getter → redirect to `/dashboard` if not admin
|
|||
|
|
3. Otherwise → allow navigation
|
|||
|
|
|
|||
|
|
`isLoggedIn()` from `utils/auth.js` simply checks for a token in `localStorage`.
|
|||
|
|
|
|||
|
|
### 5.3 State Management (Pinia Stores)
|
|||
|
|
|
|||
|
|
**`userStore` (`src/stores/userStore.js`)**
|
|||
|
|
|
|||
|
|
Central user identity state. Used by `Menu.vue`, `CharacterDetails.vue`, `UserManagementView.vue`, and the router guard.
|
|||
|
|
|
|||
|
|
| State | Purpose |
|
|||
|
|
|-------|---------|
|
|||
|
|
| `currentUser` | Logged-in user object `{ id, username, display_name, email, role, preferred_language }` |
|
|||
|
|
| `isLoading` | Loading flag for profile fetch |
|
|||
|
|
|
|||
|
|
| Getter | Returns |
|
|||
|
|
|--------|---------|
|
|||
|
|
| `isAuthenticated` | `true` if `currentUser` is not null |
|
|||
|
|
| `userRole` | `currentUser.role` or `'standard'` |
|
|||
|
|
| `isAdmin` | `role === 'admin'` |
|
|||
|
|
| `isMaintainer` | `role === 'maintainer'` or `'admin'` |
|
|||
|
|
| `isStandardUser` | any logged-in user |
|
|||
|
|
|
|||
|
|
Action `fetchCurrentUser()` calls `GET /api/user/profile`, hydrates `currentUser`, and also sets the UI language to the user's preferred language.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**`languageStore` (`src/stores/languageStore.js`)**
|
|||
|
|
|
|||
|
|
Holds current language (`de` or `en`). Unusually, the `i18n` instance itself is **created and exported from this file**, then imported in `main.js`. This coupling means language changes can be made from anywhere that imports `i18n`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**`characterCreationStore` (`src/stores/characterCreation.js`)**
|
|||
|
|
|
|||
|
|
In-memory wizard state for multi-step character creation. Tracks current step (1–5), and partial data for each step. The actual server-side session persistence is handled directly by `CharacterCreation.vue` via API calls; the Pinia store mirrors this state in memory.
|
|||
|
|
|
|||
|
|
### 5.4 Views and Components (Functional View)
|
|||
|
|
|
|||
|
|
#### Public Pages (no auth required)
|
|||
|
|
|
|||
|
|
| Component | Route | Purpose |
|
|||
|
|
|-----------|-------|---------|
|
|||
|
|
| `LandingView` | `/` | Landing page; polls `GET /api/public/version` every 5s (up to 24 retries) to show backend status; disables login button until backend responds |
|
|||
|
|
| `LoginView` → `LoginForm` | `/login` | Login form; on success stores token in `localStorage`, triggers `auth-changed` event |
|
|||
|
|
| `RegisterView` → `RegisterForm` | `/register` | Registration form |
|
|||
|
|
| `ForgotPasswordView` → `ForgotPasswordForm` | `/forgot-password` | Sends password-reset email request |
|
|||
|
|
| `ResetPasswordView` → `ResetPasswordForm` | `/reset-password` | Accepts reset token from URL, sets new password |
|
|||
|
|
| `HelpView` | `/help` | FAQ page built from i18n keys dynamically |
|
|||
|
|
| `SponsorsView` | `/sponsors` | Static credits/sponsor page |
|
|||
|
|
| `SystemInfoView` | `/system-info` | Shows system versions and live backend stats |
|
|||
|
|
|
|||
|
|
#### Authenticated Pages
|
|||
|
|
|
|||
|
|
| Component | Route | Purpose |
|
|||
|
|
|-----------|-------|---------|
|
|||
|
|
| `DashboardView` → `CharacterList` | `/dashboard` | Lists own and shared characters; shows active creation sessions; "Create New Character" button starts a new wizard session |
|
|||
|
|
| `UserProfileView` | `/profile` | Self-service: change display name, language, email, password |
|
|||
|
|
| `UserManagementView` | `/users` | Admin-only: list all users, change roles, delete users, reset passwords |
|
|||
|
|
| `MaintenanceView` → `Maintenance` | `/maintenance` | Maintainer tools: database checks, master data management |
|
|||
|
|
| `FileUploadPage` | `/upload` | Import VTT JSON + optional CSV |
|
|||
|
|
| `AusruestungView` → `AusruestungList` | `/ausruestung/:characterId` | Standalone equipment list (legacy view) |
|
|||
|
|
| `CharacterDetails` | `/character/:id` | Main character hub (see below) |
|
|||
|
|
| `CharacterCreation` | `/character/create/:sessionId` | Multi-step character creation wizard |
|
|||
|
|
|
|||
|
|
#### CharacterDetails Component — Central Hub
|
|||
|
|
|
|||
|
|
`CharacterDetails.vue` is the most complex component. It:
|
|||
|
|
1. Fetches full character data from `GET /api/characters/:id`
|
|||
|
|
2. Determines `isOwner` by comparing `character.user_id` vs `userStore.currentUser.id`
|
|||
|
|
3. Renders a **dynamic sub-component** (`<component :is="currentView">`) driven by a submenu
|
|||
|
|
4. Passes `character` and `isOwner` as props to every sub-view
|
|||
|
|
5. Listens for `@character-updated` events from sub-views to trigger a full data reload
|
|||
|
|
|
|||
|
|
**Sub-views (tabs):**
|
|||
|
|
|
|||
|
|
| Component | Business purpose |
|
|||
|
|
|-----------|----------------|
|
|||
|
|
| `DatasheetView` | Character biography, attributes (Au/Gs/Gw/In/Ko/St/Wk/Zt/PA), derived stats (Lp/Ap/B, Abwehr, Zaubern, Raufen, Resistenz), bennies, wealth. Inline double-click editing for owners |
|
|||
|
|
| `SkillView` | Skills grouped by category. Learning mode (toggle 🎓) reveals resources (EP, Gold) and action buttons: learn new skill, improve existing, add manually |
|
|||
|
|
| `WeaponView` | Weapon skills and equipped weapons |
|
|||
|
|
| `SpellView` | Spell list with learn/improve actions |
|
|||
|
|
| `EquipmentView` | Containers, general equipment, transported items |
|
|||
|
|
| `ExperianceView` | Experience and wealth tracking; audit log access |
|
|||
|
|
| `DeleteCharView` | Confirmation UI for deleting the character (owner only) |
|
|||
|
|
| `AuditLogView` | Read-only history of EP/gold changes |
|
|||
|
|
|
|||
|
|
**Overlay dialogs accessible from CharacterDetails:**
|
|||
|
|
- `ExportDialog` — multi-format export: choose PDF template, VTT JSON, or BaMoRT JSON and trigger download
|
|||
|
|
- `VisibilityDialog` — toggle character between public/private (owner only)
|
|||
|
|
|
|||
|
|
#### Learning Dialogs
|
|||
|
|
|
|||
|
|
`SkillLearnDialog.vue`, `SkillImproveDialog.vue`, `SpellLearnDialog.vue` are modal dialogs that:
|
|||
|
|
1. Call `POST /api/characters/lerncost-new` with skill name, current level, type, and action
|
|||
|
|
2. Display the computed cost breakdown (EP, TE, LE, gold, PP reduction)
|
|||
|
|
3. Allow the user to confirm → backend executes the learning/improvement and deducts resources
|
|||
|
|
|
|||
|
|
### 5.5 API Communication Layer
|
|||
|
|
|
|||
|
|
**File:** `src/utils/api.js`
|
|||
|
|
|
|||
|
|
A pre-configured Axios instance:
|
|||
|
|
- `baseURL`: `import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de'`
|
|||
|
|
- **Request interceptor:** Injects `Authorization: Bearer <token>` from `localStorage` automatically
|
|||
|
|
- **Response interceptor:** On `401`, removes the stale token from `localStorage` and logs a warning (but does not redirect — the nav guard handles that on next navigation)
|
|||
|
|
|
|||
|
|
All components and stores import this `API` instance. Raw `axios` (without the interceptor) is used only in `LandingView` and `SystemInfoView` for unauthenticated public API calls.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Core Business Logic
|
|||
|
|
|
|||
|
|
### 6.1 Character Data Model
|
|||
|
|
|
|||
|
|
The character data model maps closely to the **Midgard M5 character sheet**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Char (root entity, table: char_chars)
|
|||
|
|
├── Eigenschaften[] — 9 primary attributes (Au, Gs, Gw, In, Ko, St, Wk, Zt, PA)
|
|||
|
|
├── Lp — Life points (current + max)
|
|||
|
|
├── Ap — Action points (current + max)
|
|||
|
|
├── B — Load/burden capacity (current + max)
|
|||
|
|
├── Merkmale — Physical description (eye/hair color, height, etc.)
|
|||
|
|
├── Erfahrungsschatz — EP (experience points) + ES (experience treasure/milestone)
|
|||
|
|
├── Bennies — Luck tokens: Gg (lucky coin), Gp (lucky penny), Sg (lucky stroke)
|
|||
|
|
├── Vermoegen — Wealth: Goldstuecke, Silberstuecke, Kupferstuecke
|
|||
|
|
├── Fertigkeiten[] — Skills (Fertigkeit), each with value, base, bonus, PP, category
|
|||
|
|
├── Waffenfertigkeiten[] — Weapon skills (subtype with additional combat fields)
|
|||
|
|
├── Zauber[] — Spells with description, bonus, source reference
|
|||
|
|
├── Waffen[] — Equipped weapons (EqWaffe) with attack/defense bonuses
|
|||
|
|
├── Behaeltnisse[] — Bags/containers (EqContainer)
|
|||
|
|
├── Transportmittel[] — Vehicles (EqContainer with IsTransportation=true)
|
|||
|
|
├── Ausruestung[] — General equipment items
|
|||
|
|
└── Spezialisierung — JSON array of specialization strings
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**`FeChar`** (Frontend Character) is the API response type: it embeds `Char` plus computed fields:
|
|||
|
|
- `Git` (poison tolerance = 30 + Ko/2)
|
|||
|
|
- `CategorizedSkills` — map grouping skills by category for `SkillView` rendering
|
|||
|
|
- `InnateSkills` — innate/racial skills separated from learned ones
|
|||
|
|
|
|||
|
|
### 6.2 Derived Values Calculation
|
|||
|
|
|
|||
|
|
**File:** `backend/character/derived_values_calculator.go`
|
|||
|
|
|
|||
|
|
The backend implements the M5 formula set. Key derivations:
|
|||
|
|
|
|||
|
|
| Derived value | Formula |
|
|||
|
|
|--------------|---------|
|
|||
|
|
| AusdauerBonus | Ko/10 + St/20 |
|
|||
|
|
| SchadensBonus | St/20 + Gs/30 − 3 |
|
|||
|
|
| AngriffsBonus | Gs/20 + Gw/30 − 3 |
|
|||
|
|
| AbwehrBonus | Gw/20 + Gs/30 − 3 |
|
|||
|
|
| ZauberBonus | In/10 + Zt/20 − 3 |
|
|||
|
|
| ResistenzBonusKoerper | Ko/10 + Wk/20 − 3 |
|
|||
|
|
| ResistenzBonusGeist | In/10 + Wk/15 − 3 |
|
|||
|
|
| Abwehr | (grade-based base) + AbwehrBonus + PA (parry) |
|
|||
|
|
| Zaubern | (class-based base) + ZauberBonus |
|
|||
|
|
| Raufen | (class-based base at Grad 1) |
|
|||
|
|
|
|||
|
|
Dice-dependent values (rolled during character creation) include: `lp_max`, `ap_max`, `b_max`, `pa` (parry), `wk` (willpower). The frontend rolls dice and sends the results to `POST /api/characters/create-session/:id/derived` where the server validates and stores them.
|
|||
|
|
|
|||
|
|
The frontend component in `CharacterCreation.vue` exposes the dice-roll UI and calls `GET /api/characters/calculate-static-fields` and `POST /api/characters/calculate-rolled-field` to get the values computed by the backend rather than computing them in JavaScript.
|
|||
|
|
|
|||
|
|
### 6.3 Skill and Spell Learning System
|
|||
|
|
|
|||
|
|
**File:** `backend/character/lerncost_handler.go`
|
|||
|
|
|
|||
|
|
The learning cost engine computes what it costs (EP + TE, or EP + LE for spells) to learn a new skill/spell or improve an existing one. This is the most complex piece of business logic in the application.
|
|||
|
|
|
|||
|
|
**Input:**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "Klettern",
|
|||
|
|
"type": "skill",
|
|||
|
|
"action": "improve",
|
|||
|
|
"current_level": 10,
|
|||
|
|
"target_level": 12,
|
|||
|
|
"use_pp": 2,
|
|||
|
|
"reward": { "type": "half_ep_improvement" }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Processing steps:**
|
|||
|
|
1. Look up the character's class abbreviation (`Typ`)
|
|||
|
|
2. Normalize the skill/spell name (trim, lowercase for lookup)
|
|||
|
|
3. Find the skill in `gsmaster` master data to get `Category` and `Difficulty`
|
|||
|
|
4. Query learning cost tables:
|
|||
|
|
- For **skills**: `ClassCategoryEPCost` (EP per TE by class+category) + `SkillImprovementCost` (TE per level step) + `SkillCategoryDifficulty` (base LE)
|
|||
|
|
- For **spells**: `ClassSpellSchoolEPCost` (EP per LE by class+spell school) + `SpellLevelLECost` (LE by spell level)
|
|||
|
|
- For **weapon skills**: separate weapon skill cost tables
|
|||
|
|
5. Apply **practice points (PP)** reduction: each PP reduces required TE by 1 (capped at max PP available for category)
|
|||
|
|
6. Apply **reward bonuses** if specified:
|
|||
|
|
- `free_learning` — zero cost
|
|||
|
|
- `free_spell_learning` — zero cost for spells
|
|||
|
|
- `half_ep_improvement` — halve EP cost when improving
|
|||
|
|
- `gold_for_ep` — substitute up to half the EP with 10 GS each
|
|||
|
|
7. Return full cost breakdown plus `CanAfford` flag comparing costs to character's current EP/gold
|
|||
|
|
|
|||
|
|
For **improve** action, costs are computed **per level step** from `current_level+1` to `target_level` and aggregated: `MultiLevelCostResponse` contains both per-step and total costs.
|
|||
|
|
|
|||
|
|
The `ImproveSkill` and `LearnSkill` handlers execute the actual resource deduction and write audit log entries.
|
|||
|
|
|
|||
|
|
### 6.4 Character Creation Wizard
|
|||
|
|
|
|||
|
|
**Backend:** `backend/character/` (multiple handlers)
|
|||
|
|
**Frontend:** `frontend/src/components/CharacterCreation.vue` and `CharacterCreation/` sub-components
|
|||
|
|
**Session storage:** `models.CharacterCreationSession` (table: `char_creation_sessions`)
|
|||
|
|
|
|||
|
|
The wizard is a **server-side session** model. A session stores partial character data as JSON blobs per step. Sessions expire (stored `ExpiresAt`).
|
|||
|
|
|
|||
|
|
**Steps:**
|
|||
|
|
|
|||
|
|
| Step | Name | Backend endpoint | Data stored |
|
|||
|
|
|------|------|-----------------|-------------|
|
|||
|
|
| 1 | Basic Info | `PUT .../basic` | Name, race, class, origin, social class, faith, gender, handedness |
|
|||
|
|
| 2 | Attributes | `PUT .../attributes` | Primary attributes (rolled client-side, validated server-side) |
|
|||
|
|
| 3 | Derived Values | `PUT .../derived` | Dice-rolled derived stats (LP/AP/B/PA/Wk) |
|
|||
|
|
| 4 | Skills | `PUT .../skills` | Selected starting skills with initial values |
|
|||
|
|
| 5 | Finalize | `POST .../finalize` | Creates the final `Char` record from all session data |
|
|||
|
|
|
|||
|
|
**Available-skills-for-creation endpoint** (`POST /api/characters/available-skills-creation`):
|
|||
|
|
Returns all learnable skills for a specific class, grouped by category, with starting LP (Learning Points) that determine how many skills the character can learn at creation. LP are class+category specific (`ClassCategoryLearningPoints` table).
|
|||
|
|
|
|||
|
|
**Special abilities** (`creation_rules.go`):
|
|||
|
|
M5 allows rolling a W100 to gain a random special ability at character creation. The `GetSpecialAbilityByRoll(roll)` function maps dice outcomes to named bonuses (e.g., roll 56–60 → "Gute Reflexe+9"). This is pure backend logic; the frontend passes the rolled value as part of the creation session data, and the server calls this function when computing derived values.
|
|||
|
|
|
|||
|
|
**Dashboard integration:** `CharacterList.vue` shows in-progress sessions via `CharacterCreationSessions.vue`. Users can resume or delete draft sessions.
|
|||
|
|
|
|||
|
|
### 6.5 PDF Export Pipeline
|
|||
|
|
|
|||
|
|
**Flow (backend):**
|
|||
|
|
```
|
|||
|
|
ExportCharacterToPDF (pdfrender/handlers.go)
|
|||
|
|
↓
|
|||
|
|
LoadCharacterFromDB (full preload)
|
|||
|
|
↓
|
|||
|
|
For each template page (page1_stats.html, page2_play.html, ...):
|
|||
|
|
→ Execute Go text/template with character data
|
|||
|
|
→ chromedp: launch Chromium, navigate to data URL, print to PDF
|
|||
|
|
→ Check <!-- MaxItems: N --> overflow marker
|
|||
|
|
→ Generate continuation pages if needed (page1.2_stats.html)
|
|||
|
|
↓
|
|||
|
|
pdfcpu.MergeFiles → single merged PDF
|
|||
|
|
↓
|
|||
|
|
Save to cfg.ExportTempDir
|
|||
|
|
↓
|
|||
|
|
Return { filename: "CharName_20231225_143045.pdf" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Flow (frontend):**
|
|||
|
|
```
|
|||
|
|
ExportDialog.vue
|
|||
|
|
↓
|
|||
|
|
GET /api/pdf/templates → populate template selector
|
|||
|
|
↓
|
|||
|
|
User selects format (PDF / VTT / BaMoRT) + template
|
|||
|
|
↓
|
|||
|
|
For PDF format:
|
|||
|
|
GET /api/pdf/export/:id?template=Default_A4_Quer
|
|||
|
|
→ Response: { filename: "..." }
|
|||
|
|
→ window.open(baseURL + /api/pdf/file/<filename>, '_blank')
|
|||
|
|
|
|||
|
|
For VTT format:
|
|||
|
|
GET /api/importer/export/vtt/:id/file (blob) → browser download link
|
|||
|
|
|
|||
|
|
For BaMoRT format:
|
|||
|
|
GET /api/transfer/download/:id (blob) → browser download link
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Note:** For PDF, `window.open` is called after the async API response, making it subject to popup blockers.
|
|||
|
|
|
|||
|
|
### 6.6 Audit Log
|
|||
|
|
|
|||
|
|
**Files:** `backend/character/audit_log.go`, `audit_log_handlers.go`
|
|||
|
|
|
|||
|
|
Every change to EP, gold, or significant character values writes an immutable `AuditLogEntry` record:
|
|||
|
|
|
|||
|
|
| Field | Purpose |
|
|||
|
|
|-------|---------|
|
|||
|
|
| `CharacterID` | Which character |
|
|||
|
|
| `FieldName` | What changed (e.g., `"experience_points"`, `"goldstücke"`) |
|
|||
|
|
| `OldValue` / `NewValue` | Before and after |
|
|||
|
|
| `Difference` | `NewValue - OldValue` |
|
|||
|
|
| `Reason` | Typed constant: `manual`, `skill_learning`, `skill_improvement`, `spell_learning`, `equipment`, `reward`, `correction`, `import` |
|
|||
|
|
| `UserID` | Who made the change |
|
|||
|
|
| `Notes` | Free-text context |
|
|||
|
|
| `Timestamp` | Auto-set by GORM |
|
|||
|
|
|
|||
|
|
API endpoints:
|
|||
|
|
- `GET /api/characters/:id/audit-log` — all entries or filtered by `?field=experience_points`
|
|||
|
|
- `GET /api/characters/:id/audit-log/stats` — aggregated statistics
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Data Access and Persistence
|
|||
|
|
|
|||
|
|
### Database Connection
|
|||
|
|
|
|||
|
|
- **Production/development:** MariaDB 11.4 via GORM MySQL driver. DSN from `DATABASE_URL` env var.
|
|||
|
|
- **Tests:** SQLite (via CGO). `testutils.SetupTestDB()` copies `testdata/prepared_test_data.db` to a temp dir per test run, ensuring isolation.
|
|||
|
|
- **Global variable:** `database.DB *gorm.DB` — shared across all packages by import.
|
|||
|
|
|
|||
|
|
### Schema Migrations
|
|||
|
|
|
|||
|
|
`models.MigrateStructure(db)` is called at startup and runs GORM `AutoMigrate` for all entity types in dependency order:
|
|||
|
|
1. `game_systems`
|
|||
|
|
2. GSMaster tables (skills, spells, equipment, weapons, containers, believes, misc lookups)
|
|||
|
|
3. Character tables (chars, attributes, skills, spells, equipment sub-tables)
|
|||
|
|
4. Equipment tables
|
|||
|
|
5. Skill learning cost tables
|
|||
|
|
6. Learning/cost relation tables
|
|||
|
|
|
|||
|
|
Migration history is tracked in `schema_version` and `migration_history` tables.
|
|||
|
|
|
|||
|
|
### Test Data
|
|||
|
|
|
|||
|
|
`testdata/prepared_test_data.db` is a pre-populated SQLite snapshot containing real test characters, including **character ID 18 ("Fanjo Vetrani")** which is used as the primary test fixture throughout all backend tests.
|
|||
|
|
|
|||
|
|
### Custom Types
|
|||
|
|
|
|||
|
|
`StringArray` is a custom GORM type that serializes `[]string` as JSON into a TEXT column:
|
|||
|
|
```go
|
|||
|
|
type StringArray []string
|
|||
|
|
// Stored as: ["specialization1","specialization2"]
|
|||
|
|
```
|
|||
|
|
Used by `Char.Spezialisierung`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Module Interaction Map
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Frontend (Vue 3) │
|
|||
|
|
│ │
|
|||
|
|
│ CharacterDetails ──────→ SkillView ──────→ SkillLearnDialog │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ SpellView ──────→ SpellLearnDialog │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ └──────→ ExportDialog │ │
|
|||
|
|
│ │ │ │
|
|||
|
|
└─────────────────────┼──────────────────────────────┼────────────────┘
|
|||
|
|
│ REST API │ REST API
|
|||
|
|
┌─────────────▼──────────────┐ ┌───────────▼───────────────┐
|
|||
|
|
│ pdfrender module │ │ character module │
|
|||
|
|
│ ExportCharacterToPDF │ │ GetLernCostNewSystem │
|
|||
|
|
│ chromedp → Chromium │ │ LearnSkill / ImproveSkill │
|
|||
|
|
│ pdfcpu merge │ │ Audit log writes │
|
|||
|
|
└─────────────────────────── ┘ └───────────────────────────┘
|
|||
|
|
│
|
|||
|
|
┌────────────▼──────────────┐
|
|||
|
|
│ gsmaster module │
|
|||
|
|
│ Skill/Spell lookup │
|
|||
|
|
│ Cost table queries │
|
|||
|
|
└───────────────────────────┘
|
|||
|
|
│
|
|||
|
|
┌────────────▼──────────────┐
|
|||
|
|
│ models package │
|
|||
|
|
│ GORM entity definitions │
|
|||
|
|
│ LearnCost, Char, etc. │
|
|||
|
|
└───────────────────────────┘
|
|||
|
|
│
|
|||
|
|
┌────────────▼──────────────┐
|
|||
|
|
│ database.DB │
|
|||
|
|
│ MariaDB (prod/dev) │
|
|||
|
|
│ SQLite (test) │
|
|||
|
|
└───────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Cross-module dependencies (backend):**
|
|||
|
|
- `character` depends on `gsmaster` (skill/spell lookup for cost calculations)
|
|||
|
|
- `character` depends on `models` (all entity types)
|
|||
|
|
- `character` depends on `database` (direct DB access)
|
|||
|
|
- `gsmaster` depends on `models` and `database`
|
|||
|
|
- `pdfrender` depends on `models` and `database` (loads full character)
|
|||
|
|
- `maintenance` depends on `gsmaster`, `models`, `database`
|
|||
|
|
- `user` is depended on by all modules via `AuthMiddleware` import into `router`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Complete API Route Reference
|
|||
|
|
|
|||
|
|
### Public Routes (no authentication)
|
|||
|
|
```
|
|||
|
|
POST /register
|
|||
|
|
POST /login
|
|||
|
|
POST /password-reset/request
|
|||
|
|
GET /password-reset/validate/:token
|
|||
|
|
POST /password-reset/reset
|
|||
|
|
GET /api/public/version
|
|||
|
|
GET /api/public/systeminfo
|
|||
|
|
GET /api/pdf/file/:filename
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### User Routes (authenticated)
|
|||
|
|
```
|
|||
|
|
GET /api/user/profile
|
|||
|
|
PUT /api/user/display-name
|
|||
|
|
PUT /api/user/language
|
|||
|
|
PUT /api/user/email
|
|||
|
|
PUT /api/user/password
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### App System Routes
|
|||
|
|
```
|
|||
|
|
GET /api/version — version info (authenticated)
|
|||
|
|
GET /api/systeminfo — system info (authenticated)
|
|||
|
|
GET /api/public/version — version info (public, no auth)
|
|||
|
|
GET /api/public/systeminfo — system info (public, no auth)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Admin User Management (admin role required)
|
|||
|
|
```
|
|||
|
|
GET /api/users
|
|||
|
|
GET /api/users/:id
|
|||
|
|
PUT /api/users/:id/role
|
|||
|
|
DELETE /api/users/:id
|
|||
|
|
PUT /api/users/:id/password
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Character Routes
|
|||
|
|
```
|
|||
|
|
GET /api/characters — list own + shared characters
|
|||
|
|
POST /api/characters — create character
|
|||
|
|
GET /api/characters/:id — get full character (FeChar)
|
|||
|
|
PUT /api/characters/:id — update character
|
|||
|
|
PATCH /api/characters/:id — partial update
|
|||
|
|
DELETE /api/characters/:id — delete (owner only)
|
|||
|
|
PUT /api/characters/:id/image — update portrait
|
|||
|
|
GET /api/characters/:id/datasheet-options — dropdowns for editing
|
|||
|
|
|
|||
|
|
GET /api/characters/:id/shares — list share grants
|
|||
|
|
PUT /api/characters/:id/shares — update share grants
|
|||
|
|
GET /api/characters/:id/available-users — users available for sharing
|
|||
|
|
|
|||
|
|
GET /api/characters/:id/experience-wealth — EP + wealth snapshot
|
|||
|
|
PUT /api/characters/:id/experience — update EP
|
|||
|
|
PUT /api/characters/:id/wealth — update wealth
|
|||
|
|
|
|||
|
|
GET /api/characters/:id/audit-log — change history
|
|||
|
|
GET /api/characters/:id/audit-log/stats — audit statistics
|
|||
|
|
|
|||
|
|
POST /api/characters/lerncost-new — compute learning costs
|
|||
|
|
POST /api/characters/lerncost — alias for lerncost-new
|
|||
|
|
POST /api/characters/improve-skill-new — execute skill improvement
|
|||
|
|
POST /api/characters/improve-skill — alias
|
|||
|
|
POST /api/characters/:id/learn-skill-new — execute skill learning
|
|||
|
|
POST /api/characters/:id/learn-skill — alias
|
|||
|
|
POST /api/characters/:id/learn-spell-new — execute spell learning
|
|||
|
|
POST /api/characters/:id/learn-spell — alias
|
|||
|
|
|
|||
|
|
POST /api/characters/available-skills-new — available skills with costs
|
|||
|
|
POST /api/characters/available-skills — alias
|
|||
|
|
POST /api/characters/available-skills-creation — skills for creation wizard
|
|||
|
|
POST /api/characters/available-spells-creation — spells for creation wizard
|
|||
|
|
POST /api/characters/available-spells-new — available spells with costs
|
|||
|
|
POST /api/characters/available-spells — alias
|
|||
|
|
GET /api/characters/spell-details — spell detail lookup
|
|||
|
|
GET /api/characters/:id/reward-types — available reward types
|
|||
|
|
|
|||
|
|
GET /api/characters/:id/practice-points — PP by category
|
|||
|
|
PUT /api/characters/:id/practice-points — update PP
|
|||
|
|
POST /api/characters/:id/practice-points/add — add PP
|
|||
|
|
POST /api/characters/:id/practice-points/use — consume PP
|
|||
|
|
|
|||
|
|
GET /api/characters/skill-categories — static skill category list
|
|||
|
|
|
|||
|
|
GET /api/characters/create-sessions — list wizard sessions
|
|||
|
|
POST /api/characters/create-session — start new session
|
|||
|
|
GET /api/characters/create-session/:id — get session data
|
|||
|
|
PUT /api/characters/create-session/:id/basic — save step 1
|
|||
|
|
PUT /api/characters/create-session/:id/attributes — save step 2
|
|||
|
|
PUT /api/characters/create-session/:id/derived — save step 3
|
|||
|
|
PUT /api/characters/create-session/:id/skills — save step 4
|
|||
|
|
POST /api/characters/create-session/:id/finalize — create character from session
|
|||
|
|
DELETE /api/characters/create-session/:id — discard session
|
|||
|
|
|
|||
|
|
GET /api/characters/races — available races list
|
|||
|
|
GET /api/characters/classes — available character classes
|
|||
|
|
GET /api/characters/classes/learning-points — LP per class+category for creation
|
|||
|
|
GET /api/characters/origins — available origins
|
|||
|
|
GET /api/characters/beliefs — belief/faith search
|
|||
|
|
|
|||
|
|
POST /api/characters/calculate-static-fields — derived values (no dice)
|
|||
|
|
POST /api/characters/calculate-rolled-field — derived value requiring dice roll
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### GSMaster Routes (read: all authenticated; write: maintainer+)
|
|||
|
|
```
|
|||
|
|
GET /api/maintenance — all master data
|
|||
|
|
GET /api/maintenance/skills
|
|||
|
|
GET /api/maintenance/skills-enhanced
|
|||
|
|
GET /api/maintenance/skills/:id
|
|||
|
|
GET /api/maintenance/skills-enhanced/:id
|
|||
|
|
GET /api/maintenance/weaponskills[/*]
|
|||
|
|
GET /api/maintenance/spells[/*]
|
|||
|
|
GET /api/maintenance/equipment[/*]
|
|||
|
|
GET /api/maintenance/weapons[/*]
|
|||
|
|
POST/PUT/DELETE above (maintainer only)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Maintenance Routes (maintainer role required)
|
|||
|
|
```
|
|||
|
|
GET /api/maintenance/gsm-believes
|
|||
|
|
PUT /api/maintenance/gsm-believes/:id
|
|||
|
|
GET /api/maintenance/game-systems
|
|||
|
|
PUT /api/maintenance/game-systems/:id
|
|||
|
|
GET /api/maintenance/gsm-lit-sources
|
|||
|
|
PUT /api/maintenance/gsm-lit-sources/:id
|
|||
|
|
GET /api/maintenance/gsm-misc
|
|||
|
|
PUT /api/maintenance/gsm-misc/:id
|
|||
|
|
GET /api/maintenance/skill-improvement-cost2
|
|||
|
|
PUT /api/maintenance/skill-improvement-cost2/:id
|
|||
|
|
GET /api/maintenance/setupcheck
|
|||
|
|
GET /api/maintenance/setupcheck-dev
|
|||
|
|
GET /api/maintenance/mktestdata
|
|||
|
|
GET /api/maintenance/reconndb
|
|||
|
|
GET /api/maintenance/reloadenv
|
|||
|
|
POST /api/maintenance/transfer-sqlite-to-mariadb
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### PDF Export Routes
|
|||
|
|
```
|
|||
|
|
GET /api/pdf/templates — list available templates
|
|||
|
|
GET /api/pdf/export/:id — export character to PDF
|
|||
|
|
POST /api/pdf/cleanup — clean up old generated PDF files
|
|||
|
|
GET /api/pdf/file/:filename — download generated PDF (public, no auth)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Importer Routes
|
|||
|
|
```
|
|||
|
|
POST /api/importer/upload — import VTT JSON + CSV
|
|||
|
|
POST /api/importer/spells/csv — import spell CSV
|
|||
|
|
GET /api/importer/export/vtt/:id — export as VTT JSON
|
|||
|
|
GET /api/importer/export/vtt/:id/file — download VTT JSON file
|
|||
|
|
GET /api/importer/export/csv/:id — export as CSV
|
|||
|
|
GET /api/importer/export/spells/csv — export all spells as CSV
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Transfer Routes
|
|||
|
|
```
|
|||
|
|
GET /api/transfer/export/:id — export character JSON
|
|||
|
|
GET /api/transfer/download/:id — download character JSON file
|
|||
|
|
POST /api/transfer/import — import character JSON
|
|||
|
|
POST /api/transfer/database/export — export full database
|
|||
|
|
POST /api/transfer/database/import — import full database
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Infrastructure and Deployment
|
|||
|
|
|
|||
|
|
### Development Environment
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
docker/docker-compose.dev.yml
|
|||
|
|
bamort-backend-dev (port 8180) — Go source mounted + Air live-reload
|
|||
|
|
bamort-frontend-dev (port 5173) — Vue source mounted + Vite HMR
|
|||
|
|
bamort-mariadb-dev (port 3306) — MariaDB with health-check
|
|||
|
|
bamort-phpmyadmin-dev (port 8081) — Database GUI
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Full source code is volume-mounted into each container.
|
|||
|
|
- Backend uses `air -c .air.toml` for hot-reload on `.go` file changes.
|
|||
|
|
- Frontend uses Vite's native HMR for instant module replacement.
|
|||
|
|
- Database data persists in `docker/bamort-db-dev/` on the host.
|
|||
|
|
- Database is initialized from `docker/init-db/*.sql` on first container creation only.
|
|||
|
|
|
|||
|
|
### Production Environment
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
docker/docker-compose.yml
|
|||
|
|
bamort-backend (port 8182→8180) — compiled Go binary, no source mount
|
|||
|
|
bamort-frontend (port 8181→80) — Nginx serving pre-built Vue bundle
|
|||
|
|
bamort-mariadb (not exposed) — MariaDB accessible only within Docker network
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Backend image: multi-stage build. Stage 1 (golang:1.25-alpine) compiles the binary. Stage 2 (alpine:3.23) installs Chromium and copies only the binary + templates.
|
|||
|
|
- Frontend image: multi-stage. Stage 1 (node:22-alpine) runs `npm run build` with `VITE_*` build args baked in. Stage 2 (nginx:alpine) serves the static bundle.
|
|||
|
|
- **`VITE_API_URL`** is a **compile-time** build argument — runtime env vars cannot change it after image build.
|
|||
|
|
- Database port is intentionally not exposed in production.
|
|||
|
|
- TLS termination is expected at an external reverse proxy (e.g., Traefik).
|
|||
|
|
- Templates are volume-mounted: `./templates:/app/templates`.
|
|||
|
|
|
|||
|
|
### Starting/Stopping
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Development
|
|||
|
|
cd /data/dev/bamort
|
|||
|
|
./docker/start-dev.sh # builds + starts all dev containers
|
|||
|
|
./docker/stop-dev.sh # stops and removes dev containers
|
|||
|
|
|
|||
|
|
# Production
|
|||
|
|
./docker/start-prd.sh # builds new images while old containers run, then switches
|
|||
|
|
./docker/stop-prd.sh # stops production containers
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. User Roles and Access Control
|
|||
|
|
|
|||
|
|
| Capability | standard | maintainer | admin |
|
|||
|
|
|-----------|----------|-----------|-------|
|
|||
|
|
| Register / login | ✓ | ✓ | ✓ |
|
|||
|
|
| View own characters | ✓ | ✓ | ✓ |
|
|||
|
|
| View public/shared characters | ✓ | ✓ | ✓ |
|
|||
|
|
| Create/edit/delete own characters | ✓ | ✓ | ✓ |
|
|||
|
|
| Learn/improve skills and spells | ✓ | ✓ | ✓ |
|
|||
|
|
| Export characters to PDF | ✓ | ✓ | ✓ |
|
|||
|
|
| Import/export characters (JSON/CSV) | ✓ | ✓ | ✓ |
|
|||
|
|
| Read GSMaster data | ✓ | ✓ | ✓ |
|
|||
|
|
| Write GSMaster data | ✗ | ✓ | ✓ |
|
|||
|
|
| Maintenance operations | ✗ | ✓ | ✓ |
|
|||
|
|
| List all users | ✗ | ✗ | ✓ |
|
|||
|
|
| Change user roles | ✗ | ✗ | ✓ |
|
|||
|
|
| Delete users | ✗ | ✗ | ✓ |
|
|||
|
|
| Reset any user's password | ✗ | ✗ | ✓ |
|
|||
|
|
|
|||
|
|
**Enforcement mechanisms:**
|
|||
|
|
- Backend: `user.AuthMiddleware()` on all `/api/*` routes
|
|||
|
|
- Backend: `user.RequireMaintainer()` middleware on maintenance write routes
|
|||
|
|
- Backend: `user.RequireAdmin()` middleware on `/api/users/*` routes
|
|||
|
|
- Backend: `checkCharacterOwnership()` inside character handlers (returns 403 if user doesn't own the character)
|
|||
|
|
- Frontend: router navigation guard redirects non-admins away from `/users`
|
|||
|
|
- Frontend: `isOwner` prop controls visibility of edit/delete buttons in `CharacterDetails`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. Internationalisation (i18n)
|
|||
|
|
|
|||
|
|
**Implementation:** `vue-i18n` (v11, composition-mode `legacy: false`)
|
|||
|
|
|
|||
|
|
**Locales:** `src/locales/de/` and `src/locales/en/` — JS module files (not JSON) exporting translation objects. Both must be updated when adding new UI strings.
|
|||
|
|
|
|||
|
|
**Default language:** `de` (German)
|
|||
|
|
|
|||
|
|
**Language selection precedence:**
|
|||
|
|
1. User's stored preference in `localStorage.language`
|
|||
|
|
2. User's `preferred_language` field from their profile (set on `fetchCurrentUser()`)
|
|||
|
|
3. Fallback to `de`
|
|||
|
|
|
|||
|
|
**Language change flow:**
|
|||
|
|
- User changes language in `UserProfileView` → `PUT /api/user/language`
|
|||
|
|
- Frontend immediately sets `this.$i18n.locale` and updates `localStorage.language`
|
|||
|
|
- On next login, `userStore.fetchCurrentUser()` sets locale from profile
|
|||
|
|
|
|||
|
|
**Usage in components:**
|
|||
|
|
```vue
|
|||
|
|
{{ $t('skill.name') }} <!-- in template -->
|
|||
|
|
this.$t('error.notFound') <!-- in script -->
|
|||
|
|
this.$te('help.faq3.question') <!-- existence check for dynamic FAQ -->
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. Known Limitations and Security Notes
|
|||
|
|
|
|||
|
|
### Authentication Security
|
|||
|
|
|
|||
|
|
> **Important:** The current token scheme has cryptographic weaknesses.
|
|||
|
|
|
|||
|
|
1. **MD5 token generation** — MD5 is not a cryptographic authentication scheme. The token is derived predictably from `username + createdAt`, without a secret key. An attacker with knowledge of when a user registered could forge tokens.
|
|||
|
|
2. **No token signature verification** — `CheckToken` extracts the user ID from the token string and loads the user from the database; it does not verify that the token was actually issued by the server.
|
|||
|
|
3. **MD5 password hashing** — Passwords are hashed with MD5 (not bcrypt/argon2). MD5 is unsuitable for password storage; it is fast enough for brute-force attacks and not salted.
|
|||
|
|
4. **Token scope** — Tokens do not expire. Once issued, a token is valid indefinitely.
|
|||
|
|
|
|||
|
|
The bcrypt implementation is commented out in `user/handlers.go` and `user/model.go` — migration to bcrypt for future versions is anticipated but not yet active.
|
|||
|
|
|
|||
|
|
### Other Notes
|
|||
|
|
|
|||
|
|
- `backend/startserver.sh` contains hardcoded development credentials — this file should not be committed to public repositories.
|
|||
|
|
- The PDF download endpoint (`/api/pdf/file/:filename`) is unauthenticated by design (to allow direct browser downloads) but relies on filename unpredictability for security.
|
|||
|
|
- `FileUploadPage.vue` manually adds an `Authorization` header that is already added by the Axios interceptor — this is a redundancy, not a security issue.
|
|||
|
|
- The production `DATABASE_URL` in `docker-compose.yml` is constructed inline without fallback defaults — missing env vars will produce an empty credentials DSN.
|