mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Feat: Windows-Hello Testing (DevTools Only)
This commit is contained in:
Generated
+65
@@ -2111,6 +2111,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
"walkdir",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5472,6 +5473,16 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
@@ -5506,6 +5517,19 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -5554,6 +5578,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
@@ -5576,6 +5611,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
@@ -5629,6 +5675,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -5647,6 +5702,16 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
|
||||
@@ -32,6 +32,12 @@ urlencoding = "2"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Security_Credentials_UI",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_hello {
|
||||
use windows::{
|
||||
core::HSTRING,
|
||||
Security::Credentials::UI::{UserConsentVerificationResult, UserConsentVerifier},
|
||||
Win32::UI::WindowsAndMessaging::{
|
||||
BringWindowToTop, FindWindowW, IsIconic, SetForegroundWindow, ShowWindow, SW_RESTORE,
|
||||
},
|
||||
};
|
||||
|
||||
fn to_wide(s: &str) -> Vec<u16> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn try_focus_hello_dialog() -> bool {
|
||||
let cls = to_wide("Credential Dialog Xaml Host");
|
||||
unsafe {
|
||||
let Ok(hwnd) = FindWindowW(
|
||||
windows::core::PCWSTR(cls.as_ptr()),
|
||||
windows::core::PCWSTR::null(),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
if IsIconic(hwnd).as_bool() {
|
||||
let _ = ShowWindow(hwnd, SW_RESTORE);
|
||||
}
|
||||
let _ = BringWindowToTop(hwnd);
|
||||
let _ = SetForegroundWindow(hwnd);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn nudge_focus(retries: u32, delay_ms: u64) {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
||||
for _ in 0..retries {
|
||||
if try_focus_hello_dialog() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn authenticate(reason: &str) -> Result<(), String> {
|
||||
nudge_focus(5, 250);
|
||||
let result = UserConsentVerifier::RequestVerificationAsync(&HSTRING::from(reason))
|
||||
.and_then(|op| {
|
||||
nudge_focus(5, 250);
|
||||
op.get()
|
||||
})
|
||||
.map_err(|e| format!("internalError:{e:?}"))?;
|
||||
|
||||
match result {
|
||||
UserConsentVerificationResult::Verified => Ok(()),
|
||||
UserConsentVerificationResult::Canceled => Err("userCancel".into()),
|
||||
UserConsentVerificationResult::RetriesExhausted => Err("biometryLockout".into()),
|
||||
UserConsentVerificationResult::DeviceBusy => Err("systemCancel".into()),
|
||||
UserConsentVerificationResult::DeviceNotPresent => Err("biometryNotAvailable".into()),
|
||||
UserConsentVerificationResult::DisabledByPolicy => Err("biometryNotAvailable".into()),
|
||||
UserConsentVerificationResult::NotConfiguredForUser => Err("biometryNotEnrolled".into()),
|
||||
_ => Err("authenticationFailed".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_available() -> bool {
|
||||
use windows::Security::Credentials::UI::UserConsentVerifierAvailability;
|
||||
UserConsentVerifier::CheckAvailabilityAsync()
|
||||
.and_then(|op| op.get())
|
||||
.map(|a| a == UserConsentVerifierAvailability::Available)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn windows_hello_authenticate(_reason: String) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows_hello::authenticate(&reason);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Err("notSupported".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn windows_hello_available() -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows_hello::is_available();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
false
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod backup;
|
||||
pub mod biometric;
|
||||
pub mod server;
|
||||
pub mod storage;
|
||||
pub mod system;
|
||||
pub mod updater;
|
||||
pub mod updater;
|
||||
@@ -41,6 +41,8 @@ pub fn run() {
|
||||
commands::backup::get_auto_backup_dir,
|
||||
commands::updater::list_releases,
|
||||
commands::updater::download_and_install_update,
|
||||
commands::biometric::windows_hello_authenticate,
|
||||
commands::biometric::windows_hello_available,
|
||||
])
|
||||
.setup(|_app| Ok(()))
|
||||
.on_window_event(|window, event| {
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
import ThreeDCard from "@shared/manga/ThreeDCard.svelte";
|
||||
import { store, addToast } from "@store/state.svelte";
|
||||
import { cache } from "@core/cache/index";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface PerfSnapshot { cacheEntries: number; cacheKeys: string[]; oldestEntryMs: number | null; newestEntryMs: number | null; }
|
||||
|
||||
let perfSnapshot = $state<PerfSnapshot | null>(null);
|
||||
let perfSnapshot = $state<PerfSnapshot | null>(null);
|
||||
let splashTriggered = $state(false);
|
||||
let expOpen = $state(false);
|
||||
let appVersion = $state("…");
|
||||
let helloAvailable = $state<boolean | null>(null);
|
||||
let helloBusy = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {});
|
||||
refreshPerfMetrics();
|
||||
invoke<boolean>("windows_hello_available").then(v => helloAvailable = v).catch(() => helloAvailable = false);
|
||||
});
|
||||
|
||||
function refreshPerfMetrics() {
|
||||
@@ -49,6 +53,18 @@
|
||||
setTimeout(() => splashTriggered = false, 200);
|
||||
(window as any).__mokuShowSplash?.();
|
||||
}
|
||||
|
||||
async function testWindowsHello() {
|
||||
helloBusy = true;
|
||||
try {
|
||||
await invoke("windows_hello_authenticate", { reason: "Moku devtools test" });
|
||||
addToast({ kind: "success", title: "Windows Hello", body: "Verified successfully" });
|
||||
} catch (e: any) {
|
||||
addToast({ kind: "error", title: "Windows Hello", body: String(e) });
|
||||
} finally {
|
||||
helloBusy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="s-panel">
|
||||
@@ -81,6 +97,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Biometrics</p>
|
||||
<div class="s-section-body">
|
||||
<div class="s-row">
|
||||
<div class="s-row-info">
|
||||
<span class="s-label">Windows Hello</span>
|
||||
<span class="s-desc">Available: {helloAvailable === null ? "…" : helloAvailable ? "yes" : "no"}</span>
|
||||
</div>
|
||||
<button class="s-btn" disabled={!helloAvailable || helloBusy} onclick={testWindowsHello}>
|
||||
{helloBusy ? "…" : "Test"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-section">
|
||||
<button class="s-collapsible-trigger" onclick={() => expOpen = !expOpen} aria-expanded={expOpen}>
|
||||
<span class="s-label">Experimental</span>
|
||||
|
||||
Reference in New Issue
Block a user