feat: Enhance logging and CORS handling
- Add console output option to logger - Implement ISO time conversion for calendar events - Add OPTIONS method for API and root handlers - Introduce health check endpoint with uptime and server info - Implement manual CORS handling in `before_request` - Add `start_time` to HeroServer for uptime tracking - Add `ServerLogParams` and `log` method for server logging
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
module heroserver
|
||||
|
||||
import json
|
||||
import net.http
|
||||
import veb
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
@@ -36,8 +37,41 @@ pub fn (mut server HeroServer) auth_handler(mut ctx Context, action string) !veb
|
||||
}
|
||||
}
|
||||
|
||||
@['/api/:handler_type'; post]
|
||||
@['/api/:handler_type'; options; post]
|
||||
pub fn (mut server HeroServer) api_handler(mut ctx Context, handler_type string) veb.Result {
|
||||
if ctx.req.method == http.Method.options {
|
||||
if server.cors_enabled {
|
||||
origin := ctx.get_header(.origin) or { '' }
|
||||
if origin != ''
|
||||
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
|
||||
ctx.set_header(.access_control_allow_origin, origin)
|
||||
ctx.set_header(.access_control_allow_methods, 'GET, HEAD, PATCH, PUT, POST, DELETE, OPTIONS')
|
||||
ctx.set_header(.access_control_allow_headers, 'Content-Type, Authorization, X-Requested-With')
|
||||
ctx.set_header(.access_control_allow_credentials, 'true')
|
||||
ctx.set_header(.vary, 'Origin')
|
||||
server.log(
|
||||
message: 'CORS headers set for origin: ${origin}'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.text('')
|
||||
}
|
||||
|
||||
// Set CORS headers for POST response
|
||||
if server.cors_enabled {
|
||||
origin := ctx.get_header(.origin) or { '' }
|
||||
if origin != ''
|
||||
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
|
||||
ctx.set_header(.access_control_allow_origin, origin)
|
||||
ctx.set_header(.access_control_allow_credentials, 'true')
|
||||
ctx.set_header(.vary, 'Origin')
|
||||
server.log(
|
||||
message: 'CORS headers set for API POST response, origin: ${origin}'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For now, skip authentication for testing
|
||||
// session_key := ctx.get_header(.authorization) or {
|
||||
// return ctx.request_error('Missing session key in Authorization header')
|
||||
@@ -61,6 +95,6 @@ pub fn (mut server HeroServer) api_handler(mut ctx Context, handler_type string)
|
||||
// Handle the request using the OpenRPC handler
|
||||
response := handler.handle(request) or { return ctx.server_error('Handler error: ${err}') }
|
||||
|
||||
// Return the JSON-RPC response
|
||||
return ctx.json(response)
|
||||
ctx.set_header(.content_type, 'application/json')
|
||||
return ctx.text(response.encode())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,113 @@
|
||||
module heroserver
|
||||
|
||||
import veb
|
||||
import net.http
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import time
|
||||
|
||||
// Home page handler - returns HTML homepage
|
||||
@['/']
|
||||
// Home page handler - returns HTML homepage for GET, handles JSON-RPC for POST
|
||||
@['/'; get; options; post]
|
||||
pub fn (mut server HeroServer) home_handler(mut ctx Context) veb.Result {
|
||||
server.log(
|
||||
message: 'New request: ${ctx.req.method} /'
|
||||
)
|
||||
|
||||
// Handle CORS preflight OPTIONS request
|
||||
if ctx.req.method == http.Method.options {
|
||||
server.log(
|
||||
message: 'Handling OPTIONS preflight request for root'
|
||||
)
|
||||
|
||||
// Ensure CORS headers are set for OPTIONS response
|
||||
if server.cors_enabled {
|
||||
origin := ctx.get_header(.origin) or { '' }
|
||||
if origin != ''
|
||||
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
|
||||
ctx.set_header(.access_control_allow_origin, origin)
|
||||
ctx.set_header(.access_control_allow_methods, 'GET, HEAD, PATCH, PUT, POST, DELETE, OPTIONS')
|
||||
ctx.set_header(.access_control_allow_headers, 'Content-Type, Authorization, X-Requested-With')
|
||||
ctx.set_header(.access_control_allow_credentials, 'true')
|
||||
ctx.set_header(.vary, 'Origin')
|
||||
server.log(
|
||||
message: 'CORS headers set for origin: ${origin}'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.text('')
|
||||
}
|
||||
|
||||
// Handle POST requests as JSON-RPC
|
||||
if ctx.req.method == http.Method.post {
|
||||
server.log(
|
||||
message: 'Handling JSON-RPC request at root endpoint'
|
||||
)
|
||||
|
||||
// Set CORS headers for POST response
|
||||
if server.cors_enabled {
|
||||
origin := ctx.get_header(.origin) or { '' }
|
||||
if origin != ''
|
||||
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
|
||||
ctx.set_header(.access_control_allow_origin, origin)
|
||||
ctx.set_header(.access_control_allow_credentials, 'true')
|
||||
ctx.set_header(.vary, 'Origin')
|
||||
server.log(
|
||||
message: 'CORS headers set for POST response, origin: ${origin}'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have handlers
|
||||
if server.handlers.len == 0 {
|
||||
return ctx.server_error('No handlers registered')
|
||||
}
|
||||
|
||||
// Use the first registered handler for root requests
|
||||
handler_name := server.handlers.keys()[0]
|
||||
server.log(
|
||||
message: 'Using handler: ${handler_name}'
|
||||
)
|
||||
|
||||
mut handler := server.handlers[handler_name] or {
|
||||
return ctx.request_error('Handler not found: ${handler_name}')
|
||||
}
|
||||
|
||||
// Parse JSON-RPC request
|
||||
request := jsonrpc.decode_request(ctx.req.data) or {
|
||||
server.log(
|
||||
message: 'Invalid JSON-RPC request: ${err}'
|
||||
level: .error
|
||||
)
|
||||
return ctx.request_error('Invalid JSON-RPC request: ${err}')
|
||||
}
|
||||
|
||||
server.log(
|
||||
message: 'JSON-RPC method: ${request.method}'
|
||||
)
|
||||
|
||||
// Handle the request using the OpenRPC handler
|
||||
response := handler.handle(request) or {
|
||||
server.log(
|
||||
message: 'Handler error: ${err}'
|
||||
level: .error
|
||||
)
|
||||
return ctx.server_error('Handler error: ${err}')
|
||||
}
|
||||
|
||||
server.log(
|
||||
message: 'JSON-RPC response sent'
|
||||
)
|
||||
ctx.set_header(.content_type, 'application/json')
|
||||
return ctx.text(response.encode())
|
||||
}
|
||||
|
||||
// Handle GET requests as HTML homepage
|
||||
server.log(
|
||||
message: 'Serving HTML homepage'
|
||||
)
|
||||
|
||||
// Create a simple server info structure for the template
|
||||
server_info := HomePageData{
|
||||
base_url: get_base_url_from_context(ctx)
|
||||
@@ -22,6 +123,42 @@ pub fn (mut server HeroServer) home_handler(mut ctx Context) veb.Result {
|
||||
return ctx.html(html_content)
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
@['/health'; get]
|
||||
pub fn (mut server HeroServer) health_handler(mut ctx Context) veb.Result {
|
||||
server.log(
|
||||
message: 'Health check requested'
|
||||
)
|
||||
|
||||
// Create health status response
|
||||
current_time := time.now().unix()
|
||||
uptime := current_time - server.start_time
|
||||
|
||||
health_status := {
|
||||
'status': 'healthy'
|
||||
'timestamp': current_time.str()
|
||||
'version': '1.0.0'
|
||||
'handlers_count': server.handlers.len.str()
|
||||
'auth_enabled': server.auth_enabled.str()
|
||||
'cors_enabled': server.cors_enabled.str()
|
||||
'uptime_seconds': uptime.str()
|
||||
}
|
||||
|
||||
// Set CORS headers if enabled
|
||||
if server.cors_enabled {
|
||||
origin := ctx.get_header(.origin) or { '' }
|
||||
if origin != ''
|
||||
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
|
||||
ctx.set_header(.access_control_allow_origin, origin)
|
||||
ctx.set_header(.access_control_allow_credentials, 'true')
|
||||
ctx.set_header(.vary, 'Origin')
|
||||
}
|
||||
}
|
||||
|
||||
ctx.set_header(.content_type, 'application/json')
|
||||
return ctx.json(health_status)
|
||||
}
|
||||
|
||||
// JSON server info handler
|
||||
@['/json/:handler_type']
|
||||
pub fn (mut server HeroServer) json_handler(mut ctx Context, handler_type string) veb.Result {
|
||||
|
||||
@@ -3,6 +3,8 @@ module heroserver
|
||||
import freeflowuniverse.herolib.crypt.herocrypt
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.core.logger
|
||||
import time
|
||||
import veb
|
||||
|
||||
// Create a new HeroServer instance
|
||||
@@ -14,6 +16,12 @@ pub fn new(config HeroServerConfig) !&HeroServer {
|
||||
herocrypt.new_default()!
|
||||
}
|
||||
|
||||
// Create logger with configurable output
|
||||
mut server_logger := logger.new(
|
||||
path: config.log_path
|
||||
console_output: config.console_output
|
||||
) or { return error('Failed to create logger: ${err}') }
|
||||
|
||||
mut server := &HeroServer{
|
||||
port: config.port
|
||||
host: config.host
|
||||
@@ -24,6 +32,8 @@ pub fn new(config HeroServerConfig) !&HeroServer {
|
||||
auth_enabled: config.auth_enabled
|
||||
cors_enabled: config.cors_enabled
|
||||
allowed_origins: config.allowed_origins.clone()
|
||||
logger: server_logger
|
||||
start_time: time.now().unix()
|
||||
}
|
||||
|
||||
console.print_header('HeroServer created on port ${server.port}')
|
||||
@@ -38,15 +48,8 @@ pub fn (mut server HeroServer) register_handler(handler_type string, handler &op
|
||||
|
||||
// Start the server
|
||||
pub fn (mut server HeroServer) start() ! {
|
||||
// Configure CORS if enabled
|
||||
if server.cors_enabled {
|
||||
console.print_item('CORS enabled for origins: ${server.allowed_origins}')
|
||||
server.use(veb.cors[Context](veb.CorsOptions{
|
||||
origins: server.allowed_origins
|
||||
allowed_methods: [.get, .head, .patch, .put, .post, .delete, .options]
|
||||
allowed_headers: ['Content-Type', 'Authorization', 'X-Requested-With']
|
||||
allow_credentials: true
|
||||
}))
|
||||
}
|
||||
|
||||
// Start VEB server
|
||||
|
||||
@@ -2,6 +2,7 @@ module heroserver
|
||||
|
||||
import freeflowuniverse.herolib.crypt.herocrypt
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.core.logger
|
||||
import time
|
||||
import veb
|
||||
|
||||
@@ -9,9 +10,13 @@ import veb
|
||||
@[params]
|
||||
pub struct HeroServerConfig {
|
||||
pub mut:
|
||||
port int = 9977
|
||||
host string = 'localhost'
|
||||
auth_enabled bool = true // Whether to enable authentication
|
||||
port int = 9977
|
||||
host string = 'localhost'
|
||||
log_path string = '/tmp/heroserver_logs'
|
||||
console_output bool = true // Enable console logging by default
|
||||
|
||||
// flags
|
||||
auth_enabled bool = true // Whether to enable authentication
|
||||
// CORS configuration
|
||||
cors_enabled bool = true // Whether to enable CORS
|
||||
allowed_origins []string = ['*'] // Allowed origins for CORS, default allows all
|
||||
@@ -31,10 +36,33 @@ mut:
|
||||
challenges map[string]AuthChallenge
|
||||
cors_enabled bool
|
||||
allowed_origins []string
|
||||
logger logger.Logger // Logger instance with dual output
|
||||
start_time i64 // Server start timestamp for uptime calculation
|
||||
pub mut:
|
||||
auth_enabled bool = true // Whether authentication is required
|
||||
}
|
||||
|
||||
// Convenient logging method for the server
|
||||
@[params]
|
||||
pub struct ServerLogParams {
|
||||
pub:
|
||||
message string
|
||||
level logger.LogType = .stdout // Default to info level
|
||||
cat string = 'server' // Default category
|
||||
}
|
||||
|
||||
// Log a message using the server's logger
|
||||
pub fn (mut server HeroServer) log(params ServerLogParams) {
|
||||
server.logger.log(
|
||||
cat: params.cat
|
||||
log: params.message
|
||||
logtype: params.level
|
||||
) or {
|
||||
// Fallback to console if logging fails
|
||||
println('[${params.cat}] ${params.message}')
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication challenge data
|
||||
pub struct AuthChallenge {
|
||||
pub mut:
|
||||
@@ -169,3 +197,21 @@ pub:
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
// before_request is called before every request
|
||||
pub fn (mut server HeroServer) before_request(mut ctx Context) {
|
||||
// Handle CORS manually
|
||||
if server.cors_enabled {
|
||||
origin := ctx.get_header(.origin) or { '' }
|
||||
|
||||
// Check if origin is allowed
|
||||
if origin != ''
|
||||
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
|
||||
ctx.set_header(.access_control_allow_origin, origin)
|
||||
ctx.set_header(.access_control_allow_methods, 'GET, HEAD, PATCH, PUT, POST, DELETE, OPTIONS')
|
||||
ctx.set_header(.access_control_allow_headers, 'Content-Type, Authorization, X-Requested-With')
|
||||
ctx.set_header(.access_control_allow_credentials, 'true')
|
||||
ctx.set_header(.vary, 'Origin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user