// Server Action Router
//
// Registers action endpoints with Mars router.
// Automatically applies CSRF protection.
///|
/// Mars App type alias
pub type App = @mars.Server
///|
/// Mars Context type alias
pub type Ctx = @mars.Context
// ============================================================================
// Action Registration
// ============================================================================
///|
/// Register all actions from registry to Mars app
pub fn register_actions(app : App, registry : ActionRegistry) -> Unit {
for id in registry.action_ids() {
let action = match registry.get(id) {
Some(a) => a
None => continue
}
let path = registry.action_url(id)
let allowed_origins = registry.allowed_origins
// Create CSRF middleware for this action
let csrf_config = @middleware.CsrfConfig::for_origins(allowed_origins).with_fetch_metadata(
true,
)
// Only require JSON content-type if action.require_json is true
let csrf_mw = if action.require_json {
@middleware.csrf_with_config(
csrf_config.with_content_type("application/json"),
)
} else {
// Allow any content-type (for progressive enhancement with form submissions)
@middleware.csrf_with_config(csrf_config)
}
// Combine CSRF with action-specific middlewares
let middlewares : Array[@middleware.Middleware] = [csrf_mw]
for mw in action.middlewares {
middlewares.push(mw)
}
let middleware_chain = @middleware.pipeline(middlewares)
let handler = action.handler
app.post(
path,
@mars.handler(async fn(c) {
// Run middleware chain
let halted = @middleware.run_or_500(middleware_chain, c)
if halted {
return
}
// Get request body (async)
let body = c.body()
// Create action context
let action_ctx = ActionContext::from_ctx(c, body)
// Execute action handler
let result : ActionResult = (handler.0)(action_ctx) catch {
e => ActionResult::server_error(message=e.to_string())
}
// Convert result to response
action_result_to_response(c, result)
}),
)
}
}
///|
/// Convert action result to HTTP response
fn action_result_to_response(c : Ctx, result : ActionResult) -> Unit {
// Always set security header for action responses
c.set_header("X-Content-Type-Options", "nosniff")
match result.kind {
JsonSuccess(data) => @mars_response.send_json(c, data)
ClientRedirect(url) => {
// Return JSON with redirect instruction (for JavaScript clients)
let obj : Map[String, Json] = {}
obj["redirect"] = Json::string(url)
c.json(Json::object(obj))
}
HttpRedirectResult(url) =>
// Return actual HTTP redirect (302 with Location header)
// For non-JavaScript form submissions (progressive enhancement)
c.redirect(url)
ClientFailure(status, message) =>
@mars_response.send_json_error(c, message, status~)
ServerFailure(message) => {
// Log the actual error (implementation depends on logging setup)
let _ = message
// Don't expose internal error details in production
@mars_response.send_internal_error(c)
}
}
}