///|
/// A session and an identity, carried by cookies:
///
/// moon run src/examples/19-session --target native
///
/// A login writes the session, the response signs it into a cookie, and the next
/// request reads it back — no store on the server, only the secret. The identity
/// is resolved from that session by a backend, once per connection, so every
/// handler below reads the same `user` without asking again.
#coverage.skip
async fn main {
tour() catch {
e => println("failed: \{e}")
}
}
///|
/// The seconds since the epoch this example pretends it is, so the signed cookie
/// and its `Max-Age` come out the same on every run.
let fixed_now : Int64 = 1_700_000_000L
///|
/// Answers `POST /login` by putting a name in the session, `POST /logout` by
/// emptying it, and `GET /me` with whatever identity the auth layer resolved.
/// A login answers with a `303`, the way a form post does, so the browser's next
/// request is a `GET`.
#coverage.skip
let app : @moonasgi.AsgiApp = async fn(scope, receive, send) {
let (http_method, path) = match scope {
Http(h) => (h.http_method, h.path)
_ => ("", "")
}
// Drain the request body: a server hands it over only when asked.
let mut more = true
while more {
match receive() {
HttpRequest(more_body~, ..) => more = more_body
_ => more = false
}
}
match (http_method, path) {
("POST", "/login") => {
@mid.session(scope)["user"] = Json::string("ada")
send(
HttpResponseStart(
status=303,
headers=[("location", "/me")],
trailers=false,
),
)
send(HttpResponseBody(body=b"", more_body=false))
}
("POST", "/logout") => {
// An emptied session tells the client to drop its cookie.
@mid.session(scope).clear()
send(
HttpResponseStart(
status=303,
headers=[("location", "/me")],
trailers=false,
),
)
send(HttpResponseBody(body=b"", more_body=false))
}
("GET", "/me") => {
let who = match @mid.user(scope) {
Some(Object(fields)) =>
match fields.get("name") {
Some(String(name)) => name
_ => "nobody"
}
_ => "nobody"
}
send(HttpResponseStart(status=200, headers=[], trailers=false))
send(
HttpResponseBody(body=@utf8.encode("you are " + who), more_body=false),
)
}
_ => {
send(HttpResponseStart(status=404, headers=[], trailers=false))
send(HttpResponseBody(body=b"not found", more_body=false))
}
}
}
///|
/// The rule that turns a session into an identity: whoever the session names is
/// the user of the connection.
#coverage.skip
let from_session : &@mid.Backend = @mid.BackendFn(fn(scope) {
match @mid.session(scope).get("user") {
Some(name) => @mid.Identity::of(Json::object(Map([("name", name)])))
None => @mid.Identity::anonymous()
}
})
///|
#coverage.skip
async fn tour() -> Unit {
let sessions = @mid.Sessions::new(secret=b"a secret only the server knows", clock=fn() {
fixed_now
})
// The session layer is the outer one: the backend inside it reads what it wrote.
let service = sessions.wrap(@mid.Auth::new(from_session).wrap(app))
let client = @testing.Client::new(service, follow_redirects=true, clock=fn() {
fixed_now
})
println("before logging in: " + client.get("/me").text())
// The login's own answer is the redirect, and the redirect is followed to /me.
println("after logging in: " + client.post("/login").text())
println(
"the jar holds: " + client.cookies().map(fn(p) { p.0 }).join(", "),
)
// What the signature buys: a cookie a client edited is not a session.
let cookie = client.cookie("session").unwrap_or("")
let edited = "x" + cookie[1:].to_owned()
let stranger = @testing.Client::new(service, clock=fn() { fixed_now })
println(
"with an edited cookie: " +
stranger.get("/me", headers=[("cookie", "session=" + edited)]).text(),
)
println("after logging out: " + client.post("/logout").text())
println(
"the jar holds: " + client.cookies().map(fn(p) { p.0 }).join(", "),
)
}