multisig rhai flow POC app
This commit is contained in:
187
flowbroker/templates/create_flow.html
Normal file
187
flowbroker/templates/create_flow.html
Normal file
@@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Flowbroker - Create Flow</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<style>
|
||||
.step, .requirement {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.step h3, .step h4, .requirement h5 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.step .requirementsContainer {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid #007bff;
|
||||
padding-left: 15px;
|
||||
}
|
||||
button.removeStepBtn, button.removeRequirementBtn {
|
||||
background-color: #dc3545;
|
||||
margin-top: 5px;
|
||||
}
|
||||
button.removeStepBtn:hover, button.removeRequirementBtn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Create New Flow</h1>
|
||||
<form id="createFlowForm" action="/flows" method="post">
|
||||
<div>
|
||||
<label for="flow_name">Flow Name:</label>
|
||||
<input type="text" id="flow_name" name="flow_name" required>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div id="stepsContainer">
|
||||
<!-- Steps will be added here by JavaScript -->
|
||||
</div>
|
||||
|
||||
<button type="button" id="addStepBtn" class="addBtn">Add Step</button>
|
||||
<hr>
|
||||
<button type="submit">Create Flow</button>
|
||||
</form>
|
||||
<p><a href="/">Back to Flows List</a></p>
|
||||
|
||||
<!-- Template for a new step -->
|
||||
<template id="stepTemplate">
|
||||
<div class="step" data-step-index="">
|
||||
<h3>Step <span class="step-number"></span></h3>
|
||||
<button type="button" class="removeStepBtn">Remove This Step</button>
|
||||
<div>
|
||||
<label>Step Description (Optional):</label>
|
||||
<input type="text" name="steps[X].description" class="step-description">
|
||||
</div>
|
||||
<h4>Signature Requirements for Step <span class="step-number"></span></h4>
|
||||
<div class="requirementsContainer" data-step-index="">
|
||||
<!-- Requirements will be added here -->
|
||||
</div>
|
||||
<button type="button" class="addRequirementBtn addBtn" data-step-index="">Add Signature Requirement</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Template for a new signature requirement -->
|
||||
<template id="requirementTemplate">
|
||||
<div class="requirement" data-req-index="">
|
||||
<h5>Requirement <span class="req-number"></span></h5>
|
||||
<button type="button" class="removeRequirementBtn">Remove Requirement</button>
|
||||
<div>
|
||||
<label>Message to Sign:</label>
|
||||
<textarea name="steps[X].requirements[Y].message" rows="2" required class="req-message"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Required Public Key:</label>
|
||||
<input type="text" name="steps[X].requirements[Y].public_key" required class="req-pubkey">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const stepsContainer = document.getElementById('stepsContainer');
|
||||
const addStepBtn = document.getElementById('addStepBtn');
|
||||
const stepTemplate = document.getElementById('stepTemplate');
|
||||
const requirementTemplate = document.getElementById('requirementTemplate');
|
||||
const form = document.getElementById('createFlowForm');
|
||||
|
||||
const updateIndices = () => {
|
||||
const steps = stepsContainer.querySelectorAll('.step');
|
||||
steps.forEach((step, stepIdx) => {
|
||||
// Update step-level attributes and text
|
||||
step.dataset.stepIndex = stepIdx;
|
||||
step.querySelector('.step-number').textContent = stepIdx + 1;
|
||||
step.querySelector('.step-description').name = `steps[${stepIdx}].description`;
|
||||
|
||||
const addReqBtn = step.querySelector('.addRequirementBtn');
|
||||
if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
|
||||
|
||||
const requirements = step.querySelectorAll('.requirementsContainer .requirement');
|
||||
requirements.forEach((req, reqIdx) => {
|
||||
// Update requirement-level attributes and text
|
||||
req.dataset.reqIndex = reqIdx;
|
||||
req.querySelector('.req-number').textContent = reqIdx + 1;
|
||||
req.querySelector('.req-message').name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
|
||||
req.querySelector('.req-pubkey').name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addRequirement = (currentStepElement, stepIndex) => {
|
||||
const requirementsContainer = currentStepElement.querySelector('.requirementsContainer');
|
||||
const reqFragment = requirementTemplate.content.cloneNode(true);
|
||||
const newRequirement = reqFragment.querySelector('.requirement');
|
||||
|
||||
requirementsContainer.appendChild(newRequirement);
|
||||
updateIndices(); // Update all indices after adding
|
||||
};
|
||||
|
||||
const addStep = () => {
|
||||
const stepFragment = stepTemplate.content.cloneNode(true);
|
||||
const newStep = stepFragment.querySelector('.step');
|
||||
stepsContainer.appendChild(newStep);
|
||||
|
||||
// Add at least one requirement to the new step automatically
|
||||
const currentStepIndex = stepsContainer.querySelectorAll('.step').length - 1;
|
||||
addRequirement(newStep, currentStepIndex);
|
||||
|
||||
updateIndices(); // Update all indices after adding
|
||||
};
|
||||
|
||||
// Event delegation for remove buttons and add requirement button
|
||||
stepsContainer.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('removeStepBtn')) {
|
||||
event.target.closest('.step').remove();
|
||||
if (stepsContainer.querySelectorAll('.step').length === 0) { // Ensure at least one step
|
||||
addStep();
|
||||
}
|
||||
updateIndices();
|
||||
} else if (event.target.classList.contains('addRequirementBtn')) {
|
||||
const stepElement = event.target.closest('.step');
|
||||
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
|
||||
addRequirement(stepElement, stepIndex);
|
||||
} else if (event.target.classList.contains('removeRequirementBtn')) {
|
||||
const requirementElement = event.target.closest('.requirement');
|
||||
const stepElement = event.target.closest('.step');
|
||||
const requirementsContainer = stepElement.querySelector('.requirementsContainer');
|
||||
requirementElement.remove();
|
||||
// Ensure at least one requirement per step
|
||||
if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
|
||||
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
|
||||
addRequirement(stepElement, stepIndex);
|
||||
}
|
||||
updateIndices();
|
||||
}
|
||||
});
|
||||
|
||||
addStepBtn.addEventListener('click', addStep);
|
||||
|
||||
// Add one step by default when the page loads
|
||||
if (stepsContainer.children.length === 0) {
|
||||
addStep();
|
||||
}
|
||||
|
||||
// Optional: Validate that there's at least one step and one requirement before submit
|
||||
form.addEventListener('submit', (event) => {
|
||||
if (stepsContainer.querySelectorAll('.step').length === 0) {
|
||||
alert('Please add at least one step to the flow.');
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const steps = stepsContainer.querySelectorAll('.step');
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
if (steps[i].querySelectorAll('.requirementsContainer .requirement').length === 0) {
|
||||
alert(`Step ${i + 1} must have at least one signature requirement.`);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
28
flowbroker/templates/index.html
Normal file
28
flowbroker/templates/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Flowbroker - Flows</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Active Flows</h1>
|
||||
<a href="/flows/new">Create New Flow</a>
|
||||
<div id="flows-list">
|
||||
{% if flows %}
|
||||
<ul>
|
||||
{% for flow in flows %}
|
||||
<li>
|
||||
<strong>{{ flow.name }}</strong> (UUID: {{ flow.flow_uuid }}) - Status: {{ flow.status }}
|
||||
<br>
|
||||
Created: {{ flow.base_data.created_at | date(format="%Y-%m-%d %H:%M:%S") }} <!-- Assuming created_at is a Unix timestamp -->
|
||||
<p><a href="/flows/{{ flow.flow_uuid }}">View Details</a></p> <!-- Link uses flow_uuid -->
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No active flows. <a href="/flows/new">Create one?</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
105
flowbroker/templates/new_flow_form.html
Normal file
105
flowbroker/templates/new_flow_form.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Create Flow from Rhai Script</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f4f4f9;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 15px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.back-link {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/" class="back-link">← Back to Flow List</a>
|
||||
<h1>Create Flow from Rhai Script</h1>
|
||||
|
||||
<div id="rhai_script_examples_data" style="display: none;">
|
||||
{% for example in example_scripts %}
|
||||
<div id="rhai_example_content_{{ loop.index }}">{{ example.content }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="example_script_selector">Load Example Script:</label>
|
||||
<select id="example_script_selector">
|
||||
<option value="">-- Select an Example --</option>
|
||||
{% for example in example_scripts %}
|
||||
<option value="{{ example.name }}" data-example-id="rhai_example_content_{{ loop.index }}">{{ example.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<form action="/flows/create_script" method="POST" style="margin-top: 15px;">
|
||||
<div>
|
||||
<label for="rhai_script">Rhai Script:</label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="rhai_script" name="rhai_script" placeholder="Enter your Rhai script here or select an example above..."></textarea>
|
||||
</div>
|
||||
<button type="submit">Create Flow</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('example_script_selector').addEventListener('change', function() {
|
||||
var selectedOption = this.options[this.selectedIndex];
|
||||
var exampleId = selectedOption.getAttribute('data-example-id');
|
||||
if (exampleId) {
|
||||
var scriptContent = document.getElementById(exampleId).textContent; // Use textContent
|
||||
document.getElementById('rhai_script').value = scriptContent;
|
||||
} else {
|
||||
document.getElementById('rhai_script').value = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
8
flowbroker/templates/rhai_examples/minimal_flow.rhai
Normal file
8
flowbroker/templates/rhai_examples/minimal_flow.rhai
Normal file
@@ -0,0 +1,8 @@
|
||||
// Minimal Single Signature Flow
|
||||
let flow_id = create_flow("Quick Sign");
|
||||
|
||||
let step1_id = add_step(flow_id, "Sign the message", 0);
|
||||
add_requirement(step1_id, "any_signer_pk", "Please provide your signature.");
|
||||
|
||||
print("Minimal Flow (ID: " + flow_id + ") defined.");
|
||||
()
|
@@ -0,0 +1,18 @@
|
||||
// Flow with Multi-Requirement Step
|
||||
// If create_flow, add_step, or add_requirement fail from Rust,
|
||||
// the script will stop and the error will be reported by the server.
|
||||
|
||||
let flow_id = create_flow("Multi-Req Sign Off");
|
||||
|
||||
let step1_id = add_step(flow_id, "Initial Signatures (3 needed)", 0);
|
||||
|
||||
add_requirement(step1_id, "signer1_pk", "Signatory 1: Please sign terms.");
|
||||
add_requirement(step1_id, "signer2_pk", "Signatory 2: Please sign terms.");
|
||||
add_requirement(step1_id, "signer3_pk", "Signatory 3: Please sign terms.");
|
||||
|
||||
let step2_id = add_step(flow_id, "Final Confirmation", 1);
|
||||
|
||||
add_requirement(step2_id, "final_approver_pk", "Final approval for multi-req sign off.");
|
||||
|
||||
print("Multi-Requirement Flow (ID: " + flow_id + ") defined.");
|
||||
()
|
14
flowbroker/templates/rhai_examples/simple_two_step.rhai
Normal file
14
flowbroker/templates/rhai_examples/simple_two_step.rhai
Normal file
@@ -0,0 +1,14 @@
|
||||
// Simple Two-Step Flow
|
||||
// If create_flow, add_step, or add_requirement fail from Rust,
|
||||
// the script will stop and the error will be reported by the server.
|
||||
|
||||
let flow_id = create_flow("Simple Two-Stepper");
|
||||
|
||||
let step1_id = add_step(flow_id, "Collect Document", 0);
|
||||
add_requirement(step1_id, "user_pubkey_document", "Please sign the document hash.");
|
||||
|
||||
let step2_id = add_step(flow_id, "Approval Signature", 1);
|
||||
add_requirement(step2_id, "approver_pubkey", "Please approve the collected document.");
|
||||
|
||||
print("Simple Two-Step Flow (ID: " + flow_id + ") defined.");
|
||||
()
|
Reference in New Issue
Block a user