admin

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Index

Constants

View Source
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

func CheckBindSafety(bindAddr, token string) error

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

func ComputeSHA256(filePath string) (string, error)

ComputeSHA256 reads filePath and returns hex-encoded SHA256. Returns "" + error if the file cannot be read.

func GenerateToken added in v0.5.0

func GenerateToken() (string, error)

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

func MustPrintAutoGeneratedToken(token string, autoGenerated bool)

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

func ResolveToken() (token string, fromEnv bool)

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

func TokenMiddleware(next http.Handler, token string) http.Handler

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"`
	FooterSlogan    string `json:"footerSlogan" yaml:"footerSlogan"`
	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

type ThemeListResponse struct {
	Themes []ThemeInfo `json:"themes"`
	Active string      `json:"active"`
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL