格式化代码,后端:新增rocket实例管理

This commit is contained in:
lsy 2024-11-22 13:13:04 +08:00
parent 4bf55506b9
commit eb53c72203
31 changed files with 525 additions and 462 deletions

View File

@ -1,5 +1,5 @@
[info] [info]
install = true install = false
non_relational = false non_relational = false
[sql_config] [sql_config]

View File

@ -1,12 +1,12 @@
use jwt_compact::{alg::Ed25519, AlgorithmExt, Header, Token, UntrustedToken, TimeOptions}; use crate::utils::CustomResult;
use serde::{Serialize, Deserialize};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use ed25519_dalek::{SigningKey, VerifyingKey}; 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::fs::File;
use std::io::Write; use std::io::Write;
use std::{env, fs}; use std::{env, fs};
use crate::utils::CustomResult;
use rand::{SeedableRng, RngCore};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CustomClaims { pub struct CustomClaims {
@ -36,9 +36,7 @@ pub fn generate_key() -> CustomResult<()> {
let signing_key = SigningKey::from_bytes(&private_key_bytes); let signing_key = SigningKey::from_bytes(&private_key_bytes);
let verifying_key = signing_key.verifying_key(); let verifying_key = signing_key.verifying_key();
let base_path = env::current_dir()? let base_path = env::current_dir()?.join("assets").join("key");
.join("assets")
.join("key");
fs::create_dir_all(&base_path)?; fs::create_dir_all(&base_path)?;
File::create(base_path.join(SecretKey::Signing.as_string()))? File::create(base_path.join(SecretKey::Signing.as_string()))?
@ -64,10 +62,7 @@ pub fn generate_jwt(claims: CustomClaims, duration: Duration) -> CustomResult<St
let key_bytes = get_key(SecretKey::Signing)?; let key_bytes = get_key(SecretKey::Signing)?;
let signing_key = SigningKey::from_bytes(&key_bytes); let signing_key = SigningKey::from_bytes(&key_bytes);
let time_options = TimeOptions::new( let time_options = TimeOptions::new(Duration::seconds(0), Utc::now);
Duration::seconds(0),
Utc::now
);
let claims = jwt_compact::Claims::new(claims) let claims = jwt_compact::Claims::new(claims)
.set_duration_and_issuance(&time_options, duration) .set_duration_and_issuance(&time_options, duration)
.set_not_before(Utc::now()); .set_not_before(Utc::now());
@ -85,14 +80,12 @@ pub fn validate_jwt(token: &str) -> CustomResult<CustomClaims> {
let token = UntrustedToken::new(token)?; let token = UntrustedToken::new(token)?;
let token: Token<CustomClaims> = Ed25519.validator(&verifying).validate(&token)?; let token: Token<CustomClaims> = Ed25519.validator(&verifying).validate(&token)?;
let time_options = TimeOptions::new( let time_options = TimeOptions::new(Duration::seconds(0), Utc::now);
Duration::seconds(0), token
Utc::now .claims()
);
token.claims()
.validate_expiration(&time_options)? .validate_expiration(&time_options)?
.validate_maturity(&time_options)?; .validate_maturity(&time_options)?;
let claims = token.claims().custom.clone(); let claims = token.claims().custom.clone();
Ok(claims) Ok(claims)
} }

View File

@ -1 +1 @@
pub mod jwt; pub mod jwt;

View File

@ -1,21 +1,21 @@
use serde::{Deserialize,Serialize};
use std::{ env, fs};
use std::path::PathBuf;
use crate::utils::CustomResult; 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 struct Config {
pub info: Info, pub info: Info,
pub sql_config: SqlConfig, pub sql_config: SqlConfig,
} }
#[derive(Deserialize,Serialize,Debug,Clone,)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Info { pub struct Info {
pub install: bool, pub install: bool,
pub non_relational: bool, pub non_relational: bool,
} }
#[derive(Deserialize,Serialize,Debug,Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SqlConfig { pub struct SqlConfig {
pub db_type: String, pub db_type: String,
pub address: String, pub address: String,
@ -25,7 +25,7 @@ pub struct SqlConfig {
pub db_name: String, pub db_name: String,
} }
#[derive(Deserialize,Serialize,Debug,Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct NoSqlConfig { pub struct NoSqlConfig {
pub db_type: String, pub db_type: String,
pub address: String, pub address: String,
@ -37,18 +37,16 @@ pub struct NoSqlConfig {
impl Config { impl Config {
pub fn read() -> CustomResult<Self> { pub fn read() -> CustomResult<Self> {
let path=Self::get_path()?; let path = Self::get_path()?;
Ok(toml::from_str(&fs::read_to_string(path)?)?) Ok(toml::from_str(&fs::read_to_string(path)?)?)
} }
pub fn write(config:Config) -> CustomResult<()> { pub fn write(config: Config) -> CustomResult<()> {
let path=Self::get_path()?; let path = Self::get_path()?;
fs::write(path, toml::to_string(&config)?)?; fs::write(path, toml::to_string(&config)?)?;
Ok(()) Ok(())
} }
pub fn get_path() -> CustomResult<PathBuf> { pub fn get_path() -> CustomResult<PathBuf> {
Ok(env::current_dir()? Ok(env::current_dir()?.join("assets").join("config.toml"))
.join("assets")
.join("config.toml"))
} }
} }

View File

@ -1 +1 @@
pub mod relational; pub mod relational;

View File

@ -1,5 +1,5 @@
use crate::utils::{CustomError, CustomResult};
use regex::Regex; use regex::Regex;
use crate::utils::{CustomResult,CustomError};
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
@ -107,11 +107,7 @@ pub struct WhereCondition {
} }
impl WhereCondition { impl WhereCondition {
pub fn new( pub fn new(field: String, operator: Operator, value: Option<String>) -> CustomResult<Self> {
field: String,
operator: Operator,
value: Option<String>,
) -> CustomResult<Self> {
let field = ValidatedValue::new_identifier(field)?; let field = ValidatedValue::new_identifier(field)?;
let value = match value { let value = match value {

View File

@ -1,8 +1,8 @@
mod postgresql; mod postgresql;
use crate::config; use crate::config;
use crate::utils::{CustomError, CustomResult};
use async_trait::async_trait; use async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use crate::utils::{CustomResult,CustomError};
use std::sync::Arc; use std::sync::Arc;
pub mod builder; pub mod builder;

View File

@ -1,11 +1,10 @@
use super::{DatabaseTrait,builder}; use super::{builder, DatabaseTrait};
use crate::config; use crate::config;
use crate::utils::CustomResult;
use async_trait::async_trait; use async_trait::async_trait;
use sqlx::{Column, PgPool, Row, Executor}; use sqlx::{Column, Executor, PgPool, Row};
use std::collections::HashMap; use std::collections::HashMap;
use std::{env, fs}; use std::{env, fs};
use crate::utils::CustomResult;
#[derive(Clone)] #[derive(Clone)]
pub struct Postgresql { pub struct Postgresql {
@ -23,12 +22,23 @@ impl DatabaseTrait for Postgresql {
.join("init.sql"); .join("init.sql");
let grammar = fs::read_to_string(&path)?; 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?; 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?; let new_pool = PgPool::connect(&new_connection_str).await?;
new_pool.execute(grammar.as_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<Self> { async fn connect(db_config: &config::SqlConfig) -> CustomResult<Self> {
let connection_str = format!( let connection_str = format!(
"postgres://{}:{}@{}:{}/{}", "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) let pool = PgPool::connect(&connection_str).await?;
.await?;
Ok(Postgresql { pool }) Ok(Postgresql { pool })
} }
@ -60,9 +73,7 @@ impl DatabaseTrait for Postgresql {
sqlx_query = sqlx_query.bind(value); sqlx_query = sqlx_query.bind(value);
} }
let rows = sqlx_query let rows = sqlx_query.fetch_all(&self.pool).await?;
.fetch_all(&self.pool)
.await?;
let mut results = Vec::new(); let mut results = Vec::new();
for row in rows { for row in rows {

View File

@ -1,18 +1,14 @@
mod auth;
mod config; mod config;
mod database; mod database;
mod auth; mod manage;
mod utils;
mod routes; mod routes;
use chrono::Duration; mod utils;
use database::relational; use database::relational;
use rocket::{ use rocket::launch;
get, http::Status, launch, response::status, State
};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use utils::{CustomResult, AppResult,CustomError}; use utils::{AppResult, CustomError, CustomResult};
struct AppState { struct AppState {
db: Arc<Mutex<Option<relational::Database>>>, db: Arc<Mutex<Option<relational::Database>>>,
@ -20,7 +16,7 @@ struct AppState {
} }
impl AppState { impl AppState {
async fn get_sql(&self) -> CustomResult<relational::Database> { async fn get_sql(&self) -> CustomResult<relational::Database> {
self.db self.db
.lock() .lock()
.await .await
@ -28,34 +24,17 @@ impl AppState {
.ok_or_else(|| CustomError::from_str("Database not initialized")) .ok_or_else(|| CustomError::from_str("Database not initialized"))
} }
async fn link_sql(&self, config: config::SqlConfig) -> Result<(),CustomError> { async fn link_sql(&self, config: &config::SqlConfig) -> CustomResult<()> {
let database = relational::Database::link(&config) let database = relational::Database::link(config).await?;
.await?;
*self.db.lock().await = Some(database); *self.db.lock().await = Some(database);
Ok(()) Ok(())
} }
} }
#[get("/system")]
async fn token_system(_state: &State<AppState>) -> AppResult<status::Custom<String>> {
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] #[launch]
async fn rocket() -> _ { async fn rocket() -> _ {
let config = config::Config::read().expect("Failed to read config"); let config = config::Config::read().expect("Failed to read config");
let state = AppState { let state = AppState {
db: Arc::new(Mutex::new(None)), db: Arc::new(Mutex::new(None)),
configure: Arc::new(Mutex::new(config.clone())), configure: Arc::new(Mutex::new(config.clone())),
@ -65,35 +44,16 @@ async fn rocket() -> _ {
if config.info.install { if config.info.install {
if let Some(state) = rocket_builder.state::<AppState>() { if let Some(state) = rocket_builder.state::<AppState>() {
state.link_sql(config.sql_config.clone()) state
.link_sql(&config.sql_config)
.await .await
.expect("Failed to connect to database"); .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("/auth/token", routes::jwt_routes());
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
} }
#[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();
}

57
backend/src/manage.rs Normal file
View File

@ -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<dyn std::error::Error>> {
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);
}
}

View File

@ -0,0 +1 @@
pub mod token;

View File

@ -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<AppState>) -> AppResult<status::Custom<String>> {
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()))
}

View File

@ -1,10 +1,9 @@
pub mod auth;
pub mod intsall; pub mod intsall;
pub mod person; pub mod person;
use rocket::routes; pub mod theme;
use rocket::routes;
pub fn create_routes() -> Vec<rocket::Route> { pub fn jwt_routes() -> Vec<rocket::Route> {
routes![ routes![auth::token::token_system]
intsall::install,
]
} }

View File

@ -1,64 +1,52 @@
use serde::{Deserialize,Serialize}; use crate::database::{relational, relational::builder};
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::utils::CustomResult; 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)] #[derive(Deserialize, Serialize)]
pub struct LoginData{ pub struct LoginData {
pub name:String, pub name: String,
pub password:String pub password: String,
} }
pub struct RegisterData{ pub struct RegisterData {
pub name:String, pub name: String,
pub email:String, pub email: String,
pub password: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 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( user_params.insert(
builder::ValidatedValue::Identifier(String::from("person_name")) builder::ValidatedValue::Identifier(String::from("person_name")),
, builder::ValidatedValue::PlainText(data.name),
builder::ValidatedValue::PlainText(data.name)
); );
user_params.insert( user_params.insert(
builder::ValidatedValue::Identifier(String::from("person_email")) builder::ValidatedValue::Identifier(String::from("person_email")),
, builder::ValidatedValue::PlainText(data.email),
builder::ValidatedValue::PlainText(data.email)
); );
user_params.insert( user_params.insert(
builder::ValidatedValue::Identifier(String::from("person_password")) builder::ValidatedValue::Identifier(String::from("person_password")),
, builder::ValidatedValue::PlainText(hashed_password),
builder::ValidatedValue::PlainText(hashed_password)
); );
let builder = builder::QueryBuilder::new(builder::SqlOperation::Insert,String::from("persons"))? let builder =
.params(user_params) builder::QueryBuilder::new(builder::SqlOperation::Insert, String::from("persons"))?
; .params(user_params);
sql.get_db().execute_query(&builder).await?; sql.get_db().execute_query(&builder).await?;
Ok(()) Ok(())
} }
pub fn delete(){} pub fn delete() {}
pub fn update(){} pub fn update() {}
pub fn select(){} pub fn select() {}
pub fn check(){} pub fn check() {}

View File

@ -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 = "<data>")]
pub fn theme_current(data: Json<String>) -> AppResult<status::Custom<Json<Value>>> {
Ok(status::Custom(Status::Ok, Json(Value::Object(()))))
}

View File

@ -13,6 +13,6 @@ startTransition(() => {
document, document,
<StrictMode> <StrictMode>
<RemixBrowser /> <RemixBrowser />
</StrictMode> </StrictMode>,
); );
}); });

View File

@ -22,20 +22,20 @@ export default function handleRequest(
// This is ignored so we can keep it in the template for visibility. Feel // 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! // free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext loadContext: AppLoadContext,
) { ) {
return isbot(request.headers.get("user-agent") || "") return isbot(request.headers.get("user-agent") || "")
? handleBotRequest( ? handleBotRequest(
request, request,
responseStatusCode, responseStatusCode,
responseHeaders, responseHeaders,
remixContext remixContext,
) )
: handleBrowserRequest( : handleBrowserRequest(
request, request,
responseStatusCode, responseStatusCode,
responseHeaders, responseHeaders,
remixContext remixContext,
); );
} }
@ -43,7 +43,7 @@ function handleBotRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
responseHeaders: Headers, responseHeaders: Headers,
remixContext: EntryContext remixContext: EntryContext,
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let shellRendered = false; let shellRendered = false;
@ -65,7 +65,7 @@ function handleBotRequest(
new Response(stream, { new Response(stream, {
headers: responseHeaders, headers: responseHeaders,
status: responseStatusCode, status: responseStatusCode,
}) }),
); );
pipe(body); pipe(body);
@ -82,7 +82,7 @@ function handleBotRequest(
console.error(error); console.error(error);
} }
}, },
} },
); );
setTimeout(abort, ABORT_DELAY); setTimeout(abort, ABORT_DELAY);
@ -93,7 +93,7 @@ function handleBrowserRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
responseHeaders: Headers, responseHeaders: Headers,
remixContext: EntryContext remixContext: EntryContext,
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let shellRendered = false; let shellRendered = false;
@ -115,7 +115,7 @@ function handleBrowserRequest(
new Response(stream, { new Response(stream, {
headers: responseHeaders, headers: responseHeaders,
status: responseStatusCode, status: responseStatusCode,
}) }),
); );
pipe(body); pipe(body);
@ -132,7 +132,7 @@ function handleBrowserRequest(
console.error(error); console.error(error);
} }
}, },
} },
); );
setTimeout(abort, ABORT_DELAY); setTimeout(abort, ABORT_DELAY);

