This commit is contained in:
Generated
+8
@@ -3182,6 +3182,12 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -3355,6 +3361,7 @@ dependencies = [
|
|||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"cot",
|
"cot",
|
||||||
@@ -3368,6 +3375,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -16,5 +16,7 @@ multer = "3"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
tokio = { version = "1", features = ["fs"] }
|
tokio = { version = "1", features = ["fs"] }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
urlencoding = "2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|||||||
+235
-1
@@ -194,6 +194,7 @@ struct LoginTemplate<'a> {
|
|||||||
lang: Lang,
|
lang: Lang,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
turnstile_site_key: String,
|
turnstile_site_key: String,
|
||||||
|
oidc_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
#[derive(Debug, Template)]
|
||||||
@@ -348,11 +349,28 @@ async fn login_page(request: Request, session: Session, db: Database) -> cot::Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
let turnstile_site_key = crate::turnstile::get_site_key(&db).await?;
|
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 error = if has_query_flag(&request, "error") {
|
||||||
|
Some(lang.t().login_sso_error.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let body = LoginTemplate {
|
let body = LoginTemplate {
|
||||||
t: lang.t(),
|
t: lang.t(),
|
||||||
lang,
|
lang,
|
||||||
error: None,
|
error,
|
||||||
turnstile_site_key,
|
turnstile_site_key,
|
||||||
|
oidc_enabled,
|
||||||
}
|
}
|
||||||
.render()?;
|
.render()?;
|
||||||
html_response(body, lang)
|
html_response(body, lang)
|
||||||
@@ -442,6 +460,7 @@ async fn login_submit(request: Request, session: Session, db: Database) -> cot::
|
|||||||
lang,
|
lang,
|
||||||
error: Some(lang.t().login_error.to_string()),
|
error: Some(lang.t().login_error.to_string()),
|
||||||
turnstile_site_key,
|
turnstile_site_key,
|
||||||
|
oidc_enabled: false,
|
||||||
}
|
}
|
||||||
.render()?;
|
.render()?;
|
||||||
return html_response(body, lang);
|
return html_response(body, lang);
|
||||||
@@ -471,6 +490,7 @@ async fn login_submit(request: Request, session: Session, db: Database) -> cot::
|
|||||||
lang,
|
lang,
|
||||||
error: Some(lang.t().login_error.to_string()),
|
error: Some(lang.t().login_error.to_string()),
|
||||||
turnstile_site_key,
|
turnstile_site_key,
|
||||||
|
oidc_enabled: false,
|
||||||
}
|
}
|
||||||
.render()?;
|
.render()?;
|
||||||
html_response(body, lang)
|
html_response(body, lang)
|
||||||
@@ -482,6 +502,212 @@ async fn logout(request: Request, session: Session) -> cot::Result<Response> {
|
|||||||
Redirect::new(format!("/admin/login?lang={}", lang.code())).into_response()
|
Redirect::new(format!("/admin/login?lang={}", lang.code())).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OIDC Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SESSION_OIDC_STATE: &str = "oidc_state";
|
||||||
|
|
||||||
|
/// Read an OIDC-related setting from the DB, returning empty string if absent.
|
||||||
|
async fn oidc_setting(db: &Database, name: &str) -> cot::Result<String> {
|
||||||
|
let k = name.to_string();
|
||||||
|
Ok(query!(Setting, $key == k)
|
||||||
|
.get(db)
|
||||||
|
.await?
|
||||||
|
.map(|s| s.value)
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the OpenID Connect discovery document and extract a field.
|
||||||
|
async fn oidc_discover(issuer_url: &str, field: &str) -> Option<String> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/.well-known/openid-configuration",
|
||||||
|
issuer_url.trim_end_matches('/')
|
||||||
|
);
|
||||||
|
let resp = reqwest::Client::new().get(&url).send().await.ok()?;
|
||||||
|
let json: serde_json::Value = resp.json().await.ok()?;
|
||||||
|
json.get(field)?.as_str().map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode the payload of a JWT (base64url, no signature verification).
|
||||||
|
fn decode_jwt_payload(token: &str) -> Option<serde_json::Value> {
|
||||||
|
use base64::Engine;
|
||||||
|
let parts: Vec<&str> = token.split('.').collect();
|
||||||
|
if parts.len() != 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let payload = parts[1];
|
||||||
|
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
|
.decode(payload)
|
||||||
|
.ok()?;
|
||||||
|
serde_json::from_slice(&bytes).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||||
|
let lang = detect_lang(&request);
|
||||||
|
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
||||||
|
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
||||||
|
let site_domain = oidc_setting(&db, "site_domain").await?;
|
||||||
|
|
||||||
|
if issuer_url.trim().is_empty() || client_id.trim().is_empty() {
|
||||||
|
return Redirect::new(format!("/admin/login?lang={}&error=sso", lang.code()))
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let authorization_endpoint = match oidc_discover(&issuer_url, "authorization_endpoint").await {
|
||||||
|
Some(ep) => ep,
|
||||||
|
None => {
|
||||||
|
return Redirect::new(format!("/admin/login?lang={}&error=sso", lang.code()))
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = rand_token();
|
||||||
|
session.insert(SESSION_OIDC_STATE, state.clone()).await?;
|
||||||
|
|
||||||
|
let redirect_uri = format!(
|
||||||
|
"{}/admin/oidc/callback",
|
||||||
|
site_domain.trim_end_matches('/')
|
||||||
|
);
|
||||||
|
|
||||||
|
let redirect_url = format!(
|
||||||
|
"{}?response_type=code&client_id={}&redirect_uri={}&scope=openid+profile&state={}",
|
||||||
|
authorization_endpoint,
|
||||||
|
urlencoding::encode(&client_id),
|
||||||
|
urlencoding::encode(&redirect_uri),
|
||||||
|
urlencoding::encode(&state),
|
||||||
|
);
|
||||||
|
|
||||||
|
Redirect::new(redirect_url).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_callback(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||||
|
let lang = detect_lang(&request);
|
||||||
|
let fail = format!("/admin/login?lang={}&error=sso", lang.code());
|
||||||
|
|
||||||
|
// Extract code and state from query string
|
||||||
|
let query_str = request.uri().query().unwrap_or("");
|
||||||
|
let mut code = String::new();
|
||||||
|
let mut state = String::new();
|
||||||
|
for pair in query_str.split('&') {
|
||||||
|
if let Some(v) = pair.strip_prefix("code=") {
|
||||||
|
code = v.to_string();
|
||||||
|
} else if let Some(v) = pair.strip_prefix("state=") {
|
||||||
|
state = v.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify state
|
||||||
|
let saved_state = session
|
||||||
|
.get::<String>(SESSION_OIDC_STATE)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if code.is_empty() || state.is_empty() || state != saved_state {
|
||||||
|
return Redirect::new(fail).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear used state
|
||||||
|
let _ = session.remove::<String>(SESSION_OIDC_STATE).await;
|
||||||
|
|
||||||
|
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
||||||
|
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
||||||
|
let client_secret = oidc_setting(&db, "oidc_client_secret").await?;
|
||||||
|
let site_domain = oidc_setting(&db, "site_domain").await?;
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let redirect_uri = format!(
|
||||||
|
"{}/admin/oidc/callback",
|
||||||
|
site_domain.trim_end_matches('/')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exchange code for tokens
|
||||||
|
let token_resp = reqwest::Client::new()
|
||||||
|
.post(&token_endpoint)
|
||||||
|
.form(&[
|
||||||
|
("grant_type", "authorization_code"),
|
||||||
|
("code", &code),
|
||||||
|
("redirect_uri", &redirect_uri),
|
||||||
|
("client_id", &client_id),
|
||||||
|
("client_secret", &client_secret),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
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(_) => 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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_name = claims
|
||||||
|
.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
let login = preferred_username.clone();
|
||||||
|
let user = query!(User, $login == login).get(&db).await?;
|
||||||
|
|
||||||
|
let user = match user {
|
||||||
|
Some(u) => u,
|
||||||
|
None => {
|
||||||
|
let mut new_user = User {
|
||||||
|
id: Auto::auto(),
|
||||||
|
login: preferred_username.clone(),
|
||||||
|
password_hash: String::new(),
|
||||||
|
display_name: display_name.clone(),
|
||||||
|
telegram_chat_id: None,
|
||||||
|
telegram_notifications: Some(false),
|
||||||
|
status: "active".to_string(),
|
||||||
|
created_at: now_utc(),
|
||||||
|
updated_at: now_utc(),
|
||||||
|
};
|
||||||
|
new_user.save(&db).await?;
|
||||||
|
new_user
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.status != "active" {
|
||||||
|
return Redirect::new(fail).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let display = user
|
||||||
|
.display_name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(&user.login)
|
||||||
|
.to_string();
|
||||||
|
session.insert(SESSION_USER_ID, user.id.unwrap()).await?;
|
||||||
|
session.insert(SESSION_USER_NAME, display).await?;
|
||||||
|
|
||||||
|
Redirect::new(format!("/admin/?lang={}", lang.code())).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GET Handlers (protected)
|
// GET Handlers (protected)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -819,6 +1045,9 @@ struct SettingsForm {
|
|||||||
seo_keywords: String,
|
seo_keywords: String,
|
||||||
turnstile_site_key: String,
|
turnstile_site_key: String,
|
||||||
turnstile_secret_key: String,
|
turnstile_secret_key: String,
|
||||||
|
oidc_issuer_url: String,
|
||||||
|
oidc_client_id: String,
|
||||||
|
oidc_client_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||||
@@ -837,6 +1066,9 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot:
|
|||||||
("seo_keywords", form.seo_keywords),
|
("seo_keywords", form.seo_keywords),
|
||||||
("turnstile_site_key", form.turnstile_site_key),
|
("turnstile_site_key", form.turnstile_site_key),
|
||||||
("turnstile_secret_key", form.turnstile_secret_key),
|
("turnstile_secret_key", form.turnstile_secret_key),
|
||||||
|
("oidc_issuer_url", form.oidc_issuer_url),
|
||||||
|
("oidc_client_id", form.oidc_client_id),
|
||||||
|
("oidc_client_secret", form.oidc_client_secret),
|
||||||
] {
|
] {
|
||||||
let k = key.to_string();
|
let k = key.to_string();
|
||||||
let existing = query!(Setting, $key == k).get(&db).await?;
|
let existing = query!(Setting, $key == k).get(&db).await?;
|
||||||
@@ -2055,6 +2287,8 @@ pub fn admin_router() -> Router {
|
|||||||
Route::with_handler_and_name("/logout", logout, "admin-logout"),
|
Route::with_handler_and_name("/logout", logout, "admin-logout"),
|
||||||
Route::with_handler_and_name("/setup", setup_page, "admin-setup"),
|
Route::with_handler_and_name("/setup", setup_page, "admin-setup"),
|
||||||
Route::with_handler_and_name("/setup/submit", setup_submit, "admin-setup-submit"),
|
Route::with_handler_and_name("/setup/submit", setup_submit, "admin-setup-submit"),
|
||||||
|
Route::with_handler_and_name("/oidc/start", oidc_start, "admin-oidc-start"),
|
||||||
|
Route::with_handler_and_name("/oidc/callback", oidc_callback, "admin-oidc-callback"),
|
||||||
// Protected
|
// Protected
|
||||||
Route::with_handler_and_name("", admin_index, "admin-index-bare"),
|
Route::with_handler_and_name("", admin_index, "admin-index-bare"),
|
||||||
Route::with_handler_and_name("/", admin_index, "admin-index"),
|
Route::with_handler_and_name("/", admin_index, "admin-index"),
|
||||||
|
|||||||
+15
@@ -137,6 +137,9 @@ pub struct Translations {
|
|||||||
pub settings_seo_keywords: &'static str,
|
pub settings_seo_keywords: &'static str,
|
||||||
pub settings_turnstile_site_key: &'static str,
|
pub settings_turnstile_site_key: &'static str,
|
||||||
pub settings_turnstile_secret_key: &'static str,
|
pub settings_turnstile_secret_key: &'static str,
|
||||||
|
pub settings_oidc_issuer_url: &'static str,
|
||||||
|
pub settings_oidc_client_id: &'static str,
|
||||||
|
pub settings_oidc_client_secret: &'static str,
|
||||||
pub landing_contact_label: &'static str,
|
pub landing_contact_label: &'static str,
|
||||||
pub landing_pricing_title: &'static str,
|
pub landing_pricing_title: &'static str,
|
||||||
|
|
||||||
@@ -151,6 +154,8 @@ pub struct Translations {
|
|||||||
pub login_title: &'static str,
|
pub login_title: &'static str,
|
||||||
pub login_button: &'static str,
|
pub login_button: &'static str,
|
||||||
pub login_error: &'static str,
|
pub login_error: &'static str,
|
||||||
|
pub login_sso_button: &'static str,
|
||||||
|
pub login_sso_error: &'static str,
|
||||||
pub logout: &'static str,
|
pub logout: &'static str,
|
||||||
pub setup_title: &'static str,
|
pub setup_title: &'static str,
|
||||||
pub setup_description: &'static str,
|
pub setup_description: &'static str,
|
||||||
@@ -352,6 +357,9 @@ static RU: Translations = Translations {
|
|||||||
settings_seo_keywords: "SEO-ключевые слова (через запятую, отображаются на сайте и в мета-теге keywords)",
|
settings_seo_keywords: "SEO-ключевые слова (через запятую, отображаются на сайте и в мета-теге keywords)",
|
||||||
settings_turnstile_site_key: "Cloudflare Turnstile — Site Key (ключ виджета)",
|
settings_turnstile_site_key: "Cloudflare Turnstile — Site Key (ключ виджета)",
|
||||||
settings_turnstile_secret_key: "Cloudflare Turnstile — Secret Key (секретный ключ)",
|
settings_turnstile_secret_key: "Cloudflare Turnstile — Secret Key (секретный ключ)",
|
||||||
|
settings_oidc_issuer_url: "OIDC — URL провайдера (Issuer URL)",
|
||||||
|
settings_oidc_client_id: "OIDC — Client ID",
|
||||||
|
settings_oidc_client_secret: "OIDC — Client Secret",
|
||||||
landing_contact_label: "Или свяжитесь с нами напрямую",
|
landing_contact_label: "Или свяжитесь с нами напрямую",
|
||||||
landing_pricing_title: "Стоимость",
|
landing_pricing_title: "Стоимость",
|
||||||
|
|
||||||
@@ -386,6 +394,8 @@ static RU: Translations = Translations {
|
|||||||
login_title: "Вход в систему",
|
login_title: "Вход в систему",
|
||||||
login_button: "Войти",
|
login_button: "Войти",
|
||||||
login_error: "Неверный логин или пароль.",
|
login_error: "Неверный логин или пароль.",
|
||||||
|
login_sso_button: "Войти через SSO",
|
||||||
|
login_sso_error: "Ошибка SSO-авторизации.",
|
||||||
logout: "Выйти",
|
logout: "Выйти",
|
||||||
setup_title: "Создание администратора",
|
setup_title: "Создание администратора",
|
||||||
setup_description: "В системе нет ни одного администратора. Создайте первого для начала работы.",
|
setup_description: "В системе нет ни одного администратора. Создайте первого для начала работы.",
|
||||||
@@ -557,6 +567,9 @@ static EN: Translations = Translations {
|
|||||||
settings_seo_keywords: "SEO keywords (comma-separated, shown on site and in keywords meta tag)",
|
settings_seo_keywords: "SEO keywords (comma-separated, shown on site and in keywords meta tag)",
|
||||||
settings_turnstile_site_key: "Cloudflare Turnstile — Site Key",
|
settings_turnstile_site_key: "Cloudflare Turnstile — Site Key",
|
||||||
settings_turnstile_secret_key: "Cloudflare Turnstile — Secret Key",
|
settings_turnstile_secret_key: "Cloudflare Turnstile — Secret Key",
|
||||||
|
settings_oidc_issuer_url: "OIDC — Issuer URL",
|
||||||
|
settings_oidc_client_id: "OIDC — Client ID",
|
||||||
|
settings_oidc_client_secret: "OIDC — Client Secret",
|
||||||
landing_contact_label: "Or contact us directly",
|
landing_contact_label: "Or contact us directly",
|
||||||
landing_pricing_title: "Pricing",
|
landing_pricing_title: "Pricing",
|
||||||
|
|
||||||
@@ -591,6 +604,8 @@ static EN: Translations = Translations {
|
|||||||
login_title: "Sign In",
|
login_title: "Sign In",
|
||||||
login_button: "Sign In",
|
login_button: "Sign In",
|
||||||
login_error: "Invalid login or password.",
|
login_error: "Invalid login or password.",
|
||||||
|
login_sso_button: "Sign in with SSO",
|
||||||
|
login_sso_error: "SSO authentication failed.",
|
||||||
logout: "Sign Out",
|
logout: "Sign Out",
|
||||||
setup_title: "Create Administrator",
|
setup_title: "Create Administrator",
|
||||||
setup_description: "There are no administrators yet. Create the first one to get started.",
|
setup_description: "There are no administrators yet. Create the first one to get started.",
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
{% if let Some(err) = error.as_ref() %}
|
{% if let Some(err) = error.as_ref() %}
|
||||||
<div class="notification is-danger is-light">{{ err }}</div>
|
<div class="notification is-danger is-light">{{ err }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if oidc_enabled %}
|
||||||
|
<a href="/admin/oidc/start" class="button is-primary is-fullwidth mt-3">{{ t.login_sso_button }}</a>
|
||||||
|
{% else %}
|
||||||
<form method="post" action="/admin/login/submit">
|
<form method="post" action="/admin/login/submit">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ t.users_login }}</label>
|
<label class="label">{{ t.users_login }}</label>
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="submit" class="button is-primary is-fullwidth mt-3">{{ t.login_button }}</button>
|
<button type="submit" class="button is-primary is-fullwidth mt-3">{{ t.login_button }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -68,6 +68,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ t.settings_oidc_issuer_url }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="oidc_issuer_url" placeholder="https://keycloak.example.com/realms/myrealm" value="{% for s in &settings %}{% if s.key == "oidc_issuer_url" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ t.settings_oidc_client_id }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="oidc_client_id" value="{% for s in &settings %}{% if s.key == "oidc_client_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ t.settings_oidc_client_secret }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="password" name="oidc_client_secret" value="{% for s in &settings %}{% if s.key == "oidc_client_secret" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user