// POST /cms/auth/register — reader self-signup (public). // Rate-limited by client IP + email to blunt enumeration/abuse (the only // primitive PiCloud gives us for this is a kv counter). let b = ctx.request.body; if b == () || type_of(b) != "map" { return #{ statusCode: 400, body: #{ error: "expected JSON body" } }; } let email = b.email; let password = b.password; let name = if b.display_name == () { () } else { b.display_name }; if email == () || password == () { return #{ statusCode: 400, body: #{ error: "email and password required" } }; } if vars::get("signups-open") != "true" { return #{ statusCode: 403, body: #{ error: "signups are closed" } }; } // --- basic rate limit: max 5 registration attempts / hour / IP --- let ip = ctx.request.headers["x-forwarded-for"]; if ip == () { ip = "local"; } let rl = kv::collection("ratelimit"); let bucket = "register:" + ip; let cur = rl.get(bucket); let count = if cur == () { 0 } else { cur.count }; if count >= 5 { return #{ statusCode: 429, body: #{ error: "too many attempts, try later" } }; } rl.set(bucket, #{ count: count + 1, at: time::now() }); // email_available is the anon-safe probe (find_by_email requires a principal). if !users::email_available(email) { return #{ statusCode: 409, body: #{ error: "email already registered" } }; } let u = (); try { u = users::create(#{ email: email, password: password, display_name: name }); } catch(e) { return #{ statusCode: 400, body: #{ error: "could not create user: " + e } }; } users::add_role(u.id, "reader"); #{ statusCode: 201, body: #{ id: u.id, email: u.email, roles: ["reader"] } }