Files
Archivum/ARCHITECTURE.md
Björn Blomberg a46d56e2fc feat: integrate @tailwindcss/typography and enhance folder picker functionality
- Added @tailwindcss/typography dependency to package.json and tailwind.config.js.
- Updated FolderPicker.vue to include additional properties (isGitRepo, hasDocuments) for directories.
- Enhanced Sidebar.vue with folder creation and document handling features, including drag-and-drop functionality.
- Implemented createFolder and moveDocument mutations in gql.ts for managing folder and document operations.
- Added logic to handle folder creation and document creation prompts in Sidebar.vue.
- Updated the layout and interactions in Sidebar.vue to improve user experience.
2026-04-13 10:55:02 +02:00

18 KiB

Archivum — Arkitektur och designdokumentation

Innehållsförteckning

  1. Systemöversikt
  2. Frontend
  3. Backend
  4. Exekveringsflöden
  5. Kodstruktur
  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 <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:
  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:

  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ö)

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: <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:

  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)

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).