mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Feat: Windows-Hello Testing (DevTools Only)
This commit is contained in:
Generated
+65
@@ -2111,6 +2111,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
"windows 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5472,6 +5473,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
@@ -5506,6 +5517,19 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -5554,6 +5578,17 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"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]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
@@ -5576,6 +5611,17 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"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]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.3"
|
version = "0.59.3"
|
||||||
@@ -5629,6 +5675,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -5647,6 +5702,16 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"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]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ urlencoding = "2"
|
|||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
reqwest = { version = "0.12", features = ["blocking"] }
|
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]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
lto = true
|
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,4 +1,5 @@
|
|||||||
pub mod backup;
|
pub mod backup;
|
||||||
|
pub mod biometric;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub fn run() {
|
|||||||
commands::backup::get_auto_backup_dir,
|
commands::backup::get_auto_backup_dir,
|
||||||
commands::updater::list_releases,
|
commands::updater::list_releases,
|
||||||
commands::updater::download_and_install_update,
|
commands::updater::download_and_install_update,
|
||||||
|
commands::biometric::windows_hello_authenticate,
|
||||||
|
commands::biometric::windows_hello_available,
|
||||||
])
|
])
|
||||||
.setup(|_app| Ok(()))
|
.setup(|_app| Ok(()))
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -2,17 +2,21 @@
|
|||||||
import ThreeDCard from "@shared/manga/ThreeDCard.svelte";
|
import ThreeDCard from "@shared/manga/ThreeDCard.svelte";
|
||||||
import { store, addToast } from "@store/state.svelte";
|
import { store, addToast } from "@store/state.svelte";
|
||||||
import { cache } from "@core/cache/index";
|
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; }
|
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 splashTriggered = $state(false);
|
||||||
let expOpen = $state(false);
|
let expOpen = $state(false);
|
||||||
let appVersion = $state("…");
|
let appVersion = $state("…");
|
||||||
|
let helloAvailable = $state<boolean | null>(null);
|
||||||
|
let helloBusy = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {});
|
import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {});
|
||||||
refreshPerfMetrics();
|
refreshPerfMetrics();
|
||||||
|
invoke<boolean>("windows_hello_available").then(v => helloAvailable = v).catch(() => helloAvailable = false);
|
||||||
});
|
});
|
||||||
|
|
||||||
function refreshPerfMetrics() {
|
function refreshPerfMetrics() {
|
||||||
@@ -49,6 +53,18 @@
|
|||||||
setTimeout(() => splashTriggered = false, 200);
|
setTimeout(() => splashTriggered = false, 200);
|
||||||
(window as any).__mokuShowSplash?.();
|
(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>
|
</script>
|
||||||
|
|
||||||
<div class="s-panel">
|
<div class="s-panel">
|
||||||
@@ -81,6 +97,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="s-section">
|
||||||
<button class="s-collapsible-trigger" onclick={() => expOpen = !expOpen} aria-expanded={expOpen}>
|
<button class="s-collapsible-trigger" onclick={() => expOpen = !expOpen} aria-expanded={expOpen}>
|
||||||
<span class="s-label">Experimental</span>
|
<span class="s-label">Experimental</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user