Files
bamort/backend_findings.md
T

1363 lines
60 KiB
Markdown
Raw Normal View History

2026-04-01 15:16:12 +02:00
# BaMoRT Backend — Comprehensive Architecture & Code Findings
> Generated: 2026-03-13
> Source: `/home/de31a2/bamort/backend`
> Module: `bamort` (Go 1.24, toolchain 1.24.4)
---
## Table of Contents
1. [Module Structure Overview](#1-module-structure-overview)
2. [Entry Point — cmd/main.go](#2-entry-point--cmdmaingo)
3. [Router & Middleware Setup](#3-router--middleware-setup)
4. [Authentication & Authorization](#4-authentication--authorization)
5. [Configuration](#5-configuration)
6. [Database Layer](#6-database-layer)
7. [Models (GORM Entities)](#7-models-gorm-entities)
8. [Module-by-Module Analysis](#8-module-by-module-analysis)
- [user/](#81-user)
- [character/](#82-character)
- [equipment/](#83-equipment)
- [gsmaster/](#84-gsmaster)
- [gamesystem/](#85-gamesystem)
- [maintenance/](#86-maintenance)
- [pdfrender/](#87-pdfrender)
- [importer/](#88-importer)
- [transfer/](#89-transfer)
- [appsystem/](#810-appsystem)
- [logger/](#811-logger)
- [mail/](#812-mail)
- [router/](#813-router)
- [config/](#814-config)
- [database/](#815-database)
- [testutils/](#816-testutils)
- [api/](#817-api)
9. [Complete API Route Listing](#9-complete-api-route-listing)
10. [Testing Approach](#10-testing-approach)
11. [Key Dependencies](#11-key-dependencies)
12. [Security Observations](#12-security-observations)
13. [Cross-Module Relationships Diagram](#13-cross-module-relationships-diagram)
---
## 1. Module Structure Overview
```
backend/
├── cmd/ # Entry point (main.go)
├── config/ # App configuration loading from env/.env
├── database/ # DB connection, migrations, test helpers
├── models/ # All GORM entities and migration orchestration
├── router/ # Gin engine setup, CORS, base route group + JWT guard
├── user/ # User accounts, auth, roles, password reset
├── character/ # Core character CRUD, skills, spells, learning system
├── equipment/ # Character equipment (weapons, containers, gear)
├── gsmaster/ # Game-system master data (skills, spells, weapons, learning costs)
├── gamesystem/ # Game system record management and initial seed data
├── maintenance/ # Admin/maintainer ops DB checks, migrations, scratchpad endpoints
├── pdfrender/ # HTML→PDF export using chromedp + pdfcpu merge
├── importer/ # VTT-JSON / CSV import and export of characters/spells
├── transfer/ # JSON-based character and full-database import/export
├── appsystem/ # Version and system-info endpoints
├── logger/ # Custom leveled logger (DEBUG/INFO/WARN/ERROR)
├── mail/ # SMTP email client (password reset emails)
├── api/ # Integration tests spanning multiple modules
├── testutils/ # Shared test environment setup helpers
├── testdata/ # Prepared SQLite snapshot used by all tests
├── templates/ # PDF HTML templates (Default_A4_Quer/)
└── scripts/ # Shell scripts (e.g., SQLite→MariaDB transfer)
```
Each domain module follows the pattern:
```
module/
handlers.go HTTP handler functions (Gin controllers)
routes.go RegisterRoutes(r *gin.RouterGroup)
*_test.go Tests using setupTestEnvironment(t)
```
---
## 2. Entry Point — cmd/main.go
**File:** `cmd/main.go`
### Startup Sequence
```go
func main() {
cfg := config.Cfg // 1. Load config (auto-loaded in init())
logger.SetDebugMode(cfg.DebugMode) // 2. Configure logger
logger.SetMinLogLevel(...)
if cfg.IsProduction() {
gin.SetMode(gin.ReleaseMode) // 3. Set Gin mode
}
database.ConnectDatabase() // 4. Connect to database
pdfrender.InitializeTemplates(...) // 5. Sync PDF templates
r := gin.Default()
router.SetupGin(r) // 6. CORS middleware
protected := router.BaseRouterGrp(r) // 7. Public auth routes + JWT-protected group
// 8. Register module routes
user.RegisterRoutes(protected)
gsmaster.RegisterRoutes(protected)
character.RegisterRoutes(protected)
equipment.RegisterRoutes(protected)
maintenance.RegisterRoutes(protected)
importer.RegisterRoutes(protected)
pdfrender.RegisterRoutes(protected)
transfer.RegisterRoutes(protected)
appsystem.RegisterRoutes(protected)
// 9. Public routes (no auth)
pdfrender.RegisterPublicRoutes(r)
appsystem.RegisterPublicRoutes(r)
r.Run(cfg.GetServerAddress()) // 10. Start HTTP server
}
```
### Swagger Annotations
The file contains `@title BaMoRT API`, `@version 1`, `@host localhost:8180` indicates Swagger/OpenAPI doc generation is intended.
---
## 3. Router & Middleware Setup
### CORS (`router/setup.go`)
```go
func SetupGin(r *gin.Engine) {
allowedOrigins := []string{
config.Cfg.FrontendURL,
"http://localhost:5173",
"http://192.168.0.48:5173",
"http://192.168.0.36:5173",
"https://bamort.trokan.de",
}
r.Use(cors.New(cors.Config{
AllowOrigins: allowedOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * 3600,
}))
}
```
### Route Groups (`router/routes.go`)
**Public (unauthenticated) routes:**
| Method | Path | Handler |
|--------|------|---------|
| 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` |
**Protected group:** `/api` — guarded by `user.AuthMiddleware()`.
---
## 4. Authentication & Authorization
### Token Scheme
The project uses a **custom MD5-based token**, not JWT.
**Token Generation (`user/handlers.go`):**
```go
func GenerateToken(u *User) string {
tx := md5.Sum([]byte(u.Username + u.CreatedAt.String()))
hashString := hex.EncodeToString(tx[:])
// Insert ".{userID}:" at position 7
pos := 7
idm := "." + fmt.Sprintf("%d", u.UserID) + ":"
token := hashString[:pos] + idm + hashString[pos:]
return token
}
```
The token embeds the user ID in a predictable location. Clients send it as the `Authorization` header value (prefixed `Bearer `).
**Token Validation (`CheckToken`):**
1. Extracts user ID from position `7 + len("Bearer ")` in the `Authorization` header.
2. Loads the user from the database by that ID.
3. Returns the user or `nil`.
**No cryptographic signature is verified.** Any token with a valid user ID at the expected position is accepted.
### `AuthMiddleware()` (`user/handlers.go`)
Sets `userID`, `username`, and `user` in the Gin context for downstream handlers.
### Password Hashing
Passwords are hashed with **MD5** (not bcrypt):
```go
hashedPassword := md5.Sum([]byte(user.PasswordHash))
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
```
The bcrypt implementation is commented out.
### Roles (`user/model.go`)
| Constant | Value | Description |
|----------|-------|-------------|
| `RoleStandardUser` | `"standard"` | Default for new registrations |
| `RoleMaintainer` | `"maintainer"` | Can manage master data |
| `RoleAdmin` | `"admin"` | Can manage users |
Role-checking middleware:
- `RequireAdmin()` → used on `/api/users/*` admin endpoints
- `RequireMaintainer()` → used on maintenance/gsmaster write endpoints
---
## 5. Configuration
**File:** `config/config.go`
### `Config` Struct Fields
| Field | Env Var(s) | Default | Description |
|-------|-----------|---------|-------------|
| `ServerPort` | `API_PORT`, `SERVER_PORT` | `"8180"` | HTTP listen port |
| `DatabaseURL` | `DATABASE_URL` | `""` | DSN string for DB driver |
| `DatabaseType` | `DATABASE_TYPE` | `"mysql"` | `mysql` or `sqlite` |
| `DebugMode` | `DEBUG` | `false` | Enables verbose logging |
| `LogLevel` | `LOG_LEVEL` | `"INFO"` | `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `Environment` | `ENVIRONMENT`, `GO_ENV` | `"production"` | `production`, `development`, `test` |
| `DevTesting` | `DEVTESTING` | `"no"` | `"yes"` redirects to SQLite test DB |
| `FrontendURL` | `BASE_URL` | `"http://localhost:5173"` | CORS allowed origin |
| `TemplatesDir` | `TEMPLATES_DIR` | `"./templates"` | PDF template directory |
| `ExportTempDir` | `EXPORT_TEMP_DIR` | `"./xporttemp"` | Temporary PDF output directory |
| `MailHost` | `MAIL_HOST` | `""` | SMTP server hostname |
| `MailPort` | `MAIL_PORT` | `465` | SMTP port (465=TLS, 587=STARTTLS) |
| `MailUsername` | `MAIL_USERNAME` | `""` | SMTP credentials |
| `MailPassword` | `MAIL_PASSWORD` | `""` | SMTP credentials |
| `MailFrom` | `MAIL_FROM` | (falls back to username) | Sender email address |
### Loading Logic
1. Reads `.env` and `.env.local` files from the working directory.
2. Overrides with env vars.
3. `ENVIRONMENT=development` automatically enables `DebugMode=true` and `LogLevel=DEBUG`.
4. `ENVIRONMENT=test` or `DEVTESTING=yes` causes `ConnectDatabase()` to use the SQLite test snapshot.
---
## 6. Database Layer
**Package:** `database/`
### Connection (`database/config.go`)
```go
func ConnectDatabase() *gorm.DB {
if envIsTest || devTestingIsYes {
SetupTestDB() // Use prepared SQLite snapshot
} else {
ConnectDatabaseOrig() // MySQL or SQLite via DATABASE_URL
}
return DB
}
```
Supported drivers: `mysql` (default), `sqlite`.
### Global Variable
`database.DB *gorm.DB` — all packages import this directly.
### Test Database
- **Source:** `testdata/prepared_test_data.db` (SQLite snapshot with real test data including character ID 18 "Fanjo Vetrani")
- **Mechanism:** `SetupTestDB()` copies the snapshot to a `os.MkdirTemp` directory, opens it, and assigns it to `database.DB`.
- Each test run gets a fresh isolated copy.
### Migration Tables (`database/model.go`)
| Table | Purpose |
|-------|---------|
| `schema_version` | Current schema version tracking |
| `migration_history` | Log of applied migrations |
### Migration Orchestration (`models/database.go`)
`models.MigrateStructure(db)` calls in order:
1. `gameSystemMigrateStructure``game_systems`
2. `gsMasterMigrateStructure` — skills, spells, equipment, weapons, etc.
3. `characterMigrateStructure` — characters, skills, spells, equipment
4. `equipmentMigrateStructure`
5. `skillsMigrateStructure` — learning cost tables
6. `importerMigrateStructure` — (currently no-op)
7. `learningMigrateStructure` — all learning/cost relation tables
### `StringArray` Custom Type
```go
type StringArray []string
// Stored as JSON in TEXT column (Value+Scan implementations)
```
Used for `Char.Spezialisierung` (character specializations).
---
## 7. Models (GORM Entities)
### Base Types
```go
type BamortBase struct {
ID uint `gorm:"primaryKey"`
Name string
}
type BamortCharTrait struct {
BamortBase
CharacterID uint `gorm:"index"`
UserID uint `gorm:"index"`
}
type Magisch struct {
IstMagisch bool
Abw int
Ausgebrannt bool
}
```
---
### User Model (`user/model.go` → table: `users`)
| Field | Type | Notes |
|-------|------|-------|
| `UserID` | uint PK | |
| `Username` | string unique | |
| `DisplayName` | string | Falls back to Username if empty |
| `PasswordHash` | string | MD5 hex string |
| `Email` | string unique | |
| `Role` | string | `standard`, `maintainer`, `admin` |
| `PreferredLanguage` | string | default `de` |
| `ResetPwHash` | *string | Hidden from JSON serialization |
| `ResetPwHashExpires` | *time.Time | 14 days validity |
| `CreatedAt`, `UpdatedAt` | time.Time | |
---
### Character Model (`models/model_character.go` → table: `char_chars`)
**Main struct `Char`:**
| Field | Type | Notes |
|-------|------|-------|
| `ID` | uint PK | (via BamortBase) |
| `Name` | string | Character name |
| `GameSystem` | string | e.g. `"midgard"` |
| `GameSystemId` | uint | FK to `game_systems` |
| `UserID` | uint index | FK to `users` |
| `User` | User | FK `UserID→users.user_id` CASCADE |
| `Rasse` | string | Race (e.g. "Mensch", "Zwerg") |
| `Typ` | string | Class code (e.g. "Kr", "Ma") |
| `Alter` | int | Age |
| `Anrede` | string | Title/salutation |
| `Grad` | int | Character grade/level |
| `Gender` | string | |
| `SocialClass` | string | |
| `Groesse` | int | Height (cm) |
| `Gewicht` | int | Weight (kg) |
| `Herkunft` | string | Origin/homeland |
| `Glaube` | string | Faith |
| `Hand` | string | Dominant hand |
| `Public` | bool | Publicly visible |
| `ResistenzKoerper` | int | Body resistance (derived) |
| `ResistenzGeist` | int | Mental resistance (derived) |
| `Abwehr` | int | Defense value (derived) |
| `Zaubern` | int | Spell casting value (derived) |
| `Raufen` | int | Brawling value (derived) |
| `Lp` | Lp | Life points (has-one, CASCADE) |
| `Ap` | Ap | Action points (has-one) |
| `B` | B | Load/burden (has-one) |
| `Merkmale` | Merkmale | Physical traits (has-one) |
| `Eigenschaften` | []Eigenschaft | Attributes Au/Gs/Gw/In/Ko/St/Wk/Zt/PA |
| `Fertigkeiten` | []SkFertigkeit | Skills (has-many) |
| `Waffenfertigkeiten` | []SkWaffenfertigkeit | Weapon skills |
| `Zauber` | []SkZauber | Spells |
| `Spezialisierung` | StringArray | TEXT/JSON stored |
| `Bennies` | Bennies | Gg/Gp/Sg |
| `Vermoegen` | Vermoegen | GS/SS/KS wealth |
| `Erfahrungsschatz` | Erfahrungsschatz | EP/ES experience |
| `Waffen` | []EqWaffe | Weapons (has-many) |
| `Behaeltnisse` | []EqContainer | Containers |
| `Transportmittel` | []EqContainer | Transportation |
| `Ausruestung` | []EqAusruestung | General equipment |
| `Image` | string | Base64 or URL |
**Helper types:**
| Type | Table | Fields |
|------|-------|--------|
| `Eigenschaft` | (embedded in preload) | ID, CharacterID, UserID, Name, Value |
| `Lp` | — | ID, CharacterID, Max, Value |
| `Ap` | — | ID, CharacterID, Max, Value |
| `B` | — | ID, CharacterID, Max, Value |
| `Merkmale` | — | BamortCharTrait + Augenfarbe/Haarfarbe/Sonstige/Breite/Groesse |
| `Erfahrungsschatz` | — | BamortCharTrait + ES, EP |
| `Bennies` | — | BamortCharTrait + Gg, Gp, Sg |
| `Vermoegen` | — | BamortCharTrait + Goldstuecke, Silberstuecke, Kupferstuecke |
**`FeChar`** (frontend-facing):
Embeds `Char` + `Git` (gift tolerance = 30+Ko/2) + `CategorizedSkills` map + `InnateSkills` slice.
---
### Skill Models (`models/model_char_skills.go`)
| Type | Table | Key Fields |
|------|-------|------------|
| `SkFertigkeit` | `char_skills` | BamortCharTrait + Fertigkeitswert, BasisWert, Bonus, Pp, Category, Improvable, LearningCost |
| `SkWaffenfertigkeit` | `char_weaponskills` | Embeds SkFertigkeit |
| `SkAngeboreneFertigkeit` | (no own table) | Embeds SkFertigkeit |
| `SkZauber` | `char_spells` | BamortCharTrait + Beschreibung, Bonus, Quelle |
---
### Equipment Models (`models/model_char_equipment.go`)
| Type | Table | Key Fields |
|------|-------|------------|
| `EqAusruestung` | `equi_equipments` | BamortCharTrait + Magisch + Anzahl, ContainedIn, Bonus, Gewicht, Wert |
| `EqWaffe` | `equi_weapons` | BamortCharTrait + Magisch + Abwb, Anb, Anzahl, Schb, NameFuerSpezialisierung |
| `EqContainer` | `equi_containers` | BamortCharTrait + Magisch + IsTransportation, Tragkraft, Volumen, ExtID |
---
### GSMaster Models (`models/model_gsmaster.go`)
| Type | Table | Key Fields |
|------|-------|------------|
| `Skill` | `gsm_skills` | ID, GameSystem/Id, Name, Initialwert, BasisWert, Bonuseigenschaft, Improvable, InnateSkill, Category, Difficulty |
| `WeaponSkill` | (inherits Skill) | Embeds Skill |
| `Spell` | (via gsmaster) | ID, Name, Stufe, AP, Art, Zauberdauer, Reichweite, Ursprung, Category, LearningCategory |
| `Equipment` | (via gsmaster) | ID, Name, Gewicht, Wert, PersonalItem |
| `Weapon` | (extends Equipment) | SkillRequired, Damage, RangeNear/Middle/Far |
| `Container` | (extends Equipment) | Tragkraft, Volumen |
| `Transportation` | (extends Container) | — |
| `Believe` | (via gsmaster) | ID, GameSystem/Id, Name, Beschreibung, SourceID |
| `MiscLookup` | `gsm_misc_lookups` | ID, GameSystem/Id, Key, Value |
| `LookupList` | `gsm_lookup_lists` | Generic lookup |
| `LearnCost` | (computed, no own table) | GameSystem, Stufe, LE, TE, Ep, Money, PP |
---
### Game System (`models/model_game_system.go` → table: `game_systems`)
| Field | Notes |
|-------|-------|
| `Code` | Unique, e.g. `"M5"` |
| `Name` | e.g. `"M-System"` |
| `IsActive` | bool |
Default game system: Code=`M5`, Name=`M-System`.
---
### Learning-Cost Models (`models/model_learning_costs.go`)
| Type | Table | Purpose |
|------|-------|---------|
| `Source` | `gsm_lit_sources` | Rulebook/sourcebook references |
| `CharacterClass` | `gsm_character_classes` | Class codes (Kr, Ma, Hx …) + source link |
| `SkillCategory` | `learning_skill_categories` | Alltag, Freiland, Halbwelt, Kampf, Körper, Sozial, Unterwelt, Waffen, Wissen |
| `SkillDifficulty` | `learning_skill_difficulties` | leicht, normal, schwer, sehr schwer |
| `SpellSchool` | `learning_spell_schools` | Beherrschen, Bewegen, Erkennen, … |
| `ClassCategoryEPCost` | `learning_class_category_ep_costs` | EP per TE by class+category |
| `ClassSpellSchoolEPCost` | `learning_class_spell_school_ep_costs` | EP per LE by class+school |
| `SpellLevelLECost` | `learning_spell_level_le_costs` | LE required by spell level |
| `SkillCategoryDifficulty` | `learning_skill_category_difficulties` | LE cost for skill in category+difficulty |
| `WeaponSkillCategoryDifficulty` | (separate table) | LE cost for weapon skills |
| `SkillImprovementCost` | `learning_skill_improvement_costs` | TE to improve by current level |
| `ClassCategoryLearningPoints` | — | Starting LP per class+category for creation |
| `AuditLogEntry` | `audit_log_entries`(?) | EP/gold change audit trail |
---
### Character Creation Session (`models/model_character_creation.go`)
| Type | Table | Purpose |
|------|-------|---------|
| `CharacterCreationSession` | `char_creation_sessions` | Multi-step wizard persistence |
Fields: UserID, Name, Rasse, Typ, Herkunft, Stand, Glaube, Attributes (JSON), DerivedValues (JSON), Skills (JSON), Spells (JSON), SkillPoints (JSON), CurrentStep, ExpiresAt.
---
### Character Share (`models/model_character_share.go` → table: `char_shares`)
| Field | Notes |
|-------|-------|
| `CharacterID` | FK to char_chars |
| `UserID` | FK to users (recipient) |
| `Permission` | `"read"` or `"write"` |
---
### Database Schema/Migration Models (`database/model.go`)
| Type | Table |
|------|-------|
| `SchemaVersion` | `schema_version` |
| `MigrationHistory` | `migration_history` |
---
## 8. Module-by-Module Analysis
---
### 8.1 `user/`
**Purpose:** User account management, authentication, authorization, password reset.
**Key Files:**
- `model.go` — User struct, CRUD methods, reset-hash helpers
- `handlers.go` — RegisterUser, LoginUser, CheckToken, AuthMiddleware, password reset
- `admin_handlers.go` — ListUsers, GetUser, UpdateUserRole, ChangeUserPassword, DeleteUser
- `middleware.go` — RequireRole, RequireAdmin, RequireMaintainer
- `database.go``MigrateStructure` for `users` table
- `routes.go` — Route registration
**Business Logic:**
- Registration creates users with MD5-hashed passwords and a default `standard` role.
- Login returns a custom MD5 token containing the encoded user ID.
- Password reset uses a 32-byte random hex hash (secure) with a 14-day expiry, sent via SMTP.
- `AuthMiddleware` validates the token structure, loads the user from DB, injects `userID`/`username`/`user` into context.
**HTTP Endpoints (protected unless noted):**
| Method | Path | Handler | Auth level |
|--------|------|---------|-----------|
| POST | `/register` | `RegisterUser` | Public |
| POST | `/login` | `LoginUser` | Public |
| POST | `/password-reset/request` | `RequestPasswordReset` | Public |
| GET | `/password-reset/validate/:token` | `ValidateResetToken` | Public |
| POST | `/password-reset/reset` | `ResetPassword` | Public |
| GET | `/api/user/profile` | `GetUserProfile` | Auth |
| PUT | `/api/user/display-name` | `UpdateDisplayName` | Auth |
| PUT | `/api/user/email` | `UpdateEmail` | Auth |
| PUT | `/api/user/password` | `UpdatePassword` | Auth |
| PUT | `/api/user/language` | `UpdateLanguage` | Auth |
| GET | `/api/users` | `ListUsers` | Admin |
| GET | `/api/users/:id` | `GetUser` | Admin (own profile also allowed) |
| PUT | `/api/users/:id/role` | `UpdateUserRole` | Admin |
| PUT | `/api/users/:id/password` | `ChangeUserPassword` | Admin |
| DELETE | `/api/users/:id` | `DeleteUser` | Admin |
---
### 8.2 `character/`
**Purpose:** Core character management — CRUD, skill/spell learning system, character creation wizard, audit logging, sharing, practice points, PDF-relevant data prep.
**Key Files:**
- `handlers.go` — ListCharacters, CreateCharacter, GetCharacter, UpdateCharacter, DeleteCharacter, ToFeChar, UpdateCharacterImage, GetDatasheetOptions
- `lerncost_handler.go``GetLernCostNewSystem`, `ImproveSkill`, `LearnSkill`, `LearnSpell` — full learning economy
- `derived_values_calculator.go``CalculateStaticFields`, `CalculateRolledField` — character creation math
- `creation_rules.go``GetSpecialAbilityByRoll` — special ability table
- `audit_log.go``CreateAuditLogEntry`, `GetAuditLogForCharacter/Field`
- `share_handlers.go``GetCharacterShares`, `UpdateCharacterShares`, `GetAvailableUsersForSharing`
- `practice_points_handler.go``GetPracticePoints`, `UpdatePracticePoints`, `AddPracticePoint`, `UsePracticePoint`
- `skill_type_helper.go` — skill categorization helpers
- `system_information_handlers.go``GetSkillCategoriesHandlerStatic`
- `spell_utils.go` — spell availability and creation utilities
- `database.go``SaveCharacterToDB`
- `image_handler.go` — character image upload/handling
**Business Logic:**
- `ListCharacters`: Returns `{ self_owned: [], others: [] }` where `others` = public + shared.
- `GetCharacter`: Returns `FeChar` (augmented view with categorized skills).
- `toFeChar`: Adds bonus field from attributes, splits skills by category.
- `checkCharacterOwnership`: Guards update/delete/share with `character.UserID == c.GetUint("userID")`.
- **Learning System** (`GetLernCostNewSystem`): Calculates LE/TE/EP/Money cost for learning or improving a skill/spell/weapon. Considers: character class, skill category, difficulty, current level, practice points (PP) reductions, gold-for-EP conversion, reward types (free learning, half EP).
- **Derived Values**: `CalculateStaticFieldsLogic` implements Midgard 5th edition formulae for Ausdauer-, Schadens-, Angriffs-, Abwehr-, Zauber-Bonus, Resistenz, Abwehr, Zaubern, Raufen.
**HTTP Endpoints (`/api/characters/...`):**
| Method | Path | Handler |
|--------|------|---------|
| GET | `/api/characters` | `ListCharacters` |
| POST | `/api/characters` | `CreateCharacter` |
| GET | `/api/characters/:id` | `GetCharacter` |
| PUT/PATCH | `/api/characters/:id` | `UpdateCharacter` |
| DELETE | `/api/characters/:id` | `DeleteCharacter` |
| PUT | `/api/characters/:id/image` | `UpdateCharacterImage` |
| GET | `/api/characters/:id/datasheet-options` | `GetDatasheetOptions` |
| GET | `/api/characters/:id/shares` | `GetCharacterShares` |
| PUT | `/api/characters/:id/shares` | `UpdateCharacterShares` |
| GET | `/api/characters/:id/available-users` | `GetAvailableUsersForSharing` |
| GET | `/api/characters/:id/experience-wealth` | `GetCharacterExperienceAndWealth` |
| PUT | `/api/characters/:id/experience` | `UpdateCharacterExperience` |
| PUT | `/api/characters/:id/wealth` | `UpdateCharacterWealth` |
| GET | `/api/characters/:id/audit-log` | `GetCharacterAuditLog` |
| GET | `/api/characters/:id/audit-log/stats` | `GetAuditLogStats` |
| POST | `/api/characters/lerncost-new` | `GetLernCostNewSystem` |
| POST | `/api/characters/lerncost` | `GetLernCostNewSystem` |
| POST | `/api/characters/improve-skill-new` | `ImproveSkill` |
| POST | `/api/characters/improve-skill` | `ImproveSkill` |
| POST | `/api/characters/:id/learn-skill-new` | `LearnSkill` |
| POST | `/api/characters/:id/learn-skill` | `LearnSkill` |
| POST | `/api/characters/:id/learn-spell-new` | `LearnSpell` |
| POST | `/api/characters/:id/learn-spell` | `LearnSpell` |
| POST | `/api/characters/available-skills-new` | `GetAvailableSkillsNewSystem` |
| POST | `/api/characters/available-skills` | `GetAvailableSkillsNewSystem` |
| POST | `/api/characters/available-skills-creation` | `GetAvailableSkillsForCreation` |
| POST | `/api/characters/available-spells-creation` | `GetAvailableSpellsForCreation` |
| POST | `/api/characters/available-spells-new` | `GetAvailableSpellsNewSystem` |
| POST | `/api/characters/available-spells` | `GetAvailableSpellsNewSystem` |
| GET | `/api/characters/spell-details` | `GetSpellDetails` |
| GET | `/api/characters/:id/reward-types` | `GetRewardTypesStatic` |
| GET | `/api/characters/:id/practice-points` | `GetPracticePoints` |
| PUT | `/api/characters/:id/practice-points` | `UpdatePracticePoints` |
| POST | `/api/characters/:id/practice-points/add` | `AddPracticePoint` |
| POST | `/api/characters/:id/practice-points/use` | `UsePracticePoint` |
| GET | `/api/characters/skill-categories` | `GetSkillCategoriesHandlerStatic` |
| GET | `/api/characters/create-sessions` | `ListCharacterSessions` |
| POST | `/api/characters/create-session` | `CreateCharacterSession` |
| GET | `/api/characters/create-session/:sessionId` | `GetCharacterSession` |
| PUT | `/api/characters/create-session/:sessionId/basic` | `UpdateCharacterBasicInfo` |
| PUT | `/api/characters/create-session/:sessionId/attributes` | `UpdateCharacterAttributes` |
| PUT | `/api/characters/create-session/:sessionId/derived` | `UpdateCharacterDerivedValues` |
| PUT | `/api/characters/create-session/:sessionId/skills` | `UpdateCharacterSkills` |
| POST | `/api/characters/create-session/:sessionId/finalize` | `FinalizeCharacterCreation` |
| DELETE | `/api/characters/create-session/:sessionId` | `DeleteCharacterSession` |
| GET | `/api/characters/races` | `GetRaces` |
| GET | `/api/characters/classes` | `GetCharacterClasses` |
| GET | `/api/characters/classes/learning-points` | `GetCharacterClassLearningPoints` |
| GET | `/api/characters/origins` | `GetOrigins` |
| GET | `/api/characters/beliefs` | `SearchBeliefs` |
| POST | `/api/characters/calculate-static-fields` | `CalculateStaticFields` |
| POST | `/api/characters/calculate-rolled-field` | `CalculateRolledField` |
---
### 8.3 `equipment/`
**Purpose:** CRUD for character-owned equipment items and weapons.
**Key Files:** `handlers.go`, `routes.go`
**Business Logic:**
- `checkEquipmentOwnership`: Loads character by ID, compares `character.UserID` to logged-in user.
- All operations (CRUD) filter/verify ownership before executing DB writes.
**HTTP Endpoints:**
| Method | Path | Handler |
|--------|------|---------|
| POST | `/api/equipment` | `CreateAusruestung` |
| GET | `/api/equipment/character/:character_id` | `ListAusruestung` |
| PUT | `/api/equipment/:ausruestung_id` | `UpdateAusruestung` |
| DELETE | `/api/equipment/:ausruestung_id` | `DeleteAusruestung` |
| POST | `/api/weapons` | `CreateWaffe` |
| GET | `/api/weapons/character/:character_id` | `ListWaffen` |
| PUT | `/api/weapons/:waffe_id` | `UpdateWaffe` |
| DELETE | `/api/weapons/:waffe_id` | `DeleteWaffe` |
---
### 8.4 `gsmaster/`
**Purpose:** Game-system master data management — the reference library for skills, weapon skills, spells, equipment, weapons, and learning costs.
**Key Files:**
- `handlers.go` — Generic get/add/update/delete using Go generics (`getMDItem[T]`, `getMDItems[T]`, etc.)
- `routes.go` — Public read endpoints + maintainer-guarded write endpoints
- `learning_costs.go``LearningCostsTable` with full EP/TE cost tables for all 15 character classes
- `learning_costs_init.go``ValidateLearningCostsData`, `GetLearningCostsSummary`
- `levelup.go``CalculateImprovementCost`, `CalculateSkillImprovementCost`
- `game_system.go` — Game system lookup helper
- `misclookup.go``GetMiscLookupByKey`, `GetMiscLookupByKeyForSystem` (races, faiths, origins, etc.)
- `skill_enhanced_handlers.go`, `spell_enhanced_handlers.go`, `weapon_enhanced_handlers.go`, `weapon_skill_enhanced_handlers.go`, `equipment_enhanced_handlers.go` — Enhanced CRUD with source/difficulty enrichment
**Business Logic:**
- Learning costs are stored both as hardcoded Go maps (in `learning_costs.go`, 15 classes × 8-9 categories) and in the database (`ClassCategoryEPCost` tables).
- `GetMiscLookupByKey("faiths")` merges entries from both `gsm_misc_lookups` and `gsm_believes` tables.
- Generic handlers use interface constraints (`Creator`, `Saver`, `FirstIdGetter`, `Deleter`) via Go generics for DRY CRUD.
**HTTP Endpoints (`/api/maintenance/...`):**
| Method | Path | Handler | Auth Level |
|--------|------|---------|-----------|
| GET | `/api/maintenance` | `GetMasterData` | Auth |
| GET | `/api/maintenance/skills` | `GetMDSkills` | Auth |
| GET | `/api/maintenance/skills-enhanced` | `GetEnhancedMDSkills` | Auth |
| GET | `/api/maintenance/skills/:id` | `GetMDSkill` | Auth |
| GET | `/api/maintenance/skills-enhanced/:id` | `GetEnhancedMDSkill` | Auth |
| GET | `/api/maintenance/weaponskills` | `GetMDWeaponSkills` | Auth |
| GET | `/api/maintenance/weaponskills-enhanced` | `GetEnhancedMDWeaponSkills` | Auth |
| GET | `/api/maintenance/weaponskills/:id` | `GetMDWeaponSkill` | Auth |
| GET | `/api/maintenance/weaponskills-enhanced/:id` | `GetEnhancedMDWeaponSkill` | Auth |
| GET | `/api/maintenance/spells` | `GetMDSpells` | Auth |
| GET | `/api/maintenance/spells-enhanced` | `GetEnhancedMDSpells` | Auth |
| GET | `/api/maintenance/spells/:id` | `GetMDSpell` | Auth |
| GET | `/api/maintenance/spells-enhanced/:id` | `GetEnhancedMDSpell` | Auth |
| GET | `/api/maintenance/equipment` | `GetMDEquipments` | Auth |
| GET | `/api/maintenance/equipment-enhanced` | `GetEnhancedMDEquipment` | Auth |
| GET | `/api/maintenance/equipment/:id` | `GetMDEquipment` | Auth |
| GET | `/api/maintenance/equipment-enhanced/:id` | `GetEnhancedMDEquipmentItem` | Auth |
| GET | `/api/maintenance/weapons` | `GetMDWeapons` | Auth |
| GET | `/api/maintenance/weapons-enhanced` | `GetEnhancedMDWeapons` | Auth |
| GET | `/api/maintenance/weapons/:id` | `GetMDWeapon` | Auth |
| GET | `/api/maintenance/weapons-enhanced/:id` | `GetEnhancedMDWeapon` | Auth |
| POST | `/api/maintenance/skills-enhanced` | `CreateEnhancedMDSkill` | Maintainer |
| PUT | `/api/maintenance/skills/:id` | `UpdateMDSkill` | Maintainer |
| PUT | `/api/maintenance/skills-enhanced/:id` | `UpdateEnhancedMDSkill` | Maintainer |
| POST | `/api/maintenance/skills` | `AddSkill` | Maintainer |
| DELETE | `/api/maintenance/skills/:id` | `DeleteMDSkill` | Maintainer |
| PUT | `/api/maintenance/weaponskills/:id` | `UpdateMDWeaponSkill` | Maintainer |
| PUT | `/api/maintenance/weaponskills-enhanced/:id` | `UpdateEnhancedMDWeaponSkill` | Maintainer |
| POST | `/api/maintenance/weaponskills` | `AddWeaponSkill` | Maintainer |
| DELETE | `/api/maintenance/weaponskills/:id` | `DeleteMDWeaponSkill` | Maintainer |
| PUT | `/api/maintenance/spells/:id` | `UpdateMDSpell` | Maintainer |
| PUT | `/api/maintenance/spells-enhanced/:id` | `UpdateEnhancedMDSpell` | Maintainer |
| POST | `/api/maintenance/spells` | `AddSpell` | Maintainer |
| DELETE | `/api/maintenance/spells/:id` | `DeleteMDSpell` | Maintainer |
| PUT | `/api/maintenance/equipment/:id` | `UpdateMDEquipment` | Maintainer |
| PUT | `/api/maintenance/equipment-enhanced/:id` | `UpdateEnhancedMDEquipmentItem` | Maintainer |
| POST | `/api/maintenance/equipment` | `AddEquipment` | Maintainer |
| DELETE | `/api/maintenance/equipment/:id` | `DeleteMDEquipment` | Maintainer |
| PUT | `/api/maintenance/weapons/:id` | `UpdateMDWeapon` | Maintainer |
| PUT | `/api/maintenance/weapons-enhanced/:id` | `UpdateEnhancedMDWeapon` | Maintainer |
| POST | `/api/maintenance/weapons` | `AddWeapon` | Maintainer |
| DELETE | `/api/maintenance/weapons/:id` | `DeleteMDWeapon` | Maintainer |
---
### 8.5 `gamesystem/`
**Purpose:** Minimal package that handles migration of the `game_systems` table and seeds the initial Midgard 5 game system.
**Key File:** `database.go`
- `MigrateStructure(db)` — AutoMigrates `GameSystem`.
- `MigrateDataIfNeeded(db)` — Creates `{Code:"M5", Name:"M-System"}` if no row with ID=1 exists.
No HTTP endpoints registered.
---
### 8.6 `maintenance/`
**Purpose:** Admin/maintainer tooling — DB health checks, data migrations, test data generation, SQLite→MariaDB transfer.
**Key Files:**
- `handlers.go` — SetupCheck, MakeTestdataFromLive, ReconnectDataBase, ReloadENV, TransferSQLiteToMariaDB, migrateAllStructures
- `masterdata_handlers.go` — GetGameSystems, UpdateGameSystem, GetLitSources, UpdateLitSource, GetMisc, UpdateMisc, GetSkillImprovementCost2, UpdateSkillImprovementCost2
- `believe_handlers.go` — GetBelieves, UpdateBelieve
- `skill_migration.go` — Skill-related migration helpers
**Business Logic:**
- `migrateAllStructures` calls all module `MigrateStructure()` functions in correct dependency order.
- `MakeTestdataFromLive` exports current live DB to a SQLite file for test snapshots.
- `TransferSQLiteToMariaDB` copies records table-by-table from SQLite to MariaDB.
- All endpoints require `RequireMaintainer()` or `RequireAdmin()` — no public access.
**HTTP Endpoints (`/api/maintenance/...` — Maintainer required):**
| Method | Path | Handler |
|--------|------|---------|
| GET | `/api/maintenance/gsm-believes` | `GetBelieves` |
| PUT | `/api/maintenance/gsm-believes/:id` | `UpdateBelieve` |
| GET | `/api/maintenance/game-systems` | `GetGameSystems` |
| PUT | `/api/maintenance/game-systems/:id` | `UpdateGameSystem` |
| GET | `/api/maintenance/gsm-lit-sources` | `GetLitSources` |
| PUT | `/api/maintenance/gsm-lit-sources/:id` | `UpdateLitSource` |
| GET | `/api/maintenance/gsm-misc` | `GetMisc` |
| PUT | `/api/maintenance/gsm-misc/:id` | `UpdateMisc` |
| GET | `/api/maintenance/skill-improvement-cost2` | `GetSkillImprovementCost2` |
| PUT | `/api/maintenance/skill-improvement-cost2/:id` | `UpdateSkillImprovementCost2` |
| GET | `/api/maintenance/setupcheck` | `SetupCheck` |
| GET | `/api/maintenance/setupcheck-dev` | `SetupCheckDev` |
| GET | `/api/maintenance/mktestdata` | `MakeTestdataFromLive` |
| GET | `/api/maintenance/reconndb` | `ReconnectDataBase` |
| GET | `/api/maintenance/reloadenv` | `ReloadENV` |
| POST | `/api/maintenance/transfer-sqlite-to-mariadb` | `TransferSQLiteToMariaDB` |
*(These overlap with the `gsmaster` maintenance prefix — both modules register under `/api/maintenance` with `gsmaster` first, then `maintenance` module adds its own routes. Both use `RequireMaintainer()` for writes.)*
---
### 8.7 `pdfrender/`
**Purpose:** Generate multi-page PDF character sheets from HTML templates using headless Chromium + pdfcpu merge.
**Key Files:**
- `handlers.go``ListTemplates`, `ExportCharacterToPDF`, `CleanupExportTemp`, `GetPDFFile`
- `init.go``InitializeTemplates` — copies bundled templates to `TemplatesDir` on startup
- `chromedp.go``NewPDFRenderer`, `RenderHTMLToPDF` using chromedp
- `mapper.go``MapCharacterToViewModel` — maps `Char` to `CharacterSheetViewModel`
- `viewmodel.go` — All view model types (`CharacterSheetViewModel`, `CharacterInfo`, `AttributeValues`, `DerivedValueSet`, `SkillViewModel`, `WeaponViewModel`, `SpellViewModel`, `EquipmentViewModel`, `MagicItemViewModel`, `PageMeta`)
- `render_with_continuation.go``RenderPageWithContinuations` — auto-generates overflow pages
- `pagination.go` / `pagination_helper.go` — item distribution across pages
- `fill_capacity.go` — reads `<!-- MaxItems: N -->` comments from HTML templates
- `template_parser.go` / `template_metadata.go` — template loading and Go `html/template` execution
- `inline_resources.go` — inlines CSS/images into HTML before rendering
- `file_management.go` — manages `xporttemp` directory
**Business Logic:**
1. On startup: sync `./default_templates``cfg.TemplatesDir`.
2. `ExportCharacterToPDF`:
- Loads character, maps to view model.
- Renders pages 14 (`page_1.html` through `page_4.html`) with continuation logic.
- Each page checks `<!-- MaxItems: N -->` to determine when to create a continuation page.
- All rendered PDFs are merged via `pdfcpu`.
- Saves to `xporttemp/` directory, returns `{"filename": "..."}`.
3. `GetPDFFile`: public endpoint, serves files from `xporttemp/` by filename.
**HTTP Endpoints:**
| Method | Path | Handler | Auth |
|--------|------|---------|------|
| GET | `/api/pdf/templates` | `ListTemplates` | Auth |
| GET | `/api/pdf/export/:id` | `ExportCharacterToPDF` | Auth |
| POST | `/api/pdf/cleanup` | `CleanupExportTemp` | Auth |
| GET | `/api/pdf/file/:filename` | `GetPDFFile` | Public |
---
### 8.8 `importer/`
**Purpose:** Import characters from VTT JSON format or CSV; export characters as VTT JSON, CSV, or spell CSV.
**Key Files:**
- `handler.go``UploadFiles`, `ImportSpellCSVHandler`
- `importer.go``ImportChar`, `CheckSkill`, VTT import logic
- `importVTTJson.go``ImportVTTJSON` — main VTT import driver
- `exporter.go``ExportCharToVTT`, `ExportCharacterToCSV`
- `model.go``CharacterImport` struct (VTT format), sub-structs for Fertigkeiten/Spells/Equipment
- `routes.go` — Route registration
**Business Logic:**
- `UploadFiles`: Accepts `file_vtt` (required) + `file_csv` (optional), validates extensions (.csv/.json), calls `ImportVTTJSON`.
- `CheckSkill`: Looks up skill in `gsm_skills`, auto-creates if `autocreate=true`.
- Source IDs are cached (`sourceIDCache`) to reduce DB queries during bulk import.
- `ExportCharacterVTTFileHandler`: Returns character data as downloadable `.json` file.
**HTTP Endpoints:**
| Method | Path | Handler |
|--------|------|---------|
| POST | `/api/importer/upload` | `UploadFiles` |
| POST | `/api/importer/spells/csv` | `ImportSpellCSVHandler` |
| GET | `/api/importer/export/vtt/:id` | `ExportCharacterVTTHandler` |
| GET | `/api/importer/export/vtt/:id/file` | `ExportCharacterVTTFileHandler` |
| GET | `/api/importer/export/csv/:id` | `ExportCharacterCSVHandler` |
| GET | `/api/importer/export/spells/csv` | `ExportSpellsCSVHandler` |
---
### 8.9 `transfer/`
**Purpose:** JSON-serialized character import/export and full database backup/restore.
**Key Files:**
- `handlers.go` — HTTP handlers for all transfer operations
- `exporter.go``ExportCharacter` — serializes a full `Char` + metadata to `CharacterExport`
- `importer.go``ImportCharacter` — deserializes and recreates character in DB
- `database.go``ExportDatabase`, `ImportDatabase` — full table serialization
- `routes.go` — Route registration
**Business Logic:**
- `ExportCharacter` / `ImportCharacter`: Full round-trip JSON serialization of characters with all associations.
- `ExportDatabase` / `ImportDatabase`: Table-level JSON snapshot used for backup/restore.
- `DownloadCharacterHandler` sets `Content-Disposition: attachment` for browser download.
**HTTP Endpoints:**
| Method | Path | Handler |
|--------|------|---------|
| GET | `/api/transfer/export/:id` | `ExportCharacterHandler` |
| GET | `/api/transfer/download/:id` | `DownloadCharacterHandler` |
| POST | `/api/transfer/import` | `ImportCharacterHandler` |
| POST | `/api/transfer/database/export` | `ExportDatabaseHandler` |
| POST | `/api/transfer/database/import` | `ImportDatabaseHandler` |
---
### 8.10 `appsystem/`
**Purpose:** Exposes application version and system info (user/character counts, DB version).
**Key Files:**
- `version.go``Version = "0.2.4"`, `GetInfo()`, `GetInfo2()`
- `handlers.go``Versionsinfo`, `SystemInfo`
- `routes.go` — Route registration (both protected and public)
**HTTP Endpoints:**
| Method | Path | Handler | Auth |
|--------|------|---------|------|
| GET | `/api/version` | `Versionsinfo` | Auth |
| GET | `/api/systeminfo` | `SystemInfo` | Auth |
| GET | `/api/public/version` | `Versionsinfo` | Public |
| GET | `/api/public/systeminfo` | `SystemInfo` | Public |
---
### 8.11 `logger/`
**Purpose:** Custom leveled logger with timestamp prefixing.
**Key API:**
```go
logger.Debug(format, args...)
logger.Info(format, args...)
logger.Warn(format, args...)
logger.Error(format, args...)
logger.SetDebugMode(bool)
logger.SetMinLogLevel(logger.DEBUG | INFO | WARN | ERROR)
```
- Outputs to `os.Stdout` with format: `[YYYY-MM-DD HH:MM:SS] LEVEL: message`
- Debug messages are suppressed unless both `debugEnabled=true` AND `minLevel<=DEBUG`.
- Configured once in `main()` from `config.Cfg`.
---
### 8.12 `mail/`
**Purpose:** SMTP email client used exclusively for password-reset emails.
**Key File:** `smtp.go`
```go
type Client struct {
host, username, password, from string
port int
}
func NewClient() *Client // reads from config.Cfg
func (c *Client) Send(msg Message) error
```
- Port 465: Direct TLS (`tls.Dial`)
- Port 587: STARTTLS (`smtp.Dial` + `TLSConfig`)
- If `host == ""`: logs the email content instead of sending (fallback for unconfigured environments).
---
### 8.13 `router/`
**Purpose:** Central Gin setup and base route group creation.
**Files:**
- `setup.go``SetupGin(r)` — CORS middleware
- `routes.go``BaseRouterGrp(r)` — public auth routes + protected `/api` group
- `routes_test.go` — router tests
---
### 8.14 `config/`
**Purpose:** Application configuration loading and validation.
**Key Functions:**
- `init()` — auto-loads config on package import
- `LoadConfig()` — reads `.env`/`.env.local`, then env vars
- `loadEnvFile()` / `loadEnvFileContent()` — parse `KEY=VALUE` lines
- `cfg.IsProduction() bool`
- `cfg.GetServerAddress() string` — returns `":PORT"` format
---
### 8.15 `database/`
**Purpose:** Database connection management, test helpers, and migration execution.
**Key Functions:**
- `ConnectDatabase()` — routes to `SetupTestDB()` or `ConnectDatabaseOrig()` based on env
- `ConnectDatabaseOrig()` — opens MySQL or SQLite with GORM
- `SetupTestDB(opts ...bool)` — copies snapshot `testdata/prepared_test_data.db` → temp dir, opens it
- `MigrateStructure(db)` — migrates `schema_version` + `migration_history`
- `MigrateDataIfNeeded(db)` — ensures schema version record exists
- `GetDB()` — lazy init getter
**Path Constants:**
```go
PreparedTestDB = filepath.Join(backendDir, "testdata", "prepared_test_data.db")
TestDataDir = filepath.Join(backendDir, "maintenance", "testdata")
```
---
### 8.16 `testutils/`
**Purpose:** Shared test environment setup utility.
```go
func SetupTestEnvironment(t *testing.T)
func SetupTestEnvironmentWithConfig(t *testing.T, envVars map[string]string)
func EnsureTestEnvironment(t *testing.T)
```
- Sets `ENVIRONMENT=test` to redirect DB connection to SQLite test snapshot.
- Registers `t.Cleanup()` to restore original env vars after each test.
---
### 8.17 `api/`
**Purpose:** Integration tests that test multi-module interactions via full HTTP stack.
Key test: `TestListCharacters` — spins up Gin with all essential routes, uses `database.SetupTestDB(true)`.
---
## 9. Complete API Route Listing
### Public (No Authentication)
| Method | Path | Module | Purpose |
|--------|------|--------|---------|
| POST | `/register` | user | Register a new user |
| POST | `/login` | user | Login, get token |
| POST | `/password-reset/request` | user | Request password reset email |
| GET | `/password-reset/validate/:token` | user | Validate reset token |
| POST | `/password-reset/reset` | user | Set new password |
| GET | `/api/pdf/file/:filename` | pdfrender | Download generated PDF |
| GET | `/api/public/version` | appsystem | App version (no auth) |
| GET | `/api/public/systeminfo` | appsystem | System info (no auth) |
### Protected — All Authenticated Users (`/api/...`)
**User Management:**
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/user/profile` | Own profile |
| PUT | `/api/user/display-name` | Update display name |
| PUT | `/api/user/email` | Update email |
| PUT | `/api/user/password` | Change password |
| PUT | `/api/user/language` | Set preferred language |
**Characters:**
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/characters` | List own + shared + public characters |
| POST | `/api/characters` | Create character |
| GET | `/api/characters/:id` | Get full character (FeChar) |
| PUT/PATCH | `/api/characters/:id` | Update character |
| DELETE | `/api/characters/:id` | Delete character |
| PUT | `/api/characters/:id/image` | Upload character image |
| GET | `/api/characters/:id/datasheet-options` | PDF datasheet options |
| GET | `/api/characters/:id/shares` | List share recipients |
| PUT | `/api/characters/:id/shares` | Update share recipients |
| GET | `/api/characters/:id/available-users` | Users available to share with |
| GET | `/api/characters/:id/experience-wealth` | Current EP + wealth |
| PUT | `/api/characters/:id/experience` | Update experience points |
| PUT | `/api/characters/:id/wealth` | Update wealth |
| GET | `/api/characters/:id/audit-log` | Change history |
| GET | `/api/characters/:id/audit-log/stats` | Change statistics |
| POST | `/api/characters/lerncost` | Calculate learn/improve cost |
| POST | `/api/characters/lerncost-new` | (Alias) Calculate learn/improve cost |
| POST | `/api/characters/improve-skill` | Apply skill improvement |
| POST | `/api/characters/improve-skill-new` | (Alias) Apply skill improvement |
| POST | `/api/characters/:id/learn-skill` | Learn new skill |
| POST | `/api/characters/:id/learn-skill-new` | (Alias) Learn new skill |
| POST | `/api/characters/:id/learn-spell` | Learn new spell |
| POST | `/api/characters/:id/learn-spell-new` | (Alias) Learn new spell |
| POST | `/api/characters/available-skills` | Available skills to learn |
| POST | `/api/characters/available-skills-new` | (Alias) |
| POST | `/api/characters/available-skills-creation` | Skills for creation wizard |
| POST | `/api/characters/available-spells` | Available spells to learn |
| POST | `/api/characters/available-spells-new` | (Alias) |
| POST | `/api/characters/available-spells-creation` | Spells for creation wizard |
| GET | `/api/characters/spell-details` | Spell detail lookup |
| GET | `/api/characters/:id/reward-types` | Reward options for this context |
| GET | `/api/characters/:id/practice-points` | Current practice points |
| PUT | `/api/characters/:id/practice-points` | Update practice points |
| POST | `/api/characters/:id/practice-points/add` | Add a practice point |
| POST | `/api/characters/:id/practice-points/use` | Use a practice point |
| GET | `/api/characters/skill-categories` | Static skill category listing |
| GET | `/api/characters/create-sessions` | List creation wizard sessions |
| POST | `/api/characters/create-session` | Start creation wizard session |
| GET | `/api/characters/create-session/:sessionId` | Get session data |
| PUT | `/api/characters/create-session/:sessionId/basic` | Save basic info |
| PUT | `/api/characters/create-session/:sessionId/attributes` | Save attributes |
| PUT | `/api/characters/create-session/:sessionId/derived` | Save derived values |
| PUT | `/api/characters/create-session/:sessionId/skills` | Save skills selection |
| POST | `/api/characters/create-session/:sessionId/finalize` | Finalize creation |
| DELETE | `/api/characters/create-session/:sessionId` | Discard session |
| GET | `/api/characters/races` | Available races |
| GET | `/api/characters/classes` | Available character classes |
| GET | `/api/characters/classes/learning-points` | LP by class |
| GET | `/api/characters/origins` | Available origins |
| GET | `/api/characters/beliefs` | Faith search |
| POST | `/api/characters/calculate-static-fields` | Calc derived values (no dice) |
| POST | `/api/characters/calculate-rolled-field` | Calc derived value (with dice) |
**Equipment:**
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/equipment` | Add equipment item |
| GET | `/api/equipment/character/:character_id` | List equipment for character |
| PUT | `/api/equipment/:ausruestung_id` | Update equipment |
| DELETE | `/api/equipment/:ausruestung_id` | Delete equipment |
| POST | `/api/weapons` | Add weapon |
| GET | `/api/weapons/character/:character_id` | List weapons for character |
| PUT | `/api/weapons/:waffe_id` | Update weapon |
| DELETE | `/api/weapons/:waffe_id` | Delete weapon |
**Master Data (read — all authenticated, write — maintainers only):**
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/maintenance` | Auth | All master data overview |
| GET | `/api/maintenance/skills` | Auth | List skills |
| GET | `/api/maintenance/skills/:id` | Auth | Get skill |
| GET | `/api/maintenance/skills-enhanced` | Auth | List skills (rich) |
| GET | `/api/maintenance/skills-enhanced/:id` | Auth | Get skill (rich) |
| POST | `/api/maintenance/skills` | Maintainer | Create skill |
| POST | `/api/maintenance/skills-enhanced` | Maintainer | Create skill (rich) |
| PUT | `/api/maintenance/skills/:id` | Maintainer | Update skill |
| PUT | `/api/maintenance/skills-enhanced/:id` | Maintainer | Update skill (rich) |
| DELETE | `/api/maintenance/skills/:id` | Maintainer | Delete skill |
| GET | `/api/maintenance/weaponskills[/:id]` | Auth | Weapon skill(s) |
| GET | `/api/maintenance/weaponskills-enhanced[/:id]` | Auth | Weapon skill(s) (rich) |
| POST/PUT/DELETE | `/api/maintenance/weaponskills[/:id]` | Maintainer | Weapon skill CRUD |
| GET | `/api/maintenance/spells[/:id]` | Auth | Spell(s) |
| GET | `/api/maintenance/spells-enhanced[/:id]` | Auth | Spell(s) (rich) |
| POST/PUT/DELETE | `/api/maintenance/spells[/:id]` | Maintainer | Spell CRUD |
| GET | `/api/maintenance/equipment[/:id]` | Auth | Equipment item(s) |
| GET | `/api/maintenance/equipment-enhanced[/:id]` | Auth | Equipment (rich) |
| POST/PUT/DELETE | `/api/maintenance/equipment[/:id]` | Maintainer | Equipment CRUD |
| GET | `/api/maintenance/weapons[/:id]` | Auth | Master weapon(s) |
| GET | `/api/maintenance/weapons-enhanced[/:id]` | Auth | Master weapon(s) (rich) |
| POST/PUT/DELETE | `/api/maintenance/weapons[/:id]` | Maintainer | Weapon CRUD |
**Maintenance Admin (Maintainer required):**
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/maintenance/gsm-believes` | List faith entries |
| PUT | `/api/maintenance/gsm-believes/:id` | Update faith entry |
| GET | `/api/maintenance/game-systems` | List game systems |
| PUT | `/api/maintenance/game-systems/:id` | Update game system |
| GET | `/api/maintenance/gsm-lit-sources` | List source books |
| PUT | `/api/maintenance/gsm-lit-sources/:id` | Update source book |
| GET | `/api/maintenance/gsm-misc` | MiscLookup entries |
| PUT | `/api/maintenance/gsm-misc/:id` | Update misc entry |
| GET/PUT | `/api/maintenance/skill-improvement-cost2[/:id]` | Improvement cost table |
| GET | `/api/maintenance/setupcheck` | DB structure validation |
| GET | `/api/maintenance/setupcheck-dev` | Extended setup check |
| GET | `/api/maintenance/mktestdata` | Generate test snapshot from live DB |
| GET | `/api/maintenance/reconndb` | Reconnect database |
| GET | `/api/maintenance/reloadenv` | Reload .env config |
| POST | `/api/maintenance/transfer-sqlite-to-mariadb` | Migrate data |
**PDF:**
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/pdf/templates` | Auth | List available PDF templates |
| GET | `/api/pdf/export/:id` | Auth | Export character to PDF |
| POST | `/api/pdf/cleanup` | Auth | Clean up old PDFs |
| GET | `/api/pdf/file/:filename` | Public | Download PDF file |
**Import/Export:**
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/importer/upload` | Import character from VTT JSON |
| POST | `/api/importer/spells/csv` | Import spells from CSV |
| GET | `/api/importer/export/vtt/:id` | Export character as VTT JSON (response body) |
| GET | `/api/importer/export/vtt/:id/file` | Export character as downloadable VTT JSON |
| GET | `/api/importer/export/csv/:id` | Export character as CSV |
| GET | `/api/importer/export/spells/csv` | Export all spells as CSV |
| GET | `/api/transfer/export/:id` | Export character as BaMoRT JSON |
| GET | `/api/transfer/download/:id` | Download character as JSON file |
| POST | `/api/transfer/import` | Import character from BaMoRT JSON |
| POST | `/api/transfer/database/export` | Full DB export to JSON |
| POST | `/api/transfer/database/import` | Full DB import from JSON |
**System:**
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/version` | Auth | App version + git commit |
| GET | `/api/systeminfo` | Auth | Version + user/char counts + DB version |
**Admin (Admin role required):**
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/users` | List all users |
| GET | `/api/users/:id` | Get user by ID |
| PUT | `/api/users/:id/role` | Change user role |
| PUT | `/api/users/:id/password` | Change user password |
| DELETE | `/api/users/:id` | Delete user |
---
## 10. Testing Approach
### Test Environment Setup
Every test file that touches the database must call:
```go
func setupTestEnvironment(t *testing.T) {
original := os.Getenv("ENVIRONMENT")
os.Setenv("ENVIRONMENT", "test")
t.Cleanup(func() { os.Setenv("ENVIRONMENT", original) })
}
```
Or the shared utility:
```go
testutils.SetupTestEnvironment(t)
```
This causes `database.ConnectDatabase()` to open the SQLite test snapshot instead of MariaDB.
### Test Database Mechanism
1. `testdata/prepared_test_data.db` — a SQLite snapshot with real-world test data.
2. `database.SetupTestDB(true)` copies this to a `os.MkdirTemp` location and opens it.
3. Tests run against this isolated copy — no pollution of live data.
4. Character ID 18 ("Fanjo Vetrani") is guaranteed to exist in the test DB.
### Test Structure Conventions
- All test files use `_test.go` suffix.
- Test functions call `database.SetupTestDB(true)` or `testutils.SetupTestEnvironment(t)` first.
- Integration tests use `testify/assert`.
- Many module tests (`character_test.go`, `handlers_test.go`, etc.) use helper constructors from `test_data_helper.go`.
- The `api/api_test.go` package runs HTTP tests using `httptest.NewRecorder()`.
- `maintenance/copy_db_test.go` tests the DB snapshot refresh mechanism.
- Test data helpers (`character/test_data_helper.go`) provide `CreateTestCharacter()`, etc.
### Key Test Files by Module
| Module | Test Files |
|--------|-----------|
| `api/` | `api_test.go` — Integration |
| `character/` | `character_test.go`, `handlers_test.go`, `lerncost_handler_test.go`, `skill_update_test.go`, `learn_spell_test.go`, etc. |
| `database/` | `database_test.go`, `testhelper_test.go` |
| `gsmaster/` | `gsmaster_test.go`, `learning_costs_test.go`, `export_import_test.go` |
| `importer/` | `importer_test.go`, `charimport_test.go`, `upload_test.go` |
| `maintenance/` | `handlers_test.go`, `masterdata_handlers_test.go`, `copy_db_test.go` |
| `models/` | `model_character_test.go`, `model_char_skills_test.go`, `model_learning_costs_test.go` |
| `pdfrender/` | `chromedp_test.go`, `mapper_test.go`, `pagination_test.go`, `integration_test.go` |
| `transfer/` | `database_test.go`, `exporter_test.go`, `importer_test.go` |
| `user/` | `handlers_test.go`, `role_test.go` |
---
## 11. Key Dependencies
| Dependency | Version | Purpose |
|-----------|---------|---------|
| `github.com/gin-gonic/gin` | v1.10.0 | HTTP framework |
| `github.com/gin-contrib/cors` | v1.7.3 | CORS middleware |
| `gorm.io/gorm` | v1.25.12 | ORM |
| `gorm.io/driver/mysql` | v1.5.7 | MySQL/MariaDB driver |
| `gorm.io/driver/sqlite` | v1.5.7 | SQLite driver (tests + dev) |
| `github.com/chromedp/chromedp` | v0.14.2 | Headless Chrome for PDF rendering |
| `github.com/pdfcpu/pdfcpu` | v0.11.1 | PDF merging |
| `github.com/stretchr/testify` | v1.10.0 | Test assertions |
---
## 12. Security Observations
### Critical Issues
1. **Weak Token Authentication**: The custom token embeds the user ID in a predictable location. Any token with any valid user ID at position `7 + len("Bearer ")` is accepted. There is **no HMAC or digital signature**. An attacker can forge tokens.
2. **MD5 Password Hashing**: Passwords are stored as MD5 hashes. MD5 is cryptographically broken for password storage; brute-force and rainbow-table attacks are trivial. Should be replaced with bcrypt, argon2id, or scrypt.
3. **Token Leaks User ID**: The token format encodes the user ID in plaintext, revealing enumerable user IDs to clients.
### Moderate Concerns
4. **Debug Logging of Passwords**: `handlers.go` has `fmt.Printf("pwdh: %s", ...)` that prints password hashes to stdout on every login — a security logging issue.
5. **Hardcoded Origins in CORS**: Local network IPs (`192.168.0.48`, `192.168.0.36`) are hardcoded — appropriate for development but should be env-driven for production.
6. **Importer File Upload**: `UploadFiles` saves files to `./uploads/` relative path without sanitizing filenames for path traversal. The extension validation (`isValidFileType`) only checks the suffix.
7. **SQL Injection via `fieldName`**: `GetMiscLookupByKeyForSystem` constructs `query.Order(orderBy)` from a user-supplied `order` parameter — however the value is mapped to a safe set of constants before use, so actual injection risk is mitigated.
8. **No rate limiting**: Login and registration endpoints have no rate limiting, making them susceptible to brute-force attacks.
### Notes
- The bcrypt implementation is commented out in `handlers.go` with the MD5 hash in use — this is intentional but documents the known deficit.
- Password reset tokens use `crypto/rand` (secure, 32 bytes), which is correct.
- GORM parameterized queries protect most DB operations from SQL injection.
---
## 13. Cross-Module Relationships Diagram
```
cmd/main.go
├── config ──────────────────────────────────── read by all modules
├── logger ──────────────────────────────────── used by all modules
├── database ─────────────────────────────────── .DB used by all models
│ └── testutils ────────────────────────── test env setup
├── router
│ └── user (AuthMiddleware, role guards)
├── user ─────────────────── handlers, model, auth
│ └── mail (SMTP for password reset)
├── models ────────────────── GORM entities, MigrateStructure
│ ├── user.User (FK in Char)
│ └── database.DB (queries)
├── character ─────────────── CRUD + learning + creation wizard
│ ├── models (Char, SkFertigkeit, SkZauber, etc.)
│ ├── gsmaster (LerncostRequest, skill lookups)
│ └── database.DB
├── equipment ─────────────── Equipment/Weapon CRUD
│ ├── models (EqAusruestung, EqWaffe)
│ └── database.DB
├── gsmaster ──────────────── Master data (skills/spells/weapons)
│ ├── models (Skill, Spell, Weapon, etc.)
│ ├── user (RequireMaintainer guard)
│ └── database.DB
├── gamesystem ────────────── GameSystem seed/migration
│ └── models.GameSystem
├── maintenance ───────────── Admin tooling, migration orchestration
│ ├── database, gamesystem, user, models (all MigrateStructure)
│ └── user (RequireMaintainer guard)
├── pdfrender ─────────────── HTML→PDF pipeline
│ ├── models.Char (character data)
│ └── config (TemplatesDir, ExportTempDir)
├── importer ──────────────── VTT/CSV import/export
│ └── models (Char, Skill, Spell)
├── transfer ──────────────── JSON character + DB backup
│ └── models.Char
└── appsystem ─────────────── Version info
└── database.DB (count queries)
```
---
*End of findings — /home/de31a2/bamort/backend_findings.md*