From eb53c722032f7a6565cc27abbcf3c0de06e10d87 Mon Sep 17 00:00:00 2001 From: lsy Date: Fri, 22 Nov 2024 13:13:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E5=90=8E=E7=AB=AF=EF=BC=9A=E6=96=B0=E5=A2=9Erocket?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/assets/config.toml | 2 +- backend/src/auth/jwt.rs | 27 ++- backend/src/auth/mod.rs | 2 +- backend/src/config.rs | 24 ++- backend/src/database/mod.rs | 2 +- backend/src/database/relational/builder.rs | 8 +- backend/src/database/relational/mod.rs | 2 +- .../src/database/relational/postgresql/mod.rs | 37 +++-- backend/src/main.rs | 68 ++------ backend/src/manage.rs | 57 +++++++ backend/src/routes/auth/mod.rs | 1 + backend/src/routes/auth/token.rs | 15 ++ backend/src/routes/mod.rs | 11 +- backend/src/routes/person.rs | 70 ++++---- backend/src/routes/theme.rs | 12 ++ frontend/app/entry.client.tsx | 2 +- frontend/app/entry.server.tsx | 18 +- frontend/app/env.d.ts | 20 +-- frontend/contracts/generalContract.ts | 19 ++- frontend/contracts/pluginContract.ts | 25 ++- frontend/contracts/templateContract.ts | 20 +-- frontend/contracts/themeContract.ts | 48 +++--- frontend/hooks/createServiceContext.tsx | 16 +- frontend/hooks/servicesProvider.tsx | 15 +- frontend/package.json | 6 +- frontend/services/apiService.ts | 156 +++++++++--------- frontend/services/capabilityService.ts | 117 +++++++------ frontend/services/pluginService.ts | 86 +++++----- frontend/services/routeManager.ts | 17 +- frontend/services/themeService.ts | 39 ++--- frontend/themes/default/theme.config.ts | 45 ++--- 31 files changed, 525 insertions(+), 462 deletions(-) create mode 100644 backend/src/manage.rs create mode 100644 backend/src/routes/auth/mod.rs create mode 100644 backend/src/routes/auth/token.rs create mode 100644 backend/src/routes/theme.rs diff --git a/backend/assets/config.toml b/backend/assets/config.toml index 6c5aaca..5920086 100644 --- a/backend/assets/config.toml +++ b/backend/assets/config.toml @@ -1,5 +1,5 @@ [info] -install = true +install = false non_relational = false [sql_config] diff --git a/backend/src/auth/jwt.rs b/backend/src/auth/jwt.rs index b567024..cd74b6c 100644 --- a/backend/src/auth/jwt.rs +++ b/backend/src/auth/jwt.rs @@ -1,12 +1,12 @@ -use jwt_compact::{alg::Ed25519, AlgorithmExt, Header, Token, UntrustedToken, TimeOptions}; -use serde::{Serialize, Deserialize}; +use crate::utils::CustomResult; use chrono::{Duration, Utc}; use ed25519_dalek::{SigningKey, VerifyingKey}; +use jwt_compact::{alg::Ed25519, AlgorithmExt, Header, TimeOptions, Token, UntrustedToken}; +use rand::{RngCore, SeedableRng}; +use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::Write; use std::{env, fs}; -use crate::utils::CustomResult; -use rand::{SeedableRng, RngCore}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CustomClaims { @@ -36,9 +36,7 @@ pub fn generate_key() -> CustomResult<()> { let signing_key = SigningKey::from_bytes(&private_key_bytes); let verifying_key = signing_key.verifying_key(); - let base_path = env::current_dir()? - .join("assets") - .join("key"); + let base_path = env::current_dir()?.join("assets").join("key"); fs::create_dir_all(&base_path)?; File::create(base_path.join(SecretKey::Signing.as_string()))? @@ -64,10 +62,7 @@ pub fn generate_jwt(claims: CustomClaims, duration: Duration) -> CustomResult CustomResult { let token = UntrustedToken::new(token)?; let token: Token = Ed25519.validator(&verifying).validate(&token)?; - let time_options = TimeOptions::new( - Duration::seconds(0), - Utc::now - ); - token.claims() + let time_options = TimeOptions::new(Duration::seconds(0), Utc::now); + token + .claims() .validate_expiration(&time_options)? .validate_maturity(&time_options)?; let claims = token.claims().custom.clone(); Ok(claims) -} \ No newline at end of file +} diff --git a/backend/src/auth/mod.rs b/backend/src/auth/mod.rs index 6dbefcf..417233c 100644 --- a/backend/src/auth/mod.rs +++ b/backend/src/auth/mod.rs @@ -1 +1 @@ -pub mod jwt; \ No newline at end of file +pub mod jwt; diff --git a/backend/src/config.rs b/backend/src/config.rs index ccbd8de..082eba1 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -1,21 +1,21 @@ -use serde::{Deserialize,Serialize}; -use std::{ env, fs}; -use std::path::PathBuf; use crate::utils::CustomResult; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::{env, fs}; -#[derive(Deserialize,Serialize,Debug,Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Config { pub info: Info, pub sql_config: SqlConfig, } -#[derive(Deserialize,Serialize,Debug,Clone,)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Info { pub install: bool, pub non_relational: bool, } -#[derive(Deserialize,Serialize,Debug,Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct SqlConfig { pub db_type: String, pub address: String, @@ -25,7 +25,7 @@ pub struct SqlConfig { pub db_name: String, } -#[derive(Deserialize,Serialize,Debug,Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct NoSqlConfig { pub db_type: String, pub address: String, @@ -37,18 +37,16 @@ pub struct NoSqlConfig { impl Config { pub fn read() -> CustomResult { - let path=Self::get_path()?; + let path = Self::get_path()?; Ok(toml::from_str(&fs::read_to_string(path)?)?) } - pub fn write(config:Config) -> CustomResult<()> { - let path=Self::get_path()?; + pub fn write(config: Config) -> CustomResult<()> { + let path = Self::get_path()?; fs::write(path, toml::to_string(&config)?)?; Ok(()) } pub fn get_path() -> CustomResult { - Ok(env::current_dir()? - .join("assets") - .join("config.toml")) + Ok(env::current_dir()?.join("assets").join("config.toml")) } } diff --git a/backend/src/database/mod.rs b/backend/src/database/mod.rs index f421590..81aa43e 100644 --- a/backend/src/database/mod.rs +++ b/backend/src/database/mod.rs @@ -1 +1 @@ -pub mod relational; \ No newline at end of file +pub mod relational; diff --git a/backend/src/database/relational/builder.rs b/backend/src/database/relational/builder.rs index cd38459..cc6e12a 100644 --- a/backend/src/database/relational/builder.rs +++ b/backend/src/database/relational/builder.rs @@ -1,5 +1,5 @@ +use crate::utils::{CustomError, CustomResult}; use regex::Regex; -use crate::utils::{CustomResult,CustomError}; use std::collections::HashMap; use std::hash::Hash; @@ -107,11 +107,7 @@ pub struct WhereCondition { } impl WhereCondition { - pub fn new( - field: String, - operator: Operator, - value: Option, - ) -> CustomResult { + pub fn new(field: String, operator: Operator, value: Option) -> CustomResult { let field = ValidatedValue::new_identifier(field)?; let value = match value { diff --git a/backend/src/database/relational/mod.rs b/backend/src/database/relational/mod.rs index 9c14ff3..bb365cd 100644 --- a/backend/src/database/relational/mod.rs +++ b/backend/src/database/relational/mod.rs @@ -1,8 +1,8 @@ mod postgresql; use crate::config; +use crate::utils::{CustomError, CustomResult}; use async_trait::async_trait; use std::collections::HashMap; -use crate::utils::{CustomResult,CustomError}; use std::sync::Arc; pub mod builder; diff --git a/backend/src/database/relational/postgresql/mod.rs b/backend/src/database/relational/postgresql/mod.rs index b875f29..b761e71 100644 --- a/backend/src/database/relational/postgresql/mod.rs +++ b/backend/src/database/relational/postgresql/mod.rs @@ -1,11 +1,10 @@ -use super::{DatabaseTrait,builder}; +use super::{builder, DatabaseTrait}; use crate::config; +use crate::utils::CustomResult; use async_trait::async_trait; -use sqlx::{Column, PgPool, Row, Executor}; +use sqlx::{Column, Executor, PgPool, Row}; use std::collections::HashMap; use std::{env, fs}; -use crate::utils::CustomResult; - #[derive(Clone)] pub struct Postgresql { @@ -23,12 +22,23 @@ impl DatabaseTrait for Postgresql { .join("init.sql"); let grammar = fs::read_to_string(&path)?; - let connection_str = format!("postgres://{}:{}@{}:{}", db_config.user, db_config.password, db_config.address, db_config.port); + let connection_str = format!( + "postgres://{}:{}@{}:{}", + db_config.user, db_config.password, db_config.address, db_config.port + ); let pool = PgPool::connect(&connection_str).await?; - pool.execute(format!("CREATE DATABASE {}", db_config.db_name).as_str()).await?; + pool.execute(format!("CREATE DATABASE {}", db_config.db_name).as_str()) + .await?; - let new_connection_str = format!("postgres://{}:{}@{}:{}/{}", db_config.user, db_config.password, db_config.address, db_config.port, db_config.db_name); + let new_connection_str = format!( + "postgres://{}:{}@{}:{}/{}", + db_config.user, + db_config.password, + db_config.address, + db_config.port, + db_config.db_name + ); let new_pool = PgPool::connect(&new_connection_str).await?; new_pool.execute(grammar.as_str()).await?; @@ -39,11 +49,14 @@ impl DatabaseTrait for Postgresql { async fn connect(db_config: &config::SqlConfig) -> CustomResult { let connection_str = format!( "postgres://{}:{}@{}:{}/{}", - db_config.user, db_config.password, db_config.address, db_config.port, db_config.db_name + db_config.user, + db_config.password, + db_config.address, + db_config.port, + db_config.db_name ); - let pool = PgPool::connect(&connection_str) - .await?; + let pool = PgPool::connect(&connection_str).await?; Ok(Postgresql { pool }) } @@ -60,9 +73,7 @@ impl DatabaseTrait for Postgresql { sqlx_query = sqlx_query.bind(value); } - let rows = sqlx_query - .fetch_all(&self.pool) - .await?; + let rows = sqlx_query.fetch_all(&self.pool).await?; let mut results = Vec::new(); for row in rows { diff --git a/backend/src/main.rs b/backend/src/main.rs index 385d465..56571d2 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,18 +1,14 @@ +mod auth; mod config; mod database; -mod auth; -mod utils; +mod manage; mod routes; -use chrono::Duration; +mod utils; use database::relational; -use rocket::{ - get, http::Status, launch, response::status, State -}; +use rocket::launch; use std::sync::Arc; use tokio::sync::Mutex; -use utils::{CustomResult, AppResult,CustomError}; - - +use utils::{AppResult, CustomError, CustomResult}; struct AppState { db: Arc>>, @@ -20,7 +16,7 @@ struct AppState { } impl AppState { - async fn get_sql(&self) -> CustomResult { + async fn get_sql(&self) -> CustomResult { self.db .lock() .await @@ -28,34 +24,17 @@ impl AppState { .ok_or_else(|| CustomError::from_str("Database not initialized")) } - async fn link_sql(&self, config: config::SqlConfig) -> Result<(),CustomError> { - let database = relational::Database::link(&config) - .await?; + async fn link_sql(&self, config: &config::SqlConfig) -> CustomResult<()> { + let database = relational::Database::link(config).await?; *self.db.lock().await = Some(database); Ok(()) } - } - - -#[get("/system")] -async fn token_system(_state: &State) -> AppResult> { - let claims = auth::jwt::CustomClaims { - name: "system".into(), - }; - - auth::jwt::generate_jwt(claims, Duration::seconds(1)) - .map(|token| status::Custom(Status::Ok, token)) - .map_err(|e| status::Custom(Status::InternalServerError, e.to_string())) -} - - - #[launch] async fn rocket() -> _ { let config = config::Config::read().expect("Failed to read config"); - + let state = AppState { db: Arc::new(Mutex::new(None)), configure: Arc::new(Mutex::new(config.clone())), @@ -65,35 +44,16 @@ async fn rocket() -> _ { if config.info.install { if let Some(state) = rocket_builder.state::() { - state.link_sql(config.sql_config.clone()) + state + .link_sql(&config.sql_config) .await .expect("Failed to connect to database"); } + } else { + rocket_builder = rocket_builder.mount("/", rocket::routes![routes::intsall::install]); } - if ! config.info.install { - rocket_builder = rocket_builder - .mount("/", rocket::routes![routes::intsall::install]); - } - - rocket_builder = rocket_builder - .mount("/auth/token", rocket::routes![token_system]); + rocket_builder = rocket_builder.mount("/auth/token", routes::jwt_routes()); rocket_builder } - -#[tokio::test] -async fn test_placeholder() { - let config = config::Config::read().expect("Failed to read config"); - - let state = AppState { - db: Arc::new(Mutex::new(None)), - configure: Arc::new(Mutex::new(config.clone())), - }; - state.link_sql(config.sql_config.clone()) - .await - .expect("Failed to connect to database"); - let sql=state.get_sql().await.expect("Failed to get sql"); - let _=routes::person::insert(&sql,routes::person::RegisterData{ name: String::from("test"), email: String::from("lsy22@vip.qq.com"), password:String::from("test") }).await.unwrap(); -} - diff --git a/backend/src/manage.rs b/backend/src/manage.rs new file mode 100644 index 0000000..ba69f4f --- /dev/null +++ b/backend/src/manage.rs @@ -0,0 +1,57 @@ +use rocket::shutdown::Shutdown; +use std::env; +use std::path::Path; +use std::process::{exit, Command}; +use tokio::signal; + +// 应用管理器 +pub struct AppManager { + shutdown: Shutdown, + executable_path: String, +} + +impl AppManager { + pub fn new(shutdown: Shutdown) -> Self { + let executable_path = env::current_exe() + .expect("Failed to get executable path") + .to_string_lossy() + .into_owned(); + + Self { + shutdown, + executable_path, + } + } + + // 优雅关闭 + pub async fn graceful_shutdown(&self) { + println!("Initiating graceful shutdown..."); + + // 触发 Rocket 的优雅关闭 + self.shutdown.notify(); + + // 等待一段时间以确保连接正确关闭 + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + } + + // 重启应用 + pub async fn restart(&self) -> Result<(), Box> { + println!("Preparing to restart application..."); + + // 执行优雅关闭 + self.graceful_shutdown().await; + + // 在新进程中启动应用 + if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", &self.executable_path]) + .spawn()?; + } else { + Command::new(&self.executable_path).spawn()?; + } + + // 退出当前进程 + println!("Application restarting..."); + exit(0); + } +} diff --git a/backend/src/routes/auth/mod.rs b/backend/src/routes/auth/mod.rs new file mode 100644 index 0000000..79c66ba --- /dev/null +++ b/backend/src/routes/auth/mod.rs @@ -0,0 +1 @@ +pub mod token; diff --git a/backend/src/routes/auth/token.rs b/backend/src/routes/auth/token.rs new file mode 100644 index 0000000..9ee568d --- /dev/null +++ b/backend/src/routes/auth/token.rs @@ -0,0 +1,15 @@ +use crate::auth; +use crate::{AppResult, AppState}; +use chrono::Duration; +use rocket::{get, http::Status, response::status, State}; + +#[get("/system")] +pub async fn token_system(_state: &State) -> AppResult> { + let claims = auth::jwt::CustomClaims { + name: "system".into(), + }; + + auth::jwt::generate_jwt(claims, Duration::seconds(1)) + .map(|token| status::Custom(Status::Ok, token)) + .map_err(|e| status::Custom(Status::InternalServerError, e.to_string())) +} diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index c245595..15ea6a8 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -1,10 +1,9 @@ +pub mod auth; pub mod intsall; pub mod person; -use rocket::routes; +pub mod theme; +use rocket::routes; -pub fn create_routes() -> Vec { - routes![ - intsall::install, - ] +pub fn jwt_routes() -> Vec { + routes![auth::token::token_system] } - diff --git a/backend/src/routes/person.rs b/backend/src/routes/person.rs index 6834b9e..3016dec 100644 --- a/backend/src/routes/person.rs +++ b/backend/src/routes/person.rs @@ -1,64 +1,52 @@ -use serde::{Deserialize,Serialize}; -use crate::{config,utils}; -use crate::database::{relational,relational::builder}; -use rocket::{ - get, post, - http::Status, - response::status, - serde::json::Json, - State, -}; -use std::collections::HashMap; -use bcrypt::{hash, DEFAULT_COST}; +use crate::database::{relational, relational::builder}; use crate::utils::CustomResult; - - +use crate::{config, utils}; +use bcrypt::{hash, DEFAULT_COST}; +use rocket::{get, http::Status, post, response::status, serde::json::Json, State}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Deserialize, Serialize)] -pub struct LoginData{ - pub name:String, - pub password:String +pub struct LoginData { + pub name: String, + pub password: String, } -pub struct RegisterData{ - pub name:String, - pub email:String, - pub password:String -} +pub struct RegisterData { + pub name: String, + pub email: String, + pub password: String, +} -pub async fn insert(sql:&relational::Database,data:RegisterData) -> CustomResult<()>{ +pub async fn insert(sql: &relational::Database, data: RegisterData) -> CustomResult<()> { let hashed_password = hash(data.password, DEFAULT_COST).expect("Failed to hash password"); - - let mut user_params=HashMap::new(); + let mut user_params = HashMap::new(); user_params.insert( - builder::ValidatedValue::Identifier(String::from("person_name")) - , - builder::ValidatedValue::PlainText(data.name) + builder::ValidatedValue::Identifier(String::from("person_name")), + builder::ValidatedValue::PlainText(data.name), ); user_params.insert( - builder::ValidatedValue::Identifier(String::from("person_email")) - , - builder::ValidatedValue::PlainText(data.email) + builder::ValidatedValue::Identifier(String::from("person_email")), + builder::ValidatedValue::PlainText(data.email), ); user_params.insert( - builder::ValidatedValue::Identifier(String::from("person_password")) - , - builder::ValidatedValue::PlainText(hashed_password) + builder::ValidatedValue::Identifier(String::from("person_password")), + builder::ValidatedValue::PlainText(hashed_password), ); - let builder = builder::QueryBuilder::new(builder::SqlOperation::Insert,String::from("persons"))? - .params(user_params) - ; + let builder = + builder::QueryBuilder::new(builder::SqlOperation::Insert, String::from("persons"))? + .params(user_params); sql.get_db().execute_query(&builder).await?; Ok(()) } -pub fn delete(){} +pub fn delete() {} -pub fn update(){} +pub fn update() {} -pub fn select(){} +pub fn select() {} -pub fn check(){} \ No newline at end of file +pub fn check() {} diff --git a/backend/src/routes/theme.rs b/backend/src/routes/theme.rs new file mode 100644 index 0000000..1b5a6de --- /dev/null +++ b/backend/src/routes/theme.rs @@ -0,0 +1,12 @@ +use crate::utils::AppResult; +use rocket::{ + http::Status, + post, + response::status, + serde::json::{Json, Value}, +}; + +#[post("/current", format = "application/json", data = "")] +pub fn theme_current(data: Json) -> AppResult>> { + Ok(status::Custom(Status::Ok, Json(Value::Object(())))) +} diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx index 94d5dc0..6129132 100644 --- a/frontend/app/entry.client.tsx +++ b/frontend/app/entry.client.tsx @@ -13,6 +13,6 @@ startTransition(() => { document, - + , ); }); diff --git a/frontend/app/entry.server.tsx b/frontend/app/entry.server.tsx index 45db322..0ffd6cd 100644 --- a/frontend/app/entry.server.tsx +++ b/frontend/app/entry.server.tsx @@ -22,20 +22,20 @@ export default function handleRequest( // This is ignored so we can keep it in the template for visibility. Feel // free to delete this parameter in your app if you're not using it! // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext + loadContext: AppLoadContext, ) { return isbot(request.headers.get("user-agent") || "") ? handleBotRequest( request, responseStatusCode, responseHeaders, - remixContext + remixContext, ) : handleBrowserRequest( request, responseStatusCode, responseHeaders, - remixContext + remixContext, ); } @@ -43,7 +43,7 @@ function handleBotRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext + remixContext: EntryContext, ) { return new Promise((resolve, reject) => { let shellRendered = false; @@ -65,7 +65,7 @@ function handleBotRequest( new Response(stream, { headers: responseHeaders, status: responseStatusCode, - }) + }), ); pipe(body); @@ -82,7 +82,7 @@ function handleBotRequest( console.error(error); } }, - } + }, ); setTimeout(abort, ABORT_DELAY); @@ -93,7 +93,7 @@ function handleBrowserRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext + remixContext: EntryContext, ) { return new Promise((resolve, reject) => { let shellRendered = false; @@ -115,7 +115,7 @@ function handleBrowserRequest( new Response(stream, { headers: responseHeaders, status: responseStatusCode, - }) + }), ); pipe(body); @@ -132,7 +132,7 @@ function handleBrowserRequest( console.error(error); } }, - } + }, ); setTimeout(abort, ABORT_DELAY); diff --git a/frontend/app/env.d.ts b/frontend/app/env.d.ts index 37c0eb2..925cbdb 100644 --- a/frontend/app/env.d.ts +++ b/frontend/app/env.d.ts @@ -7,16 +7,16 @@ /// interface ImportMetaEnv { - readonly VITE_SERVER_API: string; // 用于访问API的基础URL - readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径 - readonly VITE_CONTENT_PATH: string; //mark文章存储的位置 - readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置 - readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径 - readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径 - VITE_SYSTEM_USERNAME: string; // 前端账号名称 - VITE_SYSTEM_PASSWORD: string; // 前端账号密码 + readonly VITE_SERVER_API: string; // 用于访问API的基础URL + readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径 + readonly VITE_CONTENT_PATH: string; //mark文章存储的位置 + readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置 + readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径 + readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径 + VITE_SYSTEM_USERNAME: string; // 前端账号名称 + VITE_SYSTEM_PASSWORD: string; // 前端账号密码 } interface ImportMeta { - readonly env: ImportMetaEnv -} \ No newline at end of file + readonly env: ImportMetaEnv; +} diff --git a/frontend/contracts/generalContract.ts b/frontend/contracts/generalContract.ts index ce6dcd4..08b7f46 100644 --- a/frontend/contracts/generalContract.ts +++ b/frontend/contracts/generalContract.ts @@ -1,9 +1,14 @@ -export type SerializeType = null | number | string | boolean | { [key: string]: SerializeType } | Array; +export type SerializeType = + | null + | number + | string + | boolean + | { [key: string]: SerializeType } + | Array; export interface Configuration { - [key: string]: { - title: string; - description?: string; - data: SerializeType; - }; + [key: string]: { + title: string; + description?: string; + data: SerializeType; + }; } - diff --git a/frontend/contracts/pluginContract.ts b/frontend/contracts/pluginContract.ts index 906dd2f..1b8de80 100644 --- a/frontend/contracts/pluginContract.ts +++ b/frontend/contracts/pluginContract.ts @@ -1,18 +1,17 @@ import { Configuration } from "contracts/generalContract"; export interface PluginConfig { - name: string; - version: string; - displayName: string; + name: string; + version: string; + displayName: string; + description?: string; + author?: string; + enabled: boolean; + icon?: string; + managePath?: string; + configuration?: Configuration; + routs: Set<{ description?: string; - author?: string; - enabled: boolean; - icon?: string; - managePath?: string; - configuration?: Configuration; - routs: Set<{ - description?: string; - path: string; - }>; + path: string; + }>; } - diff --git a/frontend/contracts/templateContract.ts b/frontend/contracts/templateContract.ts index 082dba6..675faba 100644 --- a/frontend/contracts/templateContract.ts +++ b/frontend/contracts/templateContract.ts @@ -1,11 +1,11 @@ export interface TemplateContract { - name: string; - description?: string; - config: { - layout?: string; - styles?: string[]; - scripts?: string[]; - }; - loader: () => Promise; - element: () => React.ReactNode; -} \ No newline at end of file + name: string; + description?: string; + config: { + layout?: string; + styles?: string[]; + scripts?: string[]; + }; + loader: () => Promise; + element: () => React.ReactNode; +} diff --git a/frontend/contracts/themeContract.ts b/frontend/contracts/themeContract.ts index 5104cc7..8378147 100644 --- a/frontend/contracts/themeContract.ts +++ b/frontend/contracts/themeContract.ts @@ -1,30 +1,30 @@ import { Configuration } from "contracts/generalContract"; export interface ThemeConfig { - name: string; - displayName: string; - icon?: string; - version: string; - description?: string; - author?: string; - templates: Map; - globalSettings?: { - layout?: string; - css?: string; - }; - configuration: Configuration; - routes: { - index: string; - post: string; - tag: string; - category: string; - error: string; - loding: string; - page: Map; - } + name: string; + displayName: string; + icon?: string; + version: string; + description?: string; + author?: string; + templates: Map; + globalSettings?: { + layout?: string; + css?: string; + }; + configuration: Configuration; + routes: { + index: string; + post: string; + tag: string; + category: string; + error: string; + loding: string; + page: Map; + }; } export interface ThemeTemplate { - path: string; - name: string; - description?: string; + path: string; + name: string; + description?: string; } diff --git a/frontend/hooks/createServiceContext.tsx b/frontend/hooks/createServiceContext.tsx index 4e20a48..39392c7 100644 --- a/frontend/hooks/createServiceContext.tsx +++ b/frontend/hooks/createServiceContext.tsx @@ -1,6 +1,6 @@ -import { createContext, useContext, ReactNode, FC } from 'react'; +import { createContext, useContext, ReactNode, FC } from "react"; -type ServiceContextReturn = { +type ServiceContextReturn = { [K in `${N}Provider`]: FC<{ children: ReactNode }>; } & { [K in `use${N}`]: () => T; @@ -8,8 +8,8 @@ type ServiceContextReturn = { export function createServiceContext( serviceName: N, - getServiceInstance: () => T -): ServiceContextReturn { + getServiceInstance: () => T, +): ServiceContextReturn { const ServiceContext = createContext(undefined); const Provider: FC<{ children: ReactNode }> = ({ children }) => ( @@ -21,7 +21,9 @@ export function createServiceContext( const useService = (): T => { const context = useContext(ServiceContext); if (context === undefined) { - throw new Error(`use${serviceName} must be used within a ${serviceName}Provider`); + throw new Error( + `use${serviceName} must be used within a ${serviceName}Provider`, + ); } return context; }; @@ -29,5 +31,5 @@ export function createServiceContext( return { [`${serviceName}Provider`]: Provider, [`use${serviceName}`]: useService, - } as ServiceContextReturn; -} \ No newline at end of file + } as ServiceContextReturn; +} diff --git a/frontend/hooks/servicesProvider.tsx b/frontend/hooks/servicesProvider.tsx index 6f16ddd..4898b6f 100644 --- a/frontend/hooks/servicesProvider.tsx +++ b/frontend/hooks/servicesProvider.tsx @@ -5,23 +5,22 @@ import { createServiceContext } from "hooks/createServiceContext"; import { ReactNode } from "react"; export const { CapabilityProvider, useCapability } = createServiceContext( - "Capability", () => CapabilityService.getInstance(), + "Capability", + () => CapabilityService.getInstance(), ); -export const { ThemeProvider, useTheme } = createServiceContext( - "Theme", () => ThemeService.getInstance(), +export const { ThemeProvider, useTheme } = createServiceContext("Theme", () => + ThemeService.getInstance(), ); -export const { ApiProvider, useApi } = createServiceContext( - "Api", () => ThemeService.getInstance(), +export const { ApiProvider, useApi } = createServiceContext("Api", () => + ThemeService.getInstance(), ); export const ServiceProvider = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); diff --git a/frontend/package.json b/frontend/package.json index 8bcbf7d..fc6829e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "remix vite:build", "dev": "remix vite:dev", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "format": "prettier --write \"./**/*.{ts,tsx,js,jsx}\"", + "lint": "eslint \"./**/*.{ts,tsx,js,jsx}\" --fix", "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, @@ -27,13 +28,14 @@ "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "autoprefixer": "^10.4.19", - "eslint": "^8.38.0", + "eslint": "^8.57.1", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8.4.38", + "prettier": "^3.3.3", "tailwindcss": "^3.4.4", "typescript": "^5.1.6", "vite": "^5.1.0", diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 6cd7421..041d38e 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -1,94 +1,96 @@ interface ApiConfig { - baseURL: string; - timeout?: number; + baseURL: string; + timeout?: number; } export class ApiService { - private static instance: ApiService; - private baseURL: string; - private timeout: number; + private static instance: ApiService; + private baseURL: string; + private timeout: number; - private constructor(config: ApiConfig) { - this.baseURL = config.baseURL; - this.timeout = config.timeout || 10000; + private constructor(config: ApiConfig) { + this.baseURL = config.baseURL; + this.timeout = config.timeout || 10000; + } + + public static getInstance(config?: ApiConfig): ApiService { + if (!this.instance && config) { + this.instance = new ApiService(config); + } + return this.instance; + } + + private async getSystemToken(): Promise { + const username = import.meta.env.VITE_SYSTEM_USERNAME; + const password = import.meta.env.VITE_SYSTEM_PASSWORD; + if (!username || !password) { + throw new Error( + "Failed to obtain the username or password of the front-end system", + ); } - public static getInstance(config?: ApiConfig): ApiService { - if (!this.instance && config) { - this.instance = new ApiService(config); - } - return this.instance; + try { + const response = await fetch(`${this.baseURL}/auth/token/system`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username, + password, + }), + }); + + if (!response.ok) { + throw new Error("Failed to get system token"); + } + + const data = await response.text(); + return data; + } catch (error) { + console.error("Error getting system token:", error); + throw error; } + } - private async getSystemToken(): Promise { - const username = import.meta.env.VITE_SYSTEM_USERNAME; - const password = import.meta.env.VITE_SYSTEM_PASSWORD; - if (!username || !password ) { - throw new Error('Failed to obtain the username or password of the front-end system'); - } + public async request( + endpoint: string, + options: RequestInit = {}, + toekn?: string, + ): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); - try { - const response = await fetch(`${this.baseURL}/auth/token/system`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username, - password, - }), - }); + try { + const headers = new Headers(options.headers); - if (!response.ok) { - throw new Error('Failed to get system token'); - } + if (toekn) { + headers.set("Authorization", `Bearer ${toekn}`); + } - const data = await response.text(); - return data; - } catch (error) { - console.error('Error getting system token:', error); - throw error; - } - } - - public async request( - endpoint: string, - options: RequestInit = {}, - toekn ?: string - ): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), this.timeout); - - try { - const headers = new Headers(options.headers); - - if (toekn) { - headers.set('Authorization', `Bearer ${toekn}`); - } - - const response = await fetch(`${this.baseURL}${endpoint}`, { - ...options, - headers, - signal: controller.signal, - }); - - if (!response.ok) { - throw new Error(`API Error: ${response.statusText}`); - } - - const data = await response.json(); - return data as T; - } catch (error: any) { - if (error.name === 'AbortError') { - throw new Error('Request timeout'); - } - throw error; - } finally { - clearTimeout(timeoutId); - } + const response = await fetch(`${this.baseURL}${endpoint}`, { + ...options, + headers, + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.statusText}`); + } + + const data = await response.json(); + return data as T; + } catch (error: any) { + if (error.name === "AbortError") { + throw new Error("Request timeout"); + } + throw error; + } finally { + clearTimeout(timeoutId); } + } } export default ApiService.getInstance({ - baseURL: import.meta.env.VITE_API_BASE_URL, + baseURL: import.meta.env.VITE_API_BASE_URL, }); diff --git a/frontend/services/capabilityService.ts b/frontend/services/capabilityService.ts index 1a2c4be..841e2aa 100644 --- a/frontend/services/capabilityService.ts +++ b/frontend/services/capabilityService.ts @@ -1,66 +1,81 @@ import { CapabilityProps } from "contracts/capabilityContract"; export class CapabilityService { - private capabilities: Map}>> = new Map(); - - private static instance: CapabilityService; + private capabilities: Map< + string, + Set<{ + source: string; + capability: CapabilityProps; + }> + > = new Map(); - private constructor() { } + private static instance: CapabilityService; - public static getInstance(): CapabilityService { - if (!this.instance) { - this.instance = new CapabilityService(); - } - return this.instance; + private constructor() {} + + public static getInstance(): CapabilityService { + if (!this.instance) { + this.instance = new CapabilityService(); } + return this.instance; + } - private register(capabilityName: string, source: string, capability: CapabilityProps) { - const handlers = this.capabilities.get(capabilityName) || new Set(); - handlers.add({ source, capability }); - } + private register( + capabilityName: string, + source: string, + capability: CapabilityProps, + ) { + const handlers = this.capabilities.get(capabilityName) || new Set(); + handlers.add({ source, capability }); + } - private executeCapabilityMethod(capabilityName: string, ...args: any[]): Set { - const results = new Set(); - const handlers = this.capabilities.get(capabilityName); + private executeCapabilityMethod( + capabilityName: string, + ...args: any[] + ): Set { + const results = new Set(); + const handlers = this.capabilities.get(capabilityName); - if (handlers) { - handlers.forEach(({ capability }) => { - const methodFunction = capability['execute']; - if (methodFunction) { - methodFunction(...args) - .then((data) => results.add(data as T)) - .catch((error) => console.error(`Error executing method ${capabilityName}:`, error)); - } - }); - } - return results; - } - - private removeCapability(source: string) { - this.capabilities.forEach((capability_s, capabilityName) => { - const newHandlers = new Set( - Array.from(capability_s).filter(capability => capability.source !== source) + if (handlers) { + handlers.forEach(({ capability }) => { + const methodFunction = capability["execute"]; + if (methodFunction) { + methodFunction(...args) + .then((data) => results.add(data as T)) + .catch((error) => + console.error(`Error executing method ${capabilityName}:`, error), ); - this.capabilities.set(capabilityName, newHandlers); - }); + } + }); + } + return results; + } + + private removeCapability(source: string) { + this.capabilities.forEach((capability_s, capabilityName) => { + const newHandlers = new Set( + Array.from(capability_s).filter( + (capability) => capability.source !== source, + ), + ); + this.capabilities.set(capabilityName, newHandlers); + }); + } + + private removeCapabilitys(capability: string) { + this.capabilities.delete(capability); + } + + public validateCapability(capability: CapabilityProps): boolean { + if (!capability.name || !capability.execute) { + return false; } - private removeCapabilitys(capability: string) { - this.capabilities.delete(capability); + const namePattern = /^[a-z][a-zA-Z0-9_]*$/; + if (!namePattern.test(capability.name)) { + return false; } - public validateCapability(capability: CapabilityProps): boolean { - if (!capability.name || !capability.execute) { - return false; - } - - const namePattern = /^[a-z][a-zA-Z0-9_]*$/; - if (!namePattern.test(capability.name)) { - return false; - } - - return true; - } + return true; + } } diff --git a/frontend/services/pluginService.ts b/frontend/services/pluginService.ts index b36ec4a..7465e4c 100644 --- a/frontend/services/pluginService.ts +++ b/frontend/services/pluginService.ts @@ -1,53 +1,55 @@ -import { PluginConfiguration } from 'types/pluginRequirement'; -import { Contracts } from 'contracts/capabilityContract'; +import { PluginConfiguration } from "types/pluginRequirement"; +import { Contracts } from "contracts/capabilityContract"; export class PluginManager { - private plugins: Map = new Map(); - private configurations: Map = new Map(); - private extensions: Map = new Map(); + private plugins: Map = new Map(); + private configurations: Map = new Map(); + private extensions: Map = new Map(); - async loadPlugins() { - const pluginDirs = await this.scanPluginDirectory(); - - for (const dir of pluginDirs) { - try { - const config = await import(`@/plugins/${dir}/plugin.config.ts`); - const plugin: PluginProps = config.default; - - this.plugins.set(plugin.name, plugin); - - if (plugin.settingsSchema) { - this.configurations.set(plugin.name, plugin.settingsSchema); - } - - if (plugin.extensions) { - Object.entries(plugin.extensions).forEach(([key, value]) => { - this.extensions.set(`${plugin.name}.${key}`, value.extension); - }); - } - - if (plugin.hooks?.onInstall) { - await plugin.hooks.onInstall({}); - } - } catch (error) { - console.error(`Failed to load plugin from directory ${dir}:`, error); - } + async loadPlugins() { + const pluginDirs = await this.scanPluginDirectory(); + + for (const dir of pluginDirs) { + try { + const config = await import(`@/plugins/${dir}/plugin.config.ts`); + const plugin: PluginProps = config.default; + + this.plugins.set(plugin.name, plugin); + + if (plugin.settingsSchema) { + this.configurations.set(plugin.name, plugin.settingsSchema); } - } - async getPluginConfig(pluginName: string): Promise { - const dbConfig = await this.fetchConfigFromDB(pluginName); - if (dbConfig) { - return dbConfig; + if (plugin.extensions) { + Object.entries(plugin.extensions).forEach(([key, value]) => { + this.extensions.set(`${plugin.name}.${key}`, value.extension); + }); } - return this.configurations.get(pluginName); - } - private async fetchConfigFromDB(pluginName: string) { - return null; + if (plugin.hooks?.onInstall) { + await plugin.hooks.onInstall({}); + } + } catch (error) { + console.error(`Failed to load plugin from directory ${dir}:`, error); + } } + } - private async scanPluginDirectory(): Promise { - return []; + async getPluginConfig( + pluginName: string, + ): Promise { + const dbConfig = await this.fetchConfigFromDB(pluginName); + if (dbConfig) { + return dbConfig; } + return this.configurations.get(pluginName); + } + + private async fetchConfigFromDB(pluginName: string) { + return null; + } + + private async scanPluginDirectory(): Promise { + return []; + } } diff --git a/frontend/services/routeManager.ts b/frontend/services/routeManager.ts index e4cd28a..13de00f 100644 --- a/frontend/services/routeManager.ts +++ b/frontend/services/routeManager.ts @@ -1,5 +1,5 @@ -import React from 'react'; // Import React -import { LoaderFunction, RouteObject } from 'react-router-dom'; +import React from "react"; // Import React +import { LoaderFunction, RouteObject } from "react-router-dom"; export class RouteManager { private static instance: RouteManager; @@ -14,18 +14,21 @@ export class RouteManager { return RouteManager.instance; } - - private createRouteElement(path: string,element:React.ReactNode,loader?:LoaderFunction,children?:RouteObject[]) { + private createRouteElement( + path: string, + element: React.ReactNode, + loader?: LoaderFunction, + children?: RouteObject[], + ) { this.routes.push({ path, element, loader, children, - }) + }); } private getRoutes(): RouteObject[] { return this.routes; } - -} \ No newline at end of file +} diff --git a/frontend/services/themeService.ts b/frontend/services/themeService.ts index 1e897df..d3cdf41 100644 --- a/frontend/services/themeService.ts +++ b/frontend/services/themeService.ts @@ -20,12 +20,12 @@ export class ThemeService { public async initialize(): Promise { try { const themeConfig = await this.api.request( - '/theme/current', - { method: 'GET' } + "/theme/current", + { method: "GET" }, ); await this.loadTheme(themeConfig); } catch (error) { - console.error('Failed to initialize theme:', error); + console.error("Failed to initialize theme:", error); throw error; } } @@ -35,14 +35,14 @@ export class ThemeService { this.currentTheme = config; await this.loadTemplates(); } catch (error) { - console.error('Failed to load theme:', error); + console.error("Failed to load theme:", error); throw error; } } private async loadTemplates(): Promise { if (!this.currentTheme) { - throw new Error('No theme configuration loaded'); + throw new Error("No theme configuration loaded"); } const loadTemplate = async (template: ThemeTemplate) => { @@ -56,9 +56,10 @@ export class ThemeService { } }; - const loadPromises = Array.from(this.currentTheme.templates.values()) - .map(template => loadTemplate(template)); - + const loadPromises = Array.from(this.currentTheme.templates.values()).map( + (template) => loadTemplate(template), + ); + await Promise.all(loadPromises); } @@ -76,18 +77,18 @@ export class ThemeService { public getTemplateByRoute(route: string): string { if (!this.currentTheme) { - throw new Error('No theme configuration loaded'); + throw new Error("No theme configuration loaded"); } let templateName: string | undefined; - if (route === '/') { + if (route === "/") { templateName = this.currentTheme.routes.index; - } else if (route.startsWith('/post/')) { + } else if (route.startsWith("/post/")) { templateName = this.currentTheme.routes.post; - } else if (route.startsWith('/tag/')) { + } else if (route.startsWith("/tag/")) { templateName = this.currentTheme.routes.tag; - } else if (route.startsWith('/category/')) { + } else if (route.startsWith("/category/")) { templateName = this.currentTheme.routes.category; } else { templateName = this.currentTheme.routes.page.get(route); @@ -103,20 +104,20 @@ export class ThemeService { public async updateThemeConfig(config: Partial): Promise { try { const updatedConfig = await this.api.request( - '/theme/config', + "/theme/config", { - method: 'PUT', + method: "PUT", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(config), - } + }, ); await this.loadTheme(updatedConfig); } catch (error) { - console.error('Failed to update theme configuration:', error); + console.error("Failed to update theme configuration:", error); throw error; } } -} \ No newline at end of file +} diff --git a/frontend/themes/default/theme.config.ts b/frontend/themes/default/theme.config.ts index d0be2ba..a5bb5a5 100644 --- a/frontend/themes/default/theme.config.ts +++ b/frontend/themes/default/theme.config.ts @@ -1,24 +1,27 @@ import { ThemeConfig } from "contracts/themeContract"; export const themeConfig: ThemeConfig = { - name: 'default', - displayName: '默认主题', - version: '1.0.0', - description: '一个简约风格的博客主题', - author: 'lsy', - entry: './index.tsx', - templates: new Map([ - ['page', { - path: './templates/page', - name: '文章列表模板', - description: '博客首页展示模板' - }], - ]), - - routes: { - post: "", - tag: "", - category: "", - page: "" - } -} \ No newline at end of file + name: "default", + displayName: "默认主题", + version: "1.0.0", + description: "一个简约风格的博客主题", + author: "lsy", + entry: "default", + templates: new Map([ + [ + "page", + { + path: "./templates/page", + name: "文章列表模板", + description: "博客首页展示模板", + }, + ], + ]), + + routes: { + post: "", + tag: "", + category: "", + page: "", + }, +};