# 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), 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| Lokala konton (bcrypt) + LDAP, JWT-liknande sessions-tokens | | 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 ` - 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: 1. Klient: SHA-256(username:password) → skickas som "password" 2. Server: bcrypt.CompareHashAndPassword(stored_hash, received_digest) 3. Vid match: slumpmässig hex-token genereras och sparas i minnet 4. Token returneras till klient, lagras i localStorage 5. Alla efterföljande requests: Authorization: Bearer Session-storage: in-memory map[token]*Session (försvinner vid omstart) Session-livstid: 7 dagar (konfigurerat i auth.go) ``` **Viktigt:** Sessions lagras enbart i minnet. Vid server-omstart måste alla klienter logga in igen. ### 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 → /archivum.db (dev-miljö) ``` ### 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` - `ldap` — valfri LDAP-konfiguration 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: (renderas direkt, ej via router) │ │ │ └─ svar: "OK" │ → app.requiresSetup = false │ ├─ app.token finns: → visas: │ └─ app.token saknas: → visas: │ └─ ready = true (spinner försvinner) ``` `SetupWizard`, `LoginView` och `AppLayout` renderas direkt i `App.vue` (inte via ``) 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 │ ▼ app.login(username, password) │ ├─ SHA-256(lowercase(username):password) → hashed │ └─ POST /graphql mutation Login($u, $p: hashed) │ ▼ handleLogin (server.go) │ ├─ db.GetUser(username) → lokal SQLite │ ├─ hittad: bcrypt.Compare(stored, hashed) → OK/fel │ └─ ej hittad + LDAP konfigurerat → ldapBind test │ ├─ Skapar slumpmässig session-token └─ Returnerar token │ ▼ Klient sparar token i localStorage AppLayout renderas ``` ### 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! # OK | REQUIRE_SETUP serverDirectories(path: String): [ServerDirectory!]! # Listar mappar (inkl. isGitRepo) documents(prefix: String): [DocumentMeta!]! document(slug: String!): Document folders: [String!]! # Listar alla mappvägar i repositoriet history(slug: String!): [CommitEntry!]! diff(slug: String!, fromHash: String!, toHash: String!): String! } type ServerDirectory { name: String! path: String! isGitRepo: Boolean! hasDocuments: Boolean! } type Mutation { setup(input: SetupInput!): Boolean! login(username: String!, password: String!): String! # returnerar session-token logout: Boolean! saveDocument(input: SaveDocumentInput!): Document! deleteDocument(slug: String!): Boolean! createFolder(path: String!): Boolean! # Skapar en ny mapp (katalog) moveDocument(oldSlug: String!, newSlug: String!): Boolean! # Flyttar ett dokument till en annan plats } ``` --- ## 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`).