- 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.
471 lines
18 KiB
Markdown
471 lines
18 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), 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
|
|
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`).
|