feat: enhance server documentation and configuration
- Add HTML homepage and JSON handler info endpoints - Implement markdown documentation generation for APIs - Introduce auth_enabled flag for server configuration - Improve documentation generation with dynamic base URLs - Refactor server initialization and handler registration
This commit is contained in:
@@ -5,9 +5,11 @@ import freeflowuniverse.herolib.hero.heromodels
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Start the server in a background thread
|
// Start the server in a background thread with authentication disabled for testing
|
||||||
spawn fn () {
|
spawn fn () {
|
||||||
rpc.start(port: 8080) or { panic('Failed to start HeroModels server: ${err}') }
|
rpc.start(port: 8080, auth_enabled: false) or {
|
||||||
|
panic('Failed to start HeroModels server: ${err}')
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Keep the main thread alive
|
// Keep the main thread alive
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ module rpc
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.schemas.openrpc
|
import freeflowuniverse.herolib.schemas.openrpc
|
||||||
import freeflowuniverse.herolib.hero.heroserver
|
import freeflowuniverse.herolib.hero.heroserver
|
||||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
|
||||||
import freeflowuniverse.herolib.ui.console
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json')
|
const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json')
|
||||||
@@ -73,21 +71,19 @@ pub fn new_heromodels_handler() !&openrpc.Handler {
|
|||||||
@[params]
|
@[params]
|
||||||
pub struct ServerArgs {
|
pub struct ServerArgs {
|
||||||
pub mut:
|
pub mut:
|
||||||
port int = 8080
|
port int = 8080
|
||||||
host string = 'localhost'
|
host string = 'localhost'
|
||||||
|
auth_enabled bool = true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(args ServerArgs) ! {
|
pub fn start(args ServerArgs) ! {
|
||||||
// Create a new heroserver instance
|
// Create a new heroserver instance
|
||||||
mut server := heroserver.new(port: args.port, host: args.host)!
|
mut server := heroserver.new(port: args.port, host: args.host, auth_enabled: args.auth_enabled)!
|
||||||
|
|
||||||
// Create and register the heromodels handler
|
// Create and register the heromodels handler
|
||||||
handler := new_heromodels_handler()!
|
handler := new_heromodels_handler()!
|
||||||
server.register_handler('heromodels', handler)!
|
server.register_handler('heromodels', handler)!
|
||||||
|
|
||||||
console.print_green('Documentation available at: http://${args.host}:${args.port}/doc/heromodels/')
|
|
||||||
console.print_green('HeroModels API available at: http://${args.host}:${args.port}/api/heromodels')
|
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
server.start()!
|
server.start()!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{
|
{
|
||||||
"name": "comment_get",
|
"name": "comment_get",
|
||||||
"summary": "Get a comment by ID",
|
"summary": "Get a comment by ID",
|
||||||
|
"description": "Retrieve a comment by its unique ID. Returns the comment object.",
|
||||||
"params": [
|
"params": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
|||||||
@@ -1,14 +1,53 @@
|
|||||||
module heroserver
|
module heroserver
|
||||||
|
|
||||||
import veb
|
import veb
|
||||||
|
import freeflowuniverse.herolib.schemas.openrpc
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonschema
|
||||||
|
|
||||||
|
// Home page handler - returns HTML homepage
|
||||||
|
@['/']
|
||||||
|
pub fn (mut server HeroServer) home_handler(mut ctx Context) veb.Result {
|
||||||
|
// Create a simple server info structure for the template
|
||||||
|
server_info := HomePageData{
|
||||||
|
base_url: get_base_url_from_context(ctx)
|
||||||
|
handlers: server.handlers
|
||||||
|
auth_enabled: server.auth_enabled
|
||||||
|
host: server.host
|
||||||
|
port: server.port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and process the HTML template
|
||||||
|
html_content := $tmpl('templates/home.html')
|
||||||
|
|
||||||
|
return ctx.html(html_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON server info handler
|
||||||
|
@['/json/:handler_type']
|
||||||
|
pub fn (mut server HeroServer) json_handler(mut ctx Context, handler_type string) veb.Result {
|
||||||
|
// Get the OpenRPC handler for the specified handler type
|
||||||
|
handler := server.handlers[handler_type] or { return ctx.not_found() }
|
||||||
|
|
||||||
|
// Create server info structure focused on this handler
|
||||||
|
server_info := create_handler_json_info(server, handler_type, handler, get_base_url_from_context(ctx))
|
||||||
|
|
||||||
|
return ctx.json(server_info)
|
||||||
|
}
|
||||||
|
|
||||||
@['/doc/:handler_type']
|
@['/doc/:handler_type']
|
||||||
pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string) veb.Result {
|
pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string) veb.Result {
|
||||||
// Get the OpenRPC handler for the specified handler type
|
// Get the OpenRPC handler for the specified handler type
|
||||||
handler := server.handlers[handler_type] or { return ctx.not_found() }
|
handler := server.handlers[handler_type] or { return ctx.not_found() }
|
||||||
|
|
||||||
// Convert the OpenRPC specification to a DocSpec
|
// Create dynamic configuration based on request
|
||||||
spec := doc_spec_from_openrpc(handler.specification, handler_type) or {
|
config := DocConfig{
|
||||||
|
base_url: get_base_url_from_context(ctx)
|
||||||
|
handler_type: handler_type
|
||||||
|
auth_enabled: server.auth_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the OpenRPC specification to a DocSpec with dynamic configuration
|
||||||
|
spec := doc_spec_from_openrpc_with_config(handler.specification, config) or {
|
||||||
return ctx.server_error('Failed to generate documentation: ${err}')
|
return ctx.server_error('Failed to generate documentation: ${err}')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,3 +56,298 @@ pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string)
|
|||||||
|
|
||||||
return ctx.html(html_content)
|
return ctx.html(html_content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@['/md/:handler_type']
|
||||||
|
pub fn (mut server HeroServer) md_handler(mut ctx Context, handler_type string) veb.Result {
|
||||||
|
// Get the OpenRPC handler for the specified handler type
|
||||||
|
handler := server.handlers[handler_type] or { return ctx.not_found() }
|
||||||
|
|
||||||
|
// Generate markdown content from the OpenRPC specification
|
||||||
|
markdown_content := generate_markdown_from_openrpc(handler.specification, handler_type,
|
||||||
|
get_base_url_from_context(ctx)) or {
|
||||||
|
return ctx.server_error('Failed to generate markdown documentation: ${err}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content type to text/plain for markdown
|
||||||
|
ctx.set_content_type('text/plain; charset=utf-8')
|
||||||
|
return ctx.text(markdown_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_base_url_from_context extracts the base URL from the VEB context
|
||||||
|
fn get_base_url_from_context(ctx Context) string {
|
||||||
|
scheme := if ctx.get_header(.x_forwarded_proto) or { '' } == 'https' { 'https' } else { 'http' }
|
||||||
|
host := ctx.get_header(.host) or { 'localhost:8080' }
|
||||||
|
return '${scheme}://${host}'
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_markdown_from_openrpc generates markdown documentation from OpenRPC specification
|
||||||
|
fn generate_markdown_from_openrpc(spec openrpc.OpenRPC, handler_type string, base_url string) !string {
|
||||||
|
mut md := ''
|
||||||
|
|
||||||
|
// Title and description
|
||||||
|
md += '# ${spec.info.title}\n\n'
|
||||||
|
if spec.info.description.len > 0 {
|
||||||
|
md += '${spec.info.description}\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic info
|
||||||
|
md += '**Version:** ${spec.info.version}\n'
|
||||||
|
md += '**Handler Type:** ${handler_type}\n'
|
||||||
|
md += '**Base URL:** ${base_url}\n\n'
|
||||||
|
|
||||||
|
// Overview
|
||||||
|
md += '## Overview\n\n'
|
||||||
|
md += 'This API provides JSON-RPC 2.0 endpoints for ${handler_type} operations.\n\n'
|
||||||
|
md += '**API Endpoint:** `${base_url}/api/${handler_type}`\n\n'
|
||||||
|
|
||||||
|
// Table of Contents
|
||||||
|
if spec.methods.len > 0 {
|
||||||
|
md += '## Table of Contents\n\n'
|
||||||
|
for method in spec.methods {
|
||||||
|
md += '- [${method.name}](#${method.name.to_lower().replace('_', '-')})\n'
|
||||||
|
}
|
||||||
|
md += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
if spec.methods.len > 0 {
|
||||||
|
md += '## API Methods\n\n'
|
||||||
|
for method in spec.methods {
|
||||||
|
md += generate_method_markdown(method, base_url, handler_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication section
|
||||||
|
md += '## Authentication\n\n'
|
||||||
|
md += 'All API requests use JSON-RPC 2.0 format and may require authentication depending on server configuration.\n\n'
|
||||||
|
md += '### Request Format\n\n'
|
||||||
|
md += '```json\n'
|
||||||
|
md += '{\n'
|
||||||
|
md += ' "jsonrpc": "2.0",\n'
|
||||||
|
md += ' "method": "method_name",\n'
|
||||||
|
md += ' "params": {\n'
|
||||||
|
md += ' "param1": "value1",\n'
|
||||||
|
md += ' "param2": "value2"\n'
|
||||||
|
md += ' },\n'
|
||||||
|
md += ' "id": 1\n'
|
||||||
|
md += '}\n'
|
||||||
|
md += '```\n\n'
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
md += '## Error Handling\n\n'
|
||||||
|
md += 'The API uses standard JSON-RPC 2.0 error codes:\n\n'
|
||||||
|
md += '- `-32700`: Parse error\n'
|
||||||
|
md += '- `-32600`: Invalid Request\n'
|
||||||
|
md += '- `-32601`: Method not found\n'
|
||||||
|
md += '- `-32602`: Invalid params\n'
|
||||||
|
md += '- `-32603`: Internal error\n\n'
|
||||||
|
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_method_markdown generates markdown documentation for a single method
|
||||||
|
fn generate_method_markdown(method openrpc.Method, base_url string, handler_type string) string {
|
||||||
|
mut md := ''
|
||||||
|
|
||||||
|
// Method header
|
||||||
|
md += '### ${method.name}\n\n'
|
||||||
|
|
||||||
|
if method.summary.len > 0 {
|
||||||
|
md += '**Summary:** ${method.summary}\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if method.description.len > 0 {
|
||||||
|
md += '${method.description}\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
if method.params.len > 0 {
|
||||||
|
md += '#### Parameters\n\n'
|
||||||
|
md += '| Name | Type | Required | Description |\n'
|
||||||
|
md += '|------|------|----------|-------------|\n'
|
||||||
|
|
||||||
|
for param in method.params {
|
||||||
|
if param is openrpc.ContentDescriptor {
|
||||||
|
param_desc := param as openrpc.ContentDescriptor
|
||||||
|
param_type := if param_desc.schema is jsonschema.Schema {
|
||||||
|
schema := param_desc.schema as jsonschema.Schema
|
||||||
|
schema.typ
|
||||||
|
} else {
|
||||||
|
'unknown'
|
||||||
|
}
|
||||||
|
required := if param_desc.required { 'Yes' } else { 'No' }
|
||||||
|
md += '| ${param_desc.name} | ${param_type} | ${required} | ${param_desc.description} |\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
md += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result
|
||||||
|
if method.result is openrpc.ContentDescriptor {
|
||||||
|
result := method.result as openrpc.ContentDescriptor
|
||||||
|
md += '#### Returns\n\n'
|
||||||
|
md += '${result.description}\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example request
|
||||||
|
md += '#### Example Request\n\n'
|
||||||
|
md += '```bash\n'
|
||||||
|
md += 'curl -X POST ${base_url}/api/${handler_type} \\\n'
|
||||||
|
md += ' -H "Content-Type: application/json" \\\n'
|
||||||
|
md += " -d '{\n"
|
||||||
|
md += ' "jsonrpc": "2.0",\n'
|
||||||
|
md += ' "method": "${method.name}",\n'
|
||||||
|
md += ' "params": {\n'
|
||||||
|
|
||||||
|
// Add example parameters
|
||||||
|
if method.params.len > 0 {
|
||||||
|
for i, param in method.params {
|
||||||
|
if param is openrpc.ContentDescriptor {
|
||||||
|
param_desc := param as openrpc.ContentDescriptor
|
||||||
|
example_value := get_example_value_for_param(param_desc)
|
||||||
|
comma := if i < method.params.len - 1 { ',' } else { '' }
|
||||||
|
md += ' "${param_desc.name}": ${example_value}${comma}\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
md += ' },\n'
|
||||||
|
md += ' "id": 1\n'
|
||||||
|
md += " }'\n"
|
||||||
|
md += '```\n\n'
|
||||||
|
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_example_value_for_param returns an example value for a parameter based on its type
|
||||||
|
fn get_example_value_for_param(param openrpc.ContentDescriptor) string {
|
||||||
|
if param.schema is jsonschema.Schema {
|
||||||
|
schema := param.schema as jsonschema.Schema
|
||||||
|
match schema.typ {
|
||||||
|
'string' { return '"example_string"' }
|
||||||
|
'integer', 'number' { return '123' }
|
||||||
|
'boolean' { return 'true' }
|
||||||
|
'array' { return '[]' }
|
||||||
|
'object' { return '{}' }
|
||||||
|
else { return '"example_value"' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '"example_value"'
|
||||||
|
}
|
||||||
|
|
||||||
|
// create_server_info_json creates a comprehensive JSON response about the server
|
||||||
|
fn create_server_info_json(server HeroServer, base_url string) ServerInfoJSON {
|
||||||
|
mut handlers := []HandlerInfoJSON{}
|
||||||
|
|
||||||
|
// Process each registered handler
|
||||||
|
for handler_name, handler in server.handlers {
|
||||||
|
mut methods := []MethodInfoJSON{}
|
||||||
|
|
||||||
|
// Extract methods from the OpenRPC specification
|
||||||
|
for method in handler.specification.methods {
|
||||||
|
methods << MethodInfoJSON{
|
||||||
|
name: method.name
|
||||||
|
summary: method.summary
|
||||||
|
description: method.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers << HandlerInfoJSON{
|
||||||
|
name: handler_name
|
||||||
|
title: handler.specification.info.title
|
||||||
|
description: handler.specification.info.description
|
||||||
|
version: handler.specification.info.version
|
||||||
|
api_endpoint: '${base_url}/api/${handler_name}'
|
||||||
|
doc_endpoint: '${base_url}/doc/${handler_name}'
|
||||||
|
md_endpoint: '${base_url}/md/${handler_name}'
|
||||||
|
methods: methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define server features
|
||||||
|
features := [
|
||||||
|
FeatureJSON{
|
||||||
|
title: 'JSON-RPC 2.0'
|
||||||
|
description: 'Full compliance with JSON-RPC 2.0 specification for reliable API communication'
|
||||||
|
icon: '🔗'
|
||||||
|
},
|
||||||
|
FeatureJSON{
|
||||||
|
title: 'Dynamic Documentation'
|
||||||
|
description: 'Auto-generated interactive documentation with curl examples and copy buttons'
|
||||||
|
icon: '📚'
|
||||||
|
},
|
||||||
|
FeatureJSON{
|
||||||
|
title: 'Secure Authentication'
|
||||||
|
description: 'Built-in cryptographic authentication with public key infrastructure'
|
||||||
|
icon: '🔐'
|
||||||
|
},
|
||||||
|
FeatureJSON{
|
||||||
|
title: 'Markdown Export'
|
||||||
|
description: 'Export API documentation as clean markdown for integration with other tools'
|
||||||
|
icon: '📝'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create endpoints information
|
||||||
|
endpoints := EndpointsJSON{
|
||||||
|
api_pattern: '/api/{handler_name}'
|
||||||
|
documentation_pattern: '/doc/{handler_name}'
|
||||||
|
markdown_pattern: '/md/{handler_name}'
|
||||||
|
home_json: '/'
|
||||||
|
home_html: '/home'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create quick start example
|
||||||
|
example_handler := if handlers.len > 0 { handlers[0].name } else { 'handler_name' }
|
||||||
|
quick_start := QuickStartJSON{
|
||||||
|
description: "All API endpoints use JSON-RPC 2.0 format. Here's a basic example:"
|
||||||
|
example: ExampleRequestJSON{
|
||||||
|
method: 'POST'
|
||||||
|
url: '${base_url}/api/${example_handler}'
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
body: '{\n "jsonrpc": "2.0",\n "method": "method_name",\n "params": {\n "param1": "value1",\n "param2": "value2"\n },\n "id": 1\n}'
|
||||||
|
description: 'Replace method_name and params with actual values from the API documentation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerInfoJSON{
|
||||||
|
server_name: 'HeroServer'
|
||||||
|
version: '1.0.0'
|
||||||
|
description: 'Modern JSON-RPC 2.0 API Gateway with Dynamic Documentation'
|
||||||
|
base_url: base_url
|
||||||
|
host: server.host
|
||||||
|
port: server.port
|
||||||
|
auth_enabled: server.auth_enabled
|
||||||
|
handlers: handlers
|
||||||
|
endpoints: endpoints
|
||||||
|
features: features
|
||||||
|
quick_start: quick_start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create_handler_json_info creates JSON response focused on a specific handler
|
||||||
|
fn create_handler_json_info(server HeroServer, handler_name string, handler &openrpc.Handler, base_url string) HandlerInfoJSON {
|
||||||
|
mut methods := []MethodInfoJSON{}
|
||||||
|
|
||||||
|
// Extract methods from the OpenRPC specification
|
||||||
|
for method in handler.specification.methods {
|
||||||
|
methods << MethodInfoJSON{
|
||||||
|
name: method.name
|
||||||
|
summary: method.summary
|
||||||
|
description: method.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HandlerInfoJSON{
|
||||||
|
name: handler_name
|
||||||
|
title: handler.specification.info.title
|
||||||
|
description: handler.specification.info.description
|
||||||
|
version: handler.specification.info.version
|
||||||
|
api_endpoint: '${base_url}/api/${handler_name}'
|
||||||
|
doc_endpoint: '${base_url}/doc/${handler_name}'
|
||||||
|
md_endpoint: '${base_url}/md/${handler_name}'
|
||||||
|
methods: methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub mut:
|
|||||||
methods []DocMethod
|
methods []DocMethod
|
||||||
objects []DocObject
|
objects []DocObject
|
||||||
auth_info AuthDocInfo
|
auth_info AuthDocInfo
|
||||||
|
base_url string // Dynamic base URL for examples
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocObject represents a logical grouping of methods.
|
// DocObject represents a logical grouping of methods.
|
||||||
@@ -31,7 +32,7 @@ pub mut:
|
|||||||
example_call string
|
example_call string
|
||||||
example_response string
|
example_response string
|
||||||
endpoint_url string
|
endpoint_url string
|
||||||
curl_example string // New field for curl command
|
curl_example string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocParam represents a parameter or result in the documentation
|
// DocParam represents a parameter or result in the documentation
|
||||||
@@ -47,9 +48,11 @@ pub mut:
|
|||||||
// AuthDocInfo contains authentication flow information
|
// AuthDocInfo contains authentication flow information
|
||||||
pub struct AuthDocInfo {
|
pub struct AuthDocInfo {
|
||||||
pub mut:
|
pub mut:
|
||||||
steps []AuthStep
|
enabled bool
|
||||||
|
steps []AuthStep
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthStep represents a single step in the authentication flow
|
||||||
pub struct AuthStep {
|
pub struct AuthStep {
|
||||||
pub mut:
|
pub mut:
|
||||||
number int
|
number int
|
||||||
@@ -60,81 +63,126 @@ pub mut:
|
|||||||
example string
|
example string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DocConfig holds configuration for documentation generation
|
||||||
|
pub struct DocConfig {
|
||||||
|
pub mut:
|
||||||
|
base_url string = 'http://localhost:8080'
|
||||||
|
handler_type string
|
||||||
|
auth_enabled bool = true
|
||||||
|
}
|
||||||
|
|
||||||
// doc_spec_from_openrpc converts an OpenRPC specification to a documentation-friendly DocSpec.
|
// doc_spec_from_openrpc converts an OpenRPC specification to a documentation-friendly DocSpec.
|
||||||
// Processes all methods, parameters, and results with proper type extraction and example generation.
|
// Processes all methods, parameters, and results with proper type extraction and example generation.
|
||||||
// Returns error if handler_type is empty or if OpenRPC spec is invalid.
|
// Returns error if handler_type is empty or if OpenRPC spec is invalid.
|
||||||
pub fn doc_spec_from_openrpc(openrpc_spec openrpc.OpenRPC, handler_type string) !DocSpec {
|
pub fn doc_spec_from_openrpc(openrpc_spec openrpc.OpenRPC, handler_type string) !DocSpec {
|
||||||
if handler_type.trim_space() == '' {
|
return doc_spec_from_openrpc_with_config(openrpc_spec, DocConfig{
|
||||||
|
handler_type: handler_type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// doc_spec_from_openrpc_with_config converts an OpenRPC specification with custom configuration
|
||||||
|
pub fn doc_spec_from_openrpc_with_config(openrpc_spec openrpc.OpenRPC, config DocConfig) !DocSpec {
|
||||||
|
if config.handler_type.trim_space() == '' {
|
||||||
return error('handler_type cannot be empty')
|
return error('handler_type cannot be empty')
|
||||||
}
|
}
|
||||||
|
|
||||||
mut doc_spec := DocSpec{
|
mut doc_spec := DocSpec{
|
||||||
info: openrpc_spec.info
|
info: openrpc_spec.info
|
||||||
auth_info: create_auth_info()
|
base_url: config.base_url
|
||||||
|
auth_info: create_auth_info_with_config(config.auth_enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process all methods
|
||||||
for method in openrpc_spec.methods {
|
for method in openrpc_spec.methods {
|
||||||
// Convert parameters
|
doc_method := process_method(method, config)!
|
||||||
mut doc_params := []DocParam{}
|
|
||||||
for param in method.params {
|
|
||||||
if param is openrpc.ContentDescriptor {
|
|
||||||
type_info := extract_type_from_schema(param.schema)
|
|
||||||
example := generate_example_from_schema(param.schema, param.name)
|
|
||||||
|
|
||||||
doc_param := DocParam{
|
|
||||||
name: param.name
|
|
||||||
description: param.description
|
|
||||||
required: param.required
|
|
||||||
type_info: type_info
|
|
||||||
example: example
|
|
||||||
}
|
|
||||||
doc_params << doc_param
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert result
|
|
||||||
mut doc_result := DocParam{}
|
|
||||||
if method.result is openrpc.ContentDescriptor {
|
|
||||||
result_cd := method.result as openrpc.ContentDescriptor
|
|
||||||
type_info := extract_type_from_schema(result_cd.schema)
|
|
||||||
example := generate_example_from_schema(result_cd.schema, result_cd.name)
|
|
||||||
|
|
||||||
doc_result = DocParam{
|
|
||||||
name: result_cd.name
|
|
||||||
description: result_cd.description
|
|
||||||
required: result_cd.required
|
|
||||||
type_info: type_info
|
|
||||||
example: example
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate example call and response
|
|
||||||
example_call := generate_example_call(doc_params)
|
|
||||||
example_response := generate_example_response(doc_result)
|
|
||||||
|
|
||||||
// Generate JSON-RPC example call
|
|
||||||
jsonrpc_call := generate_jsonrpc_example_call(method.name, doc_params)
|
|
||||||
|
|
||||||
mut doc_method := DocMethod{
|
|
||||||
name: method.name
|
|
||||||
summary: method.summary
|
|
||||||
description: method.description
|
|
||||||
params: doc_params
|
|
||||||
result: doc_result
|
|
||||||
endpoint_url: '/api/${handler_type}'
|
|
||||||
example_call: example_call
|
|
||||||
example_response: example_response
|
|
||||||
curl_example: '' // Will be set later with proper base URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate curl example with localhost as default using JSON-RPC format
|
|
||||||
doc_method.curl_example = generate_curl_example_jsonrpc(method.name, doc_params,
|
|
||||||
'http://localhost:8080', handler_type)
|
|
||||||
doc_spec.methods << doc_method
|
doc_spec.methods << doc_method
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc_spec
|
return doc_spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// process_method converts a single OpenRPC method to a DocMethod
|
||||||
|
fn process_method(method openrpc.Method, config DocConfig) !DocMethod {
|
||||||
|
// Convert parameters
|
||||||
|
doc_params := process_parameters(method.params)!
|
||||||
|
|
||||||
|
// Convert result
|
||||||
|
doc_result := process_result(method.result)!
|
||||||
|
|
||||||
|
// Generate examples
|
||||||
|
example_call := generate_example_call(doc_params)
|
||||||
|
example_response := generate_example_response(doc_result)
|
||||||
|
|
||||||
|
doc_method := DocMethod{
|
||||||
|
name: method.name
|
||||||
|
summary: method.summary
|
||||||
|
description: method.description
|
||||||
|
params: doc_params
|
||||||
|
result: doc_result
|
||||||
|
endpoint_url: '${config.base_url}/api/${config.handler_type}'
|
||||||
|
example_call: example_call
|
||||||
|
example_response: example_response
|
||||||
|
curl_example: generate_curl_example_jsonrpc(method.name, doc_params, config.base_url,
|
||||||
|
config.handler_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc_method
|
||||||
|
}
|
||||||
|
|
||||||
|
// process_parameters converts OpenRPC parameters to DocParam array
|
||||||
|
fn process_parameters(params []openrpc.ContentDescriptorRef) ![]DocParam {
|
||||||
|
mut doc_params := []DocParam{}
|
||||||
|
|
||||||
|
for param in params {
|
||||||
|
if param is openrpc.ContentDescriptor {
|
||||||
|
type_info := extract_type_from_schema(param.schema)
|
||||||
|
example := generate_example_from_schema(param.schema, param.name)
|
||||||
|
|
||||||
|
doc_params << DocParam{
|
||||||
|
name: param.name
|
||||||
|
description: param.description
|
||||||
|
type_info: type_info
|
||||||
|
required: param.required
|
||||||
|
example: example
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc_params
|
||||||
|
}
|
||||||
|
|
||||||
|
// process_result converts OpenRPC result to DocParam
|
||||||
|
fn process_result(result openrpc.ContentDescriptorRef) !DocParam {
|
||||||
|
mut doc_result := DocParam{}
|
||||||
|
|
||||||
|
if result is openrpc.ContentDescriptor {
|
||||||
|
type_info := extract_type_from_schema(result.schema)
|
||||||
|
example := generate_example_from_schema(result.schema, result.name)
|
||||||
|
|
||||||
|
doc_result = DocParam{
|
||||||
|
name: result.name
|
||||||
|
description: result.description
|
||||||
|
type_info: type_info
|
||||||
|
required: false // Results are never required
|
||||||
|
example: example
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc_result
|
||||||
|
}
|
||||||
|
|
||||||
|
// create_auth_info_with_config creates authentication documentation based on configuration
|
||||||
|
fn create_auth_info_with_config(enabled bool) AuthDocInfo {
|
||||||
|
if !enabled {
|
||||||
|
return AuthDocInfo{
|
||||||
|
enabled: false
|
||||||
|
steps: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return create_auth_info()
|
||||||
|
}
|
||||||
|
|
||||||
// extract_type_from_schema extracts the JSON Schema type from a SchemaRef.
|
// extract_type_from_schema extracts the JSON Schema type from a SchemaRef.
|
||||||
// Returns the type string (e.g., 'string', 'object', 'array') or 'reference'/'unknown' for edge cases.
|
// Returns the type string (e.g., 'string', 'object', 'array') or 'reference'/'unknown' for edge cases.
|
||||||
fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string {
|
fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string {
|
||||||
@@ -205,7 +253,8 @@ fn generate_example_response(result DocParam) string {
|
|||||||
// Create authentication documentation info
|
// Create authentication documentation info
|
||||||
fn create_auth_info() AuthDocInfo {
|
fn create_auth_info() AuthDocInfo {
|
||||||
return AuthDocInfo{
|
return AuthDocInfo{
|
||||||
steps: [
|
enabled: true
|
||||||
|
steps: [
|
||||||
AuthStep{
|
AuthStep{
|
||||||
number: 1
|
number: 1
|
||||||
title: 'Register Public Key'
|
title: 'Register Public Key'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ module heroserver
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.crypt.herocrypt
|
import freeflowuniverse.herolib.crypt.herocrypt
|
||||||
import freeflowuniverse.herolib.schemas.openrpc
|
import freeflowuniverse.herolib.schemas.openrpc
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
import veb
|
import veb
|
||||||
|
|
||||||
// Create a new HeroServer instance
|
// Create a new HeroServer instance
|
||||||
@@ -12,31 +13,42 @@ pub fn new(config HeroServerConfig) !&HeroServer {
|
|||||||
} else {
|
} else {
|
||||||
herocrypt.new_default()!
|
herocrypt.new_default()!
|
||||||
}
|
}
|
||||||
|
|
||||||
mut server := &HeroServer{
|
mut server := &HeroServer{
|
||||||
port: config.port
|
port: config.port
|
||||||
host: config.host
|
host: config.host
|
||||||
crypto_client: crypto_client
|
crypto_client: crypto_client
|
||||||
sessions: map[string]Session{}
|
sessions: map[string]Session{}
|
||||||
handlers: map[string]&openrpc.Handler{}
|
handlers: map[string]&openrpc.Handler{}
|
||||||
challenges: map[string]AuthChallenge{}
|
challenges: map[string]AuthChallenge{}
|
||||||
|
auth_enabled: config.auth_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.print_header('HeroServer created on port ${server.port}')
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register an OpenRPC handler
|
// Register an OpenRPC handler
|
||||||
pub fn (mut server HeroServer) register_handler(handler_type string, handler &openrpc.Handler) ! {
|
pub fn (mut server HeroServer) register_handler(handler_type string, handler &openrpc.Handler) ! {
|
||||||
server.handlers[handler_type] = handler
|
server.handlers[handler_type] = handler
|
||||||
|
console.print_header('Registered handler: ${handler_type}')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
pub fn (mut server HeroServer) start() ! {
|
pub fn (mut server HeroServer) start() ! {
|
||||||
// Start VEB server
|
// Start VEB server
|
||||||
|
handler_name := server.handlers.keys()[0]
|
||||||
|
console.print_green('Server starting on http://${server.host}:${server.port}')
|
||||||
|
console.print_green('HTML Homepage: http://${server.host}:${server.port}/')
|
||||||
|
console.print_green('JSON Info: http://${server.host}:${server.port}/json/${handler_name}')
|
||||||
|
console.print_green('Documentation: http://${server.host}:${server.port}/doc/${handler_name}')
|
||||||
|
console.print_green('Markdown Docs: http://${server.host}:${server.port}/md/${handler_name}')
|
||||||
|
console.print_green('API Endpoint: http://${server.host}:${server.port}/api/${handler_name}')
|
||||||
|
|
||||||
veb.run[HeroServer, Context](mut server, server.port)
|
veb.run[HeroServer, Context](mut server, server.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context struct for VEB
|
// Context struct for VEB
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
veb.Context
|
veb.Context
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import time
|
|||||||
@[params]
|
@[params]
|
||||||
pub struct HeroServerConfig {
|
pub struct HeroServerConfig {
|
||||||
pub mut:
|
pub mut:
|
||||||
port int = 9977
|
port int = 9977
|
||||||
host string = 'localhost'
|
host string = 'localhost'
|
||||||
|
auth_enabled bool = true // Whether to enable authentication
|
||||||
// Optional crypto client, will create default if not provided
|
// Optional crypto client, will create default if not provided
|
||||||
crypto_client ?&herocrypt.HeroCrypt
|
crypto_client ?&herocrypt.HeroCrypt
|
||||||
}
|
}
|
||||||
@@ -17,31 +18,43 @@ pub mut:
|
|||||||
// Main server struct
|
// Main server struct
|
||||||
pub struct HeroServer {
|
pub struct HeroServer {
|
||||||
mut:
|
mut:
|
||||||
port int
|
port int
|
||||||
host string
|
host string
|
||||||
crypto_client &herocrypt.HeroCrypt
|
crypto_client &herocrypt.HeroCrypt
|
||||||
sessions map[string]Session // sessionkey -> Session
|
sessions map[string]Session // sessionkey -> Session
|
||||||
handlers map[string]&openrpc.Handler // handlertype -> handler
|
handlers map[string]&openrpc.Handler // handlertype -> handler
|
||||||
challenges map[string]AuthChallenge
|
challenges map[string]AuthChallenge
|
||||||
|
pub mut:
|
||||||
|
auth_enabled bool = true // Whether authentication is required
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication challenge data
|
// Authentication challenge data
|
||||||
pub struct AuthChallenge {
|
pub struct AuthChallenge {
|
||||||
pub mut:
|
pub mut:
|
||||||
pubkey string
|
pubkey string
|
||||||
challenge string // unique hashed challenge
|
challenge string // unique hashed challenge
|
||||||
created_at time.Time
|
created_at time.Time
|
||||||
expires_at time.Time
|
expires_at time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Home page data for template rendering
|
||||||
|
pub struct HomePageData {
|
||||||
|
pub mut:
|
||||||
|
base_url string
|
||||||
|
handlers map[string]&openrpc.Handler
|
||||||
|
auth_enabled bool
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
|
||||||
// Active session data
|
// Active session data
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub mut:
|
pub mut:
|
||||||
session_key string
|
session_key string
|
||||||
pubkey string
|
pubkey string
|
||||||
created_at time.Time
|
created_at time.Time
|
||||||
last_activity time.Time
|
last_activity time.Time
|
||||||
expires_at time.Time
|
expires_at time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication request structures
|
// Authentication request structures
|
||||||
@@ -62,7 +75,7 @@ pub:
|
|||||||
|
|
||||||
pub struct AuthSubmitRequest {
|
pub struct AuthSubmitRequest {
|
||||||
pub:
|
pub:
|
||||||
pubkey string
|
pubkey string
|
||||||
signature string // signed challenge
|
signature string // signed challenge
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +88,72 @@ pub:
|
|||||||
pub struct APIRequest {
|
pub struct APIRequest {
|
||||||
pub:
|
pub:
|
||||||
session_key string
|
session_key string
|
||||||
method string
|
method string
|
||||||
params map[string]string
|
params map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON response structures for homepage
|
||||||
|
pub struct ServerInfoJSON {
|
||||||
|
pub:
|
||||||
|
server_name string
|
||||||
|
version string
|
||||||
|
description string
|
||||||
|
base_url string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
auth_enabled bool
|
||||||
|
handlers []HandlerInfoJSON
|
||||||
|
endpoints EndpointsJSON
|
||||||
|
features []FeatureJSON
|
||||||
|
quick_start QuickStartJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HandlerInfoJSON {
|
||||||
|
pub:
|
||||||
|
name string
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
version string
|
||||||
|
api_endpoint string
|
||||||
|
doc_endpoint string
|
||||||
|
md_endpoint string
|
||||||
|
methods []MethodInfoJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MethodInfoJSON {
|
||||||
|
pub:
|
||||||
|
name string
|
||||||
|
summary string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EndpointsJSON {
|
||||||
|
pub:
|
||||||
|
api_pattern string
|
||||||
|
documentation_pattern string
|
||||||
|
markdown_pattern string
|
||||||
|
home_json string
|
||||||
|
home_html string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FeatureJSON {
|
||||||
|
pub:
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
icon string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuickStartJSON {
|
||||||
|
pub:
|
||||||
|
description string
|
||||||
|
example ExampleRequestJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExampleRequestJSON {
|
||||||
|
pub:
|
||||||
|
method string
|
||||||
|
url string
|
||||||
|
headers map[string]string
|
||||||
|
body string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,21 +35,24 @@ fn main() {
|
|||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
- **API Calls**: `POST /api/{handler_type}/{method_name}`
|
- **HTML Homepage**: `GET /` - Returns HTML homepage with server information
|
||||||
- **Documentation**: `GET /doc/{handler_type}/`
|
- **JSON Handler Info**: `GET /json/{handler_type}` - Returns handler information in JSON format
|
||||||
|
- **API Calls**: `POST /api/{handler_type}`
|
||||||
|
- **Documentation**: `GET /doc/{handler_type}`
|
||||||
|
- **Markdown Docs**: `GET /md/{handler_type}` - Returns documentation in markdown format
|
||||||
|
|
||||||
## Authentication Flow
|
## Authentication Flow
|
||||||
|
|
||||||
1. **Register Public Key**: `POST /auth/register`
|
1. **Register Public Key**: `POST /auth/register`
|
||||||
- Body: `{"pubkey": "your_public_key"}`
|
- Body: `{"pubkey": "your_public_key"}`
|
||||||
2. **Request Challenge**: `POST /auth/authreq`
|
2. **Request Challenge**: `POST /auth/authreq`
|
||||||
- Body: `{"pubkey": "your_public_key"}`
|
- Body: `{"pubkey": "your_public_key"}`
|
||||||
- Returns a unique challenge string.
|
- Returns a unique challenge string.
|
||||||
3. **Submit Signature**: `POST /auth/auth`
|
3. **Submit Signature**: `POST /auth/auth`
|
||||||
- Sign the challenge from step 2 with your private key.
|
- Sign the challenge from step 2 with your private key.
|
||||||
- Body: `{"pubkey": "your_public_key", "signature": "your_signature"}`
|
- Body: `{"pubkey": "your_public_key", "signature": "your_signature"}`
|
||||||
- Returns a session key.
|
- Returns a session key.
|
||||||
|
|
||||||
All subsequent API calls must include the session key in the `Authorization` header:
|
All subsequent API calls must include the session key in the `Authorization` header:
|
||||||
|
|
||||||
`Authorization: Bearer {session_key}`
|
`Authorization: Bearer {session_key}`
|
||||||
|
|||||||
@@ -225,7 +225,7 @@
|
|||||||
<div class="card method-card" id="method-${method.name}">
|
<div class="card method-card" id="method-${method.name}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">${method.name}</h5>
|
<h5 class="mb-0">${method.name}</h5>
|
||||||
<div class="method-endpoint">POST ${method.endpoint_url}</div>
|
<div class="method-endpoint mt-2 mb-2">POST ${method.endpoint_url}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if method.summary.len > 0
|
@if method.summary.len > 0
|
||||||
|
|||||||
393
lib/hero/heroserver/templates/home.html
Normal file
393
lib/hero/heroserver/templates/home.html
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HeroServer - API Gateway</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--google-blue: #1a73e8;
|
||||||
|
--google-blue-hover: #1557b0;
|
||||||
|
--google-gray: #5f6368;
|
||||||
|
--google-light-gray: #f8f9fa;
|
||||||
|
--google-border: #dadce0;
|
||||||
|
--google-text: #202124;
|
||||||
|
--google-text-secondary: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
color: var(--google-text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
padding: 4rem 0;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section h1 {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--google-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section .lead {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--google-text-secondary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section p {
|
||||||
|
color: var(--google-text-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
border: 1px solid var(--google-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h5 {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--google-text);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: var(--google-text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-card {
|
||||||
|
border: 1px solid var(--google-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-url {
|
||||||
|
background: var(--google-light-gray);
|
||||||
|
border: 1px solid var(--google-border);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--google-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-custom {
|
||||||
|
background: var(--google-blue);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-google-primary {
|
||||||
|
background: var(--google-blue);
|
||||||
|
border: 1px solid var(--google-blue);
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-google-secondary {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid var(--google-border);
|
||||||
|
color: var(--google-text);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-spacing {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: var(--google-light-gray);
|
||||||
|
border: 1px solid var(--google-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-google-info {
|
||||||
|
background: #e8f0fe;
|
||||||
|
border: 1px solid #d2e3fc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: var(--google-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-google-success {
|
||||||
|
background: #e6f4ea;
|
||||||
|
border: 1px solid #ceead6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: var(--google-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid var(--google-border);
|
||||||
|
padding: 3rem 0;
|
||||||
|
margin-top: 6rem;
|
||||||
|
color: var(--google-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--google-text);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--google-text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper spacing and alignment */
|
||||||
|
.row {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-12,
|
||||||
|
.col-md-4,
|
||||||
|
.col-md-6 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix button spacing */
|
||||||
|
.d-flex.gap-2 {
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive improvements */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-section {
|
||||||
|
padding: 3rem 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card,
|
||||||
|
.endpoint-card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-spacing {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<div class="hero-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>HeroServer</h1>
|
||||||
|
<p class="lead">Modern JSON-RPC 2.0 API Gateway with Dynamic Documentation</p>
|
||||||
|
<p class="mb-4">A powerful, secure, and developer-friendly API server built with V language</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- Features Section -->
|
||||||
|
<div class="row section-spacing">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="text-center">Key Features</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="feature-card text-center">
|
||||||
|
<h5>JSON-RPC 2.0</h5>
|
||||||
|
<p>Full compliance with JSON-RPC 2.0 specification for reliable API communication</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="feature-card text-center">
|
||||||
|
<h5>Dynamic Documentation</h5>
|
||||||
|
<p>Auto-generated interactive documentation with curl examples and copy buttons</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="feature-card text-center">
|
||||||
|
<h5>Secure Authentication</h5>
|
||||||
|
<p>Built-in cryptographic authentication with public key infrastructure</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Available Endpoints Section -->
|
||||||
|
<div class="row section-spacing">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="mb-2">Available API Endpoints</h2>
|
||||||
|
<p class="text-muted mb-3">Explore the available API handlers and their documentation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
@for handler_name in server_info.handlers.keys()
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="endpoint-card">
|
||||||
|
<h5 class="mb-3">
|
||||||
|
<span class="badge-custom me-2">API</span>
|
||||||
|
${handler_name.title()}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="endpoint-url">
|
||||||
|
<strong>API Endpoint:</strong> /api/${handler_name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-url">
|
||||||
|
<strong>Documentation:</strong> /doc/${handler_name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-url">
|
||||||
|
<strong>JSON Info:</strong> /json/${handler_name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted mb-3">
|
||||||
|
JSON-RPC 2.0 API for ${handler_name} operations with full CRUD support
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="/doc/${handler_name}" class="btn btn-google-primary btn-sm">
|
||||||
|
View Documentation
|
||||||
|
</a>
|
||||||
|
<a href="/json/${handler_name}" class="btn btn-google-secondary btn-sm">
|
||||||
|
JSON Info
|
||||||
|
</a>
|
||||||
|
<a href="/md/${handler_name}" class="btn btn-google-secondary btn-sm">
|
||||||
|
Markdown Docs
|
||||||
|
</a>
|
||||||
|
<a href="/api/${handler_name}" class="btn btn-google-secondary btn-sm"
|
||||||
|
onclick="alert('This is a JSON-RPC endpoint. Use POST requests with JSON-RPC 2.0 format. See documentation for examples.'); return false;">
|
||||||
|
API Endpoint
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@end
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Start Section -->
|
||||||
|
<div class="row section-spacing">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="mb-3">Quick Start</h2>
|
||||||
|
<div class="endpoint-card">
|
||||||
|
<h5 class="mb-2">Getting Handler Information</h5>
|
||||||
|
<p class="text-muted mb-3">Get detailed information about a specific API handler in JSON format:</p>
|
||||||
|
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>curl ${server_info.base_url}/json/[handler_name]</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mb-2 mt-4">Making API Calls</h5>
|
||||||
|
<p class="text-muted mb-3">All API endpoints use JSON-RPC 2.0 format. Here's a basic example:</p>
|
||||||
|
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>curl -X POST ${server_info.base_url}/api/[handler_name] \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "method_name",
|
||||||
|
"params": {
|
||||||
|
"param1": "value1",
|
||||||
|
"param2": "value2"
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}'</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Tip:</strong> Visit the documentation pages for specific examples with
|
||||||
|
copy-to-clipboard functionality
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if server_info.auth_enabled
|
||||||
|
<!-- Authentication Section -->
|
||||||
|
<div class="row section-spacing">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="mb-3">Authentication</h2>
|
||||||
|
<div class="alert-google-info">
|
||||||
|
<h5 class="mb-2">Authentication Required</h5>
|
||||||
|
<p class="mb-0">
|
||||||
|
This server requires authentication. Please register your public key and obtain a session token
|
||||||
|
before making API calls. See the documentation for detailed authentication flow.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<!-- No Authentication Notice -->
|
||||||
|
<div class="row section-spacing">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="mb-3">Authentication</h2>
|
||||||
|
<div class="alert-google-success">
|
||||||
|
<h5 class="mb-2">Open Access</h5>
|
||||||
|
<p class="mb-0">
|
||||||
|
Authentication is currently disabled. You can make API calls directly without authentication.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@end
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="mb-0">
|
||||||
|
<strong>HeroServer</strong> - Built with V language •
|
||||||
|
<a href="https://github.com/freeflowuniverse/herolib" target="_blank" class="text-decoration-none">
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/js/bootstrap.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# 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`
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
module heroserver
|
|
||||||
|
|
||||||
import crypto.md5
|
|
||||||
import crypto.ed25519
|
|
||||||
import rand
|
|
||||||
import time
|
|
||||||
|
|
||||||
pub struct AuthConfig {
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AuthManager {
|
|
||||||
mut:
|
|
||||||
registered_keys map[string]string // pubkey -> user_id
|
|
||||||
pending_auths map[string]AuthChallenge // challenge -> challenge_data
|
|
||||||
active_sessions map[string]Session // session_key -> session_data
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AuthChallenge {
|
|
||||||
pub:
|
|
||||||
pubkey string
|
|
||||||
challenge string
|
|
||||||
created_at i64
|
|
||||||
expires_at i64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Session {
|
|
||||||
pub:
|
|
||||||
user_id string
|
|
||||||
pubkey string
|
|
||||||
created_at i64
|
|
||||||
expires_at i64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_auth_manager(config AuthConfig) &AuthManager {
|
|
||||||
// Use config if needed, for now it's just passed
|
|
||||||
_ = config
|
|
||||||
return &AuthManager{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register public key
|
|
||||||
pub fn (mut am AuthManager) register_pubkey(pubkey string) !string {
|
|
||||||
// Validate pubkey format
|
|
||||||
if pubkey.len != 64 { // ed25519 pubkey length
|
|
||||||
return error('Invalid public key format')
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id := md5.hexhash(pubkey + time.now().unix().str())
|
|
||||||
am.registered_keys[pubkey] = user_id
|
|
||||||
return user_id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate authentication challenge
|
|
||||||
pub fn (mut am AuthManager) create_auth_challenge(pubkey string) !string {
|
|
||||||
// Check if pubkey is registered
|
|
||||||
if pubkey !in am.registered_keys {
|
|
||||||
return error('Public key not registered')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate unique challenge
|
|
||||||
random_data := rand.string(32)
|
|
||||||
challenge := md5.hexhash(pubkey + random_data + time.now().unix().str())
|
|
||||||
|
|
||||||
now := time.now().unix()
|
|
||||||
am.pending_auths[challenge] = AuthChallenge{
|
|
||||||
pubkey: pubkey
|
|
||||||
challenge: challenge
|
|
||||||
created_at: now
|
|
||||||
expires_at: now + 300 // 5 minutes
|
|
||||||
}
|
|
||||||
|
|
||||||
return challenge
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature and create session
|
|
||||||
pub fn (mut am AuthManager) verify_and_create_session(challenge string, signature string) !string {
|
|
||||||
// Get challenge data
|
|
||||||
auth_challenge := am.pending_auths[challenge] or {
|
|
||||||
return error('Invalid or expired challenge')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check expiration
|
|
||||||
if time.now().unix() > auth_challenge.expires_at {
|
|
||||||
am.pending_auths.delete(challenge)
|
|
||||||
return error('Challenge expired')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature
|
|
||||||
pubkey_bytes := auth_challenge.pubkey.bytes()
|
|
||||||
challenge_bytes := challenge.bytes()
|
|
||||||
signature_bytes := signature.bytes()
|
|
||||||
|
|
||||||
ed25519.verify(pubkey_bytes, challenge_bytes, signature_bytes) or {
|
|
||||||
return error('Invalid signature')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create session
|
|
||||||
session_key := md5.hexhash(auth_challenge.pubkey + time.now().unix().str() + rand.string(16))
|
|
||||||
now := time.now().unix()
|
|
||||||
|
|
||||||
am.active_sessions[session_key] = Session{
|
|
||||||
user_id: am.registered_keys[auth_challenge.pubkey]
|
|
||||||
pubkey: auth_challenge.pubkey
|
|
||||||
created_at: now
|
|
||||||
expires_at: now + 3600 // 1 hour
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up challenge
|
|
||||||
am.pending_auths.delete(challenge)
|
|
||||||
|
|
||||||
return session_key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate session
|
|
||||||
pub fn (am AuthManager) validate_session(session_key string) bool {
|
|
||||||
session := am.active_sessions[session_key] or { return false }
|
|
||||||
return time.now().unix() < session.expires_at
|
|
||||||
}
|
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
module heroserver
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.schemas.openrpc
|
|
||||||
import os
|
|
||||||
import freeflowuniverse.herolib.schemas.jsonschema
|
|
||||||
|
|
||||||
// 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: []
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
return process_doc_template(template_content, spec, api_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
fn process_doc_template(template string, spec openrpc.OpenRPC, handler_type string) string {
|
|
||||||
mut content := template
|
|
||||||
|
|
||||||
// Replace template variables
|
|
||||||
content = content.replace('@{handler_type}', handler_type)
|
|
||||||
content = content.replace('@{spec.info.title}', spec.info.title)
|
|
||||||
content = content.replace('@{spec.info.description}', spec.info.description)
|
|
||||||
content = content.replace('@{spec.info.version}', spec.info.version)
|
|
||||||
|
|
||||||
// Generate methods documentation
|
|
||||||
mut methods_doc := ''
|
|
||||||
for method in spec.methods {
|
|
||||||
methods_doc += generate_method_doc(method)
|
|
||||||
}
|
|
||||||
content = content.replace('@{methods}', methods_doc)
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate documentation for a single method
|
|
||||||
fn generate_method_doc(method openrpc.Method) string {
|
|
||||||
mut doc := '## ${method.name}\n\n'
|
|
||||||
|
|
||||||
if method.description.len > 0 {
|
|
||||||
doc += '${method.description}\n\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
if method.params.len > 0 {
|
|
||||||
doc += '### Parameters\n\n'
|
|
||||||
for param in method.params {
|
|
||||||
// Handle both ContentDescriptor and Reference
|
|
||||||
if param is openrpc.ContentDescriptor {
|
|
||||||
if param.schema is jsonschema.Schema {
|
|
||||||
schema := param.schema as jsonschema.Schema
|
|
||||||
doc += '- **${param.name}** (${schema.typ}): ${param.description}\n'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doc += '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result
|
|
||||||
if method.result is openrpc.ContentDescriptor {
|
|
||||||
result := method.result as openrpc.ContentDescriptor
|
|
||||||
doc += '### Returns\n\n'
|
|
||||||
doc += '${result.description}\n\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Examples (would need to be added to OpenRPC spec or handled differently)
|
|
||||||
doc += '### Example\n\n'
|
|
||||||
doc += '```json\n'
|
|
||||||
doc += '// Request example would go here\n'
|
|
||||||
doc += '```\n\n'
|
|
||||||
|
|
||||||
return doc
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
module heroserver
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.schemas.openrpc
|
|
||||||
|
|
||||||
pub struct HandlerRegistry {
|
|
||||||
mut:
|
|
||||||
handlers map[string]openrpc.Handler
|
|
||||||
specs map[string]openrpc.OpenRPC
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_handler_registry() &HandlerRegistry {
|
|
||||||
return &HandlerRegistry{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register OpenRPC handler with type name
|
|
||||||
pub fn (mut hr HandlerRegistry) register(handler_type string, handler openrpc.Handler, spec openrpc.OpenRPC) {
|
|
||||||
hr.handlers[handler_type] = handler
|
|
||||||
hr.specs[handler_type] = spec
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get handler by type
|
|
||||||
pub fn (hr HandlerRegistry) get(handler_type string) ?openrpc.Handler {
|
|
||||||
return hr.handlers[handler_type]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get OpenRPC spec by type
|
|
||||||
pub fn (hr HandlerRegistry) get_spec(handler_type string) ?openrpc.OpenRPC {
|
|
||||||
return hr.specs[handler_type]
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all registered handler types
|
|
||||||
pub fn (hr HandlerRegistry) list_types() []string {
|
|
||||||
return hr.handlers.keys()
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
module heroserver
|
|
||||||
|
|
||||||
import veb
|
|
||||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
|
||||||
import freeflowuniverse.herolib.hero.crypt
|
|
||||||
import freeflowuniverse.herolib.schemas.openrpc
|
|
||||||
|
|
||||||
pub struct ServerConfig {
|
|
||||||
pub mut:
|
|
||||||
port int = 8080
|
|
||||||
auth_config AuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HeroServer {
|
|
||||||
pub mut:
|
|
||||||
config ServerConfig
|
|
||||||
auth_manager &AuthManager
|
|
||||||
handler_registry &HandlerRegistry
|
|
||||||
age_client &crypt.AGEClient
|
|
||||||
doc_registry &DocRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Context {
|
|
||||||
veb.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
pub fn (mut s HeroServer) start() ! {
|
|
||||||
veb.run[HeroServer, Context](mut s, s.config.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication endpoints
|
|
||||||
@['/register'; post]
|
|
||||||
pub fn (mut s HeroServer) register(mut ctx Context) veb.Result {
|
|
||||||
// Implementation for pubkey registration
|
|
||||||
return ctx.text('not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/authreq'; post]
|
|
||||||
pub fn (mut s HeroServer) authreq(mut ctx Context) veb.Result {
|
|
||||||
// Implementation for authentication request
|
|
||||||
return ctx.text('not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/auth'; post]
|
|
||||||
pub fn (mut s HeroServer) auth(mut ctx Context) veb.Result {
|
|
||||||
// Implementation for authentication verification
|
|
||||||
return ctx.text('not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
// API endpoints
|
|
||||||
@['/api/:handler_type'; post]
|
|
||||||
pub fn (mut s HeroServer) api(mut ctx Context, handler_type string) veb.Result {
|
|
||||||
// Validate session
|
|
||||||
session_key := ctx.get_custom_header('Authorization') or { '' }
|
|
||||||
if !s.auth_manager.validate_session(session_key) {
|
|
||||||
return ctx.request_error('Invalid session')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get handler and process request
|
|
||||||
mut handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
|
|
||||||
|
|
||||||
request := jsonrpc.decode_request(ctx.req.data) or {
|
|
||||||
return ctx.request_error('Invalid JSON-RPC request')
|
|
||||||
}
|
|
||||||
|
|
||||||
response := handler.handle(request) or { return ctx.server_error('Handler error') }
|
|
||||||
|
|
||||||
return ctx.json(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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}') }
|
|
||||||
|
|
||||||
// Redirect to the first available API documentation (preferably heroserver)
|
|
||||||
if 'heroserver' in s.doc_registry.apis {
|
|
||||||
return ctx.redirect('/docs/heroserver')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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,30 +0,0 @@
|
|||||||
# @{handler_type} API Documentation
|
|
||||||
|
|
||||||
@{spec.info.description}
|
|
||||||
|
|
||||||
**Version:** @{spec.info.version}
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This documentation provides details about the @{handler_type} API endpoints and their usage.
|
|
||||||
|
|
||||||
@{methods}
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
All API requests require a valid session key obtained through the authentication flow:
|
|
||||||
|
|
||||||
1. **Register**: Submit your public key to register
|
|
||||||
2. **Request Challenge**: Get an authentication challenge
|
|
||||||
3. **Authenticate**: Sign the challenge and submit for session key
|
|
||||||
4. **Use Session**: Include session key in subsequent API requests
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The API uses standard JSON-RPC 2.0 error codes:
|
|
||||||
|
|
||||||
- `-32700`: Parse error
|
|
||||||
- `-32600`: Invalid Request
|
|
||||||
- `-32601`: Method not found
|
|
||||||
- `-32602`: Invalid params
|
|
||||||
- `-32603`: Internal error
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
# 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
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
# 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