增加readme,删除了注释

前端:完善了插件和主题的契约
This commit is contained in:
lsy 2024-11-19 00:20:31 +08:00
parent c19d21ce7d
commit 7e4d9c1b48
22 changed files with 226 additions and 689 deletions

View File

@ -1,2 +1,17 @@
能力是主题和系统向插件暴露的接口,契约则规定了开发者在开发主题或插件时的限制和模板。
系统使用 Remix后端采用 Rust 的 Rocket。
## 项目概述
本项目是一个基于 Rust 和 TypeScript 的博客系统,旨在提供一个高效、可扩展的博客平台。系统分为前端和后端,前端使用 React 框架构建,后端使用 Rocket 框架实现 RESTful API。
## 技术栈
- **后端**: Rust, Rocket, SQLx, PostgreSQL
- **前端**: TypeScript, React, Axios, Tailwind CSS, Vite, Remix
- **身份验证**: JWT (JSON Web Tokens)
## 功能特性
- **用户管理**: 支持用户注册、登录和角色管理。
- **文章管理**: 支持文章的创建、编辑、删除和查看,支持标签和分类管理。
- **主题与插件支持**: 允许用户自定义主题和插件,增强系统的灵活性。
- **API 服务**: 提供 RESTful API 接口,供前端调用,实现数据交互。
- **错误处理与加载状态管理**: 提升用户体验。

View File

