Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528e2badaf | ||
|
|
f4e9f6d0ba | ||
|
|
3b2cdc07b7 | ||
|
|
2de936d995 | ||
|
|
9fca79b8ee | ||
|
|
3d51aa1e6a | ||
|
|
d5eb362c87 | ||
|
|
7f10ba20d3 |
Generated
+1
-1
@@ -2948,7 +2948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portal"
|
||||
version = "0.3.18"
|
||||
version = "0.3.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "portal"
|
||||
version = "0.3.18"
|
||||
version = "0.3.22"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
+16
-2
@@ -555,7 +555,21 @@ fn Hero(title: String, description: String, landing: bool, site: SiteConfig, cur
|
||||
None
|
||||
};
|
||||
let has_module = module.is_some();
|
||||
// Light theme inverts white-stroke wordmarks to ink. The house
|
||||
// mark and the sibling-site marks are all white-stroke SVGs; a
|
||||
// raster logo is a client brand and keeps its colors.
|
||||
let house_wordmark = site
|
||||
.wordmark
|
||||
.as_deref()
|
||||
.map(|w| w.ends_with(".svg"))
|
||||
.unwrap_or(true);
|
||||
let wordmark = site.wordmark.clone().unwrap_or_else(|| "/wordmark.svg".to_string());
|
||||
// Clamped server-side too, but belt and braces for the inline style.
|
||||
let wordmark_style = site
|
||||
.wordmark_height
|
||||
.filter(|h| (0.5..=6.0).contains(h))
|
||||
.map(|h| format!("height: {h}rem"))
|
||||
.unwrap_or_default();
|
||||
let site_title = site.title.clone().unwrap_or_else(|| SITE_NAME.to_string());
|
||||
|
||||
let piece_ref: NodeRef<leptos::html::Div> = NodeRef::new();
|
||||
@@ -641,8 +655,8 @@ fn Hero(title: String, description: String, landing: bool, site: SiteConfig, cur
|
||||
}
|
||||
})}
|
||||
<div class="hero-copy">
|
||||
<a class="wordmark" href="/">
|
||||
<img src=wordmark alt=site_title/>
|
||||
<a class="wordmark" class:wordmark-house=house_wordmark href="/">
|
||||
<img src=wordmark alt=site_title style=wordmark_style/>
|
||||
</a>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
|
||||
@@ -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();
|
||||
@@ -501,6 +513,11 @@ pub struct SiteConfig {
|
||||
/// `None` falls back to `/wordmark.svg`.
|
||||
#[serde(default)]
|
||||
pub wordmark: Option<String>,
|
||||
/// Wordmark display height in rem (0.5–6.0). `None` keeps the
|
||||
/// stylesheet's default. Raster logos look best at or below their
|
||||
/// intrinsic pixel height.
|
||||
#[serde(default)]
|
||||
pub wordmark_height: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub hero: HeroConfig,
|
||||
}
|
||||
@@ -2026,6 +2043,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(""),
|
||||
"<img src=\"https://x.no/a.jpg\" alt=\"site\" />"
|
||||
);
|
||||
assert_eq!(render_inline_markdown(""), "");
|
||||
assert_eq!(
|
||||
render_inline_markdown(""),
|
||||
"<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
@@ -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))
|
||||
|
||||
+20
-4
@@ -99,10 +99,10 @@
|
||||
--hero-glow: rgba(246, 245, 241, 0.85);
|
||||
}
|
||||
|
||||
/* The wordmark SVG is a hardcoded white stroke (also used raw in
|
||||
dark contexts elsewhere) - flip it to ink here rather than fork
|
||||
the asset. */
|
||||
.wordmark img {
|
||||
/* The house wordmark SVG is a hardcoded white stroke (also used
|
||||
raw in dark contexts elsewhere) - flip it to ink here rather
|
||||
than fork the asset. A content-provided logo keeps its colors. */
|
||||
.wordmark-house img {
|
||||
filter: invert(0.92);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user