crates/gunnar-ui-web/src/guest.rs
//! **A visitor with no session reads public repositories, read-only** (GUN-GUEST,
//! owner 2026-10-30): files, commits, the README and the issues. Where a signed-in
//! person sees a control that writes (open an issue, comment, close, open a pull,
//! merge, the settings), a visitor sees "Sign in to …", which leads to the login
//! page and back here.
//!
//! Nothing here is a permission. The server decides every read and refuses every
//! write to whoever may not make it; this only chooses what is drawn, from the
//! session the server described (`/auth/status`) and gunnar's per-repository
//! answer (`crate::rights`).
/// What a page draws where a control that writes would be.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Affordance {
/// The control itself: this person may try it.
Control,
/// A visitor: "Sign in to {what}", to the login page and back to `here`, and
/// a sign-up link beside it where this console signs people up.
SignIn { words: String, login: String, signup: Option<String> },
/// Signed in, and gunnar gives this person no right here: nothing.
Nothing,
}
/// Pure: **the control, a "Sign in to …", or nothing**. `signed_in` is a live
/// session (or the open console); `may` is gunnar's answer for this person on
/// this repository; `here` is the page's own path and query; `signup` is where
/// this console signs people up (`/auth/status`'s `signup_url`), if it does.
pub fn affordance(signed_in: bool, may: bool, what: &str, here: &str, signup: Option<&str>) -> Affordance {
match (signed_in, may) {
(true, true) => Affordance::Control,
(true, false) => Affordance::Nothing,
(false, _) => Affordance::SignIn {
words: format!("Sign in to {what}"),
login: crate::auth::login_url(here),
signup: signup.filter(|s| s.starts_with('/') && !s.starts_with("//")).map(str::to_owned),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
/// ★ GUN-GUEST: a visitor is offered the sign-in where a control would be, a
/// person who may is given the control, a person who may not is given nothing.
#[test]
fn a_visitor_is_asked_to_sign_in_where_a_control_would_be() {
assert_eq!(affordance(true, true, "open an issue", "/n/r/issues", None), Affordance::Control);
assert_eq!(affordance(true, false, "open an issue", "/n/r/issues", Some("/signup")), Affordance::Nothing);
assert_eq!(
affordance(false, false, "open an issue", "/n/r/issues", None),
Affordance::SignIn { words: "Sign in to open an issue".into(), login: "/login?next=/n/r/issues".into(), signup: None }
);
// A visitor's rights are never asked (there is no session to ask for):
// whatever `may` says, a visitor is asked to sign in.
assert_eq!(
affordance(false, true, "comment", "/n/r/issues/7", Some("/signup")),
Affordance::SignIn { words: "Sign in to comment".into(), login: "/login?next=/n/r/issues/7".into(), signup: Some("/signup".into()) }
);
// The sign-up link is only ever a local page.
match affordance(false, false, "comment", "/n/r/issues/7", Some("https://evil.test/")) {
Affordance::SignIn { signup, .. } => assert_eq!(signup, None),
other => panic!("{other:?}"),
}
}
}