@ -1,47 +1,39 @@
// config/mod.rs
/*
*/
use serde::Deserialize;
use std::{ env, fs};
#[derive(Deserialize,Debug,Clone)]
pub struct Config {
pub info: Info, // 配置信息
pub sql_config: SqlConfig, // 关系型数据库配置
// pub no_sql_config:NoSqlConfig, 非关系型数据库配置
pub info: Info,
pub sql_config: SqlConfig,
}
#[derive(Deserialize,Debug,Clone)]
pub struct Info {
pub install: bool, // 是否安装
pub non_relational: bool, // 是否非关系型
pub install: bool,
pub non_relational: bool,
}
#[derive(Deserialize,Debug,Clone)]
pub struct SqlConfig {
pub db_type: String, // 数据库类型
pub address: String, // 地址
pub port: u32, // 端口
pub user: String, // 用户名
pub password: String, // 密码
pub db_name: String, // 数据库名称
pub db_type: String,
pub address: String,
pub port: u32,
pub user: String,
pub password: String,
pub db_name: String,
}
#[derive(Deserialize,Debug,Clone)]
pub struct NoSqlConfig {
pub db_type: String, // 数据库类型
pub address: String, // 地址
pub port: u32, // 端口
pub user: String, // 用户名
pub password: String, // 密码
pub db_name: String, // 数据库名称
pub db_type: String,
pub address: String,
pub port: u32,
pub user: String,
pub password: String,
pub db_name: String,
}
impl Config {
/// 读取配置文件
pub fn read() -> Result<Self, Box<dyn std::error::Error>> {
let path = env::current_dir()?
.join("assets")

View File

@ -1,10 +1,3 @@
// File path: src/database/relational/mod.rs
/**
PostgreSQL
*/
mod postgresql;
use std::collections::HashMap;
use crate::config;
@ -14,70 +7,42 @@ use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum SqlOperation {
Select, // 查询操作
Insert, // 插入操作
Update, // 更新操作
Delete, // 删除操作
Select,
Insert,
Update,
Delete,
}
/// 查询构建器结构
pub struct QueryBuilder {
operation: SqlOperation, // SQL操作类型
table: String, // 表名
fields: Vec<String>, // 查询字段
params: HashMap<String, String>, // 插入或更新的参数
where_conditions: HashMap<String, String>, // WHERE条件
order_by: Option<String>, // 排序字段
limit: Option<i32>, // 限制返回的记录数
operation: SqlOperation,
table: String,
fields: Vec<String>,
params: HashMap<String, String>,
where_conditions: HashMap<String, String>,
order_by: Option<String>,
limit: Option<i32>,
}
#[async_trait]
pub trait DatabaseTrait: Send + Sync {
/**
@param database
@return Result<Self, Box<dyn Error>>
*/
async fn connect(database: config::SqlConfig) -> Result<Self, Box<dyn Error>> where Self: Sized;
/**
@param query SQL查询语句
@return Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>>
*/
async fn execute_query<'a>(
&'a self,
builder: &QueryBuilder,
) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>>;
/**
@param database
@return Result<(), Box<dyn Error>>
*/
async fn initialization(database: config::SqlConfig) -> Result<(), Box<dyn Error>> where Self: Sized;
}
#[derive(Clone)]
pub struct Database {
// 数据库实例
pub db: Arc<Box<dyn DatabaseTrait>>,
}
impl Database {
/**
@return &Box<dyn DatabaseTrait>
*/
pub fn get_db(&self) -> &Box<dyn DatabaseTrait> {
&self.db
}
/**
@param database
@return Result<Self, Box<dyn Error>>
*/
pub async fn link(database: config::SqlConfig) -> Result<Self, Box<dyn Error>> {
let db = match database.db_type.as_str() {
"postgresql" => postgresql::Postgresql::connect(database).await?,
@ -87,11 +52,6 @@ impl Database {
Ok(Self { db: Arc::new(Box::new(db)) })
}
/**
@param database
@return Result<(), Box<dyn Error>>
*/
pub async fn initial_setup(database: config::SqlConfig) -> Result<(), Box<dyn Error>> {
match database.db_type.as_str() {
"postgresql" => postgresql::Postgresql::initialization(database).await?,
@ -102,12 +62,6 @@ impl Database {
}
impl QueryBuilder {
/**
@param operation SQL操作类型
@param table
@return Self
*/
pub fn new(operation: SqlOperation, table: &str) -> Self {
QueryBuilder {
operation,
@ -120,10 +74,6 @@ impl QueryBuilder {
}
}
/**
SQL语句和参数
@return (String, Vec<String>) SQL语句和参数值
*/
pub fn build(&self) -> (String, Vec<String>) {
let mut query = String::new();
let mut values = Vec::new();
@ -131,7 +81,6 @@ impl QueryBuilder {
match self.operation {
SqlOperation::Select => {
// SELECT 操作
let fields = if self.fields.is_empty() {
"*".to_string()
} else {
@ -140,7 +89,6 @@ impl QueryBuilder {
query.push_str(&format!("SELECT {} FROM {}", fields, self.table));
// 添加 WHERE 条件
if !self.where_conditions.is_empty() {
let conditions: Vec<String> = self.where_conditions
.iter()
@ -156,7 +104,6 @@ impl QueryBuilder {
}
},
SqlOperation::Insert => {
// INSERT 操作
let fields: Vec<String> = self.params.keys().cloned().collect();
let placeholders: Vec<String> = (1..=self.params.len())
.map(|i| format!("${}", i))
@ -169,13 +116,11 @@ impl QueryBuilder {
placeholders.join(", ")
));
// 收集参数值
for field in fields {
values.push(self.params[&field].clone());
}
},
SqlOperation::Update => {
// UPDATE 操作
query.push_str(&format!("UPDATE {}", self.table));
let set_clauses: Vec<String> = self.params
@ -191,7 +136,6 @@ impl QueryBuilder {
query.push_str(" SET ");
query.push_str(&set_clauses.join(", "));
// 添加 WHERE 条件
if !self.where_conditions.is_empty() {
let conditions: Vec<String> = self.where_conditions
.iter()
@ -207,10 +151,8 @@ impl QueryBuilder {
}
},
SqlOperation::Delete => {
// DELETE 操作
query.push_str(&format!("DELETE FROM {}", self.table));
// 添加 WHERE 条件
if !self.where_conditions.is_empty() {
let conditions: Vec<String> = self.where_conditions
.iter()
@ -227,12 +169,10 @@ impl QueryBuilder {
}
}
// 添加 ORDER BY
if let Some(order) = &self.order_by {
query.push_str(&format!(" ORDER BY {}", order));
}
// 添加 LIMIT
if let Some(limit) = self.limit {
query.push_str(&format!(" LIMIT {}", limit));
}
@ -240,51 +180,26 @@ impl QueryBuilder {
(query, values)
}
/**
@param fields
@return &mut Self 便
*/
pub fn fields(&mut self, fields: Vec<String>) -> &mut Self {
self.fields = fields;
self
}
/**
@param params
@return &mut Self 便
*/
pub fn params(&mut self, params: HashMap<String, String>) -> &mut Self {
self.params = params;
self
}
/**
WHERE条件
@param conditions
@return &mut Self 便
*/
pub fn where_conditions(&mut self, conditions: HashMap<String, String>) -> &mut Self {
self.where_conditions = conditions;
self
}
/**
@param order
@return &mut Self 便
*/
pub fn order_by(&mut self, order: &str) -> &mut Self {
self.order_by = Some(order.to_string());
self
}
/**
@param limit
@return &mut Self 便
*/
pub fn limit(&mut self, limit: i32) -> &mut Self {
self.limit = Some(limit);
self

View File

@ -1,99 +1,84 @@
--- 安装自动生成uuid插件
CREATE EXTENSION IF NOT EXISTS pgcrypto;
--- 用户权限枚举
CREATE TYPE privilege_level AS ENUM ( 'contributor', 'administrators');
--- 用户信息表
CREATE TABLE persons
(
person_name VARCHAR(100) PRIMARY KEY, --- 用户名
person_email VARCHAR(255) UNIQUE NOT NULL, --- 用户邮箱
person_icon VARCHAR(255), --- 用户头像
person_password VARCHAR(255) NOT NULL, --- 用户密码
person_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 用户创建时间
person_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 用户更新时间
person_avatar VARCHAR(255), --- 用户头像URL
person_role VARCHAR(50), --- 用户角色
person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 最后登录时间
person_level privilege_level NOT NULL DEFAULT 'contributor' --- 用户权限
person_name VARCHAR(100) PRIMARY KEY,
person_email VARCHAR(255) UNIQUE NOT NULL,
person_icon VARCHAR(255),
person_password VARCHAR(255) NOT NULL,
person_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
person_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
person_avatar VARCHAR(255),
person_role VARCHAR(50),
person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
person_level privilege_level NOT NULL DEFAULT 'contributor'
);
--- 页面状态枚举
CREATE TYPE publication_status AS ENUM ('draft', 'published', 'private','hide');
--- 独立页面表
CREATE TABLE pages
(
page_id SERIAL PRIMARY KEY, --- 独立页面唯一id主键
page_meta_keywords VARCHAR(255) NOT NULL, ---mata关键字
page_meta_description VARCHAR(255) NOT NULL, ---mata描述
page_title VARCHAR(255) NOT NULL, --- 文章标题
page_content TEXT NOT NULL, --- 独立页面内容
page_mould VARCHAR(50), --- 独立页面模板名称
page_fields JSON, --- 自定义字段
page_status publication_status DEFAULT 'draft' --- 文章状态
page_id SERIAL PRIMARY KEY,
page_meta_keywords VARCHAR(255) NOT NULL,
page_meta_description VARCHAR(255) NOT NULL,
page_title VARCHAR(255) NOT NULL,
page_content TEXT NOT NULL,
page_mould VARCHAR(50),
page_fields JSON,
page_status publication_status DEFAULT 'draft'
);
--- 文章表
CREATE TABLE posts
(
post_author VARCHAR(100) NOT NULL REFERENCES persons (person_name) ON DELETE CASCADE, --- 文章作者
post_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), --- 文章主键
post_picture VARCHAR(255), --- 文章头图
post_title VARCHAR(255) NOT NULL, --- 文章标题
post_meta_keywords VARCHAR(255) NOT NULL, ---mata关键字
post_meta_description VARCHAR(255) NOT NULL, ---mata描述
post_content TEXT NOT NULL, --- 文章内容
post_status publication_status DEFAULT 'draft', --- 文章状态
post_editor BOOLEAN DEFAULT FALSE, --- 文章是否编辑未保存
post_unsaved_content TEXT, --- 未保存的文章
post_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章创建时间
post_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章更新时间
post_published_at TIMESTAMP, --- 文章发布时间
CONSTRAINT post_updated_at_check CHECK (post_updated_at >= post_created_at) --- 文章时间约束
post_author VARCHAR(100) NOT NULL REFERENCES persons (person_name) ON DELETE CASCADE,
post_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_picture VARCHAR(255),
post_title VARCHAR(255) NOT NULL,
post_meta_keywords VARCHAR(255) NOT NULL,
post_meta_description VARCHAR(255) NOT NULL,
post_content TEXT NOT NULL,
post_status publication_status DEFAULT 'draft',
post_editor BOOLEAN DEFAULT FALSE,
post_unsaved_content TEXT,
post_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
post_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
post_published_at TIMESTAMP,
CONSTRAINT post_updated_at_check CHECK (post_updated_at >= post_created_at)
);
--- 标签表
CREATE TABLE tags
(
tag_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(tag_name) = tag_name), --- 标签名称主键
tag_icon VARCHAR(255) --- 标签图标
tag_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(tag_name) = tag_name),
tag_icon VARCHAR(255)
);
--- 文章与标签的关系表
CREATE TABLE post_tags
(
post_id UUID REFERENCES posts (post_id) ON DELETE CASCADE, --- 外键约束引用posts的主键
tag_name VARCHAR(50) REFERENCES tags (tag_name) ON DELETE CASCADE, --- 外键约束引用tag的主键
PRIMARY KEY (post_id, tag_name) --- 复合主键
post_id UUID REFERENCES posts (post_id) ON DELETE CASCADE,
tag_name VARCHAR(50) REFERENCES tags (tag_name) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_name)
);
--- 分类表
CREATE TABLE categories
(
category_name VARCHAR(50) PRIMARY KEY, ---
parent_category_name VARCHAR(50), -- 父类分类 ID可以为空
FOREIGN KEY (parent_category_name) REFERENCES categories (category_name) -- 外键约束,引用同一表的 category_id
category_name VARCHAR(50) PRIMARY KEY,
parent_category_name VARCHAR(50),
FOREIGN KEY (parent_category_name) REFERENCES categories (category_name)
);
--- 文章与分类的关系表
CREATE TABLE post_categories
(
post_id UUID REFERENCES posts (post_id) ON DELETE CASCADE, --- 外键约束引用posts的主键
category_id VARCHAR(50) REFERENCES categories (category_name) ON DELETE CASCADE, --- 外键约束引用categories的主键
PRIMARY KEY (post_id, category_id) --- 复合主键
post_id UUID REFERENCES posts (post_id) ON DELETE CASCADE,
category_id VARCHAR(50) REFERENCES categories (category_name) ON DELETE CASCADE,
PRIMARY KEY (post_id, category_id)
);
--- 资源库
CREATE TABLE library
(
library_author VARCHAR(100) NOT NULL REFERENCES persons (person_name) ON DELETE CASCADE, --- 资源作者
library_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), --- 资源ID
library_name VARCHAR(255) NOT NULL, --- 资源名称
library_size BIGINT NOT NULL, --- 大小
library_storage_path VARCHAR(255) NOT NULL UNIQUE, --- 储存路径
library_type VARCHAR(50) NOT NULL, --- 资源类别
library_class VARCHAR(50), --- 资源类
library_description VARCHAR(255), --- 资源描述
library_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP --- 创建时间
library_author VARCHAR(100) NOT NULL REFERENCES persons (person_name) ON DELETE CASCADE,
library_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
library_name VARCHAR(255) NOT NULL,
library_size BIGINT NOT NULL,
library_storage_path VARCHAR(255) NOT NULL UNIQUE,
library_type VARCHAR(50) NOT NULL,
library_class VARCHAR(50),
library_description VARCHAR(255),
library_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
--- 配置文件库
CREATE TABLE config
(
config_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(config_name) = config_name), --- 配置文件名称
config_config JSON --- 配置文件
)
config_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(config_name) = config_name),
config_config JSON
);

View File

@ -1,9 +1,3 @@
// File path: src/database/relational/postgresql/mod.rs
/**
* PostgreSQL数据库的交互功能
*
*/
use super::{DatabaseTrait, QueryBuilder};
use crate::config;
use async_trait::async_trait;
@ -12,23 +6,12 @@ use std::{collections::HashMap, error::Error};
use std::{env, fs};
#[derive(Clone)]
/// PostgreSQL数据库连接池结构体
pub struct Postgresql {
/// 数据库连接池
pool: PgPool,
}
#[async_trait]
impl DatabaseTrait for Postgresql {
/**
*
*
* #
* - `db_config`:
*
* #
* - `Result<(), Box<dyn Error>>`:
*/
async fn initialization(db_config: config::SqlConfig) -> Result<(), Box<dyn Error>> {
let path = env::current_dir()?
.join("src")
@ -38,77 +21,49 @@ impl DatabaseTrait for Postgresql {
.join("init.sql");
let grammar = fs::read_to_string(&path)?;
// 创建初始连接(不指定数据库)
let connection_str = format!("postgres://{}:{}@{}:{}", db_config.user, db_config.password, db_config.address, db_config.port);
let pool = PgPool::connect(&connection_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_pool = PgPool::connect(&new_connection_str).await?;
// 执行初始化脚本
new_pool.execute(grammar.as_str()).await?;
Ok(())
}
/**
* PostgreSQL数据库并返回Postgresql实例
*
* #
* - `db_config`:
*
* #
* - `Result<Self, Box<dyn Error>>`:
*/
async fn connect(db_config: config::SqlConfig) -> Result<Self, Box<dyn Error>> {
let connection_str = format!(
"postgres://{}:{}@{}:{}/{}",
db_config.user, db_config.password, db_config.address, db_config.port, db_config.db_name
);
// 连接到数据库池
let pool = PgPool::connect(&connection_str)
.await
.map_err(|e| Box::new(e) as Box<dyn Error>)?;
// 返回Postgresql实例
Ok(Postgresql { pool })
}
/**
*
*
* #
* - `builder`:
*
* #
* - `Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>>`:
*/
async fn execute_query<'a>(
&'a self,
builder: &QueryBuilder,
) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>> {
let (query, values) = builder.build();
// 构建查询
let mut sqlx_query = sqlx::query(&query);
// 绑定参数
for value in values {
sqlx_query = sqlx_query.bind(value);
}
// 执行查询
let rows = sqlx_query
.fetch_all(&self.pool)
.await
.map_err(|e| Box::new(e) as Box<dyn Error>)?;
// 处理结果
let mut results = Vec::new();
for row in rows {
let mut map = HashMap::new();

View File

@ -1,71 +1,36 @@
// File path: /d:/data/echoes/backend/src/main.rs
mod config;
mod database;
mod secret;
/**
* API接口
*
*
* - GET /api/install:
* - GET /api/sql: SQL查询并返回结果
*/
use chrono::Duration;
use database::relational;
use once_cell::sync::Lazy;
use rocket::{get, post, http::Status, launch, response::status, routes, serde::json::Json};
use std::sync::Arc;
use tokio::sync::Mutex;
mod config; // 配置模块
mod database; // 数据库模块
mod secret; // 密钥模块
use chrono::Duration; // 引入时间持续时间
use database::relational; // 引入关系型数据库
use once_cell::sync::Lazy; // 用于延迟初始化
use rocket::{get, post, http::Status, launch, response::status, routes, serde::json::Json}; // 引入Rocket框架相关功能
use std::sync::Arc; // 引入Arc用于线程安全的引用计数
use tokio::sync::Mutex; // 引入Mutex用于异步锁
// 全局数据库连接变量
static SQL: Lazy<Arc<Mutex<Option<relational::Database>>>> =
Lazy::new(|| Arc::new(Mutex::new(None)));
/**
*
*
* #
* - `database`:
*
* #
* - `Result<(), Box<dyn std::error::Error>>`:
*/
async fn init_sql(database: config::SqlConfig) -> Result<(), Box<dyn std::error::Error>> {
let database = relational::Database::link(database).await?; // 初始化数据库
*SQL.lock().await = Some(database); // 保存数据库实例
let database = relational::Database::link(database).await?;
*SQL.lock().await = Some(database);
Ok(())
}
/**
*
*
* #
* - `Result<relational::Database, Box<dyn std::error::Error>>`:
*/
async fn get_sql() -> Result<relational::Database, Box<dyn std::error::Error>> {
SQL.lock()
.await
.clone()
.ok_or_else(|| "Database not initialized".into()) // 返回错误信息
.ok_or_else(|| "Database not initialized".into())
}
/**
*
*
* #
* - `data`:
*
* #
* - `Result<status::Custom<String>, status::Custom<String>>`:
*/
#[post("/install", format = "json", data = "<data>")]
async fn install(data: Json<config::SqlConfig>) -> Result<status::Custom<String>, status::Custom<String>> {
relational::Database::initial_setup(data.into_inner()).await.map_err(|e| {
status::Custom(
Status::InternalServerError,
format!("Database initialization failed: {}", e), // 连接失败信息
format!("Database initialization failed: {}", e),
)
})?;
@ -75,48 +40,34 @@ async fn install(data: Json<config::SqlConfig>) -> Result<status::Custom<String>
))
}
/**
* JWT接口
*
* #
* - `Result<status::Custom<String>, status::Custom<String>>`: JWT令牌或错误信息
*/
#[get("/system")]
async fn token_system() -> Result<status::Custom<String>, status::Custom<String>> {
// 创建 Claims
let claims = secret::CustomClaims {
user_id: String::from("system"), // 用户ID
device_ua: String::from("system"), // 设备用户代理
user_id: String::from("system"),
device_ua: String::from("system"),
};
// 生成JWT
let token = secret::generate_jwt(claims, Duration::seconds(1)).map_err(|e| {
status::Custom(
Status::InternalServerError,
format!("JWT generation failed: {}", e), // JWT生成失败信息
format!("JWT generation failed: {}", e),
)
})?;
Ok(status::Custom(Status::Ok, token)) // 返回JWT令牌
Ok(status::Custom(Status::Ok, token))
}
/**
* Rocket应用
*
* #
* - `rocket::Rocket`: Rocket应用实例
*/
#[launch]
async fn rocket() -> _ {
let config = config::Config::read().expect("Failed to read config"); // 读取配置
let config = config::Config::read().expect("Failed to read config");
if config.info.install {
init_sql(config.sql_config)
.await
.expect("Failed to connect to database"); // 初始化数据库连接
.expect("Failed to connect to database");
rocket::build()
.mount("/auth/token", routes![token_system])
} else {
rocket::build()
.mount("/", routes![install]) // 挂载API路由
.mount("/", routes![install])
}
}

View File

@ -1,10 +1,3 @@
// File path: src/secret.rs
/**
* JWT的生成和验证功能
* JWTJWT的相关函数
*/
use jwt_compact::{alg::Ed25519, AlgorithmExt, Header, Token, UntrustedToken, TimeOptions};
use serde::{Serialize, Deserialize};
use chrono::{Duration, Utc};
@ -15,16 +8,15 @@ use std::{env, fs};
use std::error::Error;
use rand::{SeedableRng, RngCore};
// 定义JWT的Claims结构体有效载荷
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CustomClaims {
pub user_id: String, // 用户ID
pub device_ua: String, // 用户UA
pub user_id: String,
pub device_ua: String,
}
pub enum SecretKey {
Signing, // 签名密钥
Verifying, // 验证密钥
Signing,
Verifying,
}
impl SecretKey {
@ -36,84 +28,72 @@ impl SecretKey {
}
}
/**
*
*/
pub fn generate_key() -> Result<(), Box<dyn Error>> {
let mut csprng = rand::rngs::StdRng::from_entropy(); // 使用系统熵创建随机数生成器
let mut csprng = rand::rngs::StdRng::from_entropy();
let mut private_key_bytes = [0u8; 32]; // 存储签名密钥的字节数组
csprng.fill_bytes(&mut private_key_bytes); // 生成签名密钥
let mut private_key_bytes = [0u8; 32];
csprng.fill_bytes(&mut private_key_bytes);
let signing_key = SigningKey::from_bytes(&private_key_bytes); // 从签名密钥获取SigningKey
let verifying_key = signing_key.verifying_key(); // 获取验证密钥
let signing_key = SigningKey::from_bytes(&private_key_bytes);
let verifying_key = signing_key.verifying_key();
let base_path = env::current_dir()?
.join("assets")
.join("key");
fs::create_dir_all(&base_path)?; // 创建目录
fs::create_dir_all(&base_path)?;
File::create(base_path.join(SecretKey::Signing.as_string()))?
.write_all(signing_key.as_bytes())?; // 保存签名密钥
.write_all(signing_key.as_bytes())?;
File::create(base_path.join(SecretKey::Verifying.as_string()))?
.write_all(verifying_key.as_bytes())?; // 保存验证密钥
.write_all(verifying_key.as_bytes())?;
Ok(())
}
/**
*
*/
pub fn get_key(key_type: SecretKey) -> Result<[u8; 32], Box<dyn Error>> {
let path = env::current_dir()?
.join("assets")
.join("key")
.join(key_type.as_string());
let key_bytes = fs::read(path)?; // 读取密钥文件
let mut key = [0u8; 32]; // 固定长度的数组
key.copy_from_slice(&key_bytes[..32]); // 拷贝前32个字节
let key_bytes = fs::read(path)?;
let mut key = [0u8; 32];
key.copy_from_slice(&key_bytes[..32]);
Ok(key)
}
/**
* JWT
*/
pub fn generate_jwt(claims: CustomClaims, duration: Duration) -> Result<String, Box<dyn Error>> {
let key_bytes = get_key(SecretKey::Signing)?; // 从文件读取私钥
let signing_key = SigningKey::from_bytes(&key_bytes); // 创建SigningKey
let key_bytes = get_key(SecretKey::Signing)?;
let signing_key = SigningKey::from_bytes(&key_bytes);
let time_options = TimeOptions::new(
Duration::seconds(0), // 设置时间容差为0
Utc::now // 使用当前UTC时间作为时钟函数
); // 设置时间容差为); // 默认时间选项
let claims = jwt_compact::Claims::new(claims) // 创建JWT的有效载荷
Duration::seconds(0),
Utc::now
);
let claims = jwt_compact::Claims::new(claims)
.set_duration_and_issuance(&time_options, duration)
.set_not_before(Utc::now()); // 设置不早于时间
.set_not_before(Utc::now());
let header = Header::empty(); // 创建自定义的头部
let header = Header::empty();
let token = Ed25519.token(&header, &claims, &signing_key)?; // 使用Ed25519签名JWT
let token = Ed25519.token(&header, &claims, &signing_key)?;
Ok(token)
}
/**
* JWT并返回自定义声明
*/
pub fn validate_jwt(token: &str) -> Result<CustomClaims, Box<dyn Error>> {
let key_bytes = get_key(SecretKey::Verifying)?; // 从文件读取验证密钥
let verifying = VerifyingKey::from_bytes(&key_bytes)?; // 创建VerifyingKey
let token = UntrustedToken::new(token)?; // 创建未受信任的Token
let token: Token<CustomClaims> = Ed25519.validator(&verifying).validate(&token)?; // 验证Token
let key_bytes = get_key(SecretKey::Verifying)?;
let verifying = VerifyingKey::from_bytes(&key_bytes)?;
let token = UntrustedToken::new(token)?;
let token: Token<CustomClaims> = Ed25519.validator(&verifying).validate(&token)?;
let time_options = TimeOptions::new(
Duration::seconds(0), // 设置时间容差为0
Utc::now // 使用当前UTC时间作为时钟函数
); // 设置时间容差为); // 默认时间选项
Duration::seconds(0),
Utc::now
);
token.claims()
.validate_expiration(&time_options)? // 验证过期时间
.validate_maturity(&time_options)?; // 验证成熟时间
let claims = token.claims().custom.clone(); // 获取自定义声明
.validate_expiration(&time_options)?
.validate_maturity(&time_options)?;
let claims = token.claims().custom.clone();
Ok(claims)
}

View File

@ -1,4 +1,3 @@
// File path: components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ThemeService } from '../services/themeService';
@ -28,11 +27,9 @@ export default class ErrorBoundary extends Component<Props, State> {
if (this.state.hasError) {
const themeService = ThemeService.getInstance();
try {
// 尝试使用主题的错误模板
const errorTemplate = themeService.getTemplate('error');
return <div dangerouslySetInnerHTML={{ __html: errorTemplate }} />;
} catch (e) {
// 如果无法获取主题模板,显示默认错误页面
return (
<div className="error-page">
<h1>Something went wrong</h1>

View File

@ -1,4 +1,3 @@
// File path: components/LoadingBoundary.tsx
import React, { Suspense } from 'react';
interface LoadingBoundaryProps {

View File

@ -1,13 +1,5 @@
// src/contracts/CapabilityContract.ts
/**
*
*/
export interface CapabilityProps<T> {
// 能力名称
name: string;
// 能力描述
description?: string;
// 能力执行函数
execute: (...args: any[]) => Promise<T>;
}

View File

@ -1,12 +1,9 @@
// File path: contracts\generalContract.ts
/**
*
*
* - null
* - number
* - string
* - boolean
* -
* -
*/
export type SerializeType = null | number | string | boolean | { [key: string]: SerializeType } | Array<SerializeType>;
export interface Configuration {
[key: string]: {
title: string;
description?: string;
data: SerializeType;
};
}

View File

@ -1,40 +1,18 @@
// File path: contracts\pluginContract.ts
/**
*
*
*
*
*/
import { SerializeType } from "contracts/generalContract";
import { Configuration } from "contracts/generalContract";
export interface PluginConfig {
name: string; // 插件名称
version: string; // 插件版本
displayName: string; // 插件显示名称
description?: string; // 插件描述(可选)
author?: string; // 插件作者(可选)
enabled: boolean; // 插件是否启用
icon?: string; // 插件图标URL可选
managePath?: string; // 插件管理页面路径(可选)
configuration?: PluginConfiguration; // 插件配置
name: string;
version: string;
displayName: string;
description?: string;
author?: string;
enabled: boolean;
icon?: string;
managePath?: string;
configuration?: Configuration;
routs: Set<{
description?: string; // 路由描述(可选)
path: string; // 路由路径
description?: string;
path: string;
}>;
}
/**
*
*
*
*/
export interface PluginConfiguration {
[key: string]: {
title: string; // 属性标题
description?: string; // 属性描述(可选)
data: SerializeType; // 额外数据(可选),支持序列化
};
}

View File

@ -1,17 +1,11 @@
export interface TemplateContract {
// 模板名称
name: string;
// 模板描述
description?: string;
// 模板配置
config: {
// 模板布局
layout?: string;
// 模板样式
styles?: string[];
// 模板脚本
scripts?: string[];
};
// 渲染函数
render: (props: any) => React.ReactNode;
}
loader: () => Promise<void>;
element: () => React.ReactNode;
}

View File

@ -1,50 +1,29 @@
// File path: contracts\themeTypeContract.ts
/**
*
*
*/
/**
*
*
*/
import { SerializeType } from "contracts/generalContract";
import { Configuration } from "contracts/generalContract";
export interface ThemeConfig {
name: string; // 主题的名称
displayName: string; // 主题的显示名称
icon?: string; // 主题图标URL可选
version: string; // 主题的版本号
description?: string; // 主题的描述信息
author?: string; // 主题的作者信息
templates: Map<string, ThemeTemplate>; // 主题模板的映射表
/** 主题全局配置 */
name: string;
displayName: string;
icon?: string;
version: string;
description?: string;
author?: string;
templates: Map<string, ThemeTemplate>;
globalSettings?: {
layout?: string; // 主题的布局配置
css?: string; // 主题的CSS配置
layout?: string;
css?: string;
};
/** 主题配置文件 */
settingsSchema: Record<string, {
name: string; // 属性的名称
description?: string; // 属性的描述信息
data: SerializeType; // 属性的默认数据
}>;
/** 路由 */
configuration: Configuration;
routes: {
index: string; // 首页使用的模板
post: string; // 文章使用的模板
tag: string; // 标签使用的模板
category: string; // 分类使用的模板
error: string; // 错误页面用的模板
page: Map<string, string>; // 独立页面模板
index: string;
post: string;
tag: string;
category: string;
error: string;
page: Map<string, string>;
}
}
/**
*
*
*/
export interface ThemeTemplate {
path: string; // 模板文件的路径
name: string; // 模板的名称
description?: string; // 模板的描述信息
path: string;
name: string;
description?: string;
}

View File

@ -1,40 +1,23 @@
// File path: /hooks/createServiceContext.tsx
import { createContext, useContext, ReactNode, FC } from 'react';
/**
*
*/
type ServiceContextReturn<N extends string,T> = {
[K in `${N}Provider`]: FC<{ children: ReactNode }>;
} & {
[K in `use${N}`]: () => T;
};
/**
*
*
* @param serviceName -
* @param ServiceClass - getInstance
*/
export function createServiceContext<T, N extends string>(
serviceName: N,
getServiceInstance: () => T
): ServiceContextReturn<N,T> {
const ServiceContext = createContext<T | undefined>(undefined);
/**
*
*/
const Provider: FC<{ children: ReactNode }> = ({ children }) => (
<ServiceContext.Provider value={getServiceInstance()}>
{children}
</ServiceContext.Provider>
);
/**
*
*/
const useService = (): T => {
const context = useContext(ServiceContext);
if (context === undefined) {

View File

@ -16,13 +16,6 @@ export const { ApiProvider, useApi } = createServiceContext(
"Api", () => ThemeService.getInstance(),
);
// File path:hooks/servicesProvider.tsx
/**
* ServiceProvider
*
* @param children -
*/
export const ServiceProvider = ({ children }: { children: ReactNode }) => (
<ApiProvider>
<CapabilityProvider>

View File

@ -1,15 +0,0 @@
// File path: hooks/useAsyncError.tsx
import { useState, useCallback } from 'react';
export function useAsyncError() {
const [, setError] = useState();
return useCallback(
(e: Error) => {
setError(() => {
throw e;
});
},
[setError],
);
}

View File

@ -1,32 +1,18 @@
// File path: /d:/data/echoes/frontend/services/apiService.ts
/**
* ApiConfig接口用于配置API服务的基本信息
*/
interface ApiConfig {
baseURL: string; // API的基础URL
timeout?: number; // 请求超时时间(可选)
baseURL: string;
timeout?: number;
}
export class ApiService {
private static instance: ApiService; // ApiService的单例实例
private baseURL: string; // API的基础URL
private timeout: number; // 请求超时时间
private static instance: ApiService;
private baseURL: string;
private timeout: number;
/**
* ApiService实例
* @param config ApiConfig配置对象
*/
private constructor(config: ApiConfig) {
this.baseURL = config.baseURL;
this.timeout = config.timeout || 10000; // 默认超时时间为10000毫秒
this.timeout = config.timeout || 10000;
}
/**
* ApiService的单例实例
* @param config ApiConfig配置对象
* @returns ApiService实例
*/
public static getInstance(config?: ApiConfig): ApiService {
if (!this.instance && config) {
this.instance = new ApiService(config);
@ -34,11 +20,6 @@ export class ApiService {
return this.instance;
}
/**
*
* @returns Promise<string>
* @throws Error
*/
private async getSystemToken(): Promise<string> {
const username = import.meta.env.VITE_SYSTEM_USERNAME;
const password = import.meta.env.VITE_SYSTEM_PASSWORD;
@ -63,25 +44,17 @@ export class ApiService {
}
const data = await response.text();
return data; // Assuming the token is in the 'token' field of the response
return data;
} catch (error) {
console.error('Error getting system token:', error);
throw error;
}
}
/**
* API请求
* @param endpoint API端点
* @param options
* @param requiresAuth true
* @returns Promise<T> API响应数据
* @throws Error
*/
public async request<T>(
endpoint: string,
options: RequestInit = {},
auth ?: string
toekn ?: string
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@ -89,8 +62,8 @@ export class ApiService {
try {
const headers = new Headers(options.headers);
if (auth) {
headers.set('Authorization', `Bearer ${auth}`);
if (toekn) {
headers.set('Authorization', `Bearer ${toekn}`);
}
const response = await fetch(`${this.baseURL}${endpoint}`, {

View File

@ -1,29 +1,14 @@
// File path: services/capabilityService.ts
/**
* CapabilityService
*
*/
import { CapabilityProps } from "contracts/capabilityContract";
export class CapabilityService {
// 存储能力的映射,键为能力名称,值为能力源和能力属性的集合
private capabilities: Map<string, Set<{
source: string;
capability: CapabilityProps<any>}>> = new Map();
// CapabilityService 的唯一实例
private static instance: CapabilityService;
/**
*
*/
private constructor() { }
/**
* CapabilityService
* @returns {CapabilityService} CapabilityService
*/
public static getInstance(): CapabilityService {
if (!this.instance) {
this.instance = new CapabilityService();
@ -31,23 +16,11 @@ export class CapabilityService {
return this.instance;
}
/**
*
* @param capabilityName
* @param source
* @param capability
*/
private register(capabilityName: string, source: string, capability: CapabilityProps<any>) {
const handlers = this.capabilities.get(capabilityName) || new Set();
handlers.add({ source, capability });
}
/**
*
* @param capabilityName
* @param args
* @returns {Set<T>}
*/
private executeCapabilityMethod<T>(capabilityName: string, ...args: any[]): Set<T> {
const results = new Set<T>();
const handlers = this.capabilities.get(capabilityName);
@ -65,10 +38,6 @@ export class CapabilityService {
return results;
}
/**
*
* @param source
*/
private removeCapability(source: string) {
this.capabilities.forEach((capability_s, capabilityName) => {
const newHandlers = new Set(
@ -78,28 +47,20 @@ export class CapabilityService {
});
}
/**
*
* @param capability
*/
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;
}
}
}

View File

@ -1,4 +1,3 @@
// src/core/PluginManager.ts
import { PluginConfiguration } from 'types/pluginRequirement';
import { Contracts } from 'contracts/capabilityContract';
@ -8,7 +7,6 @@ export class PluginManager {
private extensions: Map<string, ExtensionProps> = new Map();
async loadPlugins() {
// 扫描插件目录
const pluginDirs = await this.scanPluginDirectory();
for (const dir of pluginDirs) {
@ -16,22 +14,18 @@ export class PluginManager {
const config = await import(`@/plugins/${dir}/plugin.config.ts`);
const plugin: PluginProps = config.default;
// 注册插件
this.plugins.set(plugin.name, plugin);
// 加载默认配置
if (plugin.settingsSchema) {
this.configurations.set(plugin.name, plugin.settingsSchema);
}
// 注册扩展
if (plugin.extensions) {
Object.entries(plugin.extensions).forEach(([key, value]) => {
this.extensions.set(`${plugin.name}.${key}`, value.extension);
});
}
// 执行安装钩子
if (plugin.hooks?.onInstall) {
await plugin.hooks.onInstall({});
}
@ -41,24 +35,19 @@ export class PluginManager {
}
}
// 获取插件配置
async getPluginConfig(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,88 +1,40 @@
// File path: services/routeManager.ts
import React from 'react'; // Import React
import { useEffect } from 'react';
import { useRoutes, RouteObject } from 'react-router-dom';
import { ThemeService } from './themeService';
import ErrorBoundary from '../components/ErrorBoundary';
import { LoaderFunction, RouteObject } from 'react-router-dom';
export class RouteManager {
private static instance: RouteManager;
private themeService: ThemeService;
private routes: RouteObject[] = [];
private constructor(themeService: ThemeService) {
this.themeService = themeService;
}
private constructor() {}
public static getInstance(themeService?: ThemeService): RouteManager {
if (!RouteManager.instance && themeService) {
RouteManager.instance = new RouteManager(themeService);
public static getInstance(): RouteManager {
if (!RouteManager.instance) {
RouteManager.instance = new RouteManager();
}
return RouteManager.instance;
}
/**
*
*/
public async initialize(): Promise<void> {
const themeConfig = this.themeService.getThemeConfig();
if (!themeConfig) {
throw new Error('Theme configuration not loaded');
}
this.routes = [
{
path: '/',
element: this.createRouteElement(themeConfig.routes.index),
errorElement: <ErrorBoundary />,
},
{
path: '/post/:id',
element: this.createRouteElement(themeConfig.routes.post),
},
{
path: '/tag/:tag',
element: this.createRouteElement(themeConfig.routes.tag),
},
{
path: '/category/:category',
element: this.createRouteElement(themeConfig.routes.category),
},
{
path: '*',
element: this.createRouteElement(themeConfig.routes.error),
},
];
// 添加自定义页面路由
themeConfig.routes.page.forEach((template, path) => {
this.routes.push({
path,
element: this.createRouteElement(template),
});
private register(path:string, element: React.ReactNode) {
this.routes.push({
path,
element,
});
}
/**
*
*/
private createRouteElement(templateName: string) {
return (props: any) => {
const template = this.themeService.getTemplate(templateName);
// 这里可以添加模板渲染逻辑
return <div dangerouslySetInnerHTML={{ __html: template }} />;
};
private createRouteElement(path: string,element:React.ReactNode,loader?:LoaderFunction) {
this.routes.push({
path,
element,
loader?: loader
})
}
/**
*
*/
public getRoutes(): RouteObject[] {
return this.routes;
}
/**
*
*/
public addRoute(path: string, templateName: string): void {
this.routes.push({
path,

View File

@ -1,4 +1,3 @@
// File path: services/themeService.ts
import { ThemeConfig, ThemeTemplate } from "contracts/themeContract";
import { ApiService } from "./apiService";
@ -19,18 +18,12 @@ export class ThemeService {
return ThemeService.instance;
}
/**
*
*/
public async initialize(): Promise<void> {
try {
// 从API获取当前主题配置
const themeConfig = await this.api.request<ThemeConfig>(
'/theme/current',
{ method: 'GET' }
);
// 加载主题配置
await this.loadTheme(themeConfig);
} catch (error) {
console.error('Failed to initialize theme:', error);
@ -38,9 +31,6 @@ export class ThemeService {
}
}
/**
*
*/
private async loadTheme(config: ThemeConfig): Promise<void> {
try {
this.currentTheme = config;
@ -51,9 +41,6 @@ export class ThemeService {
}
}
/**
*
*/
private async loadTemplates(): Promise<void> {
if (!this.currentTheme) {
throw new Error('No theme configuration loaded');
@ -70,23 +57,16 @@ export class ThemeService {
}
};
// 并行加载所有模板
const loadPromises = Array.from(this.currentTheme.templates.values())
.map(template => loadTemplate(template));
await Promise.all(loadPromises);
}
/**
*
*/
public getThemeConfig(): ThemeConfig | undefined {
return this.currentTheme;
}
/**
*
*/
public getTemplate(templateName: string): string {
const template = this.templates.get(templateName);
if (!template) {
@ -95,9 +75,6 @@ export class ThemeService {
return template;
}
/**
*
*/
public getTemplateByRoute(route: string): string {
if (!this.currentTheme) {
throw new Error('No theme configuration loaded');
@ -105,7 +82,6 @@ export class ThemeService {
let templateName: string | undefined;
// 检查是否是预定义路由
if (route === '/') {
templateName = this.currentTheme.routes.index;
} else if (route.startsWith('/post/')) {
@ -115,7 +91,6 @@ export class ThemeService {
} else if (route.startsWith('/category/')) {
templateName = this.currentTheme.routes.category;
} else {
// 检查自定义页面路由
templateName = this.currentTheme.routes.page.get(route);
}
@ -126,9 +101,6 @@ export class ThemeService {
return this.getTemplate(templateName);
}
/**
*
*/
public async updateThemeConfig(config: Partial<ThemeConfig>): Promise<void> {
try {
const updatedConfig = await this.api.request<ThemeConfig>(