格式化代码,后端:新增rocket实例管理
This commit is contained in:
parent
4bf55506b9
commit
eb53c72203
@ -1,5 +1,5 @@
|
|||||||
[info]
|
[info]
|
||||||
install = true
|
install = false
|
||||||
non_relational = false
|
non_relational = false
|
||||||
|
|
||||||
[sql_config]
|
[sql_config]
|
||||||
|
@ -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,11 +80,9 @@ 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();
|
||||||
|
@ -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"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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,30 +24,13 @@ 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");
|
||||||
@ -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
57
backend/src/manage.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
1
backend/src/routes/auth/mod.rs
Normal file
1
backend/src/routes/auth/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod token;
|
15
backend/src/routes/auth/token.rs
Normal file
15
backend/src/routes/auth/token.rs
Normal 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()))
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
|
pub mod auth;
|
||||||
pub mod intsall;
|
pub mod intsall;
|
||||||
pub mod person;
|
pub mod person;
|
||||||
|
pub mod theme;
|
||||||
use rocket::routes;
|
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,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {}
|
||||||
|
12
backend/src/routes/theme.rs
Normal file
12
backend/src/routes/theme.rs
Normal 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(()))))
|
||||||
|
}
|
@ -13,6 +13,6 @@ startTransition(() => {
|
|||||||
document,
|
document,
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RemixBrowser />
|
<RemixBrowser />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
18
frontend/app/env.d.ts
vendored
18
frontend/app/env.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
capability: CapabilityProps<any>;
|
||||||
|
}>
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
private static instance: CapabilityService;
|
private static instance: CapabilityService;
|
||||||
|
|
||||||
private constructor() { }
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): CapabilityService {
|
public static getInstance(): CapabilityService {
|
||||||
if (!this.instance) {
|
if (!this.instance) {
|
||||||
this.instance = new CapabilityService();
|
this.instance = new CapabilityService();
|
||||||
}
|
|
||||||
return this.instance;
|
|
||||||
}
|
}
|
||||||
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeCapabilitys(capability: string) {
|
|
||||||
this.capabilities.delete(capability);
|
|
||||||
}
|
|
||||||
|
|
||||||
public validateCapability(capability: CapabilityProps<any>): 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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const namePattern = /^[a-z][a-zA-Z0-9_]*$/;
|
||||||
|
if (!namePattern.test(capability.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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,8 +56,9 @@ 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,19 +104,19 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
routes: {
|
||||||
post: "",
|
post: "",
|
||||||
tag: "",
|
tag: "",
|
||||||
category: "",
|
category: "",
|
||||||
page: ""
|
page: "",
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user