20
frontend/app/env.d.ts vendored
View File

@ -7,16 +7,16 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_SERVER_API: string; // 用于访问API的基础URL readonly VITE_SERVER_API: string; // 用于访问API的基础URL
readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径 readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径
readonly VITE_CONTENT_PATH: string; //mark文章存储的位置 readonly VITE_CONTENT_PATH: string; //mark文章存储的位置
readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置 readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置
readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径 readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径
readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径 readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径
VITE_SYSTEM_USERNAME: string; // 前端账号名称 VITE_SYSTEM_USERNAME: string; // 前端账号名称
VITE_SYSTEM_PASSWORD: string; // 前端账号密码 VITE_SYSTEM_PASSWORD: string; // 前端账号密码
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv;
} }

View File

@ -1,9 +1,14 @@
export type SerializeType = null | number | string | boolean | { [key: string]: SerializeType } | Array<SerializeType>; export type SerializeType =
| null
| number
| string
| boolean
| { [key: string]: SerializeType }
| Array<SerializeType>;
export interface Configuration { export interface Configuration {
[key: string]: { [key: string]: {
title: string; title: string;
description?: string; description?: string;
data: SerializeType; data: SerializeType;
}; };
} }

View File

@ -1,18 +1,17 @@
import { Configuration } from "contracts/generalContract"; import { Configuration } from "contracts/generalContract";
export interface PluginConfig { export interface PluginConfig {
name: string; name: string;
version: string; version: string;
displayName: string; displayName: string;
description?: string;
author?: string;
enabled: boolean;
icon?: string;
managePath?: string;
configuration?: Configuration;
routs: Set<{
description?: string; description?: string;
author?: string; path: string;
enabled: boolean; }>;
icon?: string;
managePath?: string;
configuration?: Configuration;
routs: Set<{
description?: string;
path: string;
}>;
} }

