mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
Merge branch 'main' of https://github.com/SECTL/SecScore
This commit is contained in:
Generated
+12
-1
@@ -1079,7 +1079,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4242,6 +4242,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6913,6 +6914,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
|
||||
@@ -42,6 +42,9 @@ dirs = "6.0.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
urlencoding = "2.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
|
||||
Binary file not shown.
@@ -335,7 +335,10 @@ pub async fn oauth_get_authorization_url(
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let random_bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
|
||||
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &random_bytes)
|
||||
base64::Engine::encode(
|
||||
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||
&random_bytes,
|
||||
)
|
||||
});
|
||||
|
||||
let url = format!(
|
||||
@@ -345,7 +348,10 @@ pub async fn oauth_get_authorization_url(
|
||||
urlencoding::encode(&state)
|
||||
);
|
||||
|
||||
Ok(IpcResponse::success(OAuthAuthorizationUrlResponse { url, state }))
|
||||
Ok(IpcResponse::success(OAuthAuthorizationUrlResponse {
|
||||
url,
|
||||
state,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -367,9 +373,9 @@ pub async fn oauth_exchange_code(
|
||||
("client_secret", &platform_secret),
|
||||
("redirect_uri", &callback_url),
|
||||
];
|
||||
|
||||
|
||||
println!("[OAuth] 请求参数:{:?}", params);
|
||||
|
||||
|
||||
let response = client
|
||||
.post("https://sectl.top/api/oauth/token")
|
||||
.form(¶ms)
|
||||
@@ -378,8 +384,11 @@ pub async fn oauth_exchange_code(
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
println!("[OAuth] 响应状态:{}", response.status());
|
||||
|
||||
let response_text = response.text().await.map_err(|e| format!("Failed to read response: {}", e))?;
|
||||
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response: {}", e))?;
|
||||
println!("[OAuth] 响应内容:{}", response_text);
|
||||
|
||||
if !response_text.is_empty() && response_text.contains("error") {
|
||||
|
||||
@@ -5,8 +5,9 @@ use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
|
||||
use crate::services::{
|
||||
AutoScoreAction, AutoScoreRule, AutoScoreService, AutoScoreTrigger, PermissionLevel,
|
||||
SettingsKey, SettingsValue,
|
||||
query_execution_batches, rollback_execution_batch, AutoScoreAction, AutoScoreExecutionBatch,
|
||||
AutoScoreExecutionConfig, AutoScoreFilterConfig, AutoScoreRule, AutoScoreService,
|
||||
AutoScoreTrigger, PermissionLevel, SettingsKey, SettingsValue,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -27,7 +28,13 @@ pub struct CreateAutoScoreRule {
|
||||
#[serde(rename = "studentNames")]
|
||||
pub student_names: Vec<String>,
|
||||
pub triggers: Vec<AutoScoreTrigger>,
|
||||
#[serde(rename = "triggerTree", default)]
|
||||
pub trigger_tree: Option<JsonValue>,
|
||||
pub actions: Vec<AutoScoreAction>,
|
||||
#[serde(default)]
|
||||
pub execution: AutoScoreExecutionConfig,
|
||||
#[serde(default)]
|
||||
pub filters: AutoScoreFilterConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -38,7 +45,19 @@ pub struct UpdateAutoScoreRule {
|
||||
#[serde(rename = "studentNames")]
|
||||
pub student_names: Vec<String>,
|
||||
pub triggers: Vec<AutoScoreTrigger>,
|
||||
#[serde(rename = "triggerTree", default)]
|
||||
pub trigger_tree: Option<JsonValue>,
|
||||
pub actions: Vec<AutoScoreAction>,
|
||||
#[serde(default)]
|
||||
pub execution: AutoScoreExecutionConfig,
|
||||
#[serde(default)]
|
||||
pub filters: AutoScoreFilterConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RollbackBatchParams {
|
||||
#[serde(rename = "batchId")]
|
||||
pub batch_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -60,7 +79,10 @@ fn build_rule_from_create(rule: CreateAutoScoreRule) -> AutoScoreRule {
|
||||
enabled: rule.enabled,
|
||||
student_names: rule.student_names,
|
||||
triggers: rule.triggers,
|
||||
trigger_tree: rule.trigger_tree,
|
||||
actions: rule.actions,
|
||||
execution: rule.execution,
|
||||
filters: rule.filters,
|
||||
last_executed: None,
|
||||
}
|
||||
}
|
||||
@@ -72,7 +94,10 @@ fn build_rule_from_update(rule: UpdateAutoScoreRule) -> AutoScoreRule {
|
||||
enabled: rule.enabled,
|
||||
student_names: rule.student_names,
|
||||
triggers: rule.triggers,
|
||||
trigger_tree: rule.trigger_tree,
|
||||
actions: rule.actions,
|
||||
execution: rule.execution,
|
||||
filters: rule.filters,
|
||||
last_executed: None,
|
||||
}
|
||||
}
|
||||
@@ -336,3 +361,38 @@ pub async fn auto_score_sort_rules(
|
||||
|
||||
Ok(IpcResponse::success(true))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn auto_score_query_batches(
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<Vec<AutoScoreExecutionBatch>>, String> {
|
||||
{
|
||||
let state_guard = state.read();
|
||||
let mut permissions = state_guard.permissions.write();
|
||||
if !check_admin_permission(&mut permissions, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
}
|
||||
|
||||
let batches = query_execution_batches(state.inner()).await?;
|
||||
Ok(IpcResponse::success(batches))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn auto_score_rollback_batch(
|
||||
params: RollbackBatchParams,
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<AutoScoreExecutionBatch>, String> {
|
||||
{
|
||||
let state_guard = state.read();
|
||||
let mut permissions = state_guard.permissions.write();
|
||||
if !check_admin_permission(&mut permissions, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
}
|
||||
|
||||
let batch = rollback_execution_batch(state.inner(), ¶ms.batch_id).await?;
|
||||
Ok(IpcResponse::success(batch))
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ pub struct ScoreEvent {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateScoreEvent {
|
||||
#[serde(alias = "studentName")]
|
||||
pub student_name: String,
|
||||
#[serde(alias = "reasonContent")]
|
||||
pub reason_content: String,
|
||||
pub delta: i32,
|
||||
}
|
||||
@@ -44,8 +46,10 @@ pub struct QueryEventParams {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueryByStudentParams {
|
||||
#[serde(alias = "studentName")]
|
||||
pub student_name: String,
|
||||
pub limit: Option<i32>,
|
||||
#[serde(alias = "startTime")]
|
||||
pub start_time: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
@@ -284,3 +285,55 @@ pub async fn fs_file_exists(
|
||||
|
||||
Ok(IpcResponse::success(file_path.exists()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fs_open_path(
|
||||
relative_path: String,
|
||||
folder: String,
|
||||
_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<()>, String> {
|
||||
let folder_type = ConfigFolder::from_str(&folder)
|
||||
.ok_or_else(|| format!("Invalid folder type: {}", folder))?;
|
||||
|
||||
let folder_path = ensure_folder_exists(&folder_type)?;
|
||||
let file_path = folder_path.join(&relative_path);
|
||||
|
||||
if !file_path.exists() {
|
||||
return Ok(IpcResponse::error("File not found"));
|
||||
}
|
||||
|
||||
open_path_with_system(&file_path)?;
|
||||
Ok(IpcResponse::success_empty())
|
||||
}
|
||||
|
||||
fn open_path_with_system(path: &PathBuf) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("cmd")
|
||||
.args(["/C", "start", "", path.to_string_lossy().as_ref()])
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
{
|
||||
Command::new("xdg-open")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Err("Unsupported platform".to_string())
|
||||
}
|
||||
|
||||
@@ -1,342 +1,345 @@
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tauri::Emitter;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::response::IpcResponse;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthServerStartResult {
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthCallbackResult {
|
||||
pub code: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub error_description: Option<String>,
|
||||
}
|
||||
|
||||
static OAUTH_SERVER_SHUTDOWN: once_cell::sync::Lazy<Arc<Mutex<Option<oneshot::Sender<()>>>>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn oauth_start_callback_server(
|
||||
app_handle: tauri::AppHandle,
|
||||
_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<OAuthServerStartResult>, String> {
|
||||
let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await;
|
||||
|
||||
// 如果服务器已经在运行,直接返回 URL
|
||||
if shutdown_tx.is_some() {
|
||||
let port = 16888u16;
|
||||
let url = format!("http://127.0.0.1:{}/oauth/callback", port);
|
||||
return Ok(IpcResponse::success(OAuthServerStartResult { url, port }));
|
||||
}
|
||||
|
||||
let port = 16888u16;
|
||||
let addr: SocketAddr = ([127, 0, 0, 1], port).into();
|
||||
let url = format!("http://127.0.0.1:{}/oauth/callback", port);
|
||||
let url_for_spawn = url.clone();
|
||||
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
*shutdown_tx = Some(tx);
|
||||
drop(shutdown_tx);
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let app = axum::Router::new()
|
||||
.route("/oauth/callback", axum::routing::get(handle_oauth_callback))
|
||||
.layer(axum::extract::Extension(app_handle_clone));
|
||||
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to bind OAuth callback server: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("OAuth callback server started at {}", url_for_spawn);
|
||||
|
||||
let server = axum::serve(listener, app);
|
||||
|
||||
tokio::select! {
|
||||
_ = server => {},
|
||||
_ = rx => {
|
||||
println!("OAuth callback server shutting down");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(IpcResponse::success(OAuthServerStartResult { url, port }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn oauth_stop_callback_server(
|
||||
_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<()>, String> {
|
||||
let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await;
|
||||
|
||||
if let Some(tx) = shutdown_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
Ok(IpcResponse::success(()))
|
||||
}
|
||||
|
||||
async fn handle_oauth_callback(
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
axum::extract::Extension(app_handle): axum::extract::Extension<tauri::AppHandle>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
let code = params.get("code").cloned();
|
||||
let state = params.get("state").cloned();
|
||||
let error = params.get("error").cloned();
|
||||
let error_description = params.get("error_description").cloned();
|
||||
|
||||
println!("[OAuth Callback] 收到回调 - code: {:?}, state: {:?}, error: {:?}", code, state, error);
|
||||
|
||||
let result = OAuthCallbackResult {
|
||||
code: code.clone(),
|
||||
state: state.clone(),
|
||||
error: error.clone(),
|
||||
error_description: error_description.clone(),
|
||||
};
|
||||
|
||||
match app_handle.emit("oauth-callback", result) {
|
||||
Ok(_) => println!("[OAuth Callback] Event 发送成功"),
|
||||
Err(e) => println!("[OAuth Callback] Event 发送失败:{:?}", e),
|
||||
}
|
||||
|
||||
let html = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OAuth 授权完成</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #e4e4e4;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 48px 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4);
|
||||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #a0a0a0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>授权成功</h1>
|
||||
<p>请返回 SecScore 应用查看登录结果</p>
|
||||
<p class="hint">此窗口可以关闭</p>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let error_html = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OAuth 授权失败</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #e4e4e4;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 48px 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(248, 113, 113, 0.4);
|
||||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #a0a0a0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>授权失败</h1>
|
||||
<p>请返回 SecScore 应用查看错误信息</p>
|
||||
<p class="hint">此窗口可以关闭</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let response_html = if error.is_some() { error_html } else { html };
|
||||
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
response_html,
|
||||
)
|
||||
}
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tauri::Emitter;
|
||||
use tauri::State;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::response::IpcResponse;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthServerStartResult {
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthCallbackResult {
|
||||
pub code: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub error_description: Option<String>,
|
||||
}
|
||||
|
||||
static OAUTH_SERVER_SHUTDOWN: once_cell::sync::Lazy<Arc<Mutex<Option<oneshot::Sender<()>>>>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn oauth_start_callback_server(
|
||||
app_handle: tauri::AppHandle,
|
||||
_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<OAuthServerStartResult>, String> {
|
||||
let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await;
|
||||
|
||||
// 如果服务器已经在运行,直接返回 URL
|
||||
if shutdown_tx.is_some() {
|
||||
let port = 16888u16;
|
||||
let url = format!("http://127.0.0.1:{}/oauth/callback", port);
|
||||
return Ok(IpcResponse::success(OAuthServerStartResult { url, port }));
|
||||
}
|
||||
|
||||
let port = 16888u16;
|
||||
let addr: SocketAddr = ([127, 0, 0, 1], port).into();
|
||||
let url = format!("http://127.0.0.1:{}/oauth/callback", port);
|
||||
let url_for_spawn = url.clone();
|
||||
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
*shutdown_tx = Some(tx);
|
||||
drop(shutdown_tx);
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let app = axum::Router::new()
|
||||
.route("/oauth/callback", axum::routing::get(handle_oauth_callback))
|
||||
.layer(axum::extract::Extension(app_handle_clone));
|
||||
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to bind OAuth callback server: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("OAuth callback server started at {}", url_for_spawn);
|
||||
|
||||
let server = axum::serve(listener, app);
|
||||
|
||||
tokio::select! {
|
||||
_ = server => {},
|
||||
_ = rx => {
|
||||
println!("OAuth callback server shutting down");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(IpcResponse::success(OAuthServerStartResult { url, port }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn oauth_stop_callback_server(
|
||||
_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<()>, String> {
|
||||
let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await;
|
||||
|
||||
if let Some(tx) = shutdown_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
Ok(IpcResponse::success(()))
|
||||
}
|
||||
|
||||
async fn handle_oauth_callback(
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
axum::extract::Extension(app_handle): axum::extract::Extension<tauri::AppHandle>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
let code = params.get("code").cloned();
|
||||
let state = params.get("state").cloned();
|
||||
let error = params.get("error").cloned();
|
||||
let error_description = params.get("error_description").cloned();
|
||||
|
||||
println!(
|
||||
"[OAuth Callback] 收到回调 - code: {:?}, state: {:?}, error: {:?}",
|
||||
code, state, error
|
||||
);
|
||||
|
||||
let result = OAuthCallbackResult {
|
||||
code: code.clone(),
|
||||
state: state.clone(),
|
||||
error: error.clone(),
|
||||
error_description: error_description.clone(),
|
||||
};
|
||||
|
||||
match app_handle.emit("oauth-callback", result) {
|
||||
Ok(_) => println!("[OAuth Callback] Event 发送成功"),
|
||||
Err(e) => println!("[OAuth Callback] Event 发送失败:{:?}", e),
|
||||
}
|
||||
|
||||
let html = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OAuth 授权完成</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #e4e4e4;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 48px 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4);
|
||||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #a0a0a0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>授权成功</h1>
|
||||
<p>请返回 SecScore 应用查看登录结果</p>
|
||||
<p class="hint">此窗口可以关闭</p>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let error_html = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OAuth 授权失败</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #e4e4e4;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 48px 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(248, 113, 113, 0.4);
|
||||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #a0a0a0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>授权失败</h1>
|
||||
<p>请返回 SecScore 应用查看错误信息</p>
|
||||
<p class="hint">此窗口可以关闭</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let response_html = if error.is_some() { error_html } else { html };
|
||||
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
response_html,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,14 @@ use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::BTreeSet;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use std::process::Command;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::enums::HKEY_LOCAL_MACHINE;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::RegKey;
|
||||
|
||||
use crate::services::{
|
||||
settings::{
|
||||
@@ -161,20 +166,6 @@ pub async fn settings_set(
|
||||
Ok(IpcResponse::success(()))
|
||||
}
|
||||
|
||||
fn fallback_font_families() -> Vec<String> {
|
||||
vec![
|
||||
"系统默认".to_string(),
|
||||
"Microsoft YaHei UI".to_string(),
|
||||
"Microsoft YaHei".to_string(),
|
||||
"PingFang SC".to_string(),
|
||||
"Noto Sans CJK SC".to_string(),
|
||||
"Source Han Sans SC".to_string(),
|
||||
"Segoe UI".to_string(),
|
||||
"Arial".to_string(),
|
||||
"Helvetica Neue".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn normalize_font_name(value: &str) -> String {
|
||||
value
|
||||
.trim()
|
||||
@@ -188,33 +179,20 @@ fn normalize_font_name(value: &str) -> String {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn query_system_fonts() -> Vec<String> {
|
||||
let output = Command::new("reg")
|
||||
.args([
|
||||
"query",
|
||||
r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
|
||||
])
|
||||
.output();
|
||||
let mut families = BTreeSet::new();
|
||||
|
||||
let Ok(output) = output else {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let key = hklm.open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts");
|
||||
let Ok(key) = key else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !output.status.success() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut families = BTreeSet::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if !line.contains("REG_") {
|
||||
for (value_name, _) in key.enum_values().flatten() {
|
||||
let name = normalize_font_name(&value_name);
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some((left, _)) = line.split_once("REG_") else {
|
||||
continue;
|
||||
};
|
||||
let name = normalize_font_name(left);
|
||||
if !name.is_empty() {
|
||||
families.insert(name);
|
||||
}
|
||||
families.insert(name);
|
||||
}
|
||||
|
||||
families.into_iter().collect()
|
||||
@@ -276,19 +254,5 @@ fn query_system_fonts() -> Vec<String> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn settings_get_system_fonts() -> Result<IpcResponse<Vec<String>>, String> {
|
||||
let mut fonts = query_system_fonts();
|
||||
if fonts.is_empty() {
|
||||
fonts = fallback_font_families();
|
||||
} else {
|
||||
let mut merged = BTreeSet::new();
|
||||
for item in fallback_font_families() {
|
||||
merged.insert(item);
|
||||
}
|
||||
for item in fonts {
|
||||
merged.insert(item);
|
||||
}
|
||||
fonts = merged.into_iter().collect();
|
||||
}
|
||||
|
||||
Ok(IpcResponse::success(fonts))
|
||||
Ok(IpcResponse::success(query_system_fonts()))
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ pub fn run() {
|
||||
auto_score_toggle_rule,
|
||||
auto_score_get_status,
|
||||
auto_score_sort_rules,
|
||||
auto_score_query_batches,
|
||||
auto_score_rollback_batch,
|
||||
board_get_configs,
|
||||
board_save_configs,
|
||||
board_query_sql,
|
||||
@@ -126,6 +128,7 @@ pub fn run() {
|
||||
fs_delete_file,
|
||||
fs_list_files,
|
||||
fs_file_exists,
|
||||
fs_open_path,
|
||||
http_server_start,
|
||||
http_server_stop,
|
||||
http_server_status,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,11 @@ pub mod settings;
|
||||
pub mod theme;
|
||||
|
||||
pub use auth::AuthService;
|
||||
pub use auto_score::{AutoScoreAction, AutoScoreRule, AutoScoreService, AutoScoreTrigger};
|
||||
pub use auto_score::{
|
||||
query_execution_batches, rollback_execution_batch, AutoScoreAction, AutoScoreExecutionBatch,
|
||||
AutoScoreExecutionConfig, AutoScoreFilterConfig, AutoScoreRule, AutoScoreService,
|
||||
AutoScoreTrigger,
|
||||
};
|
||||
pub use data::DataService;
|
||||
pub use logger::LoggerService;
|
||||
pub use permission::{PermissionLevel, PermissionService};
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct SettingsSpec {
|
||||
pub themes_custom: JsonValue,
|
||||
pub auto_score_enabled: bool,
|
||||
pub auto_score_rules: JsonValue,
|
||||
pub auto_score_batches: JsonValue,
|
||||
pub current_theme_id: String,
|
||||
pub dashboards_config: JsonValue,
|
||||
pub pg_connection_string: String,
|
||||
@@ -33,6 +34,7 @@ impl Default for SettingsSpec {
|
||||
themes_custom: JsonValue::Array(vec![]),
|
||||
auto_score_enabled: false,
|
||||
auto_score_rules: JsonValue::Array(vec![]),
|
||||
auto_score_batches: JsonValue::Array(vec![]),
|
||||
current_theme_id: "light-default".to_string(),
|
||||
dashboards_config: JsonValue::Array(vec![]),
|
||||
pg_connection_string: String::new(),
|
||||
@@ -64,6 +66,7 @@ pub enum SettingsKey {
|
||||
ThemesCustom,
|
||||
AutoScoreEnabled,
|
||||
AutoScoreRules,
|
||||
AutoScoreBatches,
|
||||
CurrentThemeId,
|
||||
DashboardsConfig,
|
||||
PgConnectionString,
|
||||
@@ -83,6 +86,7 @@ impl SettingsKey {
|
||||
SettingsKey::ThemesCustom => "themes_custom",
|
||||
SettingsKey::AutoScoreEnabled => "auto_score_enabled",
|
||||
SettingsKey::AutoScoreRules => "auto_score_rules",
|
||||
SettingsKey::AutoScoreBatches => "auto_score_batches",
|
||||
SettingsKey::CurrentThemeId => "current_theme_id",
|
||||
SettingsKey::DashboardsConfig => "dashboards_config",
|
||||
SettingsKey::PgConnectionString => "pg_connection_string",
|
||||
@@ -102,6 +106,7 @@ impl SettingsKey {
|
||||
"themes_custom" => Some(SettingsKey::ThemesCustom),
|
||||
"auto_score_enabled" => Some(SettingsKey::AutoScoreEnabled),
|
||||
"auto_score_rules" => Some(SettingsKey::AutoScoreRules),
|
||||
"auto_score_batches" => Some(SettingsKey::AutoScoreBatches),
|
||||
"current_theme_id" => Some(SettingsKey::CurrentThemeId),
|
||||
"dashboards_config" => Some(SettingsKey::DashboardsConfig),
|
||||
"pg_connection_string" => Some(SettingsKey::PgConnectionString),
|
||||
@@ -378,6 +383,16 @@ impl SettingsService {
|
||||
},
|
||||
);
|
||||
|
||||
defs.insert(
|
||||
SettingsKey::AutoScoreBatches,
|
||||
SettingDefinition {
|
||||
kind: SettingValueKind::Json,
|
||||
default_value: SettingsValue::Json(JsonValue::Array(vec![])),
|
||||
write_permission: PermissionRequirement::Admin,
|
||||
validate: None,
|
||||
},
|
||||
);
|
||||
|
||||
defs.insert(
|
||||
SettingsKey::CurrentThemeId,
|
||||
SettingDefinition {
|
||||
@@ -586,6 +601,10 @@ impl SettingsService {
|
||||
SettingsValue::Json(j) => j,
|
||||
_ => JsonValue::Array(vec![]),
|
||||
},
|
||||
auto_score_batches: match self.get_value(SettingsKey::AutoScoreBatches) {
|
||||
SettingsValue::Json(j) => j,
|
||||
_ => JsonValue::Array(vec![]),
|
||||
},
|
||||
current_theme_id: match self.get_value(SettingsKey::CurrentThemeId) {
|
||||
SettingsValue::String(s) => s,
|
||||
_ => "light-default".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user