Implement AsciiDoc and TipTap conversion logic, including new Admonition node and CodeBlock extension; add DiffViewer and HistoryPanel components for document version comparison; introduce password hashing utility with SHA-256.
This commit is contained in:
459
ARCHITECTURE.md
Normal file
459
ARCHITECTURE.md
Normal file
@@ -0,0 +1,459 @@
|
||||
# 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 <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):**
|
||||
```graphql
|
||||
type Query {
|
||||
systemStatus: SystemStatus! # OK | REQUIRE_SETUP
|
||||
documents(prefix: String): [DocumentMeta!]!
|
||||
document(slug: String!): Document
|
||||
history(slug: String!): [CommitEntry!]!
|
||||
diff(slug: String!, fromHash: String!, toHash: String!): String!
|
||||
}
|
||||
|
||||
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!
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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`).
|
||||
Reference in New Issue
Block a user