View File

@ -1,11 +1,11 @@
export interface TemplateContract { export interface TemplateContract {
name: string; name: string;
description?: string; description?: string;
config: { config: {
layout?: string; layout?: string;
styles?: string[]; styles?: string[];
scripts?: string[]; scripts?: string[];
}; };
loader: () => Promise<void>; loader: () => Promise<void>;
element: () => React.ReactNode; element: () => React.ReactNode;
} }

View File

@ -1,30 +1,30 @@
import { Configuration } from "contracts/generalContract"; import { Configuration } from "contracts/generalContract";
export interface ThemeConfig { export interface ThemeConfig {
name: string; name: string;
displayName: string; displayName: string;
icon?: string; icon?: string;
version: string; version: string;
description?: string; description?: string;
author?: string; author?: string;
templates: Map<string, ThemeTemplate>; templates: Map<string, ThemeTemplate>;
globalSettings?: { globalSettings?: {
layout?: string; layout?: string;
css?: string; css?: string;
}; };
configuration: Configuration; configuration: Configuration;
routes: { routes: {
index: string; index: string;
post: string; post: string;
tag: string; tag: string;
category: string; category: string;
error: string; error: string;
loding: string; loding: string;
page: Map<string, string>; page: Map<string, string>;
} };
} }
export interface ThemeTemplate { export interface ThemeTemplate {
path: string; path: string;
name: string; name: string;
description?: string; description?: string;
} }

