feat(desktop): add idle lake and Zhizhi private route
This commit is contained in:
parent
466542ac93
commit
cc802126ea
10 changed files with 385 additions and 34 deletions
|
|
@ -415,10 +415,29 @@ version = "0.18.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
|
|
@ -770,6 +789,15 @@ dependencies = [
|
|||
"syn 2.0.115",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dom_query"
|
||||
version = "0.27.0"
|
||||
|
|
@ -2052,6 +2080,12 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
|
|
@ -2909,6 +2943,22 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||
|
||||
[[package]]
|
||||
name = "publicsuffix"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
|
|
@ -3186,6 +3236,8 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
|
|
@ -3204,6 +3256,7 @@ dependencies = [
|
|||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
|
@ -3380,6 +3433,12 @@ version = "1.0.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
|
@ -3620,6 +3679,18 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.22.0"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ tauri-plugin-updater = "2.10.0"
|
|||
tauri-plugin-dialog = "=2.7.2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
url = "2"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls", "stream"] }
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,27 @@ fn validate_username(raw: &str) -> Result<String, String> {
|
|||
}
|
||||
}
|
||||
|
||||
fn expected_account_for_human_number(number: &str) -> Option<&'static str> {
|
||||
match number {
|
||||
"ICE-GL∞" => Some("bingshuo"),
|
||||
"ICE-GL-ZHI∞" => Some("zhizhi"),
|
||||
"TCS-GL-0005∞" => Some("huaer"),
|
||||
"TCS-GL-0006∞" => Some("yeye"),
|
||||
"TCS-GL-0007∞" => Some("feimao"),
|
||||
"TCS-GL-0008∞" => Some("juzi"),
|
||||
"TCS-GL-0016∞" => Some("awen"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_number_account_binding(number: &str, username: &str) -> Result<(), String> {
|
||||
match expected_account_for_human_number(number) {
|
||||
Some(expected) if username.eq_ignore_ascii_case(expected) => Ok(()),
|
||||
Some(_) => Err("HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH".into()),
|
||||
None => Err("HOLOLAKE_LOGIN_ACCOUNT_BINDING_UNREGISTERED".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动时自查:本机有没有已登录会话(会话文件在,且钥匙串里凭证还在)。
|
||||
#[tauri::command]
|
||||
pub fn check_code_repo_login(
|
||||
|
|
@ -201,10 +222,10 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 企业新账号第一次登录时在 HoloLake 内完成强制换密。
|
||||
/// 新账号第一次登录时在 HoloLake 内完成强制换密。
|
||||
/// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。
|
||||
#[tauri::command]
|
||||
pub async fn change_enterprise_first_login_password(
|
||||
pub async fn change_first_login_password(
|
||||
state: State<'_, ZeroPointState>,
|
||||
username: String,
|
||||
current_password: String,
|
||||
|
|
@ -213,16 +234,25 @@ pub async fn change_enterprise_first_login_password(
|
|||
let Some((human_number, domain)) = zero_point::verified_user_route(&state)? else {
|
||||
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
|
||||
};
|
||||
if domain == "FIFTH_DOMAIN" {
|
||||
return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_ENTERPRISE_ONLY".into());
|
||||
}
|
||||
let username = validate_username(&username)?;
|
||||
validate_number_account_binding(&human_number, &username)?;
|
||||
if current_password.is_empty() || current_password.len() > 512 {
|
||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
if new_password.len() < 14 || new_password.len() > 128 || new_password == current_password {
|
||||
return Err("HOLOLAKE_NEW_PASSWORD_POLICY_INVALID".into());
|
||||
}
|
||||
if domain == "FIFTH_DOMAIN" {
|
||||
if human_number != "ICE-GL-ZHI∞" {
|
||||
return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_NOT_PROVISIONED".into());
|
||||
}
|
||||
rotate_forgejo_forced_password(LOGIN_HOST, &username, ¤t_password, &new_password)
|
||||
.await?;
|
||||
return Ok(PasswordRotationReceipt {
|
||||
username,
|
||||
password_changed: true,
|
||||
});
|
||||
}
|
||||
let response = reqwest::Client::builder()
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.use_rustls_tls()
|
||||
|
|
@ -252,6 +282,61 @@ pub async fn change_enterprise_first_login_password(
|
|||
})
|
||||
}
|
||||
|
||||
async fn rotate_forgejo_forced_password(
|
||||
host: &str,
|
||||
username: &str,
|
||||
current_password: &str,
|
||||
new_password: &str,
|
||||
) -> Result<(), String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
let web_base = format!("https://{host}/code");
|
||||
client
|
||||
.get(format!("{web_base}/user/login"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
let login = client
|
||||
.post(format!("{web_base}/user/login"))
|
||||
.form(&[("user_name", username), ("password", current_password)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
if !login
|
||||
.url()
|
||||
.path()
|
||||
.ends_with("/user/settings/change_password")
|
||||
{
|
||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
let changed = client
|
||||
.post(format!("{web_base}/user/settings/change_password"))
|
||||
.form(&[("password", new_password), ("retype", new_password)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
if !changed.status().is_success() {
|
||||
return Err(format!(
|
||||
"HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_FAILED: {}",
|
||||
changed.status()
|
||||
));
|
||||
}
|
||||
let verify = client
|
||||
.get(format!("https://{host}/code/api/v1/user"))
|
||||
.basic_auth(username, Some(new_password))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
if !verify.status().is_success() {
|
||||
return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_FAILED".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn enterprise_authenticated_post(
|
||||
app: &AppHandle,
|
||||
state: &ZeroPointState,
|
||||
|
|
@ -273,7 +358,9 @@ async fn enterprise_authenticated_post(
|
|||
.use_rustls_tls()
|
||||
.build()
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?
|
||||
.post(format!("https://guanghu.chat/api/hololake/enterprise/{path}"))
|
||||
.post(format!(
|
||||
"https://guanghu.chat/api/hololake/enterprise/{path}"
|
||||
))
|
||||
.basic_auth(&session.username, Some(password))
|
||||
.json(&payload)
|
||||
.send()
|
||||
|
|
@ -283,7 +370,10 @@ async fn enterprise_authenticated_post(
|
|||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}", response.status()));
|
||||
return Err(format!(
|
||||
"HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
response
|
||||
.json()
|
||||
|
|
@ -347,11 +437,12 @@ pub async fn perform_code_repo_login(
|
|||
username: String,
|
||||
password: String,
|
||||
) -> Result<LoginReceipt, String> {
|
||||
let Some((_, domain)) = zero_point::verified_user_route(&state)? else {
|
||||
let Some((human_number, domain)) = zero_point::verified_user_route(&state)? else {
|
||||
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
|
||||
};
|
||||
let login_host = login_host_for_domain(&domain)?;
|
||||
let username = validate_username(&username)?;
|
||||
validate_number_account_binding(&human_number, &username)?;
|
||||
if password.is_empty() || password.len() > 512 {
|
||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
|
|
@ -452,4 +543,17 @@ mod tests {
|
|||
"HOLOLAKE_DOMAIN_ROUTE_INVALID"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_number_and_repository_account_are_one_exact_pair() {
|
||||
assert!(validate_number_account_binding("ICE-GL-ZHI∞", "zhizhi").is_ok());
|
||||
assert_eq!(
|
||||
validate_number_account_binding("ICE-GL-ZHI∞", "bingshuo").unwrap_err(),
|
||||
"HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH"
|
||||
);
|
||||
assert_eq!(
|
||||
validate_number_account_binding("ICE-GL-UNKNOWN∞", "zhizhi").unwrap_err(),
|
||||
"HOLOLAKE_LOGIN_ACCOUNT_BINDING_UNREGISTERED"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ pub fn run() {
|
|||
pncc_server_projection::query_jd_pncc_server_projection,
|
||||
code_repo_login::check_code_repo_login,
|
||||
code_repo_login::perform_code_repo_login,
|
||||
code_repo_login::change_enterprise_first_login_password,
|
||||
code_repo_login::change_first_login_password,
|
||||
code_repo_login::get_enterprise_entry,
|
||||
code_repo_login::confirm_enterprise_persona_relationship,
|
||||
code_repo_login::submit_enterprise_responsibility_receipt,
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ where
|
|||
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?;
|
||||
validate_record(&record, &id, number, session)?;
|
||||
let repository = repository_path(root, &id);
|
||||
let channel_id = register(&repository, native_channel_name(session))?;
|
||||
let channel_id = register(&repository, native_channel_name(number, session))?;
|
||||
project(&repository, record, channel_id).map(Some)
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ where
|
|||
write_json_atomic(&record_path, &record)?;
|
||||
record
|
||||
};
|
||||
let channel_id = register(&repository, native_channel_name(session))?;
|
||||
let channel_id = register(&repository, native_channel_name(number, session))?;
|
||||
project(&repository, record, channel_id)
|
||||
}
|
||||
|
||||
|
|
@ -231,10 +231,17 @@ fn validate_subject(number: &str, session: &LoginSession) -> Result<(), String>
|
|||
.all(|item| item.is_ascii_alphanumeric() || item == '-' || item == '_')
|
||||
&& matches!(
|
||||
session.host.as_str(),
|
||||
"guanghulab.com" | "guanghubingshuo.com"
|
||||
"guanghulab.com" | "guanghubingshuo.com" | "guanghu.chat"
|
||||
);
|
||||
if number_valid && account_valid {
|
||||
let identity_account_bound = match number {
|
||||
"ICE-GL∞" => session.username.eq_ignore_ascii_case("bingshuo"),
|
||||
"ICE-GL-ZHI∞" => session.username.eq_ignore_ascii_case("zhizhi"),
|
||||
_ => true,
|
||||
};
|
||||
if number_valid && account_valid && identity_account_bound {
|
||||
Ok(())
|
||||
} else if number_valid && account_valid {
|
||||
Err("HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH".into())
|
||||
} else {
|
||||
Err("HOLOLAKE_USER_PNCC_SUBJECT_INVALID".into())
|
||||
}
|
||||
|
|
@ -297,11 +304,12 @@ fn initialize_repository(repository: &Path, record: &UserPnccBindingRecord) -> R
|
|||
),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_USER_PNCC_MANIFEST_WRITE_FAILED: {error}"))?;
|
||||
let channel_title = native_channel_title(&record.user_number);
|
||||
fs::write(
|
||||
repository.join("README.md"),
|
||||
format!(
|
||||
"# GH-PNCC · 人格原生代码频道\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎:Git\n- 人类可见上层:HoloLake\n- 远端协作:Forgejo 适配器(尚未绑定远端仓库)\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n",
|
||||
record.account_username, record.user_number
|
||||
"# {}\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎:Git\n- 人类可见上层:HoloLake\n- 远端协作:等待该用户自己的服务器接入\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n",
|
||||
channel_title, record.account_username, record.user_number
|
||||
),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_USER_PNCC_README_WRITE_FAILED: {error}"))?;
|
||||
|
|
@ -355,8 +363,16 @@ fn project(
|
|||
})
|
||||
}
|
||||
|
||||
fn native_channel_name(session: &LoginSession) -> String {
|
||||
format!("{} · GH-PNCC", session.username)
|
||||
fn native_channel_title(number: &str) -> &'static str {
|
||||
match number {
|
||||
"ICE-GL-ZHI∞" => "明天见频道 · GH-PNCC",
|
||||
"ICE-GL∞" => "永恒湖心个人频道 · GH-PNCC",
|
||||
_ => "人格原生代码频道 · GH-PNCC",
|
||||
}
|
||||
}
|
||||
|
||||
fn native_channel_name(number: &str, session: &LoginSession) -> String {
|
||||
format!("{} · {}", native_channel_title(number), session.username)
|
||||
}
|
||||
|
||||
fn write_json_atomic(path: &Path, value: &UserPnccBindingRecord) -> Result<(), String> {
|
||||
|
|
@ -477,4 +493,26 @@ mod tests {
|
|||
assert!(Path::new(&first.local_path).exists());
|
||||
assert!(Path::new(&second.local_path).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhizhi_number_is_bound_to_zhizhi_account_and_mingtianjian_channel() {
|
||||
let root = tempdir().unwrap();
|
||||
let mut zhizhi = session();
|
||||
zhizhi.username = "zhizhi".into();
|
||||
let snapshot = ensure_at(root.path(), "ICE-GL-ZHI∞", &zhizhi, |_, name| {
|
||||
assert!(name.contains("明天见频道"));
|
||||
Ok("mingtianjian".into())
|
||||
})
|
||||
.unwrap();
|
||||
let readme = fs::read_to_string(Path::new(&snapshot.local_path).join("README.md")).unwrap();
|
||||
assert!(readme.contains("明天见频道"));
|
||||
|
||||
let mismatch = ensure_at(root.path(), "ICE-GL-ZHI∞", &session(), |_, _| {
|
||||
Ok("must-not-register".into())
|
||||
});
|
||||
assert_eq!(
|
||||
mismatch.unwrap_err(),
|
||||
"HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue