feat: implement built-in API documentation system
- Introduce `DocRegistry` for managing API documentation - Add automatic discovery of markdown documentation from templates - Implement a new web-based documentation viewer at `/docs` - Include basic markdown to HTML conversion logic - Register core HeroServer API documentation and an example 'comments' API
This commit is contained in:
@@ -1,9 +1,26 @@
|
||||
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.hero.heroserver
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import os
|
||||
|
||||
mut server := heroserver.new_server(heroserver.ServerConfig{
|
||||
port: 8080
|
||||
auth_config: heroserver.AuthConfig{}
|
||||
})!
|
||||
|
||||
// Register the comments API with documentation
|
||||
script_dir := os.dir(@FILE)
|
||||
openrpc_path := os.join_path(script_dir, 'openrpc.json')
|
||||
spec := openrpc.new(path: openrpc_path)!
|
||||
handler := openrpc.new_handler(openrpc_path)!
|
||||
server.register_api('comments', spec, handler)
|
||||
|
||||
// Setup documentation site
|
||||
server.setup_docs_site() or { println('Warning: Failed to setup documentation site: ${err}') }
|
||||
|
||||
println('Server starting on http://localhost:8080')
|
||||
println('Documentation available at: http://localhost:8080/docs')
|
||||
println('Comments API available at: http://localhost:8080/api/comments')
|
||||
|
||||
server.start()!
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import freeflowuniverse.herolib.hero.heroserver
|
||||
|
||||
fn testsuite_begin() {
|
||||
// a clean start
|
||||
// os.rm('./db')! //TODO: was giving issues
|
||||
}
|
||||
|
||||
fn test_heroserver_new() {
|
||||
// Create server
|
||||
mut server := heroserver.new_server(heroserver.ServerConfig{
|
||||
port: 8080
|
||||
auth_config: heroserver.AuthConfig{}
|
||||
})!
|
||||
|
||||
// Test that server was created successfully
|
||||
assert server.config.port == 8080
|
||||
assert true
|
||||
}
|
||||
94
lib/hero/heroserver/README.md
Normal file
94
lib/hero/heroserver/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# HeroServer Documentation System
|
||||
|
||||
The HeroServer features a built-in documentation system that automatically discovers and serves API documentation with a clean web interface.
|
||||
|
||||
## Adding New Documentation
|
||||
|
||||
1. **Create a markdown file** in `lib/hero/heroserver/templates/pages/`:
|
||||
|
||||
```bash
|
||||
# Example: lib/hero/heroserver/templates/pages/calendar.md
|
||||
```
|
||||
|
||||
2. **Write your documentation** using standard markdown:
|
||||
|
||||
```markdown
|
||||
# Calendar API
|
||||
|
||||
A comprehensive calendar management service.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Create Calendar
|
||||
|
||||
**Endpoint:** `POST /calendars`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "My Calendar"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cal_123",
|
||||
"name": "My Calendar"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Restart the server** - Documentation available at `http://localhost:8080/docs/calendar`
|
||||
|
||||
## Features
|
||||
|
||||
- **Auto-Discovery**: Scans `templates/pages/` for `.md` files on startup
|
||||
- **Built-in Viewer**: Professional web interface with sidebar navigation
|
||||
- **Dynamic Rendering**: Markdown converted to HTML on-demand
|
||||
- **Zero Configuration**: Just add markdown files and they appear automatically
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Your markdown files should follow this structure:
|
||||
|
||||
```markdown
|
||||
# API Name
|
||||
|
||||
Brief description of your API.
|
||||
|
||||
## Overview
|
||||
|
||||
Detailed overview...
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Create Resource
|
||||
|
||||
**Endpoint:** `POST /resources`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "example"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "123",
|
||||
"name": "example"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Standard HTTP status codes and error responses...
|
||||
|
||||
## Accessing Documentation
|
||||
|
||||
- **Main Index**: `http://localhost:8080/docs` (redirects to first available API)
|
||||
- **Specific API**: `http://localhost:8080/docs/{api_name}`
|
||||
- **Example**: `http://localhost:8080/docs/calendar`
|
||||
@@ -4,23 +4,360 @@ import freeflowuniverse.herolib.schemas.openrpc
|
||||
import os
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
|
||||
// Generate HTML documentation for handler type
|
||||
pub fn (s HeroServer) generate_documentation(handler_type string, handler openrpc.Handler) !string {
|
||||
spec := s.handler_registry.get_spec(handler_type) or {
|
||||
return error('No spec found for handler type: ${handler_type}')
|
||||
// Documentation registry for managing multiple API docs
|
||||
pub struct DocRegistry {
|
||||
pub mut:
|
||||
apis map[string]ApiDocInfo
|
||||
templates_dir string // Directory where user-provided markdown templates are stored
|
||||
discovered bool // Flag to prevent multiple template discoveries
|
||||
}
|
||||
|
||||
pub struct ApiDocInfo {
|
||||
pub:
|
||||
name string
|
||||
title string
|
||||
description string
|
||||
version string
|
||||
spec openrpc.OpenRPC
|
||||
handler openrpc.Handler
|
||||
}
|
||||
|
||||
pub fn new_doc_registry() &DocRegistry {
|
||||
templates_dir := os.join_path(os.dir(@FILE), 'templates', 'pages')
|
||||
return &DocRegistry{
|
||||
templates_dir: templates_dir
|
||||
}
|
||||
}
|
||||
|
||||
// Register an API for documentation
|
||||
pub fn (mut dr DocRegistry) register_api(name string, spec openrpc.OpenRPC, handler openrpc.Handler) {
|
||||
dr.apis[name] = ApiDocInfo{
|
||||
name: name
|
||||
title: spec.info.title
|
||||
description: spec.info.description
|
||||
version: spec.info.version
|
||||
spec: spec
|
||||
handler: handler
|
||||
}
|
||||
}
|
||||
|
||||
// Register the core HeroServer API documentation
|
||||
pub fn (mut dr DocRegistry) register_core_api() {
|
||||
// Create a minimal OpenRPC spec for the core API
|
||||
core_spec := openrpc.OpenRPC{
|
||||
openrpc: '1.2.6'
|
||||
info: openrpc.Info{
|
||||
title: 'HeroServer Core API'
|
||||
description: 'Core authentication and system endpoints for HeroServer'
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: []
|
||||
}
|
||||
|
||||
// Load and process template
|
||||
template_path := os.join_path(@VMODROOT, 'lib/hero/heroserver/templates/doc.md')
|
||||
dr.apis['heroserver'] = ApiDocInfo{
|
||||
name: 'heroserver'
|
||||
title: 'HeroServer Core API'
|
||||
description: 'Core authentication and system endpoints for HeroServer'
|
||||
version: '1.0.0'
|
||||
spec: core_spec
|
||||
handler: openrpc.Handler{
|
||||
specification: core_spec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover and register documentation from template files
|
||||
pub fn (mut dr DocRegistry) discover_template_docs() ! {
|
||||
// Prevent multiple discoveries
|
||||
if dr.discovered {
|
||||
return
|
||||
}
|
||||
dr.discovered = true
|
||||
|
||||
if !os.exists(dr.templates_dir) {
|
||||
println('Templates directory does not exist: ${dr.templates_dir}')
|
||||
return
|
||||
}
|
||||
|
||||
// Get all .md files in the templates directory
|
||||
files := os.ls(dr.templates_dir) or {
|
||||
println('Failed to read templates directory: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
for file in files {
|
||||
if file.ends_with('.md') && file != 'doc.md' {
|
||||
// Extract API name from filename (remove .md extension)
|
||||
api_name := file.replace('.md', '')
|
||||
|
||||
// Skip if already registered (e.g., heroserver_core_api.md becomes heroserver)
|
||||
if api_name == 'heroserver_core_api' {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read the markdown file to extract title and description
|
||||
file_path := os.join_path(dr.templates_dir, file)
|
||||
content := os.read_file(file_path) or {
|
||||
println('Failed to read template file ${file}: ${err}')
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract title and description from markdown
|
||||
title, description := dr.extract_doc_metadata(content, api_name)
|
||||
|
||||
// Create a minimal spec for template-based docs
|
||||
template_spec := openrpc.OpenRPC{
|
||||
openrpc: '1.2.6'
|
||||
info: openrpc.Info{
|
||||
title: title
|
||||
description: description
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: []
|
||||
}
|
||||
|
||||
// Register the template-based API
|
||||
dr.apis[api_name] = ApiDocInfo{
|
||||
name: api_name
|
||||
title: title
|
||||
description: description
|
||||
version: '1.0.0'
|
||||
spec: template_spec
|
||||
handler: openrpc.Handler{
|
||||
specification: template_spec
|
||||
}
|
||||
}
|
||||
|
||||
println('Discovered documentation template: ${api_name} (${title})')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract title and description from markdown content
|
||||
fn (dr DocRegistry) extract_doc_metadata(content string, fallback_name string) (string, string) {
|
||||
lines := content.split('\n')
|
||||
mut title := fallback_name.replace('_', ' ').title()
|
||||
mut description := 'API documentation for ${title}'
|
||||
|
||||
for line in lines {
|
||||
trimmed := line.trim_space()
|
||||
|
||||
// Look for the first H1 heading as title
|
||||
if trimmed.starts_with('# ') && title == fallback_name.replace('_', ' ').title() {
|
||||
title = trimmed[2..].trim_space()
|
||||
}
|
||||
|
||||
// Look for description in the first paragraph after title
|
||||
if trimmed.len > 0 && !trimmed.starts_with('#') && !trimmed.starts_with('**')
|
||||
&& !trimmed.starts_with('```') && description == 'API documentation for ${title}' {
|
||||
description = trimmed
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return title, description
|
||||
}
|
||||
|
||||
// Setup documentation system
|
||||
pub fn (mut s HeroServer) setup_docs_site() ! {
|
||||
// Discover documentation templates first
|
||||
s.doc_registry.discover_template_docs()!
|
||||
|
||||
println('Documentation system ready with ${s.doc_registry.apis.len} APIs')
|
||||
println('Available documentation:')
|
||||
for name, api_info in s.doc_registry.apis {
|
||||
println(' - ${api_info.title} (${name})')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate HTML documentation viewer for a specific API
|
||||
pub fn (s HeroServer) generate_docs_viewer(api_name string) !string {
|
||||
// Load the HTML template
|
||||
template_path := os.join_path(os.dir(@FILE), 'templates/docs_viewer.html')
|
||||
template_content := os.read_file(template_path) or {
|
||||
return error('Failed to read docs viewer template: ${err}')
|
||||
}
|
||||
|
||||
// Generate sidebar items
|
||||
mut sidebar_items := ''
|
||||
for name, api_info in s.doc_registry.apis {
|
||||
active_class := if name == api_name { ' class="active"' } else { '' }
|
||||
sidebar_items += '<li><a href="/docs/${name}"${active_class}>${api_info.title}</a></li>\n'
|
||||
}
|
||||
|
||||
// Get the markdown content for the requested API
|
||||
content := s.get_api_markdown_content(api_name) or {
|
||||
return error('Failed to get markdown content for ${api_name}: ${err}')
|
||||
}
|
||||
|
||||
// Convert markdown to HTML (simple conversion for now)
|
||||
html_content := s.markdown_to_html(content)
|
||||
|
||||
// Get the API title
|
||||
api_info := s.doc_registry.apis[api_name] or { return error('API ${api_name} not found') }
|
||||
|
||||
// Replace template variables
|
||||
mut result := template_content
|
||||
result = result.replace('@{title}', api_info.title)
|
||||
result = result.replace('@{sidebar_items}', sidebar_items)
|
||||
result = result.replace('@{content}', html_content)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Get markdown content for an API
|
||||
fn (s HeroServer) get_api_markdown_content(api_name string) !string {
|
||||
// Check if there's a custom template file for this API
|
||||
custom_template_path := os.join_path(s.doc_registry.templates_dir, '${api_name}.md')
|
||||
if os.exists(custom_template_path) {
|
||||
return os.read_file(custom_template_path) or {
|
||||
return error('Failed to read custom template for ${api_name}: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// For core HeroServer API, use the special template
|
||||
if api_name == 'heroserver' {
|
||||
core_template_path := os.join_path(os.dir(@FILE), 'templates/heroserver_core_api.md')
|
||||
return os.read_file(core_template_path) or {
|
||||
return error('Failed to read core API template: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// For OpenRPC-based APIs, generate from spec
|
||||
spec := s.handler_registry.get_spec(api_name) or {
|
||||
return error('No documentation found for ${api_name}')
|
||||
}
|
||||
|
||||
// Load and process the generic template
|
||||
template_path := os.join_path(os.dir(@FILE), 'templates/doc.md')
|
||||
template_content := os.read_file(template_path) or {
|
||||
return error('Failed to read documentation template: ${err}')
|
||||
}
|
||||
|
||||
// Process template with spec data
|
||||
doc_content := process_doc_template(template_content, spec, handler_type)
|
||||
return process_doc_template(template_content, spec, api_name)
|
||||
}
|
||||
|
||||
// Return HTML with Bootstrap and markdown processing
|
||||
return generate_html_wrapper(doc_content, handler_type)
|
||||
// Simple markdown to HTML converter (basic implementation)
|
||||
fn (s HeroServer) markdown_to_html(markdown string) string {
|
||||
mut html := markdown
|
||||
|
||||
// Process headers more carefully
|
||||
html = s.process_headers(html)
|
||||
|
||||
// Convert code blocks and process line by line
|
||||
lines := html.split('\n')
|
||||
mut result_lines := []string{}
|
||||
mut in_code_block := false
|
||||
|
||||
for line in lines {
|
||||
if line.starts_with('```') {
|
||||
if in_code_block {
|
||||
result_lines << '</code></pre>'
|
||||
in_code_block = false
|
||||
} else {
|
||||
code_lang := line[3..].trim_space()
|
||||
result_lines << '<pre><code class="language-${code_lang}">'
|
||||
in_code_block = true
|
||||
}
|
||||
} else if in_code_block {
|
||||
// Escape HTML in code blocks
|
||||
escaped_line := line.replace('&', '&').replace('<', '<').replace('>',
|
||||
'>')
|
||||
result_lines << escaped_line
|
||||
} else {
|
||||
// Process regular markdown
|
||||
mut processed_line := line
|
||||
|
||||
// Process inline code first (before other replacements)
|
||||
processed_line = s.process_inline_code(processed_line)
|
||||
|
||||
// Bold text
|
||||
processed_line = processed_line.replace('**', '<strong>').replace('**', '</strong>')
|
||||
|
||||
// Paragraphs
|
||||
if processed_line.trim_space() == '' {
|
||||
processed_line = '</p><p>'
|
||||
}
|
||||
|
||||
result_lines << processed_line
|
||||
}
|
||||
}
|
||||
|
||||
html = result_lines.join('\n')
|
||||
|
||||
// Wrap in paragraph tags
|
||||
html = '<p>' + html + '</p>'
|
||||
|
||||
// Clean up empty paragraphs and code elements
|
||||
html = html.replace('<p></p>', '')
|
||||
html = html.replace('<p></p><p>', '<p>')
|
||||
html = html.replace('<code></code>', '')
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
// Process inline code with proper backtick handling
|
||||
fn (s HeroServer) process_inline_code(line string) string {
|
||||
mut result := line
|
||||
mut in_code := false
|
||||
mut chars := result.runes()
|
||||
mut new_chars := []rune{}
|
||||
mut i := 0
|
||||
|
||||
for i < chars.len {
|
||||
if chars[i] == `\`` {
|
||||
if in_code {
|
||||
// Closing backtick
|
||||
new_chars << '</code>'.runes()
|
||||
in_code = false
|
||||
} else {
|
||||
// Opening backtick
|
||||
new_chars << '<code>'.runes()
|
||||
in_code = true
|
||||
}
|
||||
} else {
|
||||
new_chars << chars[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// If we ended with an unclosed code tag, close it
|
||||
if in_code {
|
||||
new_chars << '</code>'.runes()
|
||||
}
|
||||
|
||||
return new_chars.string()
|
||||
}
|
||||
|
||||
// Process headers with better detection to avoid false positives
|
||||
fn (s HeroServer) process_headers(content string) string {
|
||||
lines := content.split('\n')
|
||||
mut result_lines := []string{}
|
||||
|
||||
for line in lines {
|
||||
mut processed_line := line
|
||||
|
||||
// Only process lines that start with # and have space after
|
||||
if line.starts_with('# ') && line.len > 2 {
|
||||
processed_line = '<h1>' + line[2..].trim_space() + '</h1>'
|
||||
} else if line.starts_with('## ') && line.len > 3 {
|
||||
processed_line = '<h2>' + line[3..].trim_space() + '</h2>'
|
||||
} else if line.starts_with('### ') && line.len > 4 {
|
||||
processed_line = '<h3>' + line[4..].trim_space() + '</h3>'
|
||||
} else if line.starts_with('#### ') && line.len > 5 {
|
||||
processed_line = '<h4>' + line[5..].trim_space() + '</h4>'
|
||||
}
|
||||
|
||||
result_lines << processed_line
|
||||
}
|
||||
|
||||
return result_lines.join('\n')
|
||||
}
|
||||
|
||||
// Legacy function for backward compatibility - now redirects to markdown generation
|
||||
pub fn (s HeroServer) generate_documentation(handler_type string, handler openrpc.Handler) !string {
|
||||
return 'Documentation is now served via built-in viewer at http://localhost:8080/docs'
|
||||
}
|
||||
|
||||
// Process the markdown template with OpenRPC spec data
|
||||
@@ -81,12 +418,3 @@ fn generate_method_doc(method openrpc.Method) string {
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
// Generate HTML wrapper with Bootstrap
|
||||
fn generate_html_wrapper(markdown_content string, handler_type string) string {
|
||||
template_path := os.join_path(@VMODROOT, 'lib/hero/heroserver/templates/doc_wrapper.html')
|
||||
mut template_content := os.read_file(template_path) or { return 'Template not found' }
|
||||
template_content = template_content.replace('@{handler_type}', handler_type)
|
||||
template_content = template_content.replace('@{markdown_content}', markdown_content)
|
||||
return template_content
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ module heroserver
|
||||
import veb
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.hero.crypt
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
|
||||
pub struct ServerConfig {
|
||||
pub mut:
|
||||
@@ -16,6 +17,7 @@ pub mut:
|
||||
auth_manager &AuthManager
|
||||
handler_registry &HandlerRegistry
|
||||
age_client &crypt.AGEClient
|
||||
doc_registry &DocRegistry
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
@@ -67,16 +69,57 @@ pub fn (mut s HeroServer) api(mut ctx Context, handler_type string) veb.Result {
|
||||
return ctx.json(response)
|
||||
}
|
||||
|
||||
// Documentation endpoints
|
||||
@['/doc/:handler_type'; get]
|
||||
pub fn (mut s HeroServer) doc(mut ctx Context, handler_type string) veb.Result {
|
||||
handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
|
||||
// Documentation index endpoint - redirects to first available API
|
||||
@['/docs'; get]
|
||||
pub fn (mut s HeroServer) docs_index(mut ctx Context) veb.Result {
|
||||
// Setup documentation site if not already done
|
||||
s.setup_docs_site() or { return ctx.server_error('Documentation setup failed: ${err}') }
|
||||
|
||||
doc_html := s.generate_documentation(handler_type, handler) or {
|
||||
return ctx.server_error('Documentation generation failed')
|
||||
// Redirect to the first available API documentation (preferably heroserver)
|
||||
if 'heroserver' in s.doc_registry.apis {
|
||||
return ctx.redirect('/docs/heroserver')
|
||||
}
|
||||
|
||||
return ctx.html(doc_html)
|
||||
// If no heroserver, redirect to the first available API
|
||||
for name, _ in s.doc_registry.apis {
|
||||
return ctx.redirect('/docs/${name}')
|
||||
}
|
||||
|
||||
// If no APIs available, show a message
|
||||
return ctx.html('<h1>No API documentation available</h1><p>No APIs have been registered yet.</p>')
|
||||
}
|
||||
|
||||
// Documentation viewer for specific APIs
|
||||
@['/docs/:api_name'; get]
|
||||
pub fn (mut s HeroServer) docs_api(mut ctx Context, api_name string) veb.Result {
|
||||
// Setup documentation site if not already done
|
||||
s.setup_docs_site() or { return ctx.server_error('Documentation setup failed: ${err}') }
|
||||
|
||||
// Check if the API exists
|
||||
if api_name !in s.doc_registry.apis {
|
||||
return ctx.not_found()
|
||||
}
|
||||
|
||||
// Generate and return the documentation viewer
|
||||
html_content := s.generate_docs_viewer(api_name) or {
|
||||
return ctx.server_error('Failed to generate documentation: ${err}')
|
||||
}
|
||||
|
||||
return ctx.html(html_content)
|
||||
}
|
||||
|
||||
// Setup documentation site endpoint
|
||||
@['/docs/setup'; post]
|
||||
pub fn (mut s HeroServer) docs_setup(mut ctx Context) veb.Result {
|
||||
s.setup_docs_site() or { return ctx.server_error('Documentation setup failed: ${err}') }
|
||||
|
||||
return ctx.json('{"status": "success", "message": "Documentation site setup completed", "url": "http://localhost:8080/docs"}')
|
||||
}
|
||||
|
||||
// Register an API with both handler and documentation
|
||||
pub fn (mut s HeroServer) register_api(name string, spec openrpc.OpenRPC, handler openrpc.Handler) {
|
||||
s.handler_registry.register(name, handler, spec)
|
||||
s.doc_registry.register_api(name, spec, handler)
|
||||
}
|
||||
|
||||
// new_server creates a new HeroServer instance
|
||||
@@ -84,11 +127,16 @@ pub fn new_server(config ServerConfig) !&HeroServer {
|
||||
mut auth_manager := new_auth_manager(config.auth_config)
|
||||
mut handler_registry := new_handler_registry()
|
||||
mut age_client := crypt.new_age_client(crypt.AGEClientConfig{})!
|
||||
mut doc_registry := new_doc_registry()
|
||||
|
||||
// Register the core HeroServer API documentation
|
||||
doc_registry.register_core_api()
|
||||
|
||||
return &HeroServer{
|
||||
config: config
|
||||
auth_manager: auth_manager
|
||||
handler_registry: handler_registry
|
||||
age_client: age_client
|
||||
doc_registry: doc_registry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@{handler_type} API Documentation</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
.main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="sidebar-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>@{handler_type} API</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column" id="toc">
|
||||
<!-- Table of contents will be generated by JavaScript -->
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4 main">
|
||||
<div id="content">
|
||||
<!-- Markdown content will be rendered here -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Render markdown content
|
||||
const markdownContent = `@{markdown_content}`;
|
||||
document.getElementById('content').innerHTML = marked.parse(markdownContent);
|
||||
|
||||
// Generate table of contents
|
||||
const headers = document.querySelectorAll('#content h1, #content h2, #content h3');
|
||||
const toc = document.getElementById('toc');
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
const id = 'header-' + index;
|
||||
header.id = id;
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'nav-item';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.className = 'nav-link';
|
||||
a.href = '#' + id;
|
||||
a.textContent = header.textContent;
|
||||
a.style.paddingLeft = (header.tagName === 'H2' ? '20px' : header.tagName === 'H3' ? '40px' : '10px');
|
||||
|
||||
li.appendChild(a);
|
||||
toc.appendChild(li);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
350
lib/hero/heroserver/templates/docs_viewer.html
Normal file
350
lib/hero/heroserver/templates/docs_viewer.html
Normal file
@@ -0,0 +1,350 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@{title} - HeroServer Documentation</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e1e5e9;
|
||||
padding: 20px 0;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
padding: 0 20px 15px;
|
||||
font-size: 16px;
|
||||
color: #1a1a1a;
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
display: block;
|
||||
padding: 8px 20px;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background-color: #f1f3f4;
|
||||
color: #1976d2;
|
||||
border-left-color: #1976d2;
|
||||
}
|
||||
|
||||
.sidebar a.active {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-left-color: #1976d2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
margin-left: 280px;
|
||||
padding: 30px;
|
||||
max-width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 900px;
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #1a1a1a;
|
||||
margin: 24px 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Major section headers get stronger dividers */
|
||||
h2.major-section {
|
||||
border-bottom: 2px solid #e1e5e9;
|
||||
}
|
||||
|
||||
/* Minor section headers get light dividers */
|
||||
h2.minor-section {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #333;
|
||||
margin: 20px 0 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #333;
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f1f3f4;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #1976d2;
|
||||
margin: 12px 0;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #e1e5e9;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #4caf50;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
margin: 6px 0;
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
color: #2e7d32;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.method-post {
|
||||
border-color: #ff9800;
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.method-get {
|
||||
border-color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.method-put {
|
||||
border-color: #2196f3;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.method-delete {
|
||||
border-color: #f44336;
|
||||
background: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav class="sidebar">
|
||||
<h2>📚 API Documentation</h2>
|
||||
<ul>
|
||||
@{sidebar_items}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<div class="content-wrapper">
|
||||
<div id="markdown-content">
|
||||
@{content}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add active class to current page
|
||||
const currentPath = window.location.pathname;
|
||||
const links = document.querySelectorAll('.sidebar a');
|
||||
links.forEach(link => {
|
||||
if (link.getAttribute('href') === currentPath) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Add method classes to endpoints and manage header dividers
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Style code blocks for HTTP methods
|
||||
const codeBlocks = document.querySelectorAll('code');
|
||||
codeBlocks.forEach(code => {
|
||||
const text = code.textContent.trim();
|
||||
if (text.match(/^(GET|POST|PUT|DELETE|PATCH)\s+\//)) {
|
||||
const method = text.split(' ')[0].toLowerCase();
|
||||
code.classList.add('endpoint', `method-${method}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Manage header dividers - only add to major sections
|
||||
const h2Elements = document.querySelectorAll('h2');
|
||||
h2Elements.forEach(h2 => {
|
||||
const text = h2.textContent.toLowerCase();
|
||||
const majorSections = ['overview', 'authentication', 'endpoints', 'api', 'examples', 'error handling', 'rate limiting'];
|
||||
|
||||
if (majorSections.some(section => text.includes(section))) {
|
||||
h2.classList.add('major-section');
|
||||
} else {
|
||||
// Remove default border for minor sections
|
||||
h2.style.borderBottom = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
297
lib/hero/heroserver/templates/heroserver_core_api.md
Normal file
297
lib/hero/heroserver/templates/heroserver_core_api.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# HeroServer Core API Documentation
|
||||
|
||||
The core HeroServer API provides authentication and system endpoints that are required before accessing any registered APIs.
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
## Overview
|
||||
|
||||
The HeroServer Core API handles user authentication using Ed25519 public key cryptography and provides system information endpoints. All other APIs require authentication through these core endpoints.
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Endpoints
|
||||
|
||||
### Register Public Key
|
||||
|
||||
Register your Ed25519 public key with the server to enable authentication.
|
||||
|
||||
**Endpoint:** `POST /register`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"public_key": "your_ed25519_public_key_in_base64"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Public key registered successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"public_key": "MCowBQYDK0OAQEiA..."}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Request Authentication Challenge
|
||||
|
||||
Request a challenge that must be signed with your private key.
|
||||
|
||||
**Endpoint:** `POST /authreq`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"public_key": "your_ed25519_public_key_in_base64"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"challenge": "base64_encoded_challenge_to_sign"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/authreq \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"public_key": "MCowBQYDK0OAQEiA..."}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Authenticate with Signed Challenge
|
||||
|
||||
Submit the signed challenge to receive a session key for API access.
|
||||
|
||||
**Endpoint:** `POST /auth`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"public_key": "your_ed25519_public_key_in_base64",
|
||||
"signature": "base64_encoded_signature_of_challenge"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"session_key": "your_session_key_for_api_calls",
|
||||
"expires_at": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/auth \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"public_key": "MCowBQYDK0OAQEiA...",
|
||||
"signature": "signature_of_challenge..."
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation Endpoints
|
||||
|
||||
### Documentation Index
|
||||
|
||||
View all available APIs and their documentation.
|
||||
|
||||
**Endpoint:** `GET /docs`
|
||||
|
||||
**Response:** HTML page with list of all registered APIs
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### API-Specific Documentation
|
||||
|
||||
View documentation for a specific registered API.
|
||||
|
||||
**Endpoint:** `GET /docs/{api_name}`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `api_name`: The name of the registered API (e.g., "comments")
|
||||
|
||||
**Response:** HTML documentation page for the specified API
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/docs/comments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Access Endpoints
|
||||
|
||||
### Call Registered API Methods
|
||||
|
||||
Once authenticated, call methods on registered APIs using JSON-RPC 2.0.
|
||||
|
||||
**Endpoint:** `POST /api/{api_name}`
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: your_session_key_here
|
||||
```
|
||||
|
||||
**Request Body (JSON-RPC 2.0):**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "method_name",
|
||||
"params": {
|
||||
"param1": "value1",
|
||||
"param2": "value2"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"data": "method_response_data"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/comments \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: your_session_key" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "add_comment",
|
||||
"params": {"content": "Hello World!"},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
| Code | Message | Description |
|
||||
|------|---------|-------------|
|
||||
| `400` | Bad Request | Invalid request format or missing required fields |
|
||||
| `401` | Unauthorized | Invalid or missing authentication credentials |
|
||||
| `403` | Forbidden | Valid credentials but insufficient permissions |
|
||||
| `404` | Not Found | Requested endpoint or resource not found |
|
||||
| `500` | Internal Server Error | Server-side error occurred |
|
||||
|
||||
### JSON-RPC Errors
|
||||
|
||||
| Code | Message | Description |
|
||||
|------|---------|-------------|
|
||||
| `-32700` | Parse error | Invalid JSON was received |
|
||||
| `-32600` | Invalid Request | The JSON sent is not a valid Request object |
|
||||
| `-32601` | Method not found | The method does not exist |
|
||||
| `-32602` | Invalid params | Invalid method parameter(s) |
|
||||
| `-32603` | Internal error | Internal JSON-RPC error |
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Ed25519 Key Management
|
||||
|
||||
- **Generate secure keys**: Use a cryptographically secure random number generator
|
||||
- **Store private keys safely**: Never share or transmit private keys
|
||||
- **Rotate keys regularly**: Consider periodic key rotation for enhanced security
|
||||
|
||||
### Session Management
|
||||
|
||||
- **Session expiration**: Session keys have a limited lifetime
|
||||
- **Secure transmission**: Always use HTTPS in production
|
||||
- **Key storage**: Store session keys securely on the client side
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- **Authentication endpoints**: Limited to prevent brute force attacks
|
||||
- **API endpoints**: Rate limited per session to ensure fair usage
|
||||
- **Documentation endpoints**: Publicly accessible but may have basic rate limits
|
||||
|
||||
---
|
||||
|
||||
## Complete Authentication Flow Example
|
||||
|
||||
Here's a complete example of the authentication flow using curl:
|
||||
|
||||
```bash
|
||||
# 1. Register your public key
|
||||
curl -X POST http://localhost:8080/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"public_key": "your_public_key_here"}'
|
||||
|
||||
# 2. Request authentication challenge
|
||||
CHALLENGE=$(curl -X POST http://localhost:8080/authreq \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"public_key": "your_public_key_here"}' | jq -r '.challenge')
|
||||
|
||||
# 3. Sign the challenge (using your preferred signing method)
|
||||
SIGNATURE=$(echo "$CHALLENGE" | your_signing_tool)
|
||||
|
||||
# 4. Authenticate and get session key
|
||||
SESSION_KEY=$(curl -X POST http://localhost:8080/auth \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"public_key\": \"your_public_key_here\", \"signature\": \"$SIGNATURE\"}" | jq -r '.session_key')
|
||||
|
||||
# 5. Use session key for API calls
|
||||
curl -X POST http://localhost:8080/api/comments \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: $SESSION_KEY" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "add_comment",
|
||||
"params": {"content": "Hello from HeroServer!"},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
348
lib/hero/heroserver/templates/pages/comments.md
Normal file
348
lib/hero/heroserver/templates/pages/comments.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Comments API
|
||||
|
||||
A simple service for managing comments and discussions.
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
## Overview
|
||||
|
||||
The Comments API provides a straightforward way to manage comments, replies, and discussions. It supports threaded conversations, moderation features, and real-time updates.
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
http://localhost:8080/api/comments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All Comments API endpoints require authentication through the HeroServer Core API. Please refer to the [authentication documentation](./heroserver.md#authentication-flow) for details.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Create Comment
|
||||
|
||||
Create a new comment.
|
||||
|
||||
**Endpoint:** `POST /comments`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "This is a great article! Thanks for sharing.",
|
||||
"author": "john_doe",
|
||||
"parent_id": null,
|
||||
"metadata": {
|
||||
"article_id": "article_123",
|
||||
"section": "introduction"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "comment_456789",
|
||||
"content": "This is a great article! Thanks for sharing.",
|
||||
"author": "john_doe",
|
||||
"parent_id": null,
|
||||
"metadata": {
|
||||
"article_id": "article_123",
|
||||
"section": "introduction"
|
||||
},
|
||||
"status": "published",
|
||||
"created_at": "2024-01-20T10:00:00Z",
|
||||
"updated_at": "2024-01-20T10:00:00Z",
|
||||
"replies_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### List Comments
|
||||
|
||||
Retrieve comments with optional filtering.
|
||||
|
||||
**Endpoint:** `GET /comments`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `article_id` (optional): Filter by article ID
|
||||
- `author` (optional): Filter by author
|
||||
- `status` (optional): Filter by status (`published`, `pending`, `hidden`)
|
||||
- `parent_id` (optional): Filter by parent comment ID (for replies)
|
||||
- `limit` (optional): Maximum number of comments to return (default: 50)
|
||||
- `offset` (optional): Number of comments to skip (default: 0)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"comments": [
|
||||
{
|
||||
"id": "comment_456789",
|
||||
"content": "This is a great article! Thanks for sharing.",
|
||||
"author": "john_doe",
|
||||
"parent_id": null,
|
||||
"metadata": {
|
||||
"article_id": "article_123",
|
||||
"section": "introduction"
|
||||
},
|
||||
"status": "published",
|
||||
"created_at": "2024-01-20T10:00:00Z",
|
||||
"updated_at": "2024-01-20T10:00:00Z",
|
||||
"replies_count": 2
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
### Get Comment
|
||||
|
||||
Retrieve a specific comment by ID.
|
||||
|
||||
**Endpoint:** `GET /comments/{comment_id}`
|
||||
|
||||
**Response:** Comment object (same as create response)
|
||||
|
||||
### Update Comment
|
||||
|
||||
Update an existing comment.
|
||||
|
||||
**Endpoint:** `PUT /comments/{comment_id}`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "Updated comment content",
|
||||
"status": "published"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Updated comment object
|
||||
|
||||
### Delete Comment
|
||||
|
||||
Delete a comment (soft delete - marks as hidden).
|
||||
|
||||
**Endpoint:** `DELETE /comments/{comment_id}`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Comment deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Reply to Comment
|
||||
|
||||
Create a reply to an existing comment.
|
||||
|
||||
**Endpoint:** `POST /comments/{comment_id}/replies`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "Great point! I totally agree.",
|
||||
"author": "jane_smith"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Reply comment object with `parent_id` set to the original comment
|
||||
|
||||
---
|
||||
|
||||
## Comment Status
|
||||
|
||||
Comments can have the following statuses:
|
||||
|
||||
- **published**: Comment is visible to all users
|
||||
- **pending**: Comment is awaiting moderation
|
||||
- **hidden**: Comment has been hidden by moderators
|
||||
|
||||
---
|
||||
|
||||
## Threading
|
||||
|
||||
The Comments API supports threaded conversations:
|
||||
|
||||
- **Top-level comments**: Have `parent_id` set to `null`
|
||||
- **Replies**: Have `parent_id` set to the ID of the comment they're replying to
|
||||
- **Nested replies**: Can reply to other replies, creating deep conversation threads
|
||||
|
||||
### Example Thread Structure
|
||||
|
||||
```
|
||||
Comment A (parent_id: null)
|
||||
├── Reply 1 (parent_id: comment_A_id)
|
||||
│ └── Reply 1.1 (parent_id: reply_1_id)
|
||||
└── Reply 2 (parent_id: comment_A_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Moderation
|
||||
|
||||
### Moderate Comment
|
||||
|
||||
Update comment status for moderation.
|
||||
|
||||
**Endpoint:** `POST /comments/{comment_id}/moderate`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "hidden",
|
||||
"reason": "inappropriate_content"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "comment_456789",
|
||||
"status": "hidden",
|
||||
"moderated_at": "2024-01-20T15:30:00Z",
|
||||
"moderated_by": "moderator_123"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-time Updates
|
||||
|
||||
The Comments API supports WebSocket connections for real-time comment updates:
|
||||
|
||||
**WebSocket Endpoint:** `ws://localhost:8080/api/comments/ws`
|
||||
|
||||
**Subscribe to Article Comments:**
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "subscribe",
|
||||
"article_id": "article_123"
|
||||
}
|
||||
```
|
||||
|
||||
**Receive New Comments:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "new_comment",
|
||||
"comment": {
|
||||
"id": "comment_789",
|
||||
"content": "New comment content",
|
||||
"author": "user_456",
|
||||
"created_at": "2024-01-20T16:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
The Comments API uses standard HTTP status codes:
|
||||
|
||||
**400 Bad Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"message": "Comment content cannot be empty",
|
||||
"details": {
|
||||
"field": "content",
|
||||
"code": "required"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**404 Not Found:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "not_found",
|
||||
"message": "Comment not found",
|
||||
"details": {
|
||||
"comment_id": "comment_invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**403 Forbidden:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "forbidden",
|
||||
"message": "You can only edit your own comments",
|
||||
"details": {
|
||||
"comment_id": "comment_456789",
|
||||
"owner": "other_user"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
The Comments API implements rate limiting to prevent spam:
|
||||
|
||||
- **Comment creation**: 10 comments per minute per user
|
||||
- **Comment updates**: 20 updates per minute per user
|
||||
- **Comment retrieval**: 100 requests per minute per user
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating a Comment Thread
|
||||
|
||||
```bash
|
||||
# Create top-level comment
|
||||
curl -X POST http://localhost:8080/api/comments \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"content": "Great article about API design!",
|
||||
"author": "developer_123",
|
||||
"metadata": {"article_id": "api_design_101"}
|
||||
}'
|
||||
|
||||
# Reply to the comment
|
||||
curl -X POST http://localhost:8080/api/comments/comment_456789/replies \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"content": "I agree! The examples are very helpful.",
|
||||
"author": "reader_456"
|
||||
}'
|
||||
```
|
||||
|
||||
### Retrieving Article Comments
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/api/comments?article_id=api_design_101&status=published" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For questions and support:
|
||||
|
||||
- **Documentation**: [https://docs.heroserver.com/comments](https://docs.heroserver.com/comments)
|
||||
- **GitHub**: [https://github.com/heroserver/comments-api](https://github.com/heroserver/comments-api)
|
||||
- **Email**: comments-support@heroserver.com
|
||||
Reference in New Issue
Block a user