View File

@ -1,6 +1,6 @@
import { createContext, useContext, ReactNode, FC } from 'react'; import { createContext, useContext, ReactNode, FC } from "react";
type ServiceContextReturn<N extends string,T> = { type ServiceContextReturn<N extends string, T> = {
[K in `${N}Provider`]: FC<{ children: ReactNode }>; [K in `${N}Provider`]: FC<{ children: ReactNode }>;
} & { } & {
[K in `use${N}`]: () => T; [K in `use${N}`]: () => T;
@ -8,8 +8,8 @@ type ServiceContextReturn<N extends string,T> = {
export function createServiceContext<T, N extends string>( export function createServiceContext<T, N extends string>(
serviceName: N, serviceName: N,
getServiceInstance: () => T getServiceInstance: () => T,
): ServiceContextReturn<N,T> { ): ServiceContextReturn<N, T> {
const ServiceContext = createContext<T | undefined>(undefined); const ServiceContext = createContext<T | undefined>(undefined);
const Provider: FC<{ children: ReactNode }> = ({ children }) => ( const Provider: FC<{ children: ReactNode }> = ({ children }) => (
@ -21,7 +21,9 @@ export function createServiceContext<T, N extends string>(
const useService = (): T => { const useService = (): T => {
const context = useContext(ServiceContext); const context = useContext(ServiceContext);
if (context === undefined) { 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; return context;
}; };
@ -29,5 +31,5 @@ export function createServiceContext<T, N extends string>(
return { return {
[`${serviceName}Provider`]: Provider, [`${serviceName}Provider`]: Provider,
[`use${serviceName}`]: useService, [`use${serviceName}`]: useService,
} as ServiceContextReturn<N,T>; } as ServiceContextReturn<N, T>;
} }

View File

@ -5,23 +5,22 @@ import { createServiceContext } from "hooks/createServiceContext";
import { ReactNode } from "react"; import { ReactNode } from "react";
export const { CapabilityProvider, useCapability } = createServiceContext( export const { CapabilityProvider, useCapability } = createServiceContext(
"Capability", () => CapabilityService.getInstance(), "Capability",
() => CapabilityService.getInstance(),
); );
export const { ThemeProvider, useTheme } = createServiceContext( export const { ThemeProvider, useTheme } = createServiceContext("Theme", () =>
"Theme", () => ThemeService.getInstance(), ThemeService.getInstance(),
); );
export const { ApiProvider, useApi } = createServiceContext( export const { ApiProvider, useApi } = createServiceContext("Api", () =>
"Api", () => ThemeService.getInstance(), ThemeService.getInstance(),
); );
export const ServiceProvider = ({ children }: { children: ReactNode }) => ( export const ServiceProvider = ({ children }: { children: ReactNode }) => (
<ApiProvider> <ApiProvider>
<CapabilityProvider> <CapabilityProvider>
<ThemeProvider> <ThemeProvider>{children}</ThemeProvider>
{children}
</ThemeProvider>
</CapabilityProvider> </CapabilityProvider>
</ApiProvider> </ApiProvider>
); );

View File

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"build": "remix vite:build", "build": "remix vite:build",
"dev": "remix vite:dev", "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", "start": "remix-serve ./build/server/index.js",
"typecheck": "tsc" "typecheck": "tsc"
}, },
@ -27,13 +28,14 @@
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8.38.0", "eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^5.1.0", "vite": "^5.1.0",

View File

@ -1,94 +1,96 @@
interface ApiConfig { interface ApiConfig {
baseURL: string; baseURL: string;
timeout?: number; timeout?: number;
} }
export class ApiService { export class ApiService {
private static instance: ApiService; private static instance: ApiService;
private baseURL: string; private baseURL: string;
private timeout: number; private timeout: number;
private constructor(config: ApiConfig) { private constructor(config: ApiConfig) {
this.baseURL = config.baseURL; this.baseURL = config.baseURL;
this.timeout = config.timeout || 10000; 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<string> {
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 { try {
if (!this.instance && config) { const response = await fetch(`${this.baseURL}/auth/token/system`, {
this.instance = new ApiService(config); method: "POST",
} headers: {
return this.instance; "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<string> { public async request<T>(
const username = import.meta.env.VITE_SYSTEM_USERNAME; endpoint: string,
const password = import.meta.env.VITE_SYSTEM_PASSWORD; options: RequestInit = {},
if (!username || !password ) { toekn?: string,
throw new Error('Failed to obtain the username or password of the front-end system'); ): Promise<T> {
} const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try { try {
const response = await fetch(`${this.baseURL}/auth/token/system`, { const headers = new Headers(options.headers);
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
});
if (!response.ok) { if (toekn) {
throw new Error('Failed to get system token'); headers.set("Authorization", `Bearer ${toekn}`);
} }
const data = await response.text(); const response = await fetch(`${this.baseURL}${endpoint}`, {
return data; ...options,
} catch (error) { headers,
console.error('Error getting system token:', error); signal: controller.signal,
throw error; });
}
} if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
public async request<T>( }
endpoint: string,
options: RequestInit = {}, const data = await response.json();
toekn ?: string return data as T;
): Promise<T> { } catch (error: any) {
const controller = new AbortController(); if (error.name === "AbortError") {
const timeoutId = setTimeout(() => controller.abort(), this.timeout); throw new Error("Request timeout");
}
try { throw error;
const headers = new Headers(options.headers); } finally {
clearTimeout(timeoutId);
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);
}
} }
}
} }
export default ApiService.getInstance({ export default ApiService.getInstance({
baseURL: import.meta.env.VITE_API_BASE_URL, baseURL: import.meta.env.VITE_API_BASE_URL,
}); });

View File

@ -1,66 +1,81 @@
import { CapabilityProps } from "contracts/capabilityContract"; import { CapabilityProps } from "contracts/capabilityContract";
export class CapabilityService { export class CapabilityService {
private capabilities: Map<string, Set<{ private capabilities: Map<
source: string; string,
capability: CapabilityProps<any>}>> = new Map(); Set<{
source: string;
private static instance: CapabilityService; capability: CapabilityProps<any>;
}>
> = new Map();
private constructor() { } private static instance: CapabilityService;
public static getInstance(): CapabilityService { private constructor() {}
if (!this.instance) {
this.instance = new CapabilityService(); public static getInstance(): CapabilityService {
} if (!this.instance) {
return this.instance; this.instance = new CapabilityService();
} }
return this.instance;
}
private register(capabilityName: string, source: string, capability: CapabilityProps<any>) { private register(
const handlers = this.capabilities.get(capabilityName) || new Set(); capabilityName: string,
handlers.add({ source, capability }); source: string,
} capability: CapabilityProps<any>,
) {
const handlers = this.capabilities.get(capabilityName) || new Set();
handlers.add({ source, capability });
}
private executeCapabilityMethod<T>(capabilityName: string, ...args: any[]): Set<T> { private executeCapabilityMethod<T>(
const results = new Set<T>(); capabilityName: string,
const handlers = this.capabilities.get(capabilityName); ...args: any[]
): Set<T> {
const results = new Set<T>();
const handlers = this.capabilities.get(capabilityName);
if (handlers) { if (handlers) {
handlers.forEach(({ capability }) => { handlers.forEach(({ capability }) => {
const methodFunction = capability['execute']; const methodFunction = capability["execute"];
if (methodFunction) { if (methodFunction) {
methodFunction(...args) methodFunction(...args)
.then((data) => results.add(data as T)) .then((data) => results.add(data as T))
.catch((error) => console.error(`Error executing method ${capabilityName}:`, error)); .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)
); );
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<any>): boolean {
if (!capability.name || !capability.execute) {
return false;
} }
private removeCapabilitys(capability: string) { const namePattern = /^[a-z][a-zA-Z0-9_]*$/;
this.capabilities.delete(capability); if (!namePattern.test(capability.name)) {
return false;
} }
public validateCapability(capability: CapabilityProps<any>): boolean { return true;
if (!capability.name || !capability.execute) { }
return false;
}
const namePattern = /^[a-z][a-zA-Z0-9_]*$/;
if (!namePattern.test(capability.name)) {
return false;
}
return true;
}
} }

View File

@ -1,53 +1,55 @@
import { PluginConfiguration } from 'types/pluginRequirement'; import { PluginConfiguration } from "types/pluginRequirement";
import { Contracts } from 'contracts/capabilityContract'; import { Contracts } from "contracts/capabilityContract";
export class PluginManager { export class PluginManager {
private plugins: Map<string, PluginProps> = new Map(); private plugins: Map<string, PluginProps> = new Map();
private configurations: Map<string, PluginConfiguration> = new Map(); private configurations: Map<string, PluginConfiguration> = new Map();
private extensions: Map<string, ExtensionProps> = new Map(); private extensions: Map<string, ExtensionProps> = new Map();
async loadPlugins() { async loadPlugins() {
const pluginDirs = await this.scanPluginDirectory(); const pluginDirs = await this.scanPluginDirectory();
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
try { try {
const config = await import(`@/plugins/${dir}/plugin.config.ts`); const config = await import(`@/plugins/${dir}/plugin.config.ts`);
const plugin: PluginProps = config.default; const plugin: PluginProps = config.default;
this.plugins.set(plugin.name, plugin); this.plugins.set(plugin.name, plugin);
if (plugin.settingsSchema) { if (plugin.settingsSchema) {
this.configurations.set(plugin.name, 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 getPluginConfig(pluginName: string): Promise<PluginConfiguration | undefined> { if (plugin.extensions) {
const dbConfig = await this.fetchConfigFromDB(pluginName); Object.entries(plugin.extensions).forEach(([key, value]) => {
if (dbConfig) { this.extensions.set(`${plugin.name}.${key}`, value.extension);
return dbConfig; });
} }
return this.configurations.get(pluginName);
}
private async fetchConfigFromDB(pluginName: string) { if (plugin.hooks?.onInstall) {
return null; await plugin.hooks.onInstall({});
}
} catch (error) {
console.error(`Failed to load plugin from directory ${dir}:`, error);
}
} }
}
private async scanPluginDirectory(): Promise<string[]> { async getPluginConfig(
return []; pluginName: string,
): Promise<PluginConfiguration | undefined> {
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<string[]> {
return [];
}
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; // Import React import React from "react"; // Import React
import { LoaderFunction, RouteObject } from 'react-router-dom'; import { LoaderFunction, RouteObject } from "react-router-dom";
export class RouteManager { export class RouteManager {
private static instance: RouteManager; private static instance: RouteManager;
@ -14,18 +14,21 @@ export class RouteManager {
return RouteManager.instance; return RouteManager.instance;
} }
private createRouteElement(
private createRouteElement(path: string,element:React.ReactNode,loader?:LoaderFunction,children?:RouteObject[]) { path: string,
element: React.ReactNode,
loader?: LoaderFunction,
children?: RouteObject[],
) {
this.routes.push({ this.routes.push({
path, path,
element, element,
loader, loader,
children, children,
}) });
} }
private getRoutes(): RouteObject[] { private getRoutes(): RouteObject[] {
return this.routes; return this.routes;
} }
}
}

View File

@ -20,12 +20,12 @@ export class ThemeService {
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
try { try {
const themeConfig = await this.api.request<ThemeConfig>( const themeConfig = await this.api.request<ThemeConfig>(
'/theme/current', "/theme/current",
{ method: 'GET' } { method: "GET" },
); );
await this.loadTheme(themeConfig); await this.loadTheme(themeConfig);
} catch (error) { } catch (error) {
console.error('Failed to initialize theme:', error); console.error("Failed to initialize theme:", error);
throw error; throw error;
} }
} }
@ -35,14 +35,14 @@ export class ThemeService {
this.currentTheme = config; this.currentTheme = config;
await this.loadTemplates(); await this.loadTemplates();
} catch (error) { } catch (error) {
console.error('Failed to load theme:', error); console.error("Failed to load theme:", error);
throw error; throw error;
} }
} }
private async loadTemplates(): Promise<void> { private async loadTemplates(): Promise<void> {
if (!this.currentTheme) { if (!this.currentTheme) {
throw new Error('No theme configuration loaded'); throw new Error("No theme configuration loaded");
} }
const loadTemplate = async (template: ThemeTemplate) => { const loadTemplate = async (template: ThemeTemplate) => {
@ -56,9 +56,10 @@ export class ThemeService {
} }
}; };
const loadPromises = Array.from(this.currentTheme.templates.values()) const loadPromises = Array.from(this.currentTheme.templates.values()).map(
.map(template => loadTemplate(template)); (template) => loadTemplate(template),
);
await Promise.all(loadPromises); await Promise.all(loadPromises);
} }
@ -76,18 +77,18 @@ export class ThemeService {
public getTemplateByRoute(route: string): string { public getTemplateByRoute(route: string): string {
if (!this.currentTheme) { if (!this.currentTheme) {
throw new Error('No theme configuration loaded'); throw new Error("No theme configuration loaded");
} }
let templateName: string | undefined; let templateName: string | undefined;
if (route === '/') { if (route === "/") {
templateName = this.currentTheme.routes.index; templateName = this.currentTheme.routes.index;
} else if (route.startsWith('/post/')) { } else if (route.startsWith("/post/")) {
templateName = this.currentTheme.routes.post; templateName = this.currentTheme.routes.post;
} else if (route.startsWith('/tag/')) { } else if (route.startsWith("/tag/")) {
templateName = this.currentTheme.routes.tag; templateName = this.currentTheme.routes.tag;
} else if (route.startsWith('/category/')) { } else if (route.startsWith("/category/")) {
templateName = this.currentTheme.routes.category; templateName = this.currentTheme.routes.category;
} else { } else {
templateName = this.currentTheme.routes.page.get(route); templateName = this.currentTheme.routes.page.get(route);
@ -103,20 +104,20 @@ export class ThemeService {
public async updateThemeConfig(config: Partial<ThemeConfig>): Promise<void> { public async updateThemeConfig(config: Partial<ThemeConfig>): Promise<void> {
try { try {
const updatedConfig = await this.api.request<ThemeConfig>( const updatedConfig = await this.api.request<ThemeConfig>(
'/theme/config', "/theme/config",
{ {
method: 'PUT', method: "PUT",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(config), body: JSON.stringify(config),
} },
); );
await this.loadTheme(updatedConfig); await this.loadTheme(updatedConfig);
} catch (error) { } catch (error) {
console.error('Failed to update theme configuration:', error); console.error("Failed to update theme configuration:", error);
throw error; throw error;
} }
} }
} }

View File

@ -1,24 +1,27 @@
import { ThemeConfig } from "contracts/themeContract"; import { ThemeConfig } from "contracts/themeContract";
export const themeConfig: ThemeConfig = { export const themeConfig: ThemeConfig = {
name: 'default', name: "default",
displayName: '默认主题', displayName: "默认主题",
version: '1.0.0', version: "1.0.0",
description: '一个简约风格的博客主题', description: "一个简约风格的博客主题",
author: 'lsy', author: "lsy",
entry: './index.tsx', entry: "default",
templates: new Map([ templates: new Map([
['page', { [
path: './templates/page', "page",
name: '文章列表模板', {
description: '博客首页展示模板' path: "./templates/page",
}], name: "文章列表模板",
]), description: "博客首页展示模板",
},
routes: { ],
post: "", ]),
tag: "",
category: "", routes: {
page: "" post: "",
} tag: "",
} category: "",
page: "",
},
};