feat(PROJ-13): OpenAPI 3.0 Spec + GET /api/v1/docs Endpoint
Serves the static OpenAPI YAML via go:embed. Completes the last open acceptance criterion for PROJ-13. PROJ-44 marked Deployed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -60,7 +60,7 @@
|
||||
| PROJ-41 | Dashboard Zeitreihe + Speicherprognose | Deployed | [PROJ-41](PROJ-41-dashboard-zeitreihe.md) | 2026-04-05 |
|
||||
| PROJ-42 | Gespeicherte Suchanfragen | Deployed | [PROJ-42](PROJ-42-gespeicherte-suchanfragen.md) | 2026-04-05 |
|
||||
| PROJ-43 | Automatische Archivierungsregeln | Planned | [PROJ-43](PROJ-43-archivierungsregeln.md) | 2026-04-05 |
|
||||
| PROJ-44 | OCR-GUI-Integration (Status, Download, Such-Highlight) | In Review | [PROJ-44](PROJ-44-ocr-gui-integration.md) | 2026-05-08 |
|
||||
| PROJ-44 | OCR-GUI-Integration (Status, Download, Such-Highlight) | Deployed | [PROJ-44](PROJ-44-ocr-gui-integration.md) | 2026-05-08 |
|
||||
| PROJ-45 | IMAP Per-Folder UID-Tracking + UIDVALIDITY-Check | Deployed | [PROJ-45](PROJ-45-imap-folder-uid-tracking.md) | 2026-05-11 |
|
||||
|
||||
<!-- Add features above this line -->
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# PROJ-44: OCR-GUI-Integration (Status, Download, Such-Highlight)
|
||||
|
||||
## Status: Deployed
|
||||
**Created:** 2026-05-08
|
||||
**Last Updated:** 2026-05-11
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: PROJ-35 (OCR & Anhang-Volltext-Indexierung) — liefert `ocr_status` + `attachment_text`
|
||||
- Requires: PROJ-6 (Volltext-Suche & Filterung) — Suche-Endpoint wird erweitert
|
||||
- Requires: PROJ-7 (E-Mail-Ansicht) — Mail-Detail-Seite wird erweitert
|
||||
|
||||
## Motivation
|
||||
|
||||
PROJ-35 hat den OCR-Pipeline gebaut, aber die Resultate sind bisher nur indirekt sichtbar (über Volltext-Suche, die plötzlich mehr findet). Nutzer und Auditoren sollen:
|
||||
|
||||
- erkennen können, dass OCR auf einer Mail gelaufen ist und den Status sehen
|
||||
- den extrahierten Text als Reintext-Datei (`.txt`) herunterladen können (für GoBD-konforme Archivierung des erkannten Inhalts neben dem Original-PDF)
|
||||
- bei Suchergebnissen sehen, dass ein Treffer aus dem OCR-Text kam — und welche Stelle es war (Snippet)
|
||||
|
||||
## User Stories
|
||||
|
||||
- Als **Nutzer** möchte ich auf der Mail-Detail-Seite sehen, ob die Anhänge bereits OCR-verarbeitet wurden, damit ich weiß, ob die Suche im Volltext greift.
|
||||
- Als **Auditor** möchte ich den extrahierten Reintext einer Mail als `.txt`-Datei herunterladen, damit ich ihn unabhängig vom Original-PDF archivieren oder weiterverarbeiten kann.
|
||||
- Als **Nutzer** möchte ich in der Trefferliste der Volltext-Suche erkennen, ob ein Treffer aus dem Mail-Body, dem Subject oder dem OCR-Anhang-Text kam — und idealerweise ein Textauszug der Fundstelle.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `MailDetail`-Response (`/api/mails/{id}`) enthält neue Felder `ocr_status` und `ocr_chars` (Anzahl extrahierter Zeichen, 0 wenn nichts gefunden)
|
||||
- [ ] Mail-Detail-Seite zeigt OCR-Badge im Header neben den anderen Status-Badges (verify_ok, threading)
|
||||
- `done` → grünes Badge „OCR ✓"
|
||||
- `failed` → rotes Badge „OCR ✗"
|
||||
- `skipped` → graues Badge „OCR n/a"
|
||||
- `pending` → blaues Badge „OCR …"
|
||||
- `disabled` → kein Badge
|
||||
- [ ] Neuer Endpoint `GET /api/mails/{id}/ocr-text` liefert den extrahierten Text als `text/plain; charset=utf-8` mit `Content-Disposition: attachment; filename="<message-id>.ocr.txt"`
|
||||
- [ ] Mail-Detail-Seite hat einen Button „OCR-Text herunterladen", der nur erscheint wenn `ocr_status='done'` und `ocr_chars > 0`
|
||||
- [ ] Such-Endpoint `/api/search` liefert pro Treffer ein neues Feld `snippet` (Markdown-style mit `<b>`-Tags um Match-Wörter) und `match_field` (z.B. `body`, `subject`, `attachment_text`)
|
||||
- [ ] Such-UI zeigt unter Subject/From eine zweite Zeile mit dem Snippet und einem kleinen Badge, das die Quelle benennt (z.B. „📄 aus PDF-Anhang")
|
||||
- [ ] Audit-Log: jeder OCR-Text-Download wird als `mail:ocr_download` erfasst
|
||||
- [ ] Externe v1-API: das gleiche `ocr_status`-Feld liegt im JSON von `/api/v1/mails/{id}`
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Mail ohne OCR-Daten (`skipped`, kein `attachment_text`) → Download-Endpoint liefert 404 mit klarer Fehlermeldung
|
||||
- Mail mit `ocr_status='pending'` → 202 Accepted + Hinweis „OCR läuft noch, bitte später erneut"
|
||||
- Snippet-Generierung schlägt fehl (Manticore-Highlight-Fehler) → Treffer wird ohne Snippet zurückgegeben, kein Hard-Error
|
||||
- Tenant-Isolation: OCR-Text-Endpoint prüft Mail-Zugriff genau wie `/raw` (PROJ-7)
|
||||
- Sehr lange OCR-Texte (>1 MB) → Endpoint streamt direkt aus Manticore, kein Buffer im Memory
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Backend
|
||||
|
||||
**Erweiterung `internal/api/mails.go`:**
|
||||
```go
|
||||
type MailDetail struct {
|
||||
// ... bestehende Felder
|
||||
OCRStatus string `json:"ocr_status"` // pending|done|failed|skipped|disabled
|
||||
OCRChars int `json:"ocr_chars"` // 0 wenn kein Text
|
||||
}
|
||||
```
|
||||
|
||||
Neuer Helper auf `*storage.Store`:
|
||||
```go
|
||||
func (s *Store) GetOCRStatus(ctx context.Context, id string) (status string, chars int, err error)
|
||||
```
|
||||
Liest `ocr_status` aus PostgreSQL und holt die `attachment_text`-Länge aus Manticore (oder cached die Länge in einer neuen Spalte `ocr_chars` — siehe Tech Design).
|
||||
|
||||
**Neuer Handler `handleGetOCRText`** in `internal/api/mails.go`:
|
||||
```go
|
||||
GET /api/mails/{id}/ocr-text
|
||||
→ 200 OK + text/plain wenn vorhanden
|
||||
→ 202 Accepted wenn ocr_status='pending'
|
||||
→ 404 Not Found wenn skipped/failed/no text
|
||||
→ 401/403 wie bei /raw
|
||||
```
|
||||
|
||||
**Erweiterung `internal/index/index.go::Hit`:**
|
||||
```go
|
||||
type Hit struct {
|
||||
ID string `json:"id"`
|
||||
Score float64 `json:"score"`
|
||||
Snippet string `json:"snippet,omitempty"` // Manticore SNIPPET() output
|
||||
MatchField string `json:"match_field,omitempty"` // body|subject|attachment_text|...
|
||||
}
|
||||
```
|
||||
|
||||
**Erweiterung `internal/index/manticore.go::Search`:**
|
||||
- Pro Hit ein zweiter Query-Pass mit `CALL SNIPPET(text, query, table_name)` — oder direkt im SELECT mit `HIGHLIGHT()` (Manticore-Funktion)
|
||||
- `MatchField` wird heuristisch ermittelt: pro Feld separate Queries und sehen, welches Feld als erstes matcht — alternativ über `WEIGHT()`-Inspection
|
||||
|
||||
### Frontend
|
||||
|
||||
**`src/components/ocr-badge.tsx`** (neu):
|
||||
- Kleine wiederverwendbare Badge-Komponente, die anhand des `ocr_status`-Werts die richtige Farbe + den richtigen Text rendert
|
||||
|
||||
**`src/app/mail/[id]/page.tsx`:**
|
||||
- Badge in den Header (neben Verifikation)
|
||||
- Download-Button „OCR-Text" wenn `ocr_chars > 0`
|
||||
|
||||
**`src/app/search/page.tsx`:**
|
||||
- Such-Treffer-Komponente um Snippet + Quellen-Badge erweitern
|
||||
- Snippet wird über `dangerouslySetInnerHTML` gerendert (nur `<b>`-Tags, vorher per HTML-Sanitizer wrappen — Manticore liefert nur diese eine Markup-Form)
|
||||
|
||||
### DB-Schema (optional)
|
||||
|
||||
Optional kann `attachment_text`-Länge als `emails.ocr_chars BIGINT` cached werden, um den `Manticore SELECT length(attachment_text)`-Roundtrip pro Mail-Detail zu sparen. Tradeoff: zusätzliche Spalte vs. eine zusätzliche Manticore-Query pro Detail-Aufruf. **Entscheidung: Cache in PostgreSQL** — wird beim OCR-Worker mit dem Status zusammen geschrieben.
|
||||
|
||||
```sql
|
||||
ALTER TABLE emails ADD COLUMN IF NOT EXISTS ocr_chars BIGINT DEFAULT 0;
|
||||
```
|
||||
|
||||
## Tech Design
|
||||
|
||||
### Datenfluss für OCR-Text-Download
|
||||
|
||||
```
|
||||
Frontend (Mail-Detail)
|
||||
├ getMail(id) → MailDetail mit ocr_status + ocr_chars
|
||||
└ Wenn ocr_chars > 0: Button rendern
|
||||
└ Click → GET /api/mails/{id}/ocr-text
|
||||
├ Backend: ACL-Check (requireMailAccess)
|
||||
├ Backend: hole attachment_text aus Manticore SELECT
|
||||
├ Backend: Audit-Log "mail:ocr_download"
|
||||
└ Response: text/plain + Content-Disposition
|
||||
```
|
||||
|
||||
### Datenfluss für Such-Snippet
|
||||
|
||||
```
|
||||
Frontend (Suche)
|
||||
└ POST /api/search { query: "rechnungsnummer" }
|
||||
└ Backend Search:
|
||||
├ Manticore SELECT mit MATCH() + WEIGHT() pro Treffer
|
||||
├ Pro Treffer: CALL SNIPPET(field_value, query) für Snippet
|
||||
├ Heuristik für match_field: prüfe welches Text-Feld den Query enthält
|
||||
└ Response: Hits mit snippet + match_field
|
||||
```
|
||||
|
||||
### Audit-Log-Erweiterung
|
||||
|
||||
Neue Aktion: `mail:ocr_download` mit `mail_id` als Subject. Bestehende Audit-Pipeline (`s.audlog.Log(...)`) wird weiterverwendet.
|
||||
|
||||
## Nicht in Scope
|
||||
|
||||
- OCR-Text-Bearbeitung oder -Korrektur durch Nutzer (nur Lesen + Download)
|
||||
- Inline-Anzeige des OCR-Texts auf der Mail-Detail-Seite (nur Download — der Reintext wäre oft mehrere Seiten lang)
|
||||
- Neuverarbeitung über die GUI (das geht nur per CLI `archivmail ocr-reprocess`)
|
||||
- Sprachen jenseits dessen, was PROJ-35 ohnehin unterstützt (deu+eng)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**Implementiert (alle Akzeptanzkriterien erfüllt):**
|
||||
|
||||
- DB-Schema: `ALTER TABLE emails ADD COLUMN IF NOT EXISTS ocr_chars BIGINT DEFAULT 0` als idempotente Migration in `internal/storage/storage.go`. Storage-Helper `SetOCRResult` / `GetOCRMeta` in `internal/storage/ocr.go`.
|
||||
- OCR-Worker schreibt `ocr_chars` zusammen mit `ocr_status`. Nutzt jetzt die kanonische `emails.tenant_id` aus PostgreSQL (nicht mehr `Job.TenantID`), damit CLI-Reprocess (`tenantID == nil`) korrekt im Tenant-Index landet.
|
||||
- API-Endpoint `GET /api/mails/{id}/ocr-text` in `internal/api/ocr_handlers.go` mit ACL wie `/raw`, Status-Mapping 200/202/404 und Audit-Event `mail:ocr_download`. Endpoint löst den Manticore-Reader ebenfalls über die DB-Tenant-ID auf — gleicher Strukturfix wie im Worker.
|
||||
- `MailDetail` (+ externes v1-API) liefert `ocr_status` und `ocr_chars`.
|
||||
- Such-Hits liefern `snippet` + `match_field`. Heuristik: probiert `subject > body > attachment_text > attachment_names > from_addr > to_addr` durch MATCH()-Tests; Snippet via `SELECT SNIPPET(text, query) FROM table` (CALL SNIPPETS scheitert am Go-MySQL-Driver-Packet, Manticore 25 akzeptiert keine Options-Args).
|
||||
- Frontend: `OcrBadge`-Komponente, Download-Button auf Mail-Detail-Seite, Snippet + Quellen-Badge in Trefferliste. `sanitizeSnippet` mit Whitelist nur für `<b>`-Tags.
|
||||
- IMAP- und POP3-Live-Import triggern jetzt den OCR-Worker via Function-Pointer-Pattern (`SetOCRSubmit`) — vermeidet Zirkularimport. Vorher blieben neu importierte Mails ewig in `ocr_status='pending'`.
|
||||
|
||||
**Bugfixes während Implementierung:**
|
||||
|
||||
- **Live-Import-Bug:** IMAP/POP3 hatten keinen `ocrWorker.Submit`-Call → neue Mails standen 4 Tage auf 132 in `pending` (54 Mails bestätigt). Fix: `SetOCRSubmit`-Hook in `cmd/archivmail/main.go` verdrahtet.
|
||||
- **Tenant-Isolation-Strukturbug:** Worker und Endpoint lasen unterschiedliche Tenant-Quellen (Submitter-Kontext vs. Session) → Schreib- und Lese-Pfad landeten in verschiedenen Manticore-Indizes; Tenant-User sahen eigenen OCR-Text nicht. Fix: Beide Pfade lesen `emails.tenant_id` als Single Source of Truth.
|
||||
- **Manticore 25 / Go-MySQL-Treiber:** CALL SNIPPETS Options-Args weggekürzt, dann komplett auf `SELECT SNIPPET(...)` umgestellt — CALL-Antworten lassen sich vom Treiber nicht parsen.
|
||||
|
||||
## QA Test Results
|
||||
|
||||
Verifiziert auf **192.168.1.132** mit Commit `16013e8`:
|
||||
|
||||
| Szenario | Erwartung | Ergebnis |
|
||||
|---|---|---|
|
||||
| Tenant-2-User, eigene Mail (Anhang mit OCR-Text) | 200 + Text | OK |
|
||||
| Tenant-2-User, Mail aus Tenant-1 | 403/404 | 403 access denied — Isolation greift |
|
||||
| Admin, Tenant-Mail | 403 (SEC-29: Admin sieht keine Mail-Inhalte) | 403 — korrekt |
|
||||
| Auditor, globale Mail (tenant_id IS NULL) | 200 + Text | OK |
|
||||
| Auditor, Tenant-Mail | 403 | OK |
|
||||
| DB-Migration `ocr_chars` | Spalte vorhanden | Vorhanden + Index `idx_emails_ocr_status` |
|
||||
| Manticore-Routing | attachment_text liegt in emails_tenant_N, nicht global | Bestätigt |
|
||||
|
||||
## Deployment
|
||||
|
||||
- **2026-05-11 — 192.168.1.132 (Test):** Commit `16013e8` aktiv, alle Dienste running, QA grün.
|
||||
- **2026-05-29 — 192.168.1.131 (Prod):** Commit `fa9f777` aktiv (inkl. Security-Fixes), api 1.8 / storage 1.9, alle Dienste running.
|
||||
@@ -0,0 +1,250 @@
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: archivmail REST API
|
||||
version: "1"
|
||||
description: |
|
||||
Read-only REST API for external CRM/ERP systems.
|
||||
Authenticate with an API key obtained from the admin panel.
|
||||
All endpoints are GET-only. POST/PUT/PATCH/DELETE return 405.
|
||||
|
||||
servers:
|
||||
- url: /api/v1
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: "API key in format `am_<token>` — obtained once from Admin → API Keys."
|
||||
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
required: [error]
|
||||
|
||||
MailSummary:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique message ID (UUID)
|
||||
from:
|
||||
type: string
|
||||
to:
|
||||
type: string
|
||||
subject:
|
||||
type: string
|
||||
date:
|
||||
type: string
|
||||
format: date-time
|
||||
size:
|
||||
type: integer
|
||||
description: Raw EML size in bytes
|
||||
has_attachments:
|
||||
type: boolean
|
||||
|
||||
MailDetail:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
from:
|
||||
type: string
|
||||
to:
|
||||
type: string
|
||||
cc:
|
||||
type: string
|
||||
subject:
|
||||
type: string
|
||||
date:
|
||||
type: string
|
||||
format: date-time
|
||||
size:
|
||||
type: integer
|
||||
body_plain:
|
||||
type: string
|
||||
ocr_status:
|
||||
type: string
|
||||
enum: [pending, done, failed, skipped, disabled]
|
||||
attachments:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
index:
|
||||
type: integer
|
||||
filename:
|
||||
type: string
|
||||
content_type:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
|
||||
SearchResponse:
|
||||
type: object
|
||||
properties:
|
||||
mails:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/MailSummary"
|
||||
total:
|
||||
type: integer
|
||||
page:
|
||||
type: integer
|
||||
pages:
|
||||
type: integer
|
||||
|
||||
paths:
|
||||
/mails:
|
||||
get:
|
||||
summary: Search / list mails
|
||||
description: |
|
||||
Search the archive. All parameters are optional and combined with AND.
|
||||
|
||||
**Role `user`:** the `contact` parameter is required.
|
||||
**Role `auditor`:** all parameters are optional.
|
||||
|
||||
Results are paginated (max 100 per page).
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
description: Full-text query
|
||||
schema:
|
||||
type: string
|
||||
- name: contact
|
||||
in: query
|
||||
description: >
|
||||
Match mails where this address appears in From **or** To.
|
||||
Required for `user`-role keys.
|
||||
schema:
|
||||
type: string
|
||||
- name: from
|
||||
in: query
|
||||
description: Filter by sender address (ignored when `contact` is set)
|
||||
schema:
|
||||
type: string
|
||||
- name: to
|
||||
in: query
|
||||
description: Filter by recipient address (ignored when `contact` is set)
|
||||
schema:
|
||||
type: string
|
||||
- name: subject
|
||||
in: query
|
||||
description: Filter by subject (appended to `q`)
|
||||
schema:
|
||||
type: string
|
||||
- name: date_from
|
||||
in: query
|
||||
description: "Start of date range (RFC 3339 or YYYY-MM-DD)"
|
||||
schema:
|
||||
type: string
|
||||
- name: date_to
|
||||
in: query
|
||||
description: "End of date range (RFC 3339 or YYYY-MM-DD, inclusive)"
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 25
|
||||
maximum: 100
|
||||
responses:
|
||||
"200":
|
||||
description: Search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SearchResponse"
|
||||
"400":
|
||||
description: Missing required parameter (e.g. `contact` for user-role keys)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
description: Missing or invalid API key
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"429":
|
||||
description: Rate limit exceeded
|
||||
headers:
|
||||
Retry-After:
|
||||
schema:
|
||||
type: integer
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/mails/{message_id}:
|
||||
get:
|
||||
summary: Get mail metadata
|
||||
parameters:
|
||||
- name: message_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Mail metadata and parsed body
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MailDetail"
|
||||
"401":
|
||||
description: Missing or invalid API key
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"404":
|
||||
description: Mail not found or not accessible
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/mails/{message_id}/raw:
|
||||
get:
|
||||
summary: Download original EML
|
||||
parameters:
|
||||
- name: message_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Raw EML file
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"401":
|
||||
description: Missing or invalid API key
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"404":
|
||||
description: Mail not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
@@ -296,6 +296,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("POST /api/admin/users/{id}/totp/reset", s.authAdmin(s.handleTOTPReset))
|
||||
|
||||
// PROJ-13: External REST API v1 (API-key auth)
|
||||
s.mux.HandleFunc("GET /api/v1/docs", s.handleV1Docs)
|
||||
s.mux.HandleFunc("/api/v1/mails", s.apiKeyMw.Wrap(s.handleV1SearchMails))
|
||||
s.mux.HandleFunc("GET /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1GetMail))
|
||||
s.mux.HandleFunc("GET /api/v1/mails/{message_id}/raw", s.apiKeyMw.Wrap(s.handleV1GetMailRaw))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed openapi.yaml
|
||||
var openapiSpec []byte
|
||||
|
||||
// handleV1Docs serves the OpenAPI 3.0 spec for the external REST API.
|
||||
// GET /api/v1/docs
|
||||
func (s *Server) handleV1Docs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(openapiSpec)
|
||||
}
|
||||
Reference in New Issue
Block a user