refactor(accounts): 登录两条流拆到 accounts/login.rs(纯搬移,M236 前置)
accounts.rs 正好 400 行零余量,本轮要给测试缝换稳定身份,改前必须先拆。
搬移口径:start_login / start_browser_login / msa_auth / fail / finish /
store_session 的每一行(含签名与 doc)与搬移前逐行相同(130 行非空行
diff 为空,只有空行分隔差异);公开路径由 `pub use login::{...}` 保住
(host::accounts::start_login / start_browser_login 不变)。
列表/增删/LoginJob/MsaEntry/测试缝留在父文件:那是测试要用的私有件,
搬走它们就得放宽可见性,纯搬移口径会随之破掉。
This commit is contained in:
1 parent
32951df3d6
commit
30d4163816
3 files changed
+173
-145
No files matched your search
@@ -13,41 +13,43 @@
|
||||
//!
|
||||
//! M132:缺 client id 只在**登录入口**失败(`auth_failure`),所以说明接在账户页的登录面板,
|
||||
//! 不接启动流程——没配 id 的发布版根本建不出微软账户,启动那条路用户撞不到。
|
||||
//!
|
||||
//! M236 拆分:两条流的编排搬到 `accounts/login.rs`(纯搬移,路径靠下面的 `pub use` 保住),
|
||||
//! 本文件只留列表/增删/句柄/测试缝——原文件正好 400 行零余量,测试缝换身份前必须先拆。
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use unlucky_core::auth::account::Credentials;
|
||||
use unlucky_core::auth::microsoft::{
|
||||
now_ms, BrowserFallbackReason, MicrosoftAuth, MicrosoftSession,
|
||||
};
|
||||
use unlucky_core::auth::microsoft::{now_ms, BrowserFallbackReason};
|
||||
use unlucky_core::auth::offline::offline_meta;
|
||||
use unlucky_core::auth::{AccountStore, AuthError, AuthResult};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::auth::{AccountStore, AuthError};
|
||||
|
||||
use crate::dto::CoreEvent;
|
||||
use crate::dto::{
|
||||
valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode, AccountView,
|
||||
BrowserLoginNote, DeviceLoginView, LoginFailure,
|
||||
BrowserLoginNote, LoginFailure,
|
||||
};
|
||||
|
||||
use super::env::HostEnv;
|
||||
use super::job::{spawn, HostTask, JobSender};
|
||||
use super::opener;
|
||||
|
||||
/// 第三方轨(服务发现 / 密码登录 / 皮肤抓取)。
|
||||
pub mod authlib;
|
||||
|
||||
mod login;
|
||||
|
||||
mod refresh;
|
||||
|
||||
mod failure;
|
||||
|
||||
pub use failure::AccountFailure;
|
||||
|
||||
use failure::classify;
|
||||
pub use login::{start_browser_login, start_login};
|
||||
|
||||
pub use authlib::{fetch_skin, spawn_probe, start_login as start_authlib_login};
|
||||
|
||||
use failure::classify;
|
||||
|
||||
impl super::CoreHost {
|
||||
/// 推一条账户页横幅(一次性操作结果;页面渲染后由用户关掉)。
|
||||
pub(super) fn notice(&mut self, code: AccountNoticeCode, subject: &str, detail: &str) {
|
||||
@@ -186,80 +188,6 @@ impl LoginJob {
|
||||
}
|
||||
}
|
||||
|
||||
/// 起一次微软登录(M178:**GUI 的默认路**——设备码)。
|
||||
///
|
||||
/// 设备码流不依赖任何应用注册项,是唯一「在官方产物上开箱可用」的路;它**一个本地监听口都不开**,
|
||||
/// 也就不会撞上 `redirect_uri is not valid`(守卫断言 `listeners_opened == 0`,在 bind 现场加)。
|
||||
pub fn start_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
spawn("msa-login", move || {
|
||||
let auth = match msa_auth(&env, MsaEntry::DeviceCode, &job) {
|
||||
Ok(auth) => auth,
|
||||
Err(error) => return fail(&tx, auth_failure(&error), error.to_string()),
|
||||
};
|
||||
let tx_prompt = tx.clone();
|
||||
let result = block_on(auth.device_code_login(|prompt| {
|
||||
let _ = tx_prompt.send(HostTask::Login(Box::new(DeviceLoginView::waiting(
|
||||
&prompt.user_code,
|
||||
&prompt.verification_uri,
|
||||
prompt.expires_in.as_secs(),
|
||||
&format!("{}#{}", prompt.verification_uri, prompt.user_code),
|
||||
))));
|
||||
}));
|
||||
finish(&env, &tx, &job, result);
|
||||
});
|
||||
}
|
||||
|
||||
/// 起一次「浏览器优先、失败自动回退设备码」的登录(M178:可选加速路)。
|
||||
///
|
||||
/// 只在用户**显式要求**时才走到这里(账户页的「用浏览器登录」)。即使如此也不盲试:`core` 先做
|
||||
/// 回环注册预检,没登记就**不绑端口、不弹浏览器**,直接设备码;试了但失败同样回退并给一句说明。
|
||||
pub fn start_browser_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
spawn("msa-browser", move || {
|
||||
// **P1-1 的那一行**:这里是「用户点了用浏览器登录」,必须把 browser_first 置位,
|
||||
// 否则下面这段预检/回退编排整段不执行(复审 r1 实测的洞)。
|
||||
let auth = match msa_auth(&env, MsaEntry::Browser, &job) {
|
||||
Ok(auth) => auth,
|
||||
Err(error) => return fail(&tx, auth_failure(&error), error.to_string()),
|
||||
};
|
||||
let tx_browser = tx.clone();
|
||||
let tx_device = tx.clone();
|
||||
let result = block_on(auth.browser_then_device_login(
|
||||
|prompt| {
|
||||
// 浏览器拉不起来不是致命错误:面板里还有可复制的授权链接。
|
||||
let opened = opener::open_url(&prompt.authorization_url);
|
||||
let _ =
|
||||
tx_browser.send(HostTask::Login(Box::new(DeviceLoginView::browser_waiting(
|
||||
&prompt.authorization_url,
|
||||
prompt.timeout.as_secs(),
|
||||
opened.err().as_deref(),
|
||||
))));
|
||||
},
|
||||
|prompt, fallback| {
|
||||
let view = DeviceLoginView::waiting(
|
||||
&prompt.user_code,
|
||||
&prompt.verification_uri,
|
||||
prompt.expires_in.as_secs(),
|
||||
&format!("{}#{}", prompt.verification_uri, prompt.user_code),
|
||||
);
|
||||
let view = match fallback {
|
||||
Some(fallback) => {
|
||||
// 回退原文只进日志;面板只看码。
|
||||
eprintln!(
|
||||
"[auth] 浏览器登录未走成({}),已回退设备码:{}",
|
||||
fallback.reason.code(),
|
||||
fallback.detail
|
||||
);
|
||||
view.with_browser_note(browser_note(fallback.reason))
|
||||
}
|
||||
None => view,
|
||||
};
|
||||
let _ = tx_device.send(HostTask::Login(Box::new(view)));
|
||||
},
|
||||
));
|
||||
finish(&env, &tx, &job, result.map(|outcome| outcome.session));
|
||||
});
|
||||
}
|
||||
|
||||
/// 回退原因码 → 页面说明码(两边各留一个枚举:presenter 的 DTO 不依赖 core 的中文诊断)。
|
||||
fn browser_note(reason: BrowserFallbackReason) -> BrowserLoginNote {
|
||||
match reason {
|
||||
@@ -285,21 +213,6 @@ impl MsaEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生产入口的 `MicrosoftAuth`(client id 取环境/构建期,失败日志落 `<数据根>/logs/`)。
|
||||
///
|
||||
/// **`browser_first` 必须在这里按入口置位**(复审 r1 P1-1):只构造默认 `MicrosoftAuth` 的话,
|
||||
/// 浏览器按钮会静默退化成设备码——预检/回退码/说明全不可达。
|
||||
fn msa_auth(env: &HostEnv, entry: MsaEntry, _job: &LoginJob) -> AuthResult<MicrosoftAuth> {
|
||||
#[cfg(test)]
|
||||
test_seam::record(_job, entry);
|
||||
MicrosoftAuth::from_env().map(|auth| {
|
||||
auth.with_browser_first(entry.browser_first())
|
||||
.with_journal(Some(unlucky_core::auth::AuthJournal::in_logs_dir(
|
||||
env.layout.logs(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// 测试缝:记下生产入口**实际请求**的模式,让守卫能钉住「按钮 → browser_first=true」这条线。
|
||||
///
|
||||
/// 为什么需要它:两条入口在没有 client id 的测试构建里都会在网络之前早退,拿不到
|
||||
@@ -349,52 +262,5 @@ fn auth_failure(error: &AuthError) -> LoginFailure {
|
||||
}
|
||||
}
|
||||
|
||||
/// 失败收尾(带**原因码**):原文进日志,页面据码出 i18n(复审 r1 P1-1)。
|
||||
fn fail(tx: &JobSender, failure: LoginFailure, detail: impl Into<String>) {
|
||||
let detail = detail.into();
|
||||
eprintln!("[auth] 微软登录失败({}):{detail}", failure.code());
|
||||
let _ = tx.send(HostTask::Login(Box::new(DeviceLoginView::failed_as(
|
||||
failure, detail,
|
||||
))));
|
||||
}
|
||||
|
||||
/// 收尾:取消就吞掉结果;成功入库;失败带 core 的诊断文案(M38 的 403 也走这条)。
|
||||
fn finish(env: &HostEnv, tx: &JobSender, job: &LoginJob, result: AuthResult<MicrosoftSession>) {
|
||||
if job.is_cancelled() {
|
||||
let _ = tx.send(HostTask::Login(Box::default()));
|
||||
return;
|
||||
}
|
||||
match result {
|
||||
Ok(session) => match store_session(env, session) {
|
||||
Ok(name) => {
|
||||
let _ = tx.send(HostTask::Login(Box::new(DeviceLoginView::succeeded(&name))));
|
||||
let _ = tx.send(HostTask::Accounts(accounts(env, None)));
|
||||
}
|
||||
Err(message) => fail(tx, LoginFailure::StoreFailed, message),
|
||||
},
|
||||
Err(error) => {
|
||||
let failure = classify(&error);
|
||||
fail(tx, failure, error.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 微软会话入库(元数据 + 凭据分文件,见 `core::auth::AccountStore`)。
|
||||
fn store_session(env: &HostEnv, session: MicrosoftSession) -> Result<String, String> {
|
||||
let meta = unlucky_core::auth::account::AccountMeta::new(
|
||||
unlucky_core::auth::account::AccountKind::Microsoft,
|
||||
session.profile_id.clone(),
|
||||
session.profile_name.clone(),
|
||||
now_ms(),
|
||||
);
|
||||
let name = meta.display_name().to_string();
|
||||
let mut store = AccountStore::open(env.layout.auth_data());
|
||||
store
|
||||
.insert(meta, Credentials::Microsoft(session))
|
||||
.and_then(|()| store.save())
|
||||
.map_err(|error| format!("账户入库失败:{error}"))?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,161 @@
|
||||
//! 微软登录的两条流(设备码默认 / 浏览器优先失败回退设备码)+ 会话入库。
|
||||
//!
|
||||
//! 从 `accounts.rs` 拆出来(M236):父文件顶着 400 行硬线,而本轮要给测试缝换一个**稳定身份**
|
||||
//! (`Arc::as_ptr` 会被堆地址复用骗到,见父模块的 `test_seam`),拆出子模块才有地方落。
|
||||
//!
|
||||
//! **纯搬移**:下面每个函数体、每一行签名都与搬移前逐行相同(没有改一个字的可见性);原来的
|
||||
//! 公开路径靠父模块的 `pub use` 保住(`host::accounts::start_login` / `start_browser_login`)。
|
||||
//!
|
||||
//! 两条流除了「怎么拿到 MS access_token」之外完全共用:同一个取消槽位(父模块的 `LoginJob`)、
|
||||
//! 同一段入库(`store_session`)、同一个收尾(`finish`)——所以它们必须住在同一个文件里。
|
||||
|
||||
use unlucky_core::auth::account::Credentials;
|
||||
use unlucky_core::auth::microsoft::{now_ms, MicrosoftAuth, MicrosoftSession};
|
||||
use unlucky_core::auth::{AccountStore, AuthResult};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
|
||||
use crate::dto::{DeviceLoginView, LoginFailure};
|
||||
|
||||
use super::super::env::HostEnv;
|
||||
use super::super::job::{spawn, HostTask, JobSender};
|
||||
use super::super::opener;
|
||||
use super::failure::classify;
|
||||
#[cfg(test)]
|
||||
use super::test_seam;
|
||||
use super::{accounts, auth_failure, browser_note, LoginJob, MsaEntry};
|
||||
|
||||
/// 起一次微软登录(M178:**GUI 的默认路**——设备码)。
|
||||
///
|
||||
/// 设备码流不依赖任何应用注册项,是唯一「在官方产物上开箱可用」的路;它**一个本地监听口都不开**,
|
||||
/// 也就不会撞上 `redirect_uri is not valid`(守卫断言 `listeners_opened == 0`,在 bind 现场加)。
|
||||
pub fn start_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
spawn("msa-login", move || {
|
||||
let auth = match msa_auth(&env, MsaEntry::DeviceCode, &job) {
|
||||
Ok(auth) => auth,
|
||||
Err(error) => return fail(&tx, auth_failure(&error), error.to_string()),
|
||||
};
|
||||
let tx_prompt = tx.clone();
|
||||
let result = block_on(auth.device_code_login(|prompt| {
|
||||
let _ = tx_prompt.send(HostTask::Login(Box::new(DeviceLoginView::waiting(
|
||||
&prompt.user_code,
|
||||
&prompt.verification_uri,
|
||||
prompt.expires_in.as_secs(),
|
||||
&format!("{}#{}", prompt.verification_uri, prompt.user_code),
|
||||
))));
|
||||
}));
|
||||
finish(&env, &tx, &job, result);
|
||||
});
|
||||
}
|
||||
|
||||
/// 起一次「浏览器优先、失败自动回退设备码」的登录(M178:可选加速路)。
|
||||
///
|
||||
/// 只在用户**显式要求**时才走到这里(账户页的「用浏览器登录」)。即使如此也不盲试:`core` 先做
|
||||
/// 回环注册预检,没登记就**不绑端口、不弹浏览器**,直接设备码;试了但失败同样回退并给一句说明。
|
||||
pub fn start_browser_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
spawn("msa-browser", move || {
|
||||
// **P1-1 的那一行**:这里是「用户点了用浏览器登录」,必须把 browser_first 置位,
|
||||
// 否则下面这段预检/回退编排整段不执行(复审 r1 实测的洞)。
|
||||
let auth = match msa_auth(&env, MsaEntry::Browser, &job) {
|
||||
Ok(auth) => auth,
|
||||
Err(error) => return fail(&tx, auth_failure(&error), error.to_string()),
|
||||
};
|
||||
let tx_browser = tx.clone();
|
||||
let tx_device = tx.clone();
|
||||
let result = block_on(auth.browser_then_device_login(
|
||||
|prompt| {
|
||||
// 浏览器拉不起来不是致命错误:面板里还有可复制的授权链接。
|
||||
let opened = opener::open_url(&prompt.authorization_url);
|
||||
let _ =
|
||||
tx_browser.send(HostTask::Login(Box::new(DeviceLoginView::browser_waiting(
|
||||
&prompt.authorization_url,
|
||||
prompt.timeout.as_secs(),
|
||||
opened.err().as_deref(),
|
||||
))));
|
||||
},
|
||||
|prompt, fallback| {
|
||||
let view = DeviceLoginView::waiting(
|
||||
&prompt.user_code,
|
||||
&prompt.verification_uri,
|
||||
prompt.expires_in.as_secs(),
|
||||
&format!("{}#{}", prompt.verification_uri, prompt.user_code),
|
||||
);
|
||||
let view = match fallback {
|
||||
Some(fallback) => {
|
||||
// 回退原文只进日志;面板只看码。
|
||||
eprintln!(
|
||||
"[auth] 浏览器登录未走成({}),已回退设备码:{}",
|
||||
fallback.reason.code(),
|
||||
fallback.detail
|
||||
);
|
||||
view.with_browser_note(browser_note(fallback.reason))
|
||||
}
|
||||
None => view,
|
||||
};
|
||||
let _ = tx_device.send(HostTask::Login(Box::new(view)));
|
||||
},
|
||||
));
|
||||
finish(&env, &tx, &job, result.map(|outcome| outcome.session));
|
||||
});
|
||||
}
|
||||
|
||||
/// 生产入口的 `MicrosoftAuth`(client id 取环境/构建期,失败日志落 `<数据根>/logs/`)。
|
||||
///
|
||||
/// **`browser_first` 必须在这里按入口置位**(复审 r1 P1-1):只构造默认 `MicrosoftAuth` 的话,
|
||||
/// 浏览器按钮会静默退化成设备码——预检/回退码/说明全不可达。
|
||||
fn msa_auth(env: &HostEnv, entry: MsaEntry, _job: &LoginJob) -> AuthResult<MicrosoftAuth> {
|
||||
#[cfg(test)]
|
||||
test_seam::record(_job, entry);
|
||||
MicrosoftAuth::from_env().map(|auth| {
|
||||
auth.with_browser_first(entry.browser_first())
|
||||
.with_journal(Some(unlucky_core::auth::AuthJournal::in_logs_dir(
|
||||
env.layout.logs(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// 失败收尾(带**原因码**):原文进日志,页面据码出 i18n(复审 r1 P1-1)。
|
||||
fn fail(tx: &JobSender, failure: LoginFailure, detail: impl Into<String>) {
|
||||
let detail = detail.into();
|
||||
eprintln!("[auth] 微软登录失败({}):{detail}", failure.code());
|
||||
let _ = tx.send(HostTask::Login(Box::new(DeviceLoginView::failed_as(
|
||||
failure, detail,
|
||||
))));
|
||||
}
|
||||
|
||||
/// 收尾:取消就吞掉结果;成功入库;失败带 core 的诊断文案(M38 的 403 也走这条)。
|
||||
fn finish(env: &HostEnv, tx: &JobSender, job: &LoginJob, result: AuthResult<MicrosoftSession>) {
|
||||
if job.is_cancelled() {
|
||||
let _ = tx.send(HostTask::Login(Box::default()));
|
||||
return;
|
||||
}
|
||||
match result {
|
||||
Ok(session) => match store_session(env, session) {
|
||||
Ok(name) => {
|
||||
let _ = tx.send(HostTask::Login(Box::new(DeviceLoginView::succeeded(&name))));
|
||||
let _ = tx.send(HostTask::Accounts(accounts(env, None)));
|
||||
}
|
||||
Err(message) => fail(tx, LoginFailure::StoreFailed, message),
|
||||
},
|
||||
Err(error) => {
|
||||
let failure = classify(&error);
|
||||
fail(tx, failure, error.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 微软会话入库(元数据 + 凭据分文件,见 `core::auth::AccountStore`)。
|
||||
fn store_session(env: &HostEnv, session: MicrosoftSession) -> Result<String, String> {
|
||||
let meta = unlucky_core::auth::account::AccountMeta::new(
|
||||
unlucky_core::auth::account::AccountKind::Microsoft,
|
||||
session.profile_id.clone(),
|
||||
session.profile_name.clone(),
|
||||
now_ms(),
|
||||
);
|
||||
let name = meta.display_name().to_string();
|
||||
let mut store = AccountStore::open(env.layout.auth_data());
|
||||
store
|
||||
.insert(meta, Credentials::Microsoft(session))
|
||||
.and_then(|()| store.save())
|
||||
.map_err(|error| format!("账户入库失败:{error}"))?;
|
||||
Ok(name)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use unlucky_core::auth::AuthError;
|
||||
|
||||
use super::*;
|
||||
|
||||
use super::super::job::{HostTask, JobSender};
|
||||
use super::failure::mentions_whitelist;
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user