Files
bamort/backend_findings.md
2026-04-01 15:16:12 +02:00

1363 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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*