Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions src-tauri/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,38 @@ pub fn reset_anisette_state() -> Result<bool, AppError> {
}
}

fn parse_two_factor_response(
payload: &str,
) -> Result<TwoFactorCallbackResponse, serde_json::Error> {
serde_json::from_str(payload)
}

async fn request_two_factor_response(
window: &Window,
params: TwoFactorCallbackParams,
) -> Result<TwoFactorCallbackResponse, Report> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handler_id = window.listen("2fa-response", move |event| {
let response =
parse_two_factor_response(event.payload()).map_err(|error| error.to_string());
let _ = tx.send(response);
});

if let Err(error) = window.emit("2fa-required", params) {
window.unlisten(handler_id);
return Err(report!("Failed to emit 2fa-required event: {error}"));
}

let response = tokio::time::timeout(Duration::from_secs(300), rx.recv()).await;
window.unlisten(handler_id);

match response {
Ok(Some(Ok(response))) => Ok(response),
Ok(Some(Err(error))) => Err(report!("Invalid 2FA response: {error}")),
Ok(None) | Err(_) => Ok(TwoFactorCallbackResponse::Abort),
}
}

async fn login(
app: &AppHandle,
window: &Window,
Expand All @@ -167,24 +199,7 @@ async fn login(
move |params: TwoFactorCallbackParams| {
let window_clone = window_clone.clone();

async move {
window_clone
.emit("2fa-required", params)
.context("Failed to emit 2fa-required event")?;

let (tx, rx) = std::sync::mpsc::channel::<String>();
let handler_id = window_clone.listen("2fa-recieved", move |event| {
let code = event.payload();
let _ = tx.send(code.to_string());
});

let result = rx.recv_timeout(Duration::from_secs(120))?;
window_clone.unlisten(handler_id);

let code = result.trim_matches('"').to_string();
Ok(TwoFactorCallbackResponse::SubmitCode(code))
}
.boxed()
async move { request_two_factor_response(&window_clone, params).await }.boxed()
}
};

Expand Down Expand Up @@ -253,6 +268,42 @@ async fn login(
Ok(sideloader)
}

#[cfg(test)]
mod tests {
use super::{TwoFactorCallbackResponse, parse_two_factor_response};

#[test]
fn parses_all_two_factor_response_variants() {
let cases = [
r#""Abort""#,
r#""ResendCode""#,
r#""SendToDevices""#,
r#"{"SubmitCode":"123456"}"#,
r#"{"SendSms":2}"#,
];

for payload in cases {
assert!(
parse_two_factor_response(payload).is_ok(),
"failed to parse {payload}"
);
}
}

#[test]
fn preserves_two_factor_response_payloads() {
match parse_two_factor_response(r#"{"SubmitCode":"123456"}"#).unwrap() {
TwoFactorCallbackResponse::SubmitCode(code) => assert_eq!(code, "123456"),
_ => panic!("expected SubmitCode"),
}

match parse_two_factor_response(r#"{"SendSms":2}"#).unwrap() {
TwoFactorCallbackResponse::SendSms(id) => assert_eq!(id, 2),
_ => panic!("expected SendSms"),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CertificateInfo {
Expand Down
45 changes: 45 additions & 0 deletions src/AppleID.css
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,48 @@
.credentials-warning {
margin: 0;
}

.tfa-dialog {
display: flex;
flex-direction: column;
gap: 0.9rem;
min-width: min(26rem, 80vw);
}

.tfa-dialog h2,
.tfa-dialog p {
margin: 0;
}

.tfa-error {
padding: 0.75rem;
border: 1px solid rgba(255, 69, 58, 0.55);
border-radius: 10px;
color: var(--danger);
background: rgba(255, 69, 58, 0.12);
white-space: pre-wrap;
}

.tfa-code-form,
.tfa-actions,
.tfa-phone-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
}

.tfa-code-form input,
.tfa-code-form button,
.tfa-actions button,
.tfa-phone-list button {
width: 100%;
}

.tfa-phone-list {
padding-top: 0.25rem;
}

.tfa-phone-list-label {
color: var(--text-muted);
font-size: 0.9rem;
}
Loading