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