- Import GitHub dark theme for highlight.js in main.ts - Pass slug prop to VisualEditor in DocumentView.vue for version and normal editing modes - Update Vite configuration to increase maximum cache size and add media proxy - Add new dependencies for Tiptap extensions and highlight.js in package.json and package-lock.json
18 KiB
Archivum — Arkitektur och designdokumentation
Innehållsförteckning
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 | 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
localStorageoch skickar den somAuthorization: Bearer <token> - Kastar ett
Errorom 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
/graphqlgå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 <token>
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:
config.jsonsaknas eller är ofullständig (cfg == nil)- Databasen kunde inte öppnas (
database == nil) - 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ö)
Konfiguration
config.json hanteras av internal/config:
storage_path— rotkatalog för .adoc-filer och Git-repodb_path— sökväg till SQLite-filjwt_secret— används för session-token generation (ej JWT i klassisk mening)listen_addr— TCP-adress att lyssna på, t.ex.:4000ldap— 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: <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
│
▼
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):
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):
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:
- Skapar
/config,/data/wiki,/data/dbom de saknas - Ändrar ägare på volymerna till
PUID:PGID - Kör applikationen som
PUID:PGIDviasu-exec
docker-compose.yml (minimal .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
- Bygg/hämta ny image
docker compose downdocker compose up -d
Inga databas-migreringar behövs i de flesta uppgraderingar; SQLite-schemat är bakåtkompatibelt (CREATE TABLE IF NOT EXISTS).