diff --git a/Cargo.lock b/Cargo.lock index b179519..5a3f903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3359,7 +3359,7 @@ dependencies = [ [[package]] name = "web-petting" -version = "0.1.11" +version = "0.1.12" dependencies = [ "base64", "chrono", diff --git a/src/admin.rs b/src/admin.rs index fc39b73..184bcd4 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -194,7 +194,8 @@ struct LoginTemplate<'a> { lang: Lang, error: Option, turnstile_site_key: String, - oidc_enabled: bool, + auth_password_enabled: bool, + auth_sso_enabled: bool, } #[derive(Debug, Template)] @@ -256,6 +257,8 @@ struct SettingsTemplate<'a> { admin_name: &'a str, settings: Vec, saved: bool, + auth_password_checked: bool, + auth_sso_checked: bool, } #[derive(Debug, Template)] @@ -350,13 +353,32 @@ async fn login_page(request: Request, session: Session, db: Database) -> cot::Re let turnstile_site_key = crate::turnstile::get_site_key(&db).await?; - let oidc_enabled = { - let k = "oidc_issuer_url".to_string(); - query!(Setting, $key == k) - .get(&db) - .await? - .map(|s| !s.value.trim().is_empty()) - .unwrap_or(false) + let settings = Setting::objects().all(&db).await?; + let get_val = |key: &str| -> String { + settings + .iter() + .find(|s| s.key == key) + .map(|s| s.value.clone()) + .unwrap_or_default() + }; + + let password_setting = get_val("auth_password_enabled"); + let sso_setting = get_val("auth_sso_enabled"); + let oidc_configured = !get_val("oidc_issuer_url").trim().is_empty(); + + // Default: password enabled if setting was never saved + let auth_password_enabled = if password_setting.is_empty() { + true + } else { + password_setting == "true" + }; + let auth_sso_enabled = sso_setting == "true" && oidc_configured; + + // Fallback: if neither is enabled, show password form + let (auth_password_enabled, auth_sso_enabled) = if !auth_password_enabled && !auth_sso_enabled { + (true, false) + } else { + (auth_password_enabled, auth_sso_enabled) }; let error = if has_query_flag(&request, "error") { @@ -370,7 +392,8 @@ async fn login_page(request: Request, session: Session, db: Database) -> cot::Re lang, error, turnstile_site_key, - oidc_enabled, + auth_password_enabled, + auth_sso_enabled, } .render()?; html_response(body, lang) @@ -460,7 +483,8 @@ async fn login_submit(request: Request, session: Session, db: Database) -> cot:: lang, error: Some(lang.t().login_error.to_string()), turnstile_site_key, - oidc_enabled: false, + auth_password_enabled: true, + auth_sso_enabled: false, } .render()?; return html_response(body, lang); @@ -490,7 +514,8 @@ async fn login_submit(request: Request, session: Session, db: Database) -> cot:: lang, error: Some(lang.t().login_error.to_string()), turnstile_site_key, - oidc_enabled: false, + auth_password_enabled: true, + auth_sso_enabled: false, } .render()?; html_response(body, lang) @@ -606,6 +631,11 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot: .unwrap_or_default(); if code.is_empty() || state.is_empty() || state != saved_state { + tracing::warn!( + "OIDC state mismatch: state={state:?}, saved={saved_state:?}, code_empty={}, state_empty={}", + code.is_empty(), + state.is_empty(), + ); return Redirect::new(fail).into_response(); } @@ -620,7 +650,10 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot: // Get token endpoint from discovery let token_endpoint = match oidc_discover(&issuer_url, "token_endpoint").await { Some(ep) => ep, - None => return Redirect::new(fail).into_response(), + None => { + tracing::warn!("OIDC discovery failed for issuer_url={issuer_url:?}"); + return Redirect::new(fail).into_response(); + } }; let redirect_uri = format!( @@ -644,25 +677,40 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot: let token_json: serde_json::Value = match token_resp { Ok(resp) => match resp.json().await { Ok(v) => v, - Err(_) => return Redirect::new(fail).into_response(), + Err(e) => { + tracing::warn!("OIDC token response parse error: {e}"); + return Redirect::new(fail).into_response(); + } }, - Err(_) => return Redirect::new(fail).into_response(), + Err(e) => { + tracing::warn!("OIDC token request failed: {e}"); + return Redirect::new(fail).into_response(); + } }; let id_token = match token_json.get("id_token").and_then(|v| v.as_str()) { Some(t) => t, - None => return Redirect::new(fail).into_response(), + None => { + tracing::warn!("OIDC no id_token in response: {token_json}"); + return Redirect::new(fail).into_response(); + } }; // Decode JWT payload (no signature verification — token obtained directly from provider over TLS) let claims = match decode_jwt_payload(id_token) { Some(c) => c, - None => return Redirect::new(fail).into_response(), + None => { + tracing::warn!("OIDC JWT decode failed"); + return Redirect::new(fail).into_response(); + } }; let preferred_username = match claims.get("preferred_username").and_then(|v| v.as_str()) { Some(u) => u.to_string(), - None => return Redirect::new(fail).into_response(), + None => { + tracing::warn!("OIDC no preferred_username in claims: {claims}"); + return Redirect::new(fail).into_response(); + } }; let display_name = claims @@ -964,12 +1012,24 @@ async fn settings_page(request: Request, session: Session, db: Database) -> cot: Err(resp) => return Ok(resp), }; let settings = Setting::objects().all(&db).await?; + let auth_password_checked = settings + .iter() + .find(|s| s.key == "auth_password_enabled") + .map(|s| s.value == "true") + .unwrap_or(true); + let auth_sso_checked = settings + .iter() + .find(|s| s.key == "auth_sso_enabled") + .map(|s| s.value == "true") + .unwrap_or(false); let body = SettingsTemplate { t: lang.t(), lang, admin_name: &admin_name, settings, saved: false, + auth_password_checked, + auth_sso_checked, } .render()?; html_response(body, lang) @@ -1048,6 +1108,10 @@ struct SettingsForm { oidc_issuer_url: String, oidc_client_id: String, oidc_client_secret: String, + #[serde(default)] + auth_password_enabled: Option, + #[serde(default)] + auth_sso_enabled: Option, } async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result { @@ -1069,6 +1133,22 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: ("oidc_issuer_url", form.oidc_issuer_url), ("oidc_client_id", form.oidc_client_id), ("oidc_client_secret", form.oidc_client_secret), + ( + "auth_password_enabled", + if form.auth_password_enabled.is_some() { + "true".to_string() + } else { + "false".to_string() + }, + ), + ( + "auth_sso_enabled", + if form.auth_sso_enabled.is_some() { + "true".to_string() + } else { + "false".to_string() + }, + ), ] { let k = key.to_string(); let existing = query!(Setting, $key == k).get(&db).await?; @@ -1091,12 +1171,24 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: } let settings = Setting::objects().all(&db).await?; + let auth_password_checked = settings + .iter() + .find(|s| s.key == "auth_password_enabled") + .map(|s| s.value == "true") + .unwrap_or(true); + let auth_sso_checked = settings + .iter() + .find(|s| s.key == "auth_sso_enabled") + .map(|s| s.value == "true") + .unwrap_or(false); let rendered = SettingsTemplate { t: lang.t(), lang, admin_name: &admin_name, settings, saved: true, + auth_password_checked, + auth_sso_checked, } .render()?; html_response(rendered, lang) diff --git a/src/i18n.rs b/src/i18n.rs index f0be409..34172c7 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -140,6 +140,8 @@ pub struct Translations { pub settings_oidc_issuer_url: &'static str, pub settings_oidc_client_id: &'static str, pub settings_oidc_client_secret: &'static str, + pub settings_auth_password_enabled: &'static str, + pub settings_auth_sso_enabled: &'static str, pub landing_contact_label: &'static str, pub landing_pricing_title: &'static str, @@ -360,6 +362,8 @@ static RU: Translations = Translations { settings_oidc_issuer_url: "OIDC — URL провайдера (Issuer URL)", settings_oidc_client_id: "OIDC — Client ID", settings_oidc_client_secret: "OIDC — Client Secret", + settings_auth_password_enabled: "Вход по логину и паролю", + settings_auth_sso_enabled: "Вход через SSO (OIDC)", landing_contact_label: "Или свяжитесь с нами напрямую", landing_pricing_title: "Стоимость", @@ -570,6 +574,8 @@ static EN: Translations = Translations { settings_oidc_issuer_url: "OIDC — Issuer URL", settings_oidc_client_id: "OIDC — Client ID", settings_oidc_client_secret: "OIDC — Client Secret", + settings_auth_password_enabled: "Password login", + settings_auth_sso_enabled: "SSO login (OIDC)", landing_contact_label: "Or contact us directly", landing_pricing_title: "Pricing", diff --git a/templates/admin/login.html b/templates/admin/login.html index 64a8d22..47352c8 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -35,9 +35,11 @@ {% if let Some(err) = error.as_ref() %}
{{ err }}
{% endif %} - {% if oidc_enabled %} + {% if auth_sso_enabled %} {{ t.login_sso_button }} - {% else %} + {% endif %} + {% if auth_password_enabled %} + {% if auth_sso_enabled %}
{% endif %}
diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 658e778..f1c9d8e 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -87,6 +87,19 @@
+
+ +
+
+ +
+