feat: add tiered Oracle agent profiles
This commit is contained in:
1 parent
f13c46b903
commit
c334550252
11 files changed
+152
-38
No files matched your search
@@ -170,6 +170,7 @@ const driverProfiles: Record<
|
|||||||
},
|
},
|
||||||
sqlserver: { type: "sqlserver", port: 1433, user: "sa", label: "SQL Server", icon: "sqlserver" },
|
sqlserver: { type: "sqlserver", port: 1433, user: "sa", label: "SQL Server", icon: "sqlserver" },
|
||||||
oracle: { type: "oracle", port: 1521, user: "system", label: "Oracle", icon: "oracle" },
|
oracle: { type: "oracle", port: 1521, user: "system", label: "Oracle", icon: "oracle" },
|
||||||
|
"oracle-legacy": { type: "oracle", port: 1521, user: "system", label: "Oracle Legacy", icon: "oracle" },
|
||||||
"oracle-10g": { type: "oracle", port: 1521, user: "system", label: "Oracle 10g", icon: "oracle" },
|
"oracle-10g": { type: "oracle", port: 1521, user: "system", label: "Oracle 10g", icon: "oracle" },
|
||||||
elasticsearch: {
|
elasticsearch: {
|
||||||
type: "elasticsearch",
|
type: "elasticsearch",
|
||||||
@@ -458,6 +459,7 @@ const iconTypeMap: Record<string, string> = {
|
|||||||
clickhouse: "clickhouse",
|
clickhouse: "clickhouse",
|
||||||
sqlserver: "sqlserver",
|
sqlserver: "sqlserver",
|
||||||
oracle: "oracle",
|
oracle: "oracle",
|
||||||
|
"oracle-legacy": "oracle",
|
||||||
"oracle-10g": "oracle",
|
"oracle-10g": "oracle",
|
||||||
elasticsearch: "elasticsearch",
|
elasticsearch: "elasticsearch",
|
||||||
mariadb: "mariadb",
|
mariadb: "mariadb",
|
||||||
@@ -1955,14 +1957,17 @@ function openExternalUrl(url: string) {
|
|||||||
<div v-if="form.db_type === 'oracle'" class="grid grid-cols-4 items-center gap-4">
|
<div v-if="form.db_type === 'oracle'" class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label class="text-right text-xs">{{ t("connection.version") }}</Label>
|
<Label class="text-right text-xs">{{ t("connection.version") }}</Label>
|
||||||
<Select
|
<Select
|
||||||
:model-value="selectedType === 'oracle-10g' ? 'oracle-10g' : 'oracle'"
|
:model-value="
|
||||||
|
selectedType === 'oracle-legacy' || selectedType === 'oracle-10g' ? selectedType : 'oracle'
|
||||||
|
"
|
||||||
@update:model-value="(val) => applyProfile(String(val), true)"
|
@update:model-value="(val) => applyProfile(String(val), true)"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="col-span-3 h-8 text-sm">
|
<SelectTrigger class="col-span-3 h-8 text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="oracle">Oracle 11g+</SelectItem>
|
<SelectItem value="oracle">Oracle 19c+</SelectItem>
|
||||||
|
<SelectItem value="oracle-legacy">Oracle 11g-19c</SelectItem>
|
||||||
<SelectItem value="oracle-10g">Oracle 10g</SelectItem>
|
<SelectItem value="oracle-10g">Oracle 10g</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ const assetIcons: Record<string, string> = {
|
|||||||
elasticsearch: "elasticsearch",
|
elasticsearch: "elasticsearch",
|
||||||
oracle: "oracle",
|
oracle: "oracle",
|
||||||
"oracle-10g": "oracle",
|
"oracle-10g": "oracle",
|
||||||
|
"oracle-legacy": "oracle",
|
||||||
oracle_10g: "oracle",
|
oracle_10g: "oracle",
|
||||||
|
oracle_legacy: "oracle",
|
||||||
sqlserver: "sqlserver",
|
sqlserver: "sqlserver",
|
||||||
access: "access.png",
|
access: "access.png",
|
||||||
oceanbase: "oceanbase",
|
oceanbase: "oceanbase",
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export function showAgentDriverInstallHint(
|
|||||||
driverProfile?: string,
|
driverProfile?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!supportsDriverManagement(dbType)) return false;
|
if (!supportsDriverManagement(dbType)) return false;
|
||||||
const driverKey = dbType === "oracle" && driverProfile === "oracle-10g" ? "oracle-10g" : dbType;
|
const driverKey = dbType === "oracle" && driverProfile ? driverProfile : dbType;
|
||||||
return drivers.find((driver) => driver.db_type === driverKey)?.installed !== true;
|
return drivers.find((driver) => driver.db_type === driverKey)?.installed !== true;
|
||||||
}
|
}
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
"skipTcpProbe": true,
|
"skipTcpProbe": true,
|
||||||
"defaultPort": 1521,
|
"defaultPort": 1521,
|
||||||
"driverProfiles": [
|
"driverProfiles": [
|
||||||
|
{ "profile": "oracle-legacy", "label": "Oracle Legacy", "agentKey": "oracle-legacy" },
|
||||||
{ "profile": "oracle-10g", "label": "Oracle 10g", "agentKey": "oracle-10g" }
|
{ "profile": "oracle-10g", "label": "Oracle 10g", "agentKey": "oracle-10g" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ pub struct AgentDriverProfile {
|
|||||||
pub store_visible: bool,
|
pub store_visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ORACLE_PROFILES: &[AgentDriverProfile] =
|
const ORACLE_PROFILES: &[AgentDriverProfile] = &[
|
||||||
&[AgentDriverProfile { profile: "oracle-10g", key: "oracle-10g", label: "Oracle 10g", store_visible: true }];
|
AgentDriverProfile { profile: "oracle-legacy", key: "oracle-legacy", label: "Oracle Legacy", store_visible: true },
|
||||||
|
AgentDriverProfile { profile: "oracle-10g", key: "oracle-10g", label: "Oracle 10g", store_visible: true },
|
||||||
|
];
|
||||||
|
|
||||||
const AGENT_CATALOG: &[AgentCatalogEntry] = &[
|
const AGENT_CATALOG: &[AgentCatalogEntry] = &[
|
||||||
AgentCatalogEntry {
|
AgentCatalogEntry {
|
||||||
|
|||||||
@@ -110,14 +110,22 @@ fn postgres_like_agent_jdbc_connection_string(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_retry_oracle_with_10g_driver(config: &ConnectionConfig, err: &str) -> bool {
|
pub fn should_retry_oracle_with_10g_driver(config: &ConnectionConfig, err: &str) -> bool {
|
||||||
|
!oracle_auth_fallback_profiles(config, err).is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oracle_auth_fallback_profiles(config: &ConnectionConfig, err: &str) -> Vec<&'static str> {
|
||||||
if config.db_type != DatabaseType::Oracle {
|
if config.db_type != DatabaseType::Oracle {
|
||||||
return false;
|
return Vec::new();
|
||||||
}
|
|
||||||
if config.driver_profile.as_deref() == Some("oracle-10g") {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
let normalized = err.to_lowercase();
|
let normalized = err.to_lowercase();
|
||||||
normalized.contains("ora-28040") || normalized.contains("no matching authentication protocol")
|
if !normalized.contains("ora-28040") && !normalized.contains("no matching authentication protocol") {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
match config.driver_profile.as_deref() {
|
||||||
|
Some("oracle-10g") => Vec::new(),
|
||||||
|
Some("oracle-legacy") => vec!["oracle-10g"],
|
||||||
|
_ => vec!["oracle-legacy", "oracle-10g"],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_alternate_connect_config(config: &ConnectionConfig, err: &str) -> Option<ConnectionConfig> {
|
pub fn oracle_alternate_connect_config(config: &ConnectionConfig, err: &str) -> Option<ConnectionConfig> {
|
||||||
@@ -295,6 +303,26 @@ mod tests {
|
|||||||
assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_none());
|
assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oracle_auth_errors_use_legacy_then_10g_fallbacks() {
|
||||||
|
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));
|
||||||
|
cfg.driver_profile = Some("oracle".to_string());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
oracle_auth_fallback_profiles(&cfg, "ORA-28040: No matching authentication protocol"),
|
||||||
|
vec!["oracle-legacy", "oracle-10g"]
|
||||||
|
);
|
||||||
|
|
||||||
|
cfg.driver_profile = Some("oracle-legacy".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
oracle_auth_fallback_profiles(&cfg, "ORA-28040: No matching authentication protocol"),
|
||||||
|
vec!["oracle-10g"]
|
||||||
|
);
|
||||||
|
|
||||||
|
cfg.driver_profile = Some("oracle-10g".to_string());
|
||||||
|
assert!(oracle_auth_fallback_profiles(&cfg, "ORA-28040: No matching authentication protocol").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oracle_custom_connection_string_skips_alternate_descriptor_retry() {
|
fn oracle_custom_connection_string_skips_alternate_descriptor_retry() {
|
||||||
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));
|
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));
|
||||||
|
|||||||
@@ -152,7 +152,12 @@ mod tests {
|
|||||||
let manager = test_manager("profile-key");
|
let manager = test_manager("profile-key");
|
||||||
|
|
||||||
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, Some("oracle-10g")), Some("oracle-10g"));
|
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, Some("oracle-10g")), Some("oracle-10g"));
|
||||||
|
assert_eq!(
|
||||||
|
AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, Some("oracle-legacy")),
|
||||||
|
Some("oracle-legacy")
|
||||||
|
);
|
||||||
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, None), Some("oracle"));
|
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, None), Some("oracle"));
|
||||||
|
manager.stop_daemon_by_key("oracle-legacy").await;
|
||||||
manager.stop_daemon_by_key("oracle-10g").await;
|
manager.stop_daemon_by_key("oracle-10g").await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use mysql_async::Row as MysqlRow;
|
|||||||
|
|
||||||
use crate::agent_connection::{
|
use crate::agent_connection::{
|
||||||
agent_connect_params, mongo_legacy_error_with_auth_hint, oracle_alternate_connect_config,
|
agent_connect_params, mongo_legacy_error_with_auth_hint, oracle_alternate_connect_config,
|
||||||
should_retry_oracle_with_10g_driver,
|
oracle_auth_fallback_profiles, should_retry_oracle_with_10g_driver,
|
||||||
};
|
};
|
||||||
use crate::database_capabilities;
|
use crate::database_capabilities;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
@@ -333,19 +333,39 @@ impl AppState {
|
|||||||
})?;
|
})?;
|
||||||
} else if should_retry_oracle_with_10g_driver(&db_config, &err) {
|
} else if should_retry_oracle_with_10g_driver(&db_config, &err) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Oracle connect failed with profile {:?}: {}. Retrying with oracle-10g profile.",
|
"Oracle connect failed with profile {:?}: {}. Retrying with legacy Oracle profiles.",
|
||||||
db_config.driver_profile,
|
db_config.driver_profile,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
let mut fallback_client =
|
let mut fallback_errors = Vec::new();
|
||||||
self.agent_manager.spawn(&db_config.db_type, Some("oracle-10g")).await?;
|
let mut connected_client = None;
|
||||||
fallback_client
|
for profile in oracle_auth_fallback_profiles(&db_config, &err) {
|
||||||
.call_method::<serde_json::Value>(AgentMethod::Connect, connect_params)
|
match self.agent_manager.spawn(&db_config.db_type, Some(profile)).await {
|
||||||
.await
|
Ok(mut fallback_client) => {
|
||||||
.map_err(|fallback_err| {
|
match fallback_client
|
||||||
format!("{err}\n\nFallback with oracle-10g driver failed: {fallback_err}")
|
.call_method::<serde_json::Value>(AgentMethod::Connect, connect_params.clone())
|
||||||
})?;
|
.await
|
||||||
client = fallback_client;
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
connected_client = Some(fallback_client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(fallback_err) => {
|
||||||
|
fallback_errors.push(format!("{profile}: {fallback_err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(fallback_err) => {
|
||||||
|
fallback_errors.push(format!("{profile}: {fallback_err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client = connected_client.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"{err}\n\nFallback with legacy Oracle drivers failed: {}",
|
||||||
|
fallback_errors.join("\n")
|
||||||
|
)
|
||||||
|
})?;
|
||||||
} else {
|
} else {
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
@@ -1090,6 +1110,10 @@ mod tests {
|
|||||||
|
|
||||||
config.driver_profile = Some("oracle-10g".to_string());
|
config.driver_profile = Some("oracle-10g".to_string());
|
||||||
assert!(!should_retry_oracle_with_10g_driver(&config, "Agent RPC error (-1): ORA-12541: TNS:no listener"));
|
assert!(!should_retry_oracle_with_10g_driver(&config, "Agent RPC error (-1): ORA-12541: TNS:no listener"));
|
||||||
|
assert!(!should_retry_oracle_with_10g_driver(
|
||||||
|
&config,
|
||||||
|
"Agent RPC error (-1): ORA-28040: No matching authentication protocol"
|
||||||
|
));
|
||||||
|
|
||||||
config.driver_profile = Some("oracle".to_string());
|
config.driver_profile = Some("oracle".to_string());
|
||||||
assert!(!should_retry_oracle_with_10g_driver(
|
assert!(!should_retry_oracle_with_10g_driver(
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ fn maps_agent_database_types_to_driver_keys() {
|
|||||||
assert_eq!(agent_key(&DatabaseType::Gbase, None), Some("gbase"));
|
assert_eq!(agent_key(&DatabaseType::Gbase, None), Some("gbase"));
|
||||||
assert_eq!(agent_key(&DatabaseType::Access, None), Some("access"));
|
assert_eq!(agent_key(&DatabaseType::Access, None), Some("access"));
|
||||||
assert_eq!(agent_key(&DatabaseType::Oracle, None), Some("oracle"));
|
assert_eq!(agent_key(&DatabaseType::Oracle, None), Some("oracle"));
|
||||||
|
assert_eq!(agent_key(&DatabaseType::Oracle, Some("oracle-legacy")), Some("oracle-legacy"));
|
||||||
assert_eq!(agent_key(&DatabaseType::Oracle, Some("oracle-10g")), Some("oracle-10g"));
|
assert_eq!(agent_key(&DatabaseType::Oracle, Some("oracle-10g")), Some("oracle-10g"));
|
||||||
assert_eq!(agent_key(&DatabaseType::Postgres, None), None);
|
assert_eq!(agent_key(&DatabaseType::Postgres, None), None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ test("uses the selected Oracle driver profile for install hints", () => {
|
|||||||
"oracle",
|
"oracle",
|
||||||
[
|
[
|
||||||
{ db_type: "oracle", installed: false },
|
{ db_type: "oracle", installed: false },
|
||||||
|
{ db_type: "oracle-legacy", installed: false },
|
||||||
{ db_type: "oracle-10g", installed: true },
|
{ db_type: "oracle-10g", installed: true },
|
||||||
],
|
],
|
||||||
"oracle-10g",
|
"oracle-10g",
|
||||||
@@ -39,11 +40,24 @@ test("uses the selected Oracle driver profile for install hints", () => {
|
|||||||
"oracle",
|
"oracle",
|
||||||
[
|
[
|
||||||
{ db_type: "oracle", installed: true },
|
{ db_type: "oracle", installed: true },
|
||||||
|
{ db_type: "oracle-legacy", installed: false },
|
||||||
{ db_type: "oracle-10g", installed: false },
|
{ db_type: "oracle-10g", installed: false },
|
||||||
],
|
],
|
||||||
"oracle",
|
"oracle",
|
||||||
),
|
),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
showAgentDriverInstallHint(
|
||||||
|
"oracle",
|
||||||
|
[
|
||||||
|
{ db_type: "oracle", installed: true },
|
||||||
|
{ db_type: "oracle-legacy", installed: true },
|
||||||
|
{ db_type: "oracle-10g", installed: false },
|
||||||
|
],
|
||||||
|
"oracle-legacy",
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.equal(showAgentDriverInstallHint("oracle", [{ db_type: "oracle", installed: false }], "oracle"), true);
|
assert.equal(showAgentDriverInstallHint("oracle", [{ db_type: "oracle", installed: false }], "oracle"), true);
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@ use tauri::State;
|
|||||||
|
|
||||||
pub use dbx_core::agent_connection::{
|
pub use dbx_core::agent_connection::{
|
||||||
agent_connect_params, mongo_legacy_error_with_auth_hint, oracle_alternate_connect_config,
|
agent_connect_params, mongo_legacy_error_with_auth_hint, oracle_alternate_connect_config,
|
||||||
should_retry_oracle_with_10g_driver,
|
oracle_auth_fallback_profiles, should_retry_oracle_with_10g_driver,
|
||||||
};
|
};
|
||||||
pub use dbx_core::connection::{
|
pub use dbx_core::connection::{
|
||||||
connection_url_for_endpoint, expand_tilde, metadata_connection_config, probe_connection_endpoint,
|
connection_url_for_endpoint, expand_tilde, metadata_connection_config, probe_connection_endpoint,
|
||||||
@@ -57,16 +57,32 @@ async fn test_agent_connection(
|
|||||||
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
|
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
|
||||||
})?;
|
})?;
|
||||||
} else if should_retry_oracle_with_10g_driver(config, &err) {
|
} else if should_retry_oracle_with_10g_driver(config, &err) {
|
||||||
state
|
let mut fallback_errors = Vec::new();
|
||||||
.agent_manager
|
let mut connected = false;
|
||||||
.call_daemon_method::<serde_json::Value>(
|
for profile in oracle_auth_fallback_profiles(config, &err) {
|
||||||
&config.db_type,
|
match state
|
||||||
Some("oracle-10g"),
|
.agent_manager
|
||||||
AgentMethod::TestConnection,
|
.call_daemon_method::<serde_json::Value>(
|
||||||
connect_params,
|
&config.db_type,
|
||||||
)
|
Some(profile),
|
||||||
.await
|
AgentMethod::TestConnection,
|
||||||
.map_err(|fallback_err| format!("{err}\n\nFallback with oracle-10g driver failed: {fallback_err}"))?;
|
connect_params.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
connected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(fallback_err) => fallback_errors.push(format!("{profile}: {fallback_err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !connected {
|
||||||
|
return Err(format!(
|
||||||
|
"{err}\n\nFallback with legacy Oracle drivers failed: {}",
|
||||||
|
fallback_errors.join("\n")
|
||||||
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
@@ -102,12 +118,28 @@ async fn connect_agent_pool(
|
|||||||
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
|
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
|
||||||
})?;
|
})?;
|
||||||
} else if should_retry_oracle_with_10g_driver(config, &err) {
|
} else if should_retry_oracle_with_10g_driver(config, &err) {
|
||||||
let mut fallback_client = state.agent_manager.spawn(&config.db_type, Some("oracle-10g")).await?;
|
let mut fallback_errors = Vec::new();
|
||||||
fallback_client
|
let mut connected_client = None;
|
||||||
.call_method::<serde_json::Value>(AgentMethod::Connect, connect_params)
|
for profile in oracle_auth_fallback_profiles(config, &err) {
|
||||||
.await
|
match state.agent_manager.spawn(&config.db_type, Some(profile)).await {
|
||||||
.map_err(|fallback_err| format!("{err}\n\nFallback with oracle-10g driver failed: {fallback_err}"))?;
|
Ok(mut fallback_client) => {
|
||||||
client = fallback_client;
|
match fallback_client
|
||||||
|
.call_method::<serde_json::Value>(AgentMethod::Connect, connect_params.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
connected_client = Some(fallback_client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(fallback_err) => fallback_errors.push(format!("{profile}: {fallback_err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(fallback_err) => fallback_errors.push(format!("{profile}: {fallback_err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client = connected_client.ok_or_else(|| {
|
||||||
|
format!("{err}\n\nFallback with legacy Oracle drivers failed: {}", fallback_errors.join("\n"))
|
||||||
|
})?;
|
||||||
} else {
|
} else {
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user