Registration page: full organization application, private MA-side storage
This commit is contained in:
+154
-18
@@ -424,6 +424,31 @@ pub fn registry_list(dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn registry_applications(dir: &Path) -> Result<()> {
|
||||
let applications = registry::load_applications(®istry::applications_path(dir))?;
|
||||
if applications.is_empty() {
|
||||
println!("no applications recorded");
|
||||
return Ok(());
|
||||
}
|
||||
for app in &applications {
|
||||
println!("{} [{}] {} <{}>", app.id, app.class, app.org, app.email);
|
||||
println!(" representative: {}", app.representative);
|
||||
if !app.address.is_empty() {
|
||||
println!(" address: {}", app.address);
|
||||
}
|
||||
if !app.domain.is_empty() {
|
||||
println!(" domain: {}", app.domain);
|
||||
}
|
||||
if !app.payment.is_empty() {
|
||||
println!(" payment: {}", app.payment);
|
||||
}
|
||||
if !app.privacy_link.is_empty() {
|
||||
println!(" privacy: {}", app.privacy_link);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn registry_set_relays(dir: &Path, relays: &[String]) -> Result<()> {
|
||||
mutate_registry(dir, |doc| {
|
||||
doc.relays = relays.to_vec();
|
||||
@@ -519,6 +544,26 @@ fn sanitize_label(input: &str) -> String {
|
||||
struct SignupRequest {
|
||||
label: String,
|
||||
code: Option<String>,
|
||||
#[serde(default)]
|
||||
org: String,
|
||||
#[serde(default)]
|
||||
representative: String,
|
||||
#[serde(default)]
|
||||
email: String,
|
||||
#[serde(default)]
|
||||
address: String,
|
||||
#[serde(default)]
|
||||
domain: String,
|
||||
#[serde(default)]
|
||||
class: Option<String>,
|
||||
#[serde(default)]
|
||||
payment: String,
|
||||
#[serde(default)]
|
||||
privacy_link: String,
|
||||
#[serde(default)]
|
||||
attestation: bool,
|
||||
#[serde(default)]
|
||||
privacy_ack: bool,
|
||||
}
|
||||
|
||||
async fn registry_signup(
|
||||
@@ -539,6 +584,20 @@ async fn registry_signup(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if !request.attestation {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "content authorization must be confirmed" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if !request.privacy_ack {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "the privacy notice must be acknowledged" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let label = sanitize_label(&request.label);
|
||||
if label.is_empty() {
|
||||
return (
|
||||
@@ -576,10 +635,15 @@ async fn registry_signup(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let class = if request.class.as_deref() == Some(CLASS_ENRICHMENT) {
|
||||
CLASS_ENRICHMENT
|
||||
} else {
|
||||
CLASS_SOURCE
|
||||
};
|
||||
if let Err(error) = mutate_registry(&server.dir, |doc| {
|
||||
doc.members.push(RegistryMember {
|
||||
id: id.clone(),
|
||||
class: crate::config::CLASS_SOURCE.to_string(),
|
||||
class: class.to_string(),
|
||||
keys: Vec::new(),
|
||||
enc_key: None,
|
||||
});
|
||||
@@ -591,6 +655,25 @@ async fn registry_signup(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let application = registry::Application {
|
||||
id: id.clone(),
|
||||
org: request.org.clone(),
|
||||
representative: request.representative.clone(),
|
||||
email: request.email.clone(),
|
||||
address: request.address.clone(),
|
||||
domain: request.domain.clone(),
|
||||
class: class.to_string(),
|
||||
payment: request.payment.clone(),
|
||||
privacy_link: request.privacy_link.clone(),
|
||||
submitted_at: now_ts(),
|
||||
};
|
||||
if let Err(error) = registry::record_application(&server.dir, application) {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": error.to_string() })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let registry_url = server.registry_url.clone().unwrap_or_default();
|
||||
let ma_key = signed.doc.ma_key.clone();
|
||||
(
|
||||
@@ -707,8 +790,10 @@ const REGISTRY_PAGE: &str = r##"<!doctype html>
|
||||
.tag { color: #555; margin-top: 0; }
|
||||
.card { background: #fff; border: 1px solid #ddd; border-radius: 10px; padding: 1.1rem 1.25rem; }
|
||||
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
|
||||
input, button { font: inherit; border: 1px solid #bbb; border-radius: 6px; padding: 0.45rem 0.6rem; }
|
||||
input { width: 100%; margin: 0.2rem 0 0.9rem; }
|
||||
input, select, button { font: inherit; border: 1px solid #bbb; border-radius: 6px; padding: 0.45rem 0.6rem; }
|
||||
input, select { width: 100%; margin: 0.2rem 0 0.9rem; }
|
||||
.check { display: block; font-size: 0.92rem; margin: 0.5rem 0; }
|
||||
.check input { width: auto; margin: 0 0.4rem 0 0; }
|
||||
button { background: #174ea6; color: #fff; border: none; cursor: pointer; padding: 0.5rem 1rem; border-radius: 6px; }
|
||||
button:hover { background: #0f3d91; }
|
||||
#out { display: none; background: #101418; color: #d6f5d6; padding: 0.85rem 1rem;
|
||||
@@ -729,28 +814,55 @@ small: signed messages, budgets, honest truncation, aggregate courtesy. No annou
|
||||
scores on the wire, no in-protocol payment.</p>
|
||||
<p>Everything else — matching, ranking, retention, trust — is local.</p>
|
||||
|
||||
<h2>1. Register</h2>
|
||||
<h2>1. Register with the membership authority</h2>
|
||||
<div class="card">
|
||||
<p class="muted">Registering here creates your identifier with the membership authority (MA); the
|
||||
onboarding wizard in step 4 then binds your node's keys to it. The public registry publishes only
|
||||
your identifier, class, keys, and the federation's relays. The organization details below are kept
|
||||
privately by the MA for the membership contract — they are never published and never travel on the
|
||||
wire.</p>
|
||||
<form id="f">
|
||||
<label>Organization or handle<br>
|
||||
<input name="label" required pattern="[A-Za-z0-9 -]+" placeholder="acme-docs"></label><br>
|
||||
<label>Signup code (issued by the membership authority)<br>
|
||||
<input name="code" type="password" placeholder="signup code"></label><br>
|
||||
<label>Short name — this becomes your identifier<br>
|
||||
<input name="label" id="label" required pattern="[A-Za-z0-9 -]+" placeholder="keswick-research"></label>
|
||||
<p class="muted">identifier: <code id="preview">(type a short name)</code> — no domain or DNS of your own is needed.</p>
|
||||
<label>Signup code<br>
|
||||
<input name="code" type="password" placeholder="invite code"></label>
|
||||
<p class="muted">The code is the admission gate: members are approved, not anonymous. Ask the MA
|
||||
operator for one. (If you run the MA, it is the <code>--signup-code</code> passed to
|
||||
<code>frxd registry serve</code>.)</p>
|
||||
<label>Legal organization name<br>
|
||||
<input name="org" required placeholder="Keswick Research LLC"></label>
|
||||
<label>Representative (authorized contact person)<br>
|
||||
<input name="representative" required placeholder="Jane Keswick"></label>
|
||||
<label>Contact email<br>
|
||||
<input name="email" type="email" required placeholder="ops@example.org"></label>
|
||||
<label>Registered address<br>
|
||||
<input name="address" placeholder="street, city, country"></label>
|
||||
<label>Organization domain (optional)<br>
|
||||
<input name="domain" placeholder="example.org"></label>
|
||||
<label>What you will share<br>
|
||||
<select name="member_class">
|
||||
<option value="source">Source member — content I own or host</option>
|
||||
<option value="enrichment">Enrichment member — derived corpora (metadata-only)</option>
|
||||
</select></label>
|
||||
<label>Payment details (billing / payout — e.g. IBAN or payment handle)<br>
|
||||
<input name="payment" placeholder="kept private; the protocol itself carries no payment"></label>
|
||||
<label>Your privacy statement URL (optional)<br>
|
||||
<input name="privacy_link" placeholder="https://example.org/privacy"></label>
|
||||
<label class="check"><input type="checkbox" name="attestation" required> I will only index content I own or that users supply, and only collections I explicitly mark shared will answer queries.</label>
|
||||
<label class="check"><input type="checkbox" name="privacy_ack" required> I acknowledge the privacy notice above.</label>
|
||||
<button type="submit">Request membership</button>
|
||||
</form>
|
||||
<pre id="out"></pre>
|
||||
<p class="muted">Your identifier is <code><label>.frx.federatedsearch.org</code> — no domain or DNS of
|
||||
your own is needed. Registration returns a one-time credential block:
|
||||
<code>id=... token=... registry=... ma_key=...</code>.</p>
|
||||
</div>
|
||||
|
||||
<h2>2. Download</h2>
|
||||
<div class="card">
|
||||
<p>Static Linux x86_64 binaries (musl — no runtime dependencies):</p>
|
||||
<pre class="cmd">curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.0/frxd-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.0/frxd-linux-amd64.sha256
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.0/frx-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.0/frx-linux-amd64.sha256</pre>
|
||||
<pre class="cmd">curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frxd-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frxd-linux-amd64.sha256
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frx-linux-amd64
|
||||
curl -LO https://git.federatedsearch.org/frx/frxd/releases/download/v0.1.1/frx-linux-amd64.sha256</pre>
|
||||
<p class="muted">All releases: <a href="https://git.federatedsearch.org/frx/frxd/releases">git.federatedsearch.org/frx/frxd/releases</a>.
|
||||
Source and spec (<code>rfc.txt</code>): <a href="https://git.federatedsearch.org/frx/frxd">git.federatedsearch.org/frx/frxd</a>.</p>
|
||||
</div>
|
||||
@@ -787,13 +899,35 @@ frxd serve</pre>
|
||||
|
||||
<script>
|
||||
const out = document.getElementById("out");
|
||||
document.getElementById("f").onsubmit = async (e) => {
|
||||
const f = document.getElementById("f");
|
||||
let zone = "frx.federatedsearch.org";
|
||||
function sanitizeLabel(v) {
|
||||
let label = "", lastDash = true;
|
||||
for (const c of v.toLowerCase()) {
|
||||
if (/[a-z0-9]/.test(c)) { label += c; lastDash = false; }
|
||||
else if (!lastDash && (/\s/.test(c) || c === "-" || c === "_" || c === ".")) { label += "-"; lastDash = true; }
|
||||
if (label.length >= 32) break;
|
||||
}
|
||||
return label.replace(/^-+|-+$/g, "").slice(0, 32);
|
||||
}
|
||||
const preview = document.getElementById("preview");
|
||||
const updatePreview = () => {
|
||||
const label = sanitizeLabel(f.label.value);
|
||||
preview.textContent = label ? label + "." + zone : "(type a short name)";
|
||||
};
|
||||
f.label.addEventListener("input", updatePreview);
|
||||
f.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const label = e.target.label.value, code = e.target.code.value;
|
||||
const res = await fetch("/v1/signup", {
|
||||
method: "POST",
|
||||
headers: {"content-type": "application/json"},
|
||||
body: JSON.stringify({label, code})
|
||||
body: JSON.stringify({
|
||||
label: f.label.value, code: f.code.value,
|
||||
org: f.org.value, representative: f.representative.value, email: f.email.value,
|
||||
address: f.address.value, domain: f.domain.value, class: f.member_class.value,
|
||||
payment: f.payment.value, privacy_link: f.privacy_link.value,
|
||||
attestation: f.attestation.checked, privacy_ack: f.privacy_ack.checked
|
||||
})
|
||||
});
|
||||
const body = await res.json();
|
||||
out.style.display = "block";
|
||||
@@ -806,6 +940,8 @@ document.getElementById("f").onsubmit = async (e) => {
|
||||
try {
|
||||
const res = await fetch("/registry.json");
|
||||
const doc = await res.json();
|
||||
zone = doc.zone || zone;
|
||||
updatePreview();
|
||||
const ids = doc.members.map((m) => m.id);
|
||||
document.getElementById("members").textContent = doc.members.length === 0
|
||||
? "The registry is empty — be the first member."
|
||||
|
||||
Reference in New Issue
Block a user