mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Merge remote-tracking branch 'origin/pr/487'
# Conflicts: # crates/aether-data/src/lifecycle/bootstrap/postgres.rs # crates/aether-data/src/lifecycle/migrate/tests.rs # crates/aether-data/src/repository/oauth_providers/postgres.rs # crates/aether-data/src/repository/oauth_providers/sqlite.rs # frontend/src/views/admin/OAuthSettings.vue
This commit is contained in:
2
.mise.toml
Normal file
2
.mise.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[tools]
|
||||||
|
rust = "latest"
|
||||||
@@ -28,6 +28,8 @@ pub(crate) struct AdminOAuthProviderUpsertRequest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(super) extra_config: Option<serde_json::Value>,
|
pub(super) extra_config: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub(super) icon_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub(super) is_enabled: bool,
|
pub(super) is_enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(super) force: bool,
|
pub(super) force: bool,
|
||||||
@@ -70,6 +72,7 @@ pub(super) fn build_admin_oauth_provider_payload(
|
|||||||
"frontend_callback_url": provider.frontend_callback_url,
|
"frontend_callback_url": provider.frontend_callback_url,
|
||||||
"attribute_mapping": provider.attribute_mapping,
|
"attribute_mapping": provider.attribute_mapping,
|
||||||
"extra_config": provider.extra_config,
|
"extra_config": provider.extra_config,
|
||||||
|
"icon_url": provider.icon_url,
|
||||||
"is_enabled": provider.is_enabled,
|
"is_enabled": provider.is_enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -364,6 +367,10 @@ pub(super) fn build_admin_oauth_upsert_record(
|
|||||||
frontend_callback_url: frontend_callback_url.to_string(),
|
frontend_callback_url: frontend_callback_url.to_string(),
|
||||||
attribute_mapping: payload.attribute_mapping,
|
attribute_mapping: payload.attribute_mapping,
|
||||||
extra_config: payload.extra_config,
|
extra_config: payload.extra_config,
|
||||||
|
icon_url: payload.icon_url.and_then(|value| {
|
||||||
|
let value = value.trim().to_string();
|
||||||
|
(!value.is_empty()).then_some(value)
|
||||||
|
}),
|
||||||
is_enabled: payload.is_enabled,
|
is_enabled: payload.is_enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use axum::{
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const ADMIN_OAUTH_TEST_TIMEOUT_SECS: u64 = 5;
|
const ADMIN_OAUTH_TEST_TIMEOUT_SECS: u64 = 10;
|
||||||
const LINUXDO_AUTHORIZATION_URL: &str = "https://connect.linux.do/oauth2/authorize";
|
const LINUXDO_AUTHORIZATION_URL: &str = "https://connect.linux.do/oauth2/authorize";
|
||||||
const LINUXDO_TOKEN_URL: &str = "https://connect.linux.do/oauth2/token";
|
const LINUXDO_TOKEN_URL: &str = "https://connect.linux.do/oauth2/token";
|
||||||
|
|
||||||
@@ -54,10 +54,7 @@ async fn admin_oauth_endpoint_reachable(client: &reqwest::Client, url: &str) ->
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => response.status().as_u16() < 500,
|
||||||
let status = response.status();
|
|
||||||
status != reqwest::StatusCode::NOT_FOUND && status.as_u16() < 500
|
|
||||||
}
|
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,10 +117,16 @@ async fn build_admin_oauth_test_payload(
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let proxy_snapshot = state.app().resolve_system_proxy_snapshot().await;
|
||||||
|
let mut client_builder = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(ADMIN_OAUTH_TEST_TIMEOUT_SECS))
|
.timeout(Duration::from_secs(ADMIN_OAUTH_TEST_TIMEOUT_SECS))
|
||||||
.redirect(reqwest::redirect::Policy::limited(3))
|
.redirect(reqwest::redirect::Policy::limited(3));
|
||||||
.build();
|
if let Some(proxy_url) = proxy_snapshot.as_ref().and_then(|p| p.url.as_deref()) {
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::all(proxy_url) {
|
||||||
|
client_builder = client_builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let client = client_builder.build();
|
||||||
let Ok(client) = client else {
|
let Ok(client) = client else {
|
||||||
return Ok(json!({
|
return Ok(json!({
|
||||||
"authorization_url_reachable": false,
|
"authorization_url_reachable": false,
|
||||||
|
|||||||
@@ -1885,6 +1885,7 @@ impl<'a> AdminAppState<'a> {
|
|||||||
oauth_provider.extra_config,
|
oauth_provider.extra_config,
|
||||||
"extra_config",
|
"extra_config",
|
||||||
)),
|
)),
|
||||||
|
icon_url: None,
|
||||||
is_enabled: oauth_provider.is_enabled,
|
is_enabled: oauth_provider.is_enabled,
|
||||||
};
|
};
|
||||||
invalid!(record.validate().map_err(|err| err.to_string()));
|
invalid!(record.validate().map_err(|err| err.to_string()));
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const LINUXDO_USERINFO_URL: &str = "https://connect.linux.do/api/user";
|
|||||||
pub(crate) struct IdentityOAuthProviderSummary {
|
pub(crate) struct IdentityOAuthProviderSummary {
|
||||||
pub(crate) provider_type: String,
|
pub(crate) provider_type: String,
|
||||||
pub(crate) display_name: String,
|
pub(crate) display_name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(crate) icon_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
@@ -77,6 +79,7 @@ pub(crate) async fn list_enabled_identity_oauth_providers(
|
|||||||
.map(|provider| IdentityOAuthProviderSummary {
|
.map(|provider| IdentityOAuthProviderSummary {
|
||||||
provider_type: provider.provider_type,
|
provider_type: provider.provider_type,
|
||||||
display_name: provider.display_name,
|
display_name: provider.display_name,
|
||||||
|
icon_url: provider.icon_url,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
providers.sort_by(|left, right| left.provider_type.cmp(&right.provider_type));
|
providers.sort_by(|left, right| left.provider_type.cmp(&right.provider_type));
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ pub(super) fn sample_oauth_provider_config(provider_type: &str) -> StoredOAuthPr
|
|||||||
Some(vec!["openid".to_string()]),
|
Some(vec!["openid".to_string()]),
|
||||||
Some(json!({"email": "email"})),
|
Some(json!({"email": "email"})),
|
||||||
Some(json!({"team": true})),
|
Some(json!({"team": true})),
|
||||||
|
None,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oauth_providers ADD COLUMN icon_url VARCHAR(500);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.oauth_providers ADD COLUMN IF NOT EXISTS icon_url VARCHAR(500);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oauth_providers ADD COLUMN icon_url TEXT;
|
||||||
@@ -416,6 +416,7 @@ CREATE TABLE IF NOT EXISTS public.oauth_providers (
|
|||||||
frontend_callback_url character varying(500) NOT NULL,
|
frontend_callback_url character varying(500) NOT NULL,
|
||||||
attribute_mapping json,
|
attribute_mapping json,
|
||||||
extra_config json,
|
extra_config json,
|
||||||
|
icon_url character varying(500),
|
||||||
is_enabled boolean DEFAULT false NOT NULL,
|
is_enabled boolean DEFAULT false NOT NULL,
|
||||||
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS oauth_providers (
|
|||||||
`frontend_callback_url` VARCHAR(500) NOT NULL,
|
`frontend_callback_url` VARCHAR(500) NOT NULL,
|
||||||
`attribute_mapping` JSON,
|
`attribute_mapping` JSON,
|
||||||
`extra_config` JSON,
|
`extra_config` JSON,
|
||||||
|
`icon_url` VARCHAR(500),
|
||||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`created_at` BIGINT NOT NULL,
|
`created_at` BIGINT NOT NULL,
|
||||||
`updated_at` BIGINT NOT NULL,
|
`updated_at` BIGINT NOT NULL,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ CREATE TABLE IF NOT EXISTS public.oauth_providers (
|
|||||||
frontend_callback_url character varying(500) NOT NULL,
|
frontend_callback_url character varying(500) NOT NULL,
|
||||||
attribute_mapping jsonb,
|
attribute_mapping jsonb,
|
||||||
extra_config jsonb,
|
extra_config jsonb,
|
||||||
|
icon_url character varying(500),
|
||||||
is_enabled boolean DEFAULT false NOT NULL,
|
is_enabled boolean DEFAULT false NOT NULL,
|
||||||
created_at bigint NOT NULL,
|
created_at bigint NOT NULL,
|
||||||
updated_at bigint NOT NULL
|
updated_at bigint NOT NULL
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS oauth_providers (
|
|||||||
frontend_callback_url TEXT NOT NULL,
|
frontend_callback_url TEXT NOT NULL,
|
||||||
attribute_mapping TEXT,
|
attribute_mapping TEXT,
|
||||||
extra_config TEXT,
|
extra_config TEXT,
|
||||||
|
icon_url TEXT,
|
||||||
is_enabled INTEGER NOT NULL DEFAULT 0,
|
is_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL
|
updated_at INTEGER NOT NULL
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ name = "extra_config"
|
|||||||
type = "json"
|
type = "json"
|
||||||
nullable = true
|
nullable = true
|
||||||
|
|
||||||
|
[[table.oauth_providers.columns]]
|
||||||
|
name = "icon_url"
|
||||||
|
type = "text"
|
||||||
|
length = 500
|
||||||
|
nullable = true
|
||||||
|
|
||||||
[[table.oauth_providers.columns]]
|
[[table.oauth_providers.columns]]
|
||||||
name = "is_enabled"
|
name = "is_enabled"
|
||||||
type = "bool"
|
type = "bool"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tracing::info;
|
|||||||
// Generated by build.rs from schema/bootstrap/postgres.
|
// Generated by build.rs from schema/bootstrap/postgres.
|
||||||
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
||||||
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
||||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260519000000;
|
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260519120000;
|
||||||
|
|
||||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||||
SELECT COUNT(*)::BIGINT
|
SELECT COUNT(*)::BIGINT
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ fn empty_database_snapshot_covers_current_cutoff_versions() {
|
|||||||
20260516000000,
|
20260516000000,
|
||||||
20260518000000,
|
20260518000000,
|
||||||
20260519000000,
|
20260519000000,
|
||||||
|
20260519120000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -598,7 +599,9 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
|||||||
20260512090000,
|
20260512090000,
|
||||||
20260512110000,
|
20260512110000,
|
||||||
20260516000000,
|
20260516000000,
|
||||||
|
20260518000000,
|
||||||
20260519000000,
|
20260519000000,
|
||||||
|
20260519120000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -616,7 +619,9 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
|||||||
20260512090000,
|
20260512090000,
|
||||||
20260512110000,
|
20260512110000,
|
||||||
20260516000000,
|
20260516000000,
|
||||||
|
20260518000000,
|
||||||
20260519000000,
|
20260519000000,
|
||||||
|
20260519120000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1136,6 +1141,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
|
|||||||
20260516000000,
|
20260516000000,
|
||||||
20260518000000,
|
20260518000000,
|
||||||
20260519000000,
|
20260519000000,
|
||||||
|
20260519120000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ impl OAuthProviderWriteRepository for InMemoryOAuthProviderRepository {
|
|||||||
record.scopes.clone(),
|
record.scopes.clone(),
|
||||||
record.attribute_mapping.clone(),
|
record.attribute_mapping.clone(),
|
||||||
record.extra_config.clone(),
|
record.extra_config.clone(),
|
||||||
|
record.icon_url.clone(),
|
||||||
record.is_enabled,
|
record.is_enabled,
|
||||||
)
|
)
|
||||||
.with_timestamps(created_at, now);
|
.with_timestamps(created_at, now);
|
||||||
@@ -150,6 +151,7 @@ mod tests {
|
|||||||
frontend_callback_url: "https://frontend.example.com/auth/callback".to_string(),
|
frontend_callback_url: "https://frontend.example.com/auth/callback".to_string(),
|
||||||
attribute_mapping: Some(serde_json::json!({"email": "email"})),
|
attribute_mapping: Some(serde_json::json!({"email": "email"})),
|
||||||
extra_config: Some(serde_json::json!({"team": true})),
|
extra_config: Some(serde_json::json!({"team": true})),
|
||||||
|
icon_url: None,
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ SELECT
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
@@ -67,6 +68,7 @@ SELECT
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
@@ -176,13 +178,14 @@ INSERT INTO oauth_providers (
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
CASE ? WHEN 'set' THEN ? WHEN 'clear' THEN NULL ELSE NULL END,
|
CASE ? WHEN 'set' THEN ? WHEN 'clear' THEN NULL ELSE NULL END,
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
display_name = VALUES(display_name),
|
display_name = VALUES(display_name),
|
||||||
@@ -200,6 +203,7 @@ ON DUPLICATE KEY UPDATE
|
|||||||
frontend_callback_url = VALUES(frontend_callback_url),
|
frontend_callback_url = VALUES(frontend_callback_url),
|
||||||
attribute_mapping = VALUES(attribute_mapping),
|
attribute_mapping = VALUES(attribute_mapping),
|
||||||
extra_config = VALUES(extra_config),
|
extra_config = VALUES(extra_config),
|
||||||
|
icon_url = VALUES(icon_url),
|
||||||
is_enabled = VALUES(is_enabled),
|
is_enabled = VALUES(is_enabled),
|
||||||
updated_at = VALUES(updated_at)
|
updated_at = VALUES(updated_at)
|
||||||
"#,
|
"#,
|
||||||
@@ -217,6 +221,7 @@ ON DUPLICATE KEY UPDATE
|
|||||||
.bind(&record.frontend_callback_url)
|
.bind(&record.frontend_callback_url)
|
||||||
.bind(json_to_string(record.attribute_mapping.as_ref())?)
|
.bind(json_to_string(record.attribute_mapping.as_ref())?)
|
||||||
.bind(json_to_string(record.extra_config.as_ref())?)
|
.bind(json_to_string(record.extra_config.as_ref())?)
|
||||||
|
.bind(record.icon_url.as_deref())
|
||||||
.bind(record.is_enabled)
|
.bind(record.is_enabled)
|
||||||
.bind(now as i64)
|
.bind(now as i64)
|
||||||
.bind(now as i64)
|
.bind(now as i64)
|
||||||
@@ -361,6 +366,7 @@ fn map_oauth_provider_row(row: &MySqlRow) -> Result<StoredOAuthProviderConfig, D
|
|||||||
row.try_get("extra_config").map_sql_err()?,
|
row.try_get("extra_config").map_sql_err()?,
|
||||||
"oauth_providers.extra_config",
|
"oauth_providers.extra_config",
|
||||||
)?,
|
)?,
|
||||||
|
row.try_get("icon_url").map_sql_err()?,
|
||||||
row.try_get("is_enabled").map_sql_err()?,
|
row.try_get("is_enabled").map_sql_err()?,
|
||||||
)
|
)
|
||||||
.with_timestamps(
|
.with_timestamps(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ SELECT
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
||||||
@@ -77,6 +78,7 @@ INSERT INTO oauth_providers (
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
@@ -99,6 +101,7 @@ VALUES (
|
|||||||
$12,
|
$12,
|
||||||
$13,
|
$13,
|
||||||
$14,
|
$14,
|
||||||
|
$15,
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
)
|
)
|
||||||
@@ -118,6 +121,7 @@ SET display_name = EXCLUDED.display_name,
|
|||||||
frontend_callback_url = EXCLUDED.frontend_callback_url,
|
frontend_callback_url = EXCLUDED.frontend_callback_url,
|
||||||
attribute_mapping = EXCLUDED.attribute_mapping,
|
attribute_mapping = EXCLUDED.attribute_mapping,
|
||||||
extra_config = EXCLUDED.extra_config,
|
extra_config = EXCLUDED.extra_config,
|
||||||
|
icon_url = EXCLUDED.icon_url,
|
||||||
is_enabled = EXCLUDED.is_enabled,
|
is_enabled = EXCLUDED.is_enabled,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
RETURNING
|
RETURNING
|
||||||
@@ -133,6 +137,7 @@ RETURNING
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
||||||
@@ -230,6 +235,7 @@ impl OAuthProviderWriteRepository for SqlxOAuthProviderRepository {
|
|||||||
.bind(&record.frontend_callback_url)
|
.bind(&record.frontend_callback_url)
|
||||||
.bind(record.attribute_mapping.as_ref())
|
.bind(record.attribute_mapping.as_ref())
|
||||||
.bind(record.extra_config.as_ref())
|
.bind(record.extra_config.as_ref())
|
||||||
|
.bind(record.icon_url.as_deref())
|
||||||
.bind(record.is_enabled)
|
.bind(record.is_enabled)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
@@ -330,6 +336,7 @@ fn map_oauth_provider_row(row: &PgRow) -> Result<StoredOAuthProviderConfig, Data
|
|||||||
parse_scopes(row.try_get("scopes").map_postgres_err()?)?,
|
parse_scopes(row.try_get("scopes").map_postgres_err()?)?,
|
||||||
row.try_get("attribute_mapping").map_postgres_err()?,
|
row.try_get("attribute_mapping").map_postgres_err()?,
|
||||||
row.try_get("extra_config").map_postgres_err()?,
|
row.try_get("extra_config").map_postgres_err()?,
|
||||||
|
row.try_get("icon_url").map_postgres_err()?,
|
||||||
row.try_get("is_enabled").map_postgres_err()?,
|
row.try_get("is_enabled").map_postgres_err()?,
|
||||||
)
|
)
|
||||||
.with_timestamps(
|
.with_timestamps(
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ SELECT
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
@@ -162,13 +163,14 @@ INSERT INTO oauth_providers (
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping,
|
attribute_mapping,
|
||||||
extra_config,
|
extra_config,
|
||||||
|
icon_url,
|
||||||
is_enabled,
|
is_enabled,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
CASE ? WHEN 'set' THEN ? WHEN 'clear' THEN NULL ELSE NULL END,
|
CASE ? WHEN 'set' THEN ? WHEN 'clear' THEN NULL ELSE NULL END,
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
ON CONFLICT(provider_type) DO UPDATE SET
|
ON CONFLICT(provider_type) DO UPDATE SET
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
@@ -186,6 +188,7 @@ ON CONFLICT(provider_type) DO UPDATE SET
|
|||||||
frontend_callback_url = excluded.frontend_callback_url,
|
frontend_callback_url = excluded.frontend_callback_url,
|
||||||
attribute_mapping = excluded.attribute_mapping,
|
attribute_mapping = excluded.attribute_mapping,
|
||||||
extra_config = excluded.extra_config,
|
extra_config = excluded.extra_config,
|
||||||
|
icon_url = excluded.icon_url,
|
||||||
is_enabled = excluded.is_enabled,
|
is_enabled = excluded.is_enabled,
|
||||||
updated_at = excluded.updated_at
|
updated_at = excluded.updated_at
|
||||||
"#,
|
"#,
|
||||||
@@ -203,6 +206,7 @@ ON CONFLICT(provider_type) DO UPDATE SET
|
|||||||
.bind(&record.frontend_callback_url)
|
.bind(&record.frontend_callback_url)
|
||||||
.bind(json_to_string(record.attribute_mapping.as_ref())?)
|
.bind(json_to_string(record.attribute_mapping.as_ref())?)
|
||||||
.bind(json_to_string(record.extra_config.as_ref())?)
|
.bind(json_to_string(record.extra_config.as_ref())?)
|
||||||
|
.bind(record.icon_url.as_deref())
|
||||||
.bind(record.is_enabled)
|
.bind(record.is_enabled)
|
||||||
.bind(now as i64)
|
.bind(now as i64)
|
||||||
.bind(now as i64)
|
.bind(now as i64)
|
||||||
@@ -350,6 +354,7 @@ fn map_oauth_provider_row(row: &SqliteRow) -> Result<StoredOAuthProviderConfig,
|
|||||||
row.try_get("extra_config").map_sql_err()?,
|
row.try_get("extra_config").map_sql_err()?,
|
||||||
"oauth_providers.extra_config",
|
"oauth_providers.extra_config",
|
||||||
)?,
|
)?,
|
||||||
|
row.try_get("icon_url").map_sql_err()?,
|
||||||
row.try_get("is_enabled").map_sql_err()?,
|
row.try_get("is_enabled").map_sql_err()?,
|
||||||
)
|
)
|
||||||
.with_timestamps(
|
.with_timestamps(
|
||||||
@@ -381,6 +386,7 @@ mod tests {
|
|||||||
frontend_callback_url: "https://frontend.example.com/auth/callback".to_string(),
|
frontend_callback_url: "https://frontend.example.com/auth/callback".to_string(),
|
||||||
attribute_mapping: Some(serde_json::json!({"email": "email"})),
|
attribute_mapping: Some(serde_json::json!({"email": "email"})),
|
||||||
extra_config: Some(serde_json::json!({"team": true})),
|
extra_config: Some(serde_json::json!({"team": true})),
|
||||||
|
icon_url: None,
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct StoredOAuthProviderConfig {
|
|||||||
pub frontend_callback_url: String,
|
pub frontend_callback_url: String,
|
||||||
pub attribute_mapping: Option<serde_json::Value>,
|
pub attribute_mapping: Option<serde_json::Value>,
|
||||||
pub extra_config: Option<serde_json::Value>,
|
pub extra_config: Option<serde_json::Value>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
pub is_enabled: bool,
|
pub is_enabled: bool,
|
||||||
pub created_at_unix_ms: Option<u64>,
|
pub created_at_unix_ms: Option<u64>,
|
||||||
pub updated_at_unix_secs: Option<u64>,
|
pub updated_at_unix_secs: Option<u64>,
|
||||||
@@ -66,6 +67,7 @@ impl StoredOAuthProviderConfig {
|
|||||||
frontend_callback_url,
|
frontend_callback_url,
|
||||||
attribute_mapping: None,
|
attribute_mapping: None,
|
||||||
extra_config: None,
|
extra_config: None,
|
||||||
|
icon_url: None,
|
||||||
is_enabled: false,
|
is_enabled: false,
|
||||||
created_at_unix_ms: None,
|
created_at_unix_ms: None,
|
||||||
updated_at_unix_secs: None,
|
updated_at_unix_secs: None,
|
||||||
@@ -82,6 +84,7 @@ impl StoredOAuthProviderConfig {
|
|||||||
scopes: Option<Vec<String>>,
|
scopes: Option<Vec<String>>,
|
||||||
attribute_mapping: Option<serde_json::Value>,
|
attribute_mapping: Option<serde_json::Value>,
|
||||||
extra_config: Option<serde_json::Value>,
|
extra_config: Option<serde_json::Value>,
|
||||||
|
icon_url: Option<String>,
|
||||||
is_enabled: bool,
|
is_enabled: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.client_secret_encrypted = client_secret_encrypted;
|
self.client_secret_encrypted = client_secret_encrypted;
|
||||||
@@ -91,6 +94,7 @@ impl StoredOAuthProviderConfig {
|
|||||||
self.scopes = scopes;
|
self.scopes = scopes;
|
||||||
self.attribute_mapping = attribute_mapping;
|
self.attribute_mapping = attribute_mapping;
|
||||||
self.extra_config = extra_config;
|
self.extra_config = extra_config;
|
||||||
|
self.icon_url = icon_url;
|
||||||
self.is_enabled = is_enabled;
|
self.is_enabled = is_enabled;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -145,6 +149,7 @@ pub struct UpsertOAuthProviderConfigRecord {
|
|||||||
pub frontend_callback_url: String,
|
pub frontend_callback_url: String,
|
||||||
pub attribute_mapping: Option<serde_json::Value>,
|
pub attribute_mapping: Option<serde_json::Value>,
|
||||||
pub extra_config: Option<serde_json::Value>,
|
pub extra_config: Option<serde_json::Value>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
pub is_enabled: bool,
|
pub is_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,8 +336,11 @@ SELECT
|
|||||||
users.role::text AS role,
|
users.role::text AS role,
|
||||||
users.auth_source::text AS auth_source,
|
users.auth_source::text AS auth_source,
|
||||||
users.allowed_providers,
|
users.allowed_providers,
|
||||||
|
users.allowed_providers_mode,
|
||||||
users.allowed_api_formats,
|
users.allowed_api_formats,
|
||||||
|
users.allowed_api_formats_mode,
|
||||||
users.allowed_models,
|
users.allowed_models,
|
||||||
|
users.allowed_models_mode,
|
||||||
users.is_active,
|
users.is_active,
|
||||||
users.is_deleted,
|
users.is_deleted,
|
||||||
users.created_at,
|
users.created_at,
|
||||||
@@ -353,7 +356,7 @@ const TOUCH_OAUTH_LINK_SQL: &str = r#"
|
|||||||
UPDATE user_oauth_links
|
UPDATE user_oauth_links
|
||||||
SET provider_username = COALESCE($3, provider_username),
|
SET provider_username = COALESCE($3, provider_username),
|
||||||
provider_email = COALESCE($4, provider_email),
|
provider_email = COALESCE($4, provider_email),
|
||||||
extra_data = COALESCE($5, extra_data),
|
extra_data = COALESCE($5::json, extra_data),
|
||||||
last_login_at = $6
|
last_login_at = $6
|
||||||
WHERE provider_type = $1
|
WHERE provider_type = $1
|
||||||
AND provider_user_id = $2
|
AND provider_user_id = $2
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import apiClient from './client'
|
|||||||
export interface OAuthProviderInfo {
|
export interface OAuthProviderInfo {
|
||||||
provider_type: string
|
provider_type: string
|
||||||
display_name: string
|
display_name: string
|
||||||
|
icon_url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuthProvidersResponse {
|
export interface OAuthProvidersResponse {
|
||||||
@@ -46,6 +47,7 @@ export interface OAuthProviderAdminConfig {
|
|||||||
frontend_callback_url: string
|
frontend_callback_url: string
|
||||||
attribute_mapping?: Record<string, unknown> | null
|
attribute_mapping?: Record<string, unknown> | null
|
||||||
extra_config?: Record<string, unknown> | null
|
extra_config?: Record<string, unknown> | null
|
||||||
|
icon_url?: string | null
|
||||||
is_enabled: boolean
|
is_enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ export interface OAuthProviderUpsertRequest {
|
|||||||
frontend_callback_url: string
|
frontend_callback_url: string
|
||||||
attribute_mapping?: Record<string, unknown> | null
|
attribute_mapping?: Record<string, unknown> | null
|
||||||
extra_config?: Record<string, unknown> | null
|
extra_config?: Record<string, unknown> | null
|
||||||
|
icon_url?: string | null
|
||||||
is_enabled: boolean
|
is_enabled: boolean
|
||||||
force?: boolean
|
force?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<span
|
<span
|
||||||
class="oauth-icon"
|
class="oauth-icon"
|
||||||
v-html="getOAuthIcon(oauthProviders[0].provider_type)"
|
v-html="getOAuthIcon(oauthProviders[0].provider_type, oauthProviders[0].icon_url)"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
<span>使用 {{ oauthProviders[0].display_name }} 登录</span>
|
<span>使用 {{ oauthProviders[0].display_name }} 登录</span>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<span
|
<span
|
||||||
class="oauth-icon-lg"
|
class="oauth-icon-lg"
|
||||||
v-html="getOAuthIcon(p.provider_type)"
|
v-html="getOAuthIcon(p.provider_type, p.icon_url)"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export const OAUTH_ICONS: Record<string, string> = {
|
|||||||
// Default icon when provider type is not found
|
// Default icon when provider type is not found
|
||||||
const DEFAULT_ICON = OAUTH_ICONS.github
|
const DEFAULT_ICON = OAUTH_ICONS.github
|
||||||
|
|
||||||
export function getOAuthIcon(providerType: string): string {
|
export function getOAuthIcon(providerType: string, iconUrl?: string | null): string {
|
||||||
return OAUTH_ICONS[providerType.toLowerCase()] || DEFAULT_ICON
|
const builtin = OAUTH_ICONS[providerType.toLowerCase()]
|
||||||
|
if (builtin) return builtin
|
||||||
|
if (iconUrl) return `<img src="${iconUrl}" alt="" style="width:100%;height:100%;object-fit:contain;" />`
|
||||||
|
return DEFAULT_ICON
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,197 +131,209 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- 新建时的 Display Name -->
|
<!-- 新建时的 Display Name -->
|
||||||
<div
|
<div
|
||||||
v-if="selectedType === '__new__'"
|
v-if="selectedType === '__new__'"
|
||||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Label class="block text-sm font-medium">显示名称</Label>
|
<Label class="block text-sm font-medium">显示名称</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="form.new_display_name"
|
v-model="form.new_display_name"
|
||||||
class="mt-1"
|
class="mt-1"
|
||||||
placeholder="例如:My OIDC Provider"
|
placeholder="例如:My OIDC Provider"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">配置标识</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.new_provider_type"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="custom_oidc_work"
|
|
||||||
autocomplete="off"
|
|
||||||
@blur="normalizeNewProviderType"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- 凭证配置 -->
|
<Label class="block text-sm font-medium">配置标识</Label>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<Input
|
||||||
<div>
|
v-model="form.new_provider_type"
|
||||||
<Label class="block text-sm font-medium">Client ID</Label>
|
class="mt-1"
|
||||||
<Input
|
placeholder="custom_oidc_work"
|
||||||
v-model="form.client_id"
|
autocomplete="off"
|
||||||
class="mt-1"
|
@blur="normalizeNewProviderType"
|
||||||
placeholder="client_id"
|
/>
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Client Secret</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.client_secret"
|
|
||||||
masked
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 回调地址 -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.redirect_uri"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">前端回调页</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.frontend_callback_url"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="http://localhost:5173/auth/callback"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- custom_oidc 必填端点 -->
|
|
||||||
<div
|
|
||||||
v-if="isSelectedCustomProvider"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.authorization_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="https://example.com/oauth/authorize"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Token URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.token_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="https://example.com/oauth/token"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.userinfo_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="https://example.com/api/user"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 高级选项(折叠) -->
|
|
||||||
<details class="group">
|
|
||||||
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
高级选项
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Scopes</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.scopes_input"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<p class="mt-1 text-xs text-muted-foreground">
|
|
||||||
空格/逗号分隔;留空使用默认值
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- linuxdo 的可选端点覆盖 -->
|
|
||||||
<div
|
|
||||||
v-if="!isSelectedCustomProvider"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.authorization_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Token URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.token_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.userinfo_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
|
||||||
<Textarea
|
|
||||||
v-model="form.attribute_mapping_json"
|
|
||||||
class="mt-1 font-mono text-xs"
|
|
||||||
rows="3"
|
|
||||||
placeholder="{"id": "user_id", "username": "login"}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">
|
|
||||||
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
v-model="form.extra_config_json"
|
|
||||||
class="mt-1 font-mono text-xs"
|
|
||||||
rows="3"
|
|
||||||
:placeholder="extraConfigPlaceholder"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
v-if="isSelectedCustomProvider"
|
|
||||||
class="mt-1 text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 测试结果 -->
|
<!-- 凭证配置 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Client ID</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.client_id"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="client_id"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Client Secret</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.client_secret"
|
||||||
|
masked
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回调地址 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.redirect_uri"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">前端回调页</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.frontend_callback_url"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="http://localhost:5173/auth/callback"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- custom_oidc 必填端点 -->
|
||||||
|
<div
|
||||||
|
v-if="isSelectedCustomProvider"
|
||||||
|
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.authorization_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/oauth/authorize"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Token URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.token_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/oauth/token"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.userinfo_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/api/user"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图标 URL -->
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">图标 URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.icon_url"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/icon.svg"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
登录页显示的 Provider 图标,留空使用默认图标
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 高级选项(折叠) -->
|
||||||
|
<details class="group">
|
||||||
|
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
高级选项
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Scopes</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.scopes_input"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
空格/逗号分隔;留空使用默认值
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- linuxdo 的可选端点覆盖 -->
|
||||||
|
<div
|
||||||
|
v-if="!isSelectedCustomProvider"
|
||||||
|
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.authorization_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Token URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.token_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.userinfo_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
||||||
|
<Textarea
|
||||||
|
v-model="form.attribute_mapping_json"
|
||||||
|
class="mt-1 font-mono text-xs"
|
||||||
|
rows="3"
|
||||||
|
placeholder="{"id": "user_id", "username": "login"}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">
|
||||||
|
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
v-model="form.extra_config_json"
|
||||||
|
class="mt-1 font-mono text-xs"
|
||||||
|
rows="3"
|
||||||
|
:placeholder="extraConfigPlaceholder"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="isSelectedCustomProvider"
|
||||||
|
class="mt-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="lastTestResult"
|
v-if="lastTestResult"
|
||||||
class="mt-6 rounded-lg border border-border p-4 text-sm"
|
class="mt-6 rounded-lg border border-border p-4 text-sm"
|
||||||
@@ -402,6 +414,7 @@ interface OAuthConfigForm {
|
|||||||
frontend_callback_url: string
|
frontend_callback_url: string
|
||||||
attribute_mapping_json: string
|
attribute_mapping_json: string
|
||||||
extra_config_json: string
|
extra_config_json: string
|
||||||
|
icon_url: string
|
||||||
new_provider_type: string
|
new_provider_type: string
|
||||||
new_display_name: string
|
new_display_name: string
|
||||||
}
|
}
|
||||||
@@ -423,6 +436,7 @@ const form = ref<OAuthConfigForm>({
|
|||||||
frontend_callback_url: '',
|
frontend_callback_url: '',
|
||||||
attribute_mapping_json: '',
|
attribute_mapping_json: '',
|
||||||
extra_config_json: '',
|
extra_config_json: '',
|
||||||
|
icon_url: '',
|
||||||
new_provider_type: '',
|
new_provider_type: '',
|
||||||
new_display_name: '',
|
new_display_name: '',
|
||||||
})
|
})
|
||||||
@@ -589,6 +603,7 @@ function handleClickAdd() {
|
|||||||
frontend_callback_url: defaultFrontendCallbackUrl(),
|
frontend_callback_url: defaultFrontendCallbackUrl(),
|
||||||
attribute_mapping_json: '',
|
attribute_mapping_json: '',
|
||||||
extra_config_json: '',
|
extra_config_json: '',
|
||||||
|
icon_url: '',
|
||||||
new_provider_type: providerType,
|
new_provider_type: providerType,
|
||||||
new_display_name: '',
|
new_display_name: '',
|
||||||
}
|
}
|
||||||
@@ -633,6 +648,7 @@ function syncFormFromSelected() {
|
|||||||
frontend_callback_url: cfg?.frontend_callback_url || defaultFrontendCallbackUrl(),
|
frontend_callback_url: cfg?.frontend_callback_url || defaultFrontendCallbackUrl(),
|
||||||
attribute_mapping_json: cfg?.attribute_mapping ? JSON.stringify(cfg.attribute_mapping, null, 2) : '',
|
attribute_mapping_json: cfg?.attribute_mapping ? JSON.stringify(cfg.attribute_mapping, null, 2) : '',
|
||||||
extra_config_json: cfg?.extra_config ? JSON.stringify(cfg.extra_config, null, 2) : '',
|
extra_config_json: cfg?.extra_config ? JSON.stringify(cfg.extra_config, null, 2) : '',
|
||||||
|
icon_url: cfg?.icon_url || '',
|
||||||
new_provider_type: '',
|
new_provider_type: '',
|
||||||
new_display_name: '',
|
new_display_name: '',
|
||||||
}
|
}
|
||||||
@@ -658,6 +674,7 @@ async function toggleProviderEnabled(providerType: string, enabled: boolean, for
|
|||||||
frontend_callback_url: cfg.frontend_callback_url,
|
frontend_callback_url: cfg.frontend_callback_url,
|
||||||
attribute_mapping: cfg.attribute_mapping || null,
|
attribute_mapping: cfg.attribute_mapping || null,
|
||||||
extra_config: cfg.extra_config || null,
|
extra_config: cfg.extra_config || null,
|
||||||
|
icon_url: cfg.icon_url || null,
|
||||||
is_enabled: enabled,
|
is_enabled: enabled,
|
||||||
force,
|
force,
|
||||||
}
|
}
|
||||||
@@ -732,6 +749,7 @@ async function handleSave() {
|
|||||||
frontend_callback_url: form.value.frontend_callback_url.trim(),
|
frontend_callback_url: form.value.frontend_callback_url.trim(),
|
||||||
attribute_mapping: parseJsonOrNull(form.value.attribute_mapping_json),
|
attribute_mapping: parseJsonOrNull(form.value.attribute_mapping_json),
|
||||||
extra_config: parseJsonOrNull(form.value.extra_config_json),
|
extra_config: parseJsonOrNull(form.value.extra_config_json),
|
||||||
|
icon_url: form.value.icon_url.trim() || null,
|
||||||
is_enabled: existingConfig?.is_enabled || false,
|
is_enabled: existingConfig?.is_enabled || false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -387,7 +387,7 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<div
|
<div
|
||||||
class="oauth-icon shrink-0"
|
class="oauth-icon shrink-0"
|
||||||
v-html="getOAuthIcon(p.provider_type)"
|
v-html="getOAuthIcon(p.provider_type, p.icon_url)"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user