- ed25519-deploy-nyckel genereras av servern (config-volymen, 0600), GIT_SSH_COMMAND med egen known_hosts (accept-new) - inställningar: remote-URL, live-gren, read-only, auto-synk-intervall - bakgrundssynk: endast fast-forward, flaggar attention vid merge-behov - pull/push, skapa/byt gren, merge-popup (ours/theirs per fil eller allt), abort, reset från remote med automatisk backup-gren - headless-konfig via config.json eller updateGitSync-mutationen - openssh-client tillagd i runtime-imagen; mutex kring alla git-operationer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
611 lines
26 KiB
Markdown
611 lines
26 KiB
Markdown
# Archivum — Arkitektur och designdokumentation
|
|
|
|
## Innehållsförteckning
|
|
|
|
1. [Systemöversikt](#1-systemöversikt)
|
|
2. [Frontend](#2-frontend)
|
|
3. [Backend](#3-backend)
|
|
4. [Exekveringsflöden](#4-exekveringsflöden)
|
|
5. [Kodstruktur](#5-kodstruktur)
|
|
6. [Containerisering och driftsättning](#6-containerisering-och-driftsättning)
|
|
|
|
---
|
|
|
|
## 1. Systemöversikt
|
|
|
|
Archivum är en självhostad wiki/dokumenthanterare. Alla dokument lagras som AsciiDoc-filer (`.adoc`) på disk och versionshanteras automatiskt med Git. Frontenden är en SPA (Single-Page Application) som kommunicerar med backenden uteslutande via GraphQL.
|
|
|
|
```
|
|
Webbläsare
|
|
│ HTTPS / HTTP
|
|
▼
|
|
┌──────────────────────────────┐
|
|
│ Go HTTP-server :4000 │
|
|
│ ┌──────────┐ ┌──────────┐ │
|
|
│ │ /graphql │ │ SPA / │ │
|
|
│ └────┬─────┘ └──────────┘ │
|
|
│ │ │
|
|
│ ┌────▼──────────────────┐ │
|
|
│ │ GraphQL dispatcher │ │
|
|
│ └─┬──────┬──────┬───────┘ │
|
|
│ Auth Storage Git │
|
|
│ (SQLite)(disk) (os/exec) │
|
|
└──────────────────────────────┘
|
|
```
|
|
|
|
### Teknologistack
|
|
|
|
| Lager | Teknik |
|
|
|--------------|----------------------------------------------------------------|
|
|
| Frontend | Vue 3 (Composition API), Vite, TypeScript, Tailwind CSS |
|
|
| State | Pinia |
|
|
| Routing | Vue Router 4 |
|
|
| Editor | TipTap 2 (visuell, med custom image extension), CodeMirror 6 (AsciiDoc-källa) |
|
|
| AsciiDoc | Asciidoctor.js (rendering i webbläsaren) |
|
|
| PWA | vite-plugin-pwa (Service Worker, manifest) |
|
|
| Backend | Go 1.22, HTTP-standardbibliotek |
|
|
| Databas | SQLite via modernc.org/sqlite (CGO-fri, multi-arch) |
|
|
| Autentisering| OIDC/SSO (Authentik) + lokala konton (bcrypt) + LDAP, sessions-tokens i minnet |
|
|
| RBAC / ACL | Roll via OIDC-grupper; allow/deny-ACL per sökväg för användare & grupper |
|
|
| Versionshantering | Git via `os/exec` |
|
|
| Containerisering | Docker (multi-stage, multi-arch: amd64 + arm64) |
|
|
|
|
---
|
|
|
|
## 2. Frontend
|
|
|
|
### Paket och beroenden
|
|
|
|
```
|
|
frontend/
|
|
├── src/
|
|
│ ├── App.vue # Rotemponent — kontrollerar setup/login/app-läge
|
|
│ ├── main.ts # Appstart, monterar Vue + Pinia + Router
|
|
│ ├── lib/
|
|
│ │ ├── gql.ts # Minimal GraphQL-klient (fetch-baserad)
|
|
│ │ └── crypto.ts # Lösenords-hashing (SHA-256 via Web Crypto API)
|
|
│ ├── stores/
|
|
│ │ ├── app.ts # Global state: token, requiresSetup, login/logout
|
|
│ │ └── theme.ts # Mörkt/ljust läge, persistas i localStorage
|
|
│ ├── router/index.ts # SPA-routes
|
|
│ ├── views/ # Sidkomponenter
|
|
│ ├── components/ # Återanvändningsbara UI-komponenter
|
|
│ │ ├── editor/ # Visuell och källkodseditor
|
|
│ │ ├── layout/ # AppLayout, Sidebar, ThemeToggle
|
|
│ │ └── wizard/ # Installationsguide (SetupWizard)
|
|
│ ├── bridge/
|
|
│ │ └── asciidoc-bridge.ts # AsciiDoc ↔ TipTap JSON-konvertering
|
|
│ └── assets/main.css # Tailwind-bas + CSS-variabler
|
|
```
|
|
|
|
### Routing
|
|
|
|
| Sökväg | Komponent | Åtkomstkrav |
|
|
|---------------|------------------|---------------------|
|
|
| `/` | HomeView | Inloggad |
|
|
| `/doc/:slug` | DocumentView | Inloggad |
|
|
| `/admin` | AdminView | Inloggad, admin |
|
|
| `/setup` | SetupWizard | Ej konfigurerat |
|
|
|
|
Routeskydd hanteras **inte** med router guards utan direkt i `App.vue` — se avsnitt 4.1.
|
|
|
|
### GraphQL-klienten (`lib/gql.ts`)
|
|
|
|
Alla API-anrop görs med en enkel `fetch`-wrapper. Klienten:
|
|
- Läser JWT-token från `localStorage` och skickar den som `Authorization: Bearer <token>`
|
|
- Kastar ett `Error` om svaret innehåller GraphQL-errors
|
|
- Hanterar automatisk utloggning vid `UNAUTHORIZED`-fel
|
|
|
|
### Lösenordshantering (`lib/crypto.ts`)
|
|
|
|
Lösenordet hashas på klienten **innan** det skickas till servern med:
|
|
```
|
|
SHA-256(lowercase(username) + ":" + password)
|
|
```
|
|
Web Crypto API (`crypto.subtle.digest`) används — ingen extern beroende. Username fungerar som domän-separator (salt) så att samma lösenord ger olika digest för olika konton. Backenden tar emot denna digest och lagrar `bcrypt(digest)`.
|
|
|
|
### PWA
|
|
|
|
Konfigureras i `vite.config.ts` via `vite-plugin-pwa`:
|
|
- Service Worker med `skipWaiting` + `clientsClaim` (ny SW aktiveras omedelbart)
|
|
- Cachar statiska assets (JS, CSS, fonts)
|
|
- HTML och `/graphql` går alltid till nätverket (`NetworkFirst`)
|
|
- `navigateFallback: null` — förhindrar att SW returnerar cachat HTML för routes
|
|
|
|
---
|
|
|
|
## 3. Backend
|
|
|
|
### Paket
|
|
|
|
```
|
|
backend/
|
|
├── cmd/server/main.go # Startpunkt — laddar config, startar HTTP-server
|
|
├── internal/
|
|
│ ├── config/config.go # Config-struktur, Load/Save, ErrRequireSetup
|
|
│ ├── auth/auth.go # Session-hantering, bcrypt, LDAP-bind
|
|
│ ├── db/db.go # SQLite-wrapper — users-tabell
|
|
│ ├── storage/storage.go # Filhantering — Read/Write/List .adoc-filer
|
|
│ ├── git/git.go # Git-wrapper via os/exec — Commit/Log/Diff
|
|
│ └── graph/
|
|
│ ├── server.go # HTTP-handler + handgjord GraphQL-dispatcher
|
|
│ ├── resolver.go # Resolver-stub (plats för gqlgen-genererad kod)
|
|
│ └── schema.graphql # GraphQL-schema
|
|
```
|
|
|
|
### GraphQL-dispatcher
|
|
|
|
Backenden använder **inte** gqlgen-genererad kod i produktion. `server.go` implementerar en handgjord dispatcher som parsar query-strängen med `strings.Contains` och router till respektive handler-funktion. Detta ger enkel deployment utan kodgenerering men kräver manuell underhåll av schema och handlers.
|
|
|
|
### Autentisering och sessioner
|
|
|
|
```
|
|
Login-flöde (lokalt konto):
|
|
1. Frontend: kontrollerar authType (query userAuthType) → "local"
|
|
2. Frontend: SHA-256(username:password) → skickas som "password"
|
|
3. Backend: bcrypt.CompareHashAndPassword(stored_hash, received_digest)
|
|
4. Vid match: slumpmässig hex-token genereras och sparas i minnet
|
|
5. Token returneras till klient, lagras i localStorage
|
|
|
|
Login-flöde (LDAP-konto):
|
|
1. Frontend: kontrollerar authType → "ldap"
|
|
2. Frontend: skickar klartextlösenord i fältet "ldapPassword" (EJ hashat)
|
|
3. Backend: hämtar användarens DN via LDAP-sökning (baseDN + filter)
|
|
4. Backend: binder till LDAP med DN + klartextlösenord (ldapBind)
|
|
5. Vid lyckad bind: session skapas med roll "user"
|
|
|
|
Login-flöde (gäst):
|
|
1. Frontend: klickar "Continue as Guest" → skickar username="guest", password=""
|
|
2. Backend: identifierar gäst, skapar session utan lösenordskontroll, roll "guest"
|
|
3. Gäst ser bara dokument/mappar som admin explicit gett tillåtelse via ACL
|
|
|
|
Session-storage: in-memory map[token]*Session (försvinner vid omstart)
|
|
Session-livstid: 24 timmar (konfigurerat i auth.go)
|
|
|
|
**Rollbaserad UI-synlighet:**
|
|
Frontendens Pinia-store (`app.ts`) lagrar den inloggade användarens roll (`admin`, `user`, `guest`) via query `currentUserRole` som körs direkt efter inloggning. Admin-panelslänken i sidofältet visas bara om `role === 'admin'`.
|
|
```
|
|
|
|
**Viktigt:** Sessions lagras enbart i minnet. Vid server-omstart måste alla klienter logga in igen. LDAP-konfigurationsändringar (via admin-panelen) nollställer **inte** sessioner — befintliga sessioner är fortfarande giltiga.
|
|
|
|
### Gästanvändare
|
|
|
|
En inbyggd `guest`-användare skapas automatiskt vid installation och vid serverstart (migrations-safe). Gästen:
|
|
- Kräver inget lösenord
|
|
- Har roll `"guest"` (skild från `"user"` och `"admin"`)
|
|
- Kan som standard inte se något
|
|
- Admin ger tillgång per dokument eller mapp via ACL-panelen
|
|
|
|
### Autentisering via OIDC (Authentik)
|
|
|
|
Archivum är en confidential OIDC-klient mot Authentik. Flödet är rent
|
|
browser-redirect och ligger utanför GraphQL (två HTTP-endpoints):
|
|
|
|
```
|
|
Login-sida → GET /auth/oidc/login
|
|
→ 302 till Authentik (authorize, scope: openid profile email groups, state)
|
|
→ användaren autentiseras (Authentik → OpenLDAP)
|
|
→ 302 tillbaka GET /auth/oidc/callback?code&state
|
|
├─ validerar state (CSRF), byter code→token (backend↔Authentik, TLS)
|
|
├─ verifierar id_token (JWKS via go-oidc), läser preferred_username + groups
|
|
├─ mappar grupp → roll: Archivum-admin → admin, annars user
|
|
├─ speglar användare + grupper till SQLite (för admin-UI + ACL-mål)
|
|
├─ login-grind: användare/grupp måste vara tillåten
|
|
└─ 302 till /oidc/callback#token=… (SPA:n läser token ur fragmentet)
|
|
```
|
|
|
|
Konfiguration ligger i `config.oidc` (issuer, client_id/secret, grupp-mappning)
|
|
och sätts i admin-panelen. Discovery körs i bakgrunden vid start/ändring så att
|
|
en otillgänglig IdP inte blockerar servern. Se `docs/AUTHENTIK_SETUP.md`.
|
|
|
|
### Åtkomstkontroll (allow/deny-ACL)
|
|
|
|
ACL-poster lagras i SQLite-tabellen `acl`. Varje post kopplar en **sökväg** (slug
|
|
eller mappnamn) till ett **subjekt** (användare eller grupp), en **effekt**
|
|
(`allow`/`deny`) och sju rättighetsflaggor:
|
|
|
|
| Flagga | Innebär |
|
|
|------------|----------------------------------------|
|
|
| canSearch | Syns i sökning/listning |
|
|
| canView | Syns i mapplistning |
|
|
| canRead | Kan öppna och läsa innehållet |
|
|
| canEdit | Kan redigera och spara |
|
|
| canCreate | Kan skapa nya dokument i mappen |
|
|
| canDelete | Kan radera |
|
|
| canMove | Kan flytta/byta namn |
|
|
|
|
**Effektiv rättighet** (`db.EffectiveAccess`) beräknas för (användare, sökväg,
|
|
handling) genom att kombinera **alla** poster som gäller subjektet — användaren
|
|
själv **plus alla dess grupper** (från OIDC-claimet eller lokalt medlemskap) —
|
|
över sökvägen **och alla dess överliggande mappar**:
|
|
|
|
1. Standard = **deny**.
|
|
2. En `allow` på handlingen ger tillgång.
|
|
3. En `deny` på handlingen **vinner alltid** över allow.
|
|
|
|
Alltså: tillgång ges bara om någon post tillåter och ingen post nekar. Sökvägar
|
|
är hierarkiska (en `allow` på mappen `docs` gäller allt under `docs/`).
|
|
|
|
Enforcement sker för **alla icke-admins** (inloggade användare, `Archivum-reader`
|
|
och den publika/gäst-användaren) i alla operationer — listning, läsning, spara,
|
|
radera, flytta, skapa, historik/diff och bilduppladdning. **Admin kringgår ACL.**
|
|
|
|
> **Känd begränsning:** `/media/<bild>` serveras utan ACL eftersom bilder laddas
|
|
> via `<img>` utan Authorization-header. Skydda känsliga bilder på annat sätt
|
|
> (t.ex. signerade URL:er) om det behövs.
|
|
|
|
### Setup-detektion — `needsSetup`
|
|
|
|
Backenden räknar ett system som oklonfigurerat (`REQUIRE_SETUP`) om **något** av följande är sant:
|
|
|
|
1. `config.json` saknas eller är ofullständig (`cfg == nil`)
|
|
2. Databasen kunde inte öppnas (`database == nil`)
|
|
3. Databasen är tom — ingen adminanvändare har skapats (`!database.HasUsers()`)
|
|
|
|
`db.HasUsers()` gör en enkel `SELECT COUNT(*) FROM users` och returnerar `true` om minst ett konto finns. Detta innebär att en nyskapad, tom SQLite-fil (som Go skapar vid `db.New()`) fortfarande triggar setup-läge.
|
|
|
|
### DB-sökväg — `resolveDBPath()`
|
|
|
|
Setup-handleren bestämmer DB-sökvägen dynamiskt för att fungera i både Docker och dev-miljö:
|
|
|
|
```
|
|
resolveDBPath(configPath):
|
|
├─ Kan /data/db skapas/skrivas? → /data/db/archivum.db (Docker-volym)
|
|
└─ Annars → <config-katalog>/archivum.db (dev-miljö)
|
|
```
|
|
|
|
### Git-synk mot remote (SSH)
|
|
|
|
Wiki-repot kan synkas mot ett remote git-repo (t.ex. Gitea) över SSH.
|
|
Implementation: `internal/git/sync.go` (remote-operationer + mutex) och
|
|
`internal/graph/gitsync.go` (GraphQL-handlers, nyckelhantering, bakgrunds-
|
|
ticker). Admin-UI: `frontend/src/components/GitSyncSection.vue` med
|
|
`MergeConflictDialog.vue` som popup vid konflikter.
|
|
|
|
- **SSH-nyckel:** servern genererar själv ett ed25519-nyckelpar vid första
|
|
aktivering, lagrat i `<config-dir>/ssh/` (0600). Publika nyckeln visas i
|
|
admin-panelen och registreras som deploy key hos git-värden. `GIT_SSH_COMMAND`
|
|
pekar på nyckeln + egen `known_hosts` (accept-new) eftersom appen kör utan
|
|
hemkatalog. Privata nyckeln exponeras aldrig via API:t.
|
|
- **Live-gren** (`live_branch`): grenen som bakgrundssynken håller uppdaterad.
|
|
Bakgrundssynk (intervall i `auto_sync_minutes`, 0 = manuell) gör endast
|
|
fast-forward-pulls; allt som kräver riktig merge flaggas för admin
|
|
(`attention` i `gitSyncStatus`).
|
|
- **Read-only** (`read_only`): pull tillåts men aldrig push (spärras i backend).
|
|
- **Grenar:** skapa och byt gren i admin-panelen; remote-grenar spåras
|
|
automatiskt vid byte.
|
|
- **Merge-konflikter:** pull lämnar mergen öppen och UI:t visar en popup där
|
|
man väljer lokal/remote per fil eller för allt; alternativt avbryt mergen.
|
|
- **Reset från remote:** tvingar lokala grenen att matcha remoten; lokalt
|
|
läge sparas alltid först i en backup-gren (`backup/<timestamp>`). Används
|
|
också för bootstrap när historikerna är orelaterade (`UNRELATED_HISTORIES`).
|
|
- **Headless-konfiguration:** allt kan sättas via `config.json`-sektionen
|
|
`git_sync` + omstart, eller via GraphQL-mutationen `updateGitSync` — inget
|
|
webbgui krävs.
|
|
|
|
### Konfiguration
|
|
|
|
`config.json` hanteras av `internal/config`:
|
|
- `storage_path` — rotkatalog för .adoc-filer och Git-repo
|
|
- `db_path` — sökväg till SQLite-fil
|
|
- `jwt_secret` — används för session-token generation (ej JWT i klassisk mening)
|
|
- `listen_addr` — TCP-adress att lyssna på, t.ex. `:4000`
|
|
- `public_url` — extern bas-URL (t.ex. `https://archivum.brasse-pc.eu`), används
|
|
för att härleda OIDC-redirect-URI
|
|
- `git_sync` — se avsnittet ovan: `enabled`, `remote_url`, `live_branch`,
|
|
`read_only`, `auto_sync_minutes`
|
|
- `oidc` — valfri OIDC/SSO-konfiguration (Authentik):
|
|
- `enabled`, `issuer`, `client_id`, `client_secret`, `redirect_url`
|
|
- `groups_claim` (default `groups`), `username_claim` (default `preferred_username`)
|
|
- `admin_group` (default `Archivum-admin`), `reader_group` (default `Archivum-reader`)
|
|
- `ldap` — valfri LDAP-konfiguration (direkt bind, äldre alternativ):
|
|
- `url` — LDAP-server URL, t.ex. `ldap://openldap:389`
|
|
- `base_dn` — rot-DN att söka ifrån, t.ex. `dc=example,dc=com`
|
|
- `admin_user` — tjänstekonto DN för uppslag (valfritt)
|
|
- `admin_pass` — tjänstekontoets lösenord (valfritt)
|
|
|
|
Om filen saknas returnerar `Load()` `ErrRequireSetup` — backenden startar i setup-läge.
|
|
|
|
---
|
|
|
|
## 4. Exekveringsflöden
|
|
|
|
### 4.1 Appstart och setup-detektion
|
|
|
|
```
|
|
Browser laddar /
|
|
│
|
|
▼
|
|
App.vue onMounted()
|
|
│
|
|
├─ theme.init() (laddar tema från localStorage)
|
|
│
|
|
├─ app.checkStatus() → POST /graphql { systemStatus }
|
|
│ │ Backend: needsSetup = cfg==nil || db==nil || !db.HasUsers()
|
|
│ │
|
|
│ ├─ svar: "REQUIRE_SETUP"
|
|
│ │ → app.requiresSetup = true
|
|
│ │ → visas: <SetupWizard /> (renderas direkt, ej via router)
|
|
│ │
|
|
│ └─ svar: "OK"
|
|
│ → app.requiresSetup = false
|
|
│ ├─ app.token finns: → visas: <AppLayout />
|
|
│ └─ app.token saknas: → visas: <LoginView />
|
|
│
|
|
└─ ready = true (spinner försvinner)
|
|
```
|
|
|
|
`SetupWizard`, `LoginView` och `AppLayout` renderas direkt i `App.vue` (inte via `<router-view>`) för att undvika race conditions mellan `router.replace()` och Vue-renderingen.
|
|
|
|
### 4.2 Installationsguiden (SetupWizard)
|
|
|
|
```
|
|
Steg 1: Ange storage-sökväg, admin-användare, lösenord
|
|
Steg 2: Valfri LDAP-konfiguration
|
|
Steg 3: Bekräftelse
|
|
|
|
Vid submit:
|
|
1. SHA-256(adminUser:adminPass) → hashedPass
|
|
2. POST /graphql mutation Setup { setup(input: {..., adminPass: hashedPass}) }
|
|
├─ Backend: resolveDBPath() → bestämmer db_path (Docker vs dev)
|
|
├─ Backend: skapar SQLite-DB och admin-användare med bcrypt(hashedPass) ← FÖRST
|
|
├─ Backend: sparar config.json ← SEDAN
|
|
└─ Backend: initierar storage-katalog och Git-repo
|
|
3. app.requiresSetup = false
|
|
4. app.login(adminUser, adminPass) → SHA-256 igen → POST /graphql mutation Login
|
|
5. Redirect till /
|
|
```
|
|
|
|
Ordningen är viktig: om DB-skapandet eller user-insert misslyckas sparas **ingen** `config.json`. Nästa sidladdning ger återigen `REQUIRE_SETUP` och guiden visas på nytt. Tidigare sparades config först, vilket kunde lämna ett halvfärdigt tillstånd där backenden startade med config men utan adminanvändare.
|
|
|
|
### 4.3 Inloggningsflöde
|
|
|
|
```
|
|
Användaren fyller i användarnamn + lösenord (eller klickar "Continue as Guest")
|
|
│
|
|
▼
|
|
app.login(username, password)
|
|
│
|
|
├─ query userAuthType(username) → "local" | "ldap" | "guest"
|
|
│
|
|
├─ authType == "local":
|
|
│ SHA-256(lowercase(username):password) → hashed
|
|
│ → POST /graphql mutation Login(u, p: hashed)
|
|
│
|
|
├─ authType == "ldap":
|
|
│ → POST /graphql mutation Login(u, p: "", ldapPassword: plaintext)
|
|
│
|
|
└─ authType == "guest":
|
|
→ POST /graphql mutation Login(u: "guest", p: "")
|
|
│
|
|
▼
|
|
handleLogin (server.go)
|
|
│
|
|
├─ "guest": returnerar token utan lösenordskontroll
|
|
│
|
|
├─ "ldap" (is_ldap=1 i DB): ldapBind(username, ldapPassword)
|
|
│ → OK: session med roll "user"
|
|
│
|
|
└─ "local" (is_ldap=0): bcrypt.Compare(stored, hashed)
|
|
→ OK: session med roll baserad på DB-värde
|
|
|
|
Gäst-session:
|
|
- Roll "guest" — se ACL-enforcement i handleDocuments / handleFolders
|
|
- Kan bara se dokument/mappar som admin explicit beviljat via ACL
|
|
```
|
|
|
|
### 4.4 Spara dokument
|
|
|
|
```
|
|
Användaren redigerar och klickar Spara
|
|
│
|
|
▼
|
|
POST /graphql mutation SaveDocument
|
|
{ slug, content (AsciiDoc), commitMessage, author }
|
|
│
|
|
▼
|
|
handleSaveDocument (server.go)
|
|
│
|
|
├─ Kontrollerar session (Bearer token → Manager.Validate())
|
|
├─ storage.Write(slug, content) → skriver slug.adoc till disk
|
|
└─ git.Commit(slug.adoc, message, author, email)
|
|
│
|
|
└─ os/exec: git add → git commit
|
|
```
|
|
|
|
### 4.5 Versionshistorik och diff
|
|
|
|
```
|
|
GET /graphql { history(slug) }
|
|
│
|
|
└─ git.Log(slug.adoc) → os/exec: git log --format=...
|
|
→ []LogEntry { Hash, Author, Date, Subject }
|
|
|
|
GET /graphql { diff(slug, fromHash, toHash) }
|
|
│
|
|
└─ git.Diff(slug.adoc, from, to) → os/exec: git diff
|
|
→ unified diff-sträng
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Kodstruktur
|
|
|
|
### Fullständig katalogstruktur
|
|
|
|
```
|
|
Archivum/
|
|
├── backend/
|
|
│ ├── cmd/server/main.go # Entrypoint
|
|
│ ├── config.json # Lokal dev-konfiguration (gitignoreras ej — bör .gitignore:as i produktion)
|
|
│ ├── go.mod / go.sum
|
|
│ └── internal/
|
|
│ ├── auth/auth.go # Session, bcrypt, LDAP
|
|
│ ├── config/config.go # Config-struktur och fil-I/O
|
|
│ ├── db/db.go # SQLite users-tabell
|
|
│ ├── git/git.go # Git-wrapper
|
|
│ ├── storage/storage.go # .adoc fil-I/O
|
|
│ └── graph/
|
|
│ ├── schema.graphql # GraphQL-schema (source of truth)
|
|
│ ├── resolver.go # Resolver-stub
|
|
│ └── server.go # HTTP-server + dispatcher + alla handlers
|
|
│
|
|
├── frontend/
|
|
│ ├── index.html
|
|
│ ├── vite.config.ts # Vite + PWA + dev-proxy konfiguration
|
|
│ ├── tailwind.config.js
|
|
│ ├── package.json
|
|
│ └── src/
|
|
│ ├── App.vue # Rotkomponent med setup/login/app-routing
|
|
│ ├── main.ts
|
|
│ ├── lib/
|
|
│ │ ├── gql.ts # GraphQL fetch-klient
|
|
│ │ └── crypto.ts # SHA-256 lösenordshashing
|
|
│ ├── stores/
|
|
│ │ ├── app.ts # Global app-state (Pinia)
|
|
│ │ └── theme.ts # Temahantering (Pinia)
|
|
│ ├── router/index.ts
|
|
│ ├── views/
|
|
│ │ ├── HomeView.vue # Dokumentlista
|
|
│ │ ├── DocumentView.vue # Visa/redigera dokument
|
|
│ │ ├── AdminView.vue # Administrationspanel
|
|
│ │ └── LoginView.vue # Inloggningsformulär
|
|
│ ├── components/
|
|
│ │ ├── editor/
|
|
│ │ │ ├── VisualEditor.vue # TipTap WYSIWYG-editor
|
|
│ │ │ └── SourceEditor.vue # CodeMirror AsciiDoc-källeditor
|
|
│ │ ├── layout/
|
|
│ │ │ ├── AppLayout.vue # Huvudlayout med sidebar
|
|
│ │ │ ├── Sidebar.vue # Dokumentnavigering
|
|
│ │ │ └── ThemeToggle.vue # Ljust/mörkt läge-knapp
|
|
│ │ └── wizard/
|
|
│ │ └── SetupWizard.vue # Installationsguide (3 steg)
|
|
│ ├── bridge/
|
|
│ │ └── asciidoc-bridge.ts # AsciiDoc ↔ TipTap JSON
|
|
│ └── assets/main.css
|
|
│
|
|
├── docker/
|
|
│ ├── Dockerfile # Multi-stage build (frontend → backend → runtime)
|
|
│ ├── entrypoint.sh # PUID/PGID-hantering, katalogskapande
|
|
│ ├── docker-compose.yml # Dev/standard deploy
|
|
│ └── docker-compose.prod.yml # Produktionsinställningar
|
|
│
|
|
└── ARCHITECTURE.md # Detta dokument
|
|
```
|
|
|
|
### Nyckelgränssnitt och datakontrakt
|
|
|
|
**GraphQL-schema (urval):**
|
|
```graphql
|
|
type Query {
|
|
systemStatus: SystemStatus!
|
|
userAuthType(username: String!): String! # "local" | "ldap" | "guest"
|
|
currentUserRole: String! # Returnerar rollen för inloggad session ("admin" | "user" | "guest")
|
|
documents(prefix: String): [DocumentMeta!]!
|
|
document(slug: String!): Document
|
|
folders: [String!]!
|
|
history(slug: String!): [CommitEntry!]!
|
|
diff(slug: String!, fromHash: String!, toHash: String!): String!
|
|
acl(path: String!): [ACLEntry!]!
|
|
aclSubjects: ACLSubjects! # Användare + grupper för ACL-picker (inkl. guest)
|
|
ldapBrowse(url: String, baseDN: String, adminUser: String, adminPass: String): LDAPTree!
|
|
}
|
|
|
|
type Mutation {
|
|
login(username: String!, password: String!, ldapPassword: String): String!
|
|
logout: Boolean!
|
|
setup(input: SetupInput!): Boolean!
|
|
saveDocument(input: SaveDocumentInput!): Document!
|
|
deleteDocument(slug: String!): Boolean!
|
|
updateLdapConfig(input: LDAPInput): Boolean!
|
|
importLdapSubject(type: String!, name: String!): Boolean!
|
|
setAcl(input: ACLInput!): Boolean!
|
|
removeAcl(id: Int!): Boolean!
|
|
createFolder(path: String!): Boolean!
|
|
moveDocument(oldSlug: String!, newSlug: String!): Boolean!
|
|
}
|
|
|
|
input LDAPInput {
|
|
url: String!
|
|
baseDN: String
|
|
adminUser: String!
|
|
adminPass: String # Utelämnas → befintligt lösenord behålls
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Containerisering och driftsättning
|
|
|
|
### Dockerfile (multi-stage)
|
|
|
|
```
|
|
Stage 1 (frontend-builder) — node:20-alpine
|
|
npm install && npm run build → /app/frontend/dist
|
|
|
|
Stage 2 (backend-builder) — golang:1.22-alpine
|
|
CGO_ENABLED=0 go build → /archivum (statisk binär)
|
|
|
|
Stage 3 (runtime) — alpine:3.20
|
|
Installerar: su-exec, git
|
|
Kopierar: dist/ → /srv/archivum/ui
|
|
archivum → /usr/local/bin/archivum
|
|
entrypoint.sh
|
|
Volumes: /config, /data
|
|
Port: 4000
|
|
Entrypoint: entrypoint.sh
|
|
```
|
|
|
|
### Volymer och kataloger
|
|
|
|
| Volym | Innehåll | Env-variabel |
|
|
|-----------|---------------------------------------------------|----------------|
|
|
| `/config` | `config.json` | `DOCKER_PATH` |
|
|
| `/data` | `wiki/` (AsciiDoc + Git-repo), `db/archivum.db` | — |
|
|
|
|
### Miljövariabler
|
|
|
|
| Variabel | Standard | Beskrivning |
|
|
|---------------|-------------------|------------------------------------------|
|
|
| `DOCKER_PATH` | — | Host-sökväg som monteras som `/config` |
|
|
| `UI_DIR` | `/srv/archivum/ui`| Sökväg till kompilerad frontend |
|
|
| `PUID` | `1000` | Filsystemsägare (UID) |
|
|
| `PGID` | `1000` | Filsystemsägare (GID) |
|
|
| `TZ` | `Europe/Stockholm`| Tidszon |
|
|
| `HOST_PORT` | `8080` | Extern port i docker-compose |
|
|
|
|
### Multi-arch build
|
|
|
|
Bilden stöder `linux/amd64` (server/desktop) och `linux/arm64` (Raspberry Pi 5):
|
|
```bash
|
|
docker buildx build \
|
|
--platform linux/amd64,linux/arm64 \
|
|
-t registry:5000/archivum:latest \
|
|
-f docker/Dockerfile --push .
|
|
```
|
|
|
|
### entrypoint.sh — PUID/PGID-hantering
|
|
|
|
Containern startar som `root`. `entrypoint.sh`:
|
|
1. Skapar `/config`, `/data/wiki`, `/data/db` om de saknas
|
|
2. Ändrar ägare på volymerna till `PUID:PGID`
|
|
3. Kör applikationen som `PUID:PGID` via `su-exec`
|
|
|
|
### docker-compose.yml (minimal .env)
|
|
|
|
```env
|
|
DOCKER_PATH=/opt/archivum # Host-sökväg — config/ och data/ skapas här
|
|
HOST_PORT=8080
|
|
PUID=1000
|
|
PGID=1000
|
|
TZ=Europe/Stockholm
|
|
```
|
|
|
|
### Uppgraderingsförfarande
|
|
|
|
1. Bygg/hämta ny image
|
|
2. `docker compose down`
|
|
3. `docker compose up -d`
|
|
|
|
Inga databas-migreringar behövs i de flesta uppgraderingar; SQLite-schemat är bakåtkompatibelt (`CREATE TABLE IF NOT EXISTS`).
|