Documentation
¶
Index ¶
- Constants
- func CheckBindSafety(bindAddr, token string) error
- func ComputeSHA256(filePath string) (string, error)
- func GenerateToken() (string, error)
- func MustPrintAutoGeneratedToken(token string, autoGenerated bool)
- func NewHandler(opts HandlerOptions) http.Handler
- func ResolveToken() (token string, fromEnv bool)
- func TokenMiddleware(next http.Handler, token string) http.Handler
- type APIError
- type AuditAction
- type AuditLogger
- type AuditRecord
- type ContentDetail
- type ContentItem
- type ContentListResponse
- type CreateContentRequest
- type HandlerOptions
- type LanguageInfo
- type ParamsSettings
- type PluginManageRequest
- type PluginManageResponse
- type SiblingResponse
- type SiteSettings
- type StatusResponse
- type ThemeInfo
- type ThemeListResponse
- type TreeNode
- type UpdateContentRequest
Constants ¶
const AdminTokenEnvVar = "HUAN_ADMIN_TOKEN"
AdminTokenEnvVar is the environment variable name for the admin auth token. When set, all admin API requests must include it as a Bearer token. When unset and bind is loopback, a one-time token is auto-generated and printed to stderr. When unset and bind is non-loopback, CheckBindSafety returns an error and the server refuses to start.
Variables ¶
This section is empty.
Functions ¶
func CheckBindSafety ¶ added in v0.5.0
CheckBindSafety enforces ADR 0011 L1: a non-loopback bind requires HUAN_ADMIN_TOKEN to be set, otherwise the admin panel would expose unrestricted file write to the LAN. Loopback binds are safe by default (CSRF + local privilege already implied by local access).
func ComputeSHA256 ¶ added in v0.5.0
ComputeSHA256 reads filePath and returns hex-encoded SHA256. Returns "" + error if the file cannot be read.
func GenerateToken ¶ added in v0.5.0
GenerateToken returns a 32-char hex token from 16 bytes of crypto/rand. Suitable as a one-time admin token printed to stderr.
func MustPrintAutoGeneratedToken ¶ added in v0.5.0
MustPrintAutoGeneratedToken prints the token to stderr exactly once when auto-generated (loopback case). Caller passes autoGenerated=true from ResolveToken(). This is informational — losing it just means the user must restart to see it again; they can always set HUAN_ADMIN_TOKEN explicitly to make it deterministic.
func NewHandler ¶
func NewHandler(opts HandlerOptions) http.Handler
NewHandler creates an http.Handler for the admin panel (SPA + API). It serves the embedded React app on /admin/ and provides /admin/api/* JSON endpoints. API endpoints are wrapped in TokenMiddleware (L2); write operations are recorded via AuditLogger to memoryDir/daily-{date}.md (L4).
Callers MUST run CheckBindSafety(bindAddr, token) BEFORE constructing the handler to enforce L1 (fail-fast on non-loopback bind without token).
func ResolveToken ¶ added in v0.5.0
ResolveToken reads HUAN_ADMIN_TOKEN from the environment. Returns (token, fromEnv). When fromEnv=false, the env was unset; the caller is responsible for deciding what to do based on bind safety:
- Loopback bind: auto-generate via GenerateToken() and print once.
- Non-loopback bind: CheckBindSafety returns an error before this point, so this branch is unreachable.
Splitting env-read from generation lets CheckBindSafety enforce the "non-loopback requires explicit env token" rule — auto-generated tokens must NOT satisfy non-loopback binds (otherwise fail-fast is bypassed).
func TokenMiddleware ¶ added in v0.5.0
TokenMiddleware enforces ADR 0011 L2: every admin API request must carry the configured token. The token may be in the Authorization header (Bearer scheme) or X-Huan-Admin-Token header. Comparison uses crypto/subtle.ConstantTimeCompare to prevent timing attacks.
The static SPA under /admin/ (without /api/) is intentionally NOT gated — the SPA needs to load to prompt for the token. The SPA serves no sensitive data itself; all sensitive data flows through /admin/api/*.
Types ¶
type APIError ¶
type APIError struct {
Error string `json:"error"`
}
APIError represents a JSON error response.
type AuditAction ¶ added in v0.5.0
type AuditAction string
AuditAction is a string-typed enum for the audit log entry kind.
const ( ActionContentCreate AuditAction = "content.create" ActionContentUpdate AuditAction = "content.update" ActionContentDelete AuditAction = "content.delete" ActionSettingsUpdate AuditAction = "settings.update" ActionSettingsYAML AuditAction = "settings.yaml.update" ActionMediaUpload AuditAction = "media.upload" ActionMediaDelete AuditAction = "media.delete" ActionPluginLoad AuditAction = "plugin.load" ActionPluginUnload AuditAction = "plugin.unload" ActionPluginReload AuditAction = "plugin.reload" )
type AuditLogger ¶ added in v0.5.0
type AuditLogger struct {
// contains filtered or unexported fields
}
AuditLogger writes admin write-operations to the project's daily note (memory/daily/{YYYY-MM-DD}.md), implementing ADR 0011 L4. The log is appended in markdown section format that fits the existing daily-note convention, so audit entries are grep-able, git-tracked, and reviewable alongside other daily context.
func NewAuditLogger ¶ added in v0.5.0
func NewAuditLogger(memoryDir string) *AuditLogger
NewAuditLogger returns a logger that writes to memoryDir/{YYYY-MM-DD}.md. The directory is created on first write if missing.
func (*AuditLogger) Log ¶ added in v0.5.0
func (a *AuditLogger) Log(rec AuditRecord) error
Log appends one audit record to today's daily note. The format matches the daily-note section style so entries grep cleanly alongside manual notes. Concurrent calls are serialized by a mutex (file-level locking would also work, but mutex is sufficient for a single-process server).
type AuditRecord ¶ added in v0.5.0
type AuditRecord struct {
Action AuditAction
Path string // relative path within content/ static/ or "huan.yaml"
BeforeSHA string
AfterSHA string
OccurredAt time.Time
}
AuditRecord captures one admin write-operation. BeforeSHA is the hex-encoded SHA256 of the file before the operation (empty for create). AfterSHA is the hex-encoded SHA256 after (empty for delete).
type ContentDetail ¶
type ContentDetail struct {
ContentItem
RawContent string `json:"rawContent"`
Frontmatter map[string]interface{} `json:"frontmatter"`
}
ContentDetail is the API response for reading a single file (full detail).
type ContentItem ¶
type ContentItem struct {
Title string `json:"title"`
RelPath string `json:"relPath"`
FilePath string `json:"filePath"`
Section string `json:"section"`
Kind string `json:"kind"`
Draft bool `json:"draft"`
Hidden bool `json:"hidden"`
Date string `json:"date"`
Tags []string `json:"tags"`
Description string `json:"description"`
Slug string `json:"slug"`
Language string `json:"language"`
URL string `json:"url"`
}
ContentItem is the API response for a single content file.
type ContentListResponse ¶
type ContentListResponse struct {
Sections map[string][]ContentItem `json:"sections"`
Tree []*TreeNode `json:"tree"`
Total int `json:"total"`
}
ContentListResponse wraps the content listing.
type CreateContentRequest ¶
type CreateContentRequest struct {
Section string `json:"section"`
Filename string `json:"filename"` // e.g. "my-post" (without .md)
Title string `json:"title"`
Draft bool `json:"draft"`
}
CreateContentRequest is the API body for creating a new content file.
type HandlerOptions ¶ added in v0.5.0
type HandlerOptions struct {
Cfg *config.Config
SourceDir string
Rebuild func()
ServeURL string
BindAddr string // informational; used for nothing currently but kept for future allow-listing
Token string // required; gates all /admin/api/* requests (L2)
MemoryDir string // optional; if empty, audit log is disabled
PluginManager *plugin.LifecycleManager
ThemeManager *theme.Manager
}
HandlerOptions configures the admin handler. Per ADR 0011, the token is mandatory — loopback binds auto-generate one (printed to stderr by the caller via ResolveToken); non-loopback binds must set HUAN_ADMIN_TOKEN (enforced by CheckBindSafety before this struct is built).
type LanguageInfo ¶
type LanguageInfo struct {
Language string `json:"language"`
RelPath string `json:"relPath"`
Title string `json:"title"`
Draft bool `json:"draft"`
}
LanguageInfo represents a single language version of a content file.
type ParamsSettings ¶
type ParamsSettings struct {
SubTitle string `json:"subTitle" yaml:"subTitle"`
Description string `json:"description" yaml:"description"`
Copyrights string `json:"copyrights" yaml:"copyrights"`
GoogleAnalytics string `json:"googleAnalytics" yaml:"googleAnalytics"`
CDNURL string `json:"cdnURL" yaml:"cdnURL"`
EnableMathJax bool `json:"enableMathJax" yaml:"enableMathJax"`
EnableSummary bool `json:"enableSummary" yaml:"enableSummary"`
}
type PluginManageRequest ¶ added in v0.7.0
type PluginManageRequest struct {
Name string `json:"name"` // plugin name (for unload/reload)
Path string `json:"path"` // .so file path (for load/reload)
}
PluginManageRequest is the API body for plugin load/unload/reload operations.
type PluginManageResponse ¶ added in v0.7.0
type PluginManageResponse struct {
Status string `json:"status"`
Plugin *plugin.PluginInfo `json:"plugin,omitempty"`
}
PluginManageResponse wraps the plugin management API response.
type SiblingResponse ¶
type SiblingResponse struct {
Current string `json:"current"`
Siblings []LanguageInfo `json:"siblings"`
}
SiblingResponse wraps the sibling language versions for a content file.
type SiteSettings ¶
type SiteSettings struct {
Title string `json:"title" yaml:"title"`
EnableEmoji bool `json:"enableEmoji" yaml:"enableEmoji"`
Minify bool `json:"minify" yaml:"minify"`
Paginate int `json:"paginate" yaml:"paginate"`
SummaryLen int `json:"summaryLength" yaml:"summaryLength"`
Params ParamsSettings `json:"params" yaml:"params"`
}
SiteSettings holds the Phase 1 config fields manageable from the admin UI. JSON tags use snake_case for the frontend API.
type StatusResponse ¶
type StatusResponse struct {
Title string `json:"title"`
BaseURL string `json:"baseURL"`
ServeURL string `json:"serveURL"`
Total int `json:"total"`
Published int `json:"published"`
Drafts int `json:"drafts"`
Sections int `json:"sections"`
Languages []string `json:"languages"`
MediaCount int `json:"mediaCount"`
SectionBreakdown map[string]int `json:"sectionBreakdown"`
RecentContent []ContentItem `json:"recentContent,omitempty"`
}
StatusResponse holds site overview stats for the dashboard.
type ThemeInfo ¶ added in v0.7.0
type ThemeInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Author string `json:"author"`
Description string `json:"description"`
Active bool `json:"active"`
Templates int `json:"templates"`
Funcs int `json:"funcs"`
}
ThemeInfo describes a single theme plugin for the admin API.
type ThemeListResponse ¶ added in v0.7.0
ThemeListResponse wraps the theme listing API response.
type TreeNode ¶
type TreeNode struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"` // "folder" or "file"
Count int `json:"count"` // number of content items in this subtree (folders only)
Children []*TreeNode `json:"children"`
}
TreeNode represents a node in the content directory tree for the admin navigation.
type UpdateContentRequest ¶
type UpdateContentRequest struct {
Frontmatter map[string]interface{} `json:"frontmatter"`
RawContent string `json:"rawContent"`
}
UpdateContentRequest is the API body for updating a content file.