6 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5 3b2cdc07b7 Release 0.3.21
Test / test (push) Failing after 29s
Publish release / publish (push) Successful in 1m42s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:10:49 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 2de936d995 Hero paragraph centers its measure box
max-width: 46ch without auto margins left-anchored the box inside the
centered column; the text centered in the wrong frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:10:44 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 9fca79b8ee Release 0.3.20
Test / test (push) Failing after 25s
Publish release / publish (push) Successful in 1m36s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:54:40 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 3d51aa1e6a Sign-in becomes optional: KANIDM_URL unset disables auth cleanly
A content-only instance (westra preview) has no review desk and no
Kanidm client; booting no longer demands one. Auth routes answer 503
'sign-in is not configured on this instance'; everything public
renders as usual.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:54:35 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 d5eb362c87 Release 0.3.19
Test / test (push) Failing after 25s
Publish release / publish (push) Successful in 1m40s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:47:01 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 7f10ba20d3 Markdown images in descriptions, gated and framed
Images already flowed through render_inline_markdown ungated; they
now take the same scheme gate as links (https or same-origin only -
no data:, no plain http) and render as full-width framed figures in
alternative, feature, and item-card descriptions. Carries whole-site
imagery for content-driven instances (westra).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:46:56 +02:00
5 changed files with 65 additions and 6 deletions
Generated
+1 -1
View File
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]]
name = "portal"
version = "0.3.18"
version = "0.3.21"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "portal"
version = "0.3.18"
version = "0.3.21"
edition = "2021"
[lib]
+21
View File
@@ -199,6 +199,18 @@ pub fn render_inline_markdown(text: &str) -> String {
in_link -= 1;
None
}
// Images take the same gate as links: https or same-origin
// only - no data:, no plain http.
Event::Start(Tag::Image { dest_url, .. })
if !(dest_url.starts_with("https://") || dest_url.starts_with('/')) =>
{
in_link += 1;
None
}
Event::End(TagEnd::Image) if in_link > 0 => {
in_link -= 1;
None
}
other => Some(other),
});
let mut out = String::new();
@@ -2026,6 +2038,15 @@ alternatives:
fn markdown_drops_html_and_unsafe_links() {
assert_eq!(render_inline_markdown("x <script>y</script> z"), "x y z");
assert_eq!(render_inline_markdown("[bad](javascript:alert(1))"), "bad");
assert_eq!(
render_inline_markdown("![site](https://x.no/a.jpg)"),
"<img src=\"https://x.no/a.jpg\" alt=\"site\" />"
);
assert_eq!(render_inline_markdown("![x](data:image/png;base64,AA)"), "");
assert_eq!(
render_inline_markdown("![local](/images/a.jpg)"),
"<img src=\"/images/a.jpg\" alt=\"local\" />"
);
assert_eq!(render_inline_markdown("[ok](/shape)"), "<a href=\"/shape\">ok</a>");
assert_eq!(render_inline_markdown("[mail](mailto:bl@uhhm.no)"), "<a href=\"mailto:bl@uhhm.no\">mail</a>");
}
+26 -4
View File
@@ -33,6 +33,13 @@ type OidcClient = CoreClient<
>;
pub struct Oidc {
/// `None` when `KANIDM_URL` is unset: a content-only instance with
/// sign-in disabled - auth routes answer 503, everything public
/// renders as usual.
inner: Option<OidcInner>,
}
struct OidcInner {
client: OidcClient,
http: openidconnect::reqwest::Client,
}
@@ -45,7 +52,13 @@ const REDIRECT_KEY: &str = "oidc_post_login_redirect";
impl Oidc {
/// Discovers the provider and builds the client from environment:
/// `KANIDM_URL`, `OAUTH2_CLIENT_ID`, `OAUTH2_CLIENT_SECRET`, `PUBLIC_URL`.
/// With `KANIDM_URL` unset, sign-in is disabled instead of fatal -
/// the shape of a public content instance without a review desk.
pub async fn from_env() -> anyhow::Result<Self> {
if std::env::var("KANIDM_URL").is_err() {
tracing::warn!("KANIDM_URL not set - sign-in disabled on this instance");
return Ok(Self { inner: None });
}
let kanidm_url = require_env("KANIDM_URL")?;
let client_id = require_env("OAUTH2_CLIENT_ID")?;
let client_secret = require_env("OAUTH2_CLIENT_SECRET")?;
@@ -75,7 +88,16 @@ impl Oidc {
)
.set_redirect_uri(redirect);
Ok(Self { client, http })
Ok(Self {
inner: Some(OidcInner { client, http }),
})
}
fn configured(&self) -> Result<&OidcInner, HandlerError> {
self.inner.as_ref().ok_or((
StatusCode::SERVICE_UNAVAILABLE,
"sign-in is not configured on this instance".to_string(),
))
}
}
@@ -118,10 +140,10 @@ pub async fn login(
session.insert(REDIRECT_KEY, redirect).await.map_err(internal)?;
}
let oidc = state.oidc.configured()?;
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_state, nonce) = state
.oidc
let (auth_url, csrf_state, nonce) = oidc
.client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
@@ -182,7 +204,7 @@ pub async fn callback(
));
}
let oidc = &state.oidc;
let oidc = state.oidc.configured()?;
let token_response = oidc
.client
.exchange_code(AuthorizationCode::new(params.code))
+16
View File
@@ -193,6 +193,10 @@ main.not-found {
color: var(--ink-dim);
font-size: 1.05rem;
max-width: 46ch;
/* The measure cap shrinks the box below the copy column; without
auto margins the box left-anchors and its centered text centers
in the wrong frame. */
margin-inline: auto;
}
/* A content-shipped hero module (site.yaml hero.kind: module) draws
@@ -536,6 +540,18 @@ main.not-found {
color: var(--accent);
}
/* images inside markdown descriptions: full-width figures in the
card's flow, framed like the rest of the press sheet */
.alt-description img,
.item-card-description img,
.feature p img {
display: block;
width: 100%;
margin: 0.6rem 0 0.2rem;
border-radius: 0.5rem;
border: 0.06rem solid var(--line);
}
.alt-description code,
.feature p code {
font-size: 0.9em;