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`),所以说明接在账户页的登录面板,
|
//! M132:缺 client id 只在**登录入口**失败(`auth_failure`),所以说明接在账户页的登录面板,
|
||||||
//! 不接启动流程——没配 id 的发布版根本建不出微软账户,启动那条路用户撞不到。
|
//! 不接启动流程——没配 id 的发布版根本建不出微软账户,启动那条路用户撞不到。
|
||||||
|
//!
|
||||||
|
//! M236 拆分:两条流的编排搬到 `accounts/login.rs`(纯搬移,路径靠下面的 `pub use` 保住),
|
||||||
|
//! 本文件只留列表/增删/句柄/测试缝——原文件正好 400 行零余量,测试缝换身份前必须先拆。
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use unlucky_core::auth::account::Credentials;
|
use unlucky_core::auth::account::Credentials;
|
||||||
use unlucky_core::auth::microsoft::{
|
use unlucky_core::auth::microsoft::{now_ms, BrowserFallbackReason};
|
||||||
now_ms, BrowserFallbackReason, MicrosoftAuth, MicrosoftSession,
|
|
||||||
};
|
|
||||||
use unlucky_core::auth::offline::offline_meta;
|
use unlucky_core::auth::offline::offline_meta;
|
||||||
use unlucky_core::auth::{AccountStore, AuthError, AuthResult};
|
use unlucky_core::auth::{AccountStore, AuthError};
|
||||||
use unlucky_core::download::blocking::block_on;
|
|
||||||
|
|
||||||
use crate::dto::CoreEvent;
|
use crate::dto::CoreEvent;
|
||||||
use crate::dto::{
|
use crate::dto::{
|
||||||
valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode, AccountView,
|
valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode, AccountView,
|
||||||
BrowserLoginNote, DeviceLoginView, LoginFailure,
|
BrowserLoginNote, LoginFailure,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::env::HostEnv;
|
use super::env::HostEnv;
|
||||||
use super::job::{spawn, HostTask, JobSender};
|
|
||||||
use super::opener;
|
|
||||||
|
|
||||||
/// 第三方轨(服务发现 / 密码登录 / 皮肤抓取)。
|
/// 第三方轨(服务发现 / 密码登录 / 皮肤抓取)。
|
||||||
pub mod authlib;
|
pub mod authlib;
|
||||||
|
|
||||||
|
mod login;
|
||||||
|
|
||||||
mod refresh;
|
mod refresh;
|
||||||
|
|
||||||
mod failure;
|
mod failure;
|
||||||
|
|
||||||
pub use failure::AccountFailure;
|
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};
|
pub use authlib::{fetch_skin, spawn_probe, start_login as start_authlib_login};
|
||||||
|
|
||||||
|
use failure::classify;
|
||||||
|
|
||||||
impl super::CoreHost {
|
impl super::CoreHost {
|
||||||
/// 推一条账户页横幅(一次性操作结果;页面渲染后由用户关掉)。
|
/// 推一条账户页横幅(一次性操作结果;页面渲染后由用户关掉)。
|
||||||
pub(super) fn notice(&mut self, code: AccountNoticeCode, subject: &str, detail: &str) {
|
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 的中文诊断)。
|
/// 回退原因码 → 页面说明码(两边各留一个枚举:presenter 的 DTO 不依赖 core 的中文诊断)。
|
||||||
fn browser_note(reason: BrowserFallbackReason) -> BrowserLoginNote {
|
fn browser_note(reason: BrowserFallbackReason) -> BrowserLoginNote {
|
||||||
match reason {
|
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」这条线。
|
/// 测试缝:记下生产入口**实际请求**的模式,让守卫能钉住「按钮 → browser_first=true」这条线。
|
||||||
///
|
///
|
||||||
/// 为什么需要它:两条入口在没有 client id 的测试构建里都会在网络之前早退,拿不到
|
/// 为什么需要它:两条入口在没有 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)]
|
#[cfg(test)]
|
||||||
mod tests;
|
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::*;
|
||||||
|
|
||||||
|
use super::super::job::{HostTask, JobSender};
|
||||||
use super::failure::mentions_whitelist;
|
use super::failure::mentions_whitelist;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user