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
|
||||
|
||||
fn main() {
|
||||
// Start the server in a background thread
|
||||
// Start the server in a background thread with authentication disabled for testing
|
||||
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
|
||||
|
||||
@@ -2,8 +2,6 @@ module rpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.hero.heroserver
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import os
|
||||
|
||||
const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json')
|
||||
@@ -73,21 +71,19 @@ pub fn new_heromodels_handler() !&openrpc.Handler {
|
||||
@[params]
|
||||
pub struct ServerArgs {
|
||||
pub mut:
|
||||
port int = 8080
|
||||
host string = 'localhost'
|
||||
port int = 8080
|
||||
host string = 'localhost'
|
||||
auth_enabled bool = true
|
||||
}
|
||||
|
||||
pub fn start(args ServerArgs) ! {
|
||||
// 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
|
||||
handler := new_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
|
||||
server.start()!
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{
|
||||
"name": "comment_get",
|
||||
"summary": "Get a comment by ID",
|
||||
"description": "Retrieve a comment by its unique ID. Returns the comment object.",
|
||||
"params": [
|
||||
{
|
||||
"name": "id",
|
||||
|
||||
@@ -1,14 +1,53 @@
|
||||
module heroserver
|
||||
|
||||
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']
|
||||
pub fn (mut server HeroServer) doc_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() }
|
||||
|
||||
// Convert the OpenRPC specification to a DocSpec
|
||||
spec := doc_spec_from_openrpc(handler.specification, handler_type) or {
|
||||
// Create dynamic configuration based on request
|
||||
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}')
|
||||
}
|
||||
|
||||
@@ -17,3 +56,298 @@ pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string)
|
||||
|
||||
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
|
||||
objects []DocObject
|
||||
auth_info AuthDocInfo
|
||||
base_url string // Dynamic base URL for examples
|
||||
}
|
||||
|
||||
// DocObject represents a logical grouping of methods.
|
||||
@@ -31,7 +32,7 @@ pub mut:
|
||||
example_call string
|
||||
example_response string
|
||||
endpoint_url string
|
||||
curl_example string // New field for curl command
|
||||
curl_example string
|
||||
}
|
||||
|
||||
// DocParam represents a parameter or result in the documentation
|
||||
@@ -47,9 +48,11 @@ pub mut:
|
||||
// AuthDocInfo contains authentication flow information
|
||||
pub struct AuthDocInfo {
|
||||
pub mut:
|
||||
steps []AuthStep
|
||||
enabled bool
|
||||
steps []AuthStep
|
||||
}
|
||||
|
||||
// AuthStep represents a single step in the authentication flow
|
||||
pub struct AuthStep {
|
||||
pub mut:
|
||||
number int
|
||||
@@ -60,81 +63,126 @@ pub mut:
|
||||
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.
|
||||
// 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.
|
||||
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')
|
||||
}
|
||||
|
||||
mut doc_spec := DocSpec{
|
||||
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 {
|
||||
// Convert parameters
|
||||
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_method := process_method(method, config)!
|
||||
doc_spec.methods << doc_method
|
||||
}
|
||||
|
||||
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.
|
||||
// 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 {
|
||||
@@ -205,7 +253,8 @@ fn generate_example_response(result DocParam) string {
|
||||
// Create authentication documentation info
|
||||
fn create_auth_info() AuthDocInfo {
|
||||
return AuthDocInfo{
|
||||
steps: [
|
||||
enabled: true
|
||||
steps: [
|
||||
AuthStep{
|
||||
number: 1
|
||||
title: 'Register Public Key'
|
||||
|
||||
@@ -2,6 +2,7 @@ module heroserver
|
||||
|
||||
import freeflowuniverse.herolib.crypt.herocrypt
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import veb
|
||||
|
||||
// Create a new HeroServer instance
|
||||
@@ -14,25 +15,36 @@ pub fn new(config HeroServerConfig) !&HeroServer {
|
||||
}
|
||||
|
||||
mut server := &HeroServer{
|
||||
port: config.port
|
||||
host: config.host
|
||||
port: config.port
|
||||
host: config.host
|
||||
crypto_client: crypto_client
|
||||
sessions: map[string]Session{}
|
||||
handlers: map[string]&openrpc.Handler{}
|
||||
challenges: map[string]AuthChallenge{}
|
||||
sessions: map[string]Session{}
|
||||
handlers: map[string]&openrpc.Handler{}
|
||||
challenges: map[string]AuthChallenge{}
|
||||
auth_enabled: config.auth_enabled
|
||||
}
|
||||
|
||||
console.print_header('HeroServer created on port ${server.port}')
|
||||
return server
|
||||
}
|
||||
|
||||
// Register an OpenRPC handler
|
||||
pub fn (mut server HeroServer) register_handler(handler_type string, handler &openrpc.Handler) ! {
|
||||
server.handlers[handler_type] = handler
|
||||
console.print_header('Registered handler: ${handler_type}')
|
||||
}
|
||||
|
||||
// Start the server
|
||||
pub fn (mut server HeroServer) start() ! {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ import time
|
||||
@[params]
|
||||
pub struct HeroServerConfig {
|
||||
pub mut:
|
||||
port int = 9977
|
||||
host string = 'localhost'
|
||||
port int = 9977
|
||||
host string = 'localhost'
|
||||
auth_enabled bool = true // Whether to enable authentication
|
||||
// Optional crypto client, will create default if not provided
|
||||
crypto_client ?&herocrypt.HeroCrypt
|
||||
}
|
||||
@@ -17,31 +18,43 @@ pub mut:
|
||||
// Main server struct
|
||||
pub struct HeroServer {
|
||||
mut:
|
||||
port int
|
||||
host string
|
||||
port int
|
||||
host string
|
||||
crypto_client &herocrypt.HeroCrypt
|
||||
sessions map[string]Session // sessionkey -> Session
|
||||
handlers map[string]&openrpc.Handler // handlertype -> handler
|
||||
challenges map[string]AuthChallenge
|
||||
sessions map[string]Session // sessionkey -> Session
|
||||
handlers map[string]&openrpc.Handler // handlertype -> handler
|
||||
challenges map[string]AuthChallenge
|
||||
pub mut:
|
||||
auth_enabled bool = true // Whether authentication is required
|
||||
}
|
||||
|
||||
// Authentication challenge data
|
||||
pub struct AuthChallenge {
|
||||
pub mut:
|
||||
pubkey string
|
||||
challenge string // unique hashed challenge
|
||||
pubkey string
|
||||
challenge string // unique hashed challenge
|
||||
created_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
|
||||
pub struct Session {
|
||||
pub mut:
|
||||
session_key string
|
||||
pubkey string
|
||||
created_at time.Time
|
||||
session_key string
|
||||
pubkey string
|
||||
created_at time.Time
|
||||
last_activity time.Time
|
||||
expires_at time.Time
|
||||
expires_at time.Time
|
||||
}
|
||||
|
||||
// Authentication request structures
|
||||
@@ -62,7 +75,7 @@ pub:
|
||||
|
||||
pub struct AuthSubmitRequest {
|
||||
pub:
|
||||
pubkey string
|
||||
pubkey string
|
||||
signature string // signed challenge
|
||||
}
|
||||
|
||||
@@ -75,6 +88,72 @@ pub:
|
||||
pub struct APIRequest {
|
||||
pub:
|
||||
session_key string
|
||||
method string
|
||||
params map[string]string
|
||||
method 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,17 +35,20 @@ fn main() {
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- **API Calls**: `POST /api/{handler_type}/{method_name}`
|
||||
- **Documentation**: `GET /doc/{handler_type}/`
|
||||
- **HTML Homepage**: `GET /` - Returns HTML homepage with server information
|
||||
- **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
|
||||
|
||||
1. **Register Public Key**: `POST /auth/register`
|
||||
1. **Register Public Key**: `POST /auth/register`
|
||||
- Body: `{"pubkey": "your_public_key"}`
|
||||
2. **Request Challenge**: `POST /auth/authreq`
|
||||
2. **Request Challenge**: `POST /auth/authreq`
|
||||
- Body: `{"pubkey": "your_public_key"}`
|
||||
- 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.
|
||||
- Body: `{"pubkey": "your_public_key", "signature": "your_signature"}`
|
||||
- Returns a session key.
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<div class="card method-card" id="method-${method.name}">
|
||||
<div class="card-header">
|
||||
<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 class="card-body">
|
||||
@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