数据库:由后端动态构建
后端:更新数据库配置,支持MySQL和SQLite,重构SQL查询构建,重构数据库初始化操作; 前端:调整初始化状态逻辑,改进主题切换功能。
This commit is contained in:
parent
bc42edd38e
commit
3daf6280a7
@ -3,13 +3,19 @@ name = "echoes"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { version = "0.5", features = ["json"] }
|
rocket = { version = "0.5", features = ["json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "postgres"] }
|
sqlx = { version = "0.8.2", features = [
|
||||||
|
"runtime-tokio-native-tls",
|
||||||
|
"postgres",
|
||||||
|
"mysql",
|
||||||
|
"sqlite"
|
||||||
|
] }
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
jwt-compact = { version = "0.8.0", features = ["ed25519-dalek"] }
|
jwt-compact = { version = "0.8.0", features = ["ed25519-dalek"] }
|
||||||
ed25519-dalek = "2.1.1"
|
ed25519-dalek = "2.1.1"
|
||||||
@ -17,5 +23,4 @@ rand = "0.8.5"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
bcrypt = "0.16"
|
bcrypt = "0.16"
|
||||||
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
@ -23,8 +23,12 @@ pub async fn token_system(
|
|||||||
state: &State<Arc<AppState>>,
|
state: &State<Arc<AppState>>,
|
||||||
data: Json<TokenSystemData>,
|
data: Json<TokenSystemData>,
|
||||||
) -> AppResult<String> {
|
) -> AppResult<String> {
|
||||||
|
let sql = state
|
||||||
|
.sql_get()
|
||||||
|
.await
|
||||||
|
.into_app_result()?;
|
||||||
let mut builder =
|
let mut builder =
|
||||||
builder::QueryBuilder::new(builder::SqlOperation::Select, "users".to_string())
|
builder::QueryBuilder::new(builder::SqlOperation::Select, sql.table_name("users"), sql.get_type())
|
||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
builder
|
builder
|
||||||
.add_field("password_hash".to_string())
|
.add_field("password_hash".to_string())
|
||||||
@ -56,9 +60,8 @@ pub async fn token_system(
|
|||||||
builder::Condition::new(
|
builder::Condition::new(
|
||||||
"role".to_string(),
|
"role".to_string(),
|
||||||
builder::Operator::Eq,
|
builder::Operator::Eq,
|
||||||
Some(builder::SafeValue::Enum(
|
Some(builder::SafeValue::Text(
|
||||||
"administrator".into(),
|
"administrator".into(),
|
||||||
"user_role".into(),
|
|
||||||
builder::ValidationLevel::Standard,
|
builder::ValidationLevel::Standard,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
@ -66,10 +69,7 @@ pub async fn token_system(
|
|||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
let values = state
|
let values = sql
|
||||||
.sql_get()
|
|
||||||
.await
|
|
||||||
.into_app_result()?
|
|
||||||
.get_db()
|
.get_db()
|
||||||
.execute_query(&builder)
|
.execute_query(&builder)
|
||||||
.await
|
.await
|
||||||
@ -83,6 +83,8 @@ pub async fn token_system(
|
|||||||
status::Custom(Status::NotFound, "Invalid system user or password".into())
|
status::Custom(Status::NotFound, "Invalid system user or password".into())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
println!("{}\n{}",&data.password,password.clone());
|
||||||
|
|
||||||
security::bcrypt::verify_hash(&data.password, password)
|
security::bcrypt::verify_hash(&data.password, password)
|
||||||
.map_err(|_| status::Custom(Status::Forbidden, "Invalid password".into()))?;
|
.map_err(|_| status::Custom(Status::Forbidden, "Invalid password".into()))?;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ use super::SystemToken;
|
|||||||
use crate::storage::{sql, sql::builder};
|
use crate::storage::{sql, sql::builder};
|
||||||
use crate::common::error::{AppResult, AppResultInto, CustomResult};
|
use crate::common::error::{AppResult, AppResultInto, CustomResult};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use rocket::data;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
get,
|
get,
|
||||||
http::Status,
|
http::Status,
|
||||||
@ -40,7 +41,7 @@ pub async fn get_setting(
|
|||||||
name: String,
|
name: String,
|
||||||
) -> CustomResult<Json<Value>> {
|
) -> CustomResult<Json<Value>> {
|
||||||
let name_condition = builder::Condition::new(
|
let name_condition = builder::Condition::new(
|
||||||
"key".to_string(),
|
"name".to_string(),
|
||||||
builder::Operator::Eq,
|
builder::Operator::Eq,
|
||||||
Some(builder::SafeValue::Text(
|
Some(builder::SafeValue::Text(
|
||||||
format!("{}_{}", comfig_type, name),
|
format!("{}_{}", comfig_type, name),
|
||||||
@ -51,7 +52,7 @@ pub async fn get_setting(
|
|||||||
let where_clause = builder::WhereClause::Condition(name_condition);
|
let where_clause = builder::WhereClause::Condition(name_condition);
|
||||||
|
|
||||||
let mut sql_builder =
|
let mut sql_builder =
|
||||||
builder::QueryBuilder::new(builder::SqlOperation::Select, "settings".to_string())?;
|
builder::QueryBuilder::new(builder::SqlOperation::Select, sql.table_name("settings"),sql.get_type())?;
|
||||||
|
|
||||||
sql_builder
|
sql_builder
|
||||||
.add_condition(where_clause)
|
.add_condition(where_clause)
|
||||||
@ -69,9 +70,9 @@ pub async fn insert_setting(
|
|||||||
data: Json<Value>,
|
data: Json<Value>,
|
||||||
) -> CustomResult<()> {
|
) -> CustomResult<()> {
|
||||||
let mut builder =
|
let mut builder =
|
||||||
builder::QueryBuilder::new(builder::SqlOperation::Insert, "settings".to_string())?;
|
builder::QueryBuilder::new(builder::SqlOperation::Insert, sql.table_name("settings"),sql.get_type())?;
|
||||||
builder.set_value(
|
builder.set_value(
|
||||||
"key".to_string(),
|
"name".to_string(),
|
||||||
builder::SafeValue::Text(
|
builder::SafeValue::Text(
|
||||||
format!("{}_{}", comfig_type, name).to_string(),
|
format!("{}_{}", comfig_type, name).to_string(),
|
||||||
builder::ValidationLevel::Strict,
|
builder::ValidationLevel::Strict,
|
||||||
@ -79,7 +80,7 @@ pub async fn insert_setting(
|
|||||||
)?;
|
)?;
|
||||||
builder.set_value(
|
builder.set_value(
|
||||||
"data".to_string(),
|
"data".to_string(),
|
||||||
builder::SafeValue::Json(data.into_inner()),
|
builder::SafeValue::Text(data.to_string(),builder::ValidationLevel::Relaxed),
|
||||||
)?;
|
)?;
|
||||||
sql.get_db().execute_query(&builder).await?;
|
sql.get_db().execute_query(&builder).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -91,7 +92,7 @@ pub async fn system_config_get(
|
|||||||
_token: SystemToken,
|
_token: SystemToken,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let sql = state.sql_get().await.into_app_result()?;
|
let sql = state.sql_get().await.into_app_result()?;
|
||||||
let settings = get_setting(&sql, "system".to_string(), "settings".to_string())
|
let settings = get_setting(&sql, "system".to_string(), sql.table_name("settings"))
|
||||||
.await
|
.await
|
||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
|
@ -1,35 +1,22 @@
|
|||||||
use super::{settings, users};
|
use super::{settings, users};
|
||||||
|
use crate::common::config;
|
||||||
|
use crate::common::error::{AppResult, AppResultInto};
|
||||||
|
use crate::common::helpers;
|
||||||
use crate::security;
|
use crate::security;
|
||||||
use crate::storage::sql;
|
use crate::storage::sql;
|
||||||
use crate::common::error::{AppResult, AppResultInto};
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::common::config;
|
|
||||||
use crate::common::helpers;
|
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use rocket::data;
|
||||||
use rocket::{http::Status, post, response::status, serde::json::Json, State};
|
use rocket::{http::Status, post, response::status, serde::json::Json, State};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize,Debug)]
|
#[post("/sql", format = "application/json", data = "<sql_config>")]
|
||||||
pub struct InstallData {
|
pub async fn setup_sql(
|
||||||
username: String,
|
sql_config: Json<config::SqlConfig>,
|
||||||
email: String,
|
|
||||||
password: String,
|
|
||||||
sql_config: config::SqlConfig,
|
|
||||||
}
|
|
||||||
#[derive(Deserialize, Serialize,Debug)]
|
|
||||||
pub struct InstallReplyData {
|
|
||||||
token: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/sql", format = "application/json", data = "<data>")]
|
|
||||||
pub async fn steup_sql(
|
|
||||||
data: Json<InstallData>,
|
|
||||||
state: &State<Arc<AppState>>,
|
state: &State<Arc<AppState>>,
|
||||||
) -> AppResult<status::Custom<Json<InstallReplyData>>> {
|
) -> AppResult<String> {
|
||||||
let mut config = config::Config::read().unwrap_or_default();
|
let mut config = config::Config::read().unwrap_or_default();
|
||||||
if config.init.sql {
|
if config.init.sql {
|
||||||
return Err(status::Custom(
|
return Err(status::Custom(
|
||||||
@ -37,19 +24,57 @@ pub async fn steup_sql(
|
|||||||
"Database already initialized".to_string(),
|
"Database already initialized".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sql_config = sql_config.into_inner();
|
||||||
|
|
||||||
|
config.init.sql = true;
|
||||||
|
config.sql_config = sql_config.clone();
|
||||||
|
sql::Database::initial_setup(sql_config.clone())
|
||||||
|
.await
|
||||||
|
.into_app_result()?;
|
||||||
|
|
||||||
|
config::Config::write(config).into_app_result()?;
|
||||||
|
state.trigger_restart().await.into_app_result()?;
|
||||||
|
Ok("Database installation successful".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct StepAccountData {
|
||||||
|
username: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct InstallReplyData {
|
||||||
|
token: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[post("/administrator", format = "application/json", data = "<data>")]
|
||||||
|
pub async fn setup_account(
|
||||||
|
data: Json<StepAccountData>,
|
||||||
|
state: &State<Arc<AppState>>,
|
||||||
|
) -> AppResult<status::Custom<Json<InstallReplyData>>> {
|
||||||
|
let mut config = config::Config::read().unwrap_or_default();
|
||||||
|
if config.init.administrator {
|
||||||
|
return Err(status::Custom(
|
||||||
|
Status::BadRequest,
|
||||||
|
"Administrator user has been set".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
security::jwt::generate_key().into_app_result()?;
|
||||||
|
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
let sql = {
|
let sql = {
|
||||||
config.init.sql = true;
|
state.sql_link(&config.sql_config).await.into_app_result()?;
|
||||||
config.sql_config = data.sql_config.clone();
|
|
||||||
sql::Database::initial_setup(data.sql_config.clone())
|
|
||||||
.await
|
|
||||||
.into_app_result()?;
|
|
||||||
security::jwt::generate_key().into_app_result()?;
|
|
||||||
state.sql_link(&data.sql_config).await.into_app_result()?;
|
|
||||||
state.sql_get().await.into_app_result()?
|
state.sql_get().await.into_app_result()?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let system_credentials = (
|
let system_credentials = (
|
||||||
helpers::generate_random_string(20),
|
helpers::generate_random_string(20),
|
||||||
helpers::generate_random_string(20),
|
helpers::generate_random_string(20),
|
||||||
@ -79,7 +104,6 @@ pub async fn steup_sql(
|
|||||||
.await
|
.await
|
||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
|
|
||||||
|
|
||||||
settings::insert_setting(
|
settings::insert_setting(
|
||||||
&sql,
|
&sql,
|
||||||
"system".to_string(),
|
"system".to_string(),
|
||||||
@ -93,11 +117,13 @@ pub async fn steup_sql(
|
|||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
|
|
||||||
let token = security::jwt::generate_jwt(
|
let token = security::jwt::generate_jwt(
|
||||||
security::jwt::CustomClaims { name: data.username },
|
security::jwt::CustomClaims {
|
||||||
|
name: data.username,
|
||||||
|
},
|
||||||
Duration::days(7),
|
Duration::days(7),
|
||||||
)
|
)
|
||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
|
config.init.administrator=true;
|
||||||
config::Config::write(config).into_app_result()?;
|
config::Config::write(config).into_app_result()?;
|
||||||
state.trigger_restart().await.into_app_result()?;
|
state.trigger_restart().await.into_app_result()?;
|
||||||
|
|
||||||
|
@ -20,37 +20,32 @@ pub struct RegisterData {
|
|||||||
pub role: String,
|
pub role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_user(sql: &sql::Database, data: RegisterData) -> CustomResult<()> {
|
pub async fn insert_user(sql: &sql::Database , data: RegisterData) -> CustomResult<()> {
|
||||||
let role = match data.role.as_str() {
|
let role = match data.role.as_str() {
|
||||||
"administrator" | "contributor" => data.role,
|
"administrator" | "contributor" => data.role,
|
||||||
_ => return Err("Invalid role. Must be either 'administrator' or 'contributor'".into_custom_error()),
|
_ => return Err("Invalid role. Must be either 'administrator' or 'contributor'".into_custom_error()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let password_hash = bcrypt::generate_hash(&data.password)?;
|
||||||
|
|
||||||
let mut builder =
|
let mut builder =
|
||||||
builder::QueryBuilder::new(builder::SqlOperation::Insert, "users".to_string())?;
|
builder::QueryBuilder::new(builder::SqlOperation::Insert, sql.table_name("users"), sql.get_type())?;
|
||||||
builder
|
builder
|
||||||
.set_value(
|
.set_value(
|
||||||
"username".to_string(),
|
"username".to_string(),
|
||||||
builder::SafeValue::Text(data.username, builder::ValidationLevel::Relaxed),
|
builder::SafeValue::Text(data.username, builder::ValidationLevel::Standard),
|
||||||
)?
|
)?
|
||||||
.set_value(
|
.set_value(
|
||||||
"email".to_string(),
|
"email".to_string(),
|
||||||
builder::SafeValue::Text(data.email, builder::ValidationLevel::Relaxed),
|
builder::SafeValue::Text(data.email, builder::ValidationLevel::Standard),
|
||||||
)?
|
)?
|
||||||
.set_value(
|
.set_value(
|
||||||
"password_hash".to_string(),
|
"password_hash".to_string(),
|
||||||
builder::SafeValue::Text(
|
builder::SafeValue::Text(password_hash, builder::ValidationLevel::Relaxed),
|
||||||
bcrypt::generate_hash(&data.password)?,
|
|
||||||
builder::ValidationLevel::Relaxed,
|
|
||||||
),
|
|
||||||
)?
|
)?
|
||||||
.set_value(
|
.set_value(
|
||||||
"role".to_string(),
|
"role".to_string(),
|
||||||
builder::SafeValue::Enum(
|
builder::SafeValue::Text(role, builder::ValidationLevel::Strict),
|
||||||
role,
|
|
||||||
"user_role".to_string(),
|
|
||||||
builder::ValidationLevel::Standard,
|
|
||||||
),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
sql.get_db().execute_query(&builder).await?;
|
sql.get_db().execute_query(&builder).await?;
|
||||||
|
@ -47,17 +47,19 @@ pub struct SqlConfig {
|
|||||||
pub user: String,
|
pub user: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub db_name: String,
|
pub db_name: String,
|
||||||
|
pub db_prefix:String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SqlConfig {
|
impl Default for SqlConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
db_type: "postgresql".to_string(),
|
db_type: "sqllite".to_string(),
|
||||||
address: "localhost".to_string(),
|
address: "".to_string(),
|
||||||
port: 5432,
|
port: 0,
|
||||||
user: "postgres".to_string(),
|
user: "".to_string(),
|
||||||
password: "postgres".to_string(),
|
password: "".to_string(),
|
||||||
db_name: "echoes".to_string(),
|
db_name: "echoes".to_string(),
|
||||||
|
db_prefix: "echoes_".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ mod common;
|
|||||||
mod storage;
|
mod storage;
|
||||||
mod api;
|
mod api;
|
||||||
|
|
||||||
use storage::sql;
|
use crate::common::config;
|
||||||
use common::error::{CustomErrorInto, CustomResult};
|
use common::error::{CustomErrorInto, CustomResult};
|
||||||
use rocket::Shutdown;
|
use rocket::Shutdown;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use storage::sql;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use crate::common::config;
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
db: Arc<Mutex<Option<sql::Database>>>,
|
db: Arc<Mutex<Option<sql::Database>>>,
|
||||||
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
||||||
@ -25,11 +25,7 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sql_get(&self) -> CustomResult<sql::Database> {
|
pub async fn sql_get(&self) -> CustomResult<sql::Database> {
|
||||||
self.db
|
self.db.lock().await.clone().ok_or_else(|| "数据库未连接".into_custom_error())
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.clone()
|
|
||||||
.ok_or_else(|| "数据库未连接".into_custom_error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sql_link(&self, config: &config::SqlConfig) -> CustomResult<()> {
|
pub async fn sql_link(&self, config: &config::SqlConfig) -> CustomResult<()> {
|
||||||
@ -37,57 +33,53 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn set_shutdown(&self, shutdown: Shutdown) {
|
pub async fn set_shutdown(&self, shutdown: Shutdown) {
|
||||||
*self.shutdown.lock().await = Some(shutdown);
|
*self.shutdown.lock().await = Some(shutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn trigger_restart(&self) -> CustomResult<()> {
|
pub async fn trigger_restart(&self) -> CustomResult<()> {
|
||||||
*self.restart_progress.lock().await = true;
|
*self.restart_progress.lock().await = true;
|
||||||
self.shutdown
|
self.shutdown.lock().await.take().ok_or_else(|| "未能获取rocket的shutdown".into_custom_error())?.notify();
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.take()
|
|
||||||
.ok_or_else(|| "未能获取rocket的shutdown".into_custom_error())?
|
|
||||||
.notify();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
async fn main() -> CustomResult<()> {
|
async fn main() -> CustomResult<()> {
|
||||||
let config = config::Config::read().unwrap_or_default();
|
let config = config::Config::read().unwrap_or_else(|e| {
|
||||||
|
eprintln!("配置读取失败: {}", e);
|
||||||
|
config::Config::default()
|
||||||
|
});
|
||||||
let state = Arc::new(AppState::new());
|
let state = Arc::new(AppState::new());
|
||||||
|
|
||||||
let rocket_config = rocket::Config::figment()
|
let rocket_config = rocket::Config::figment().merge(("address", config.address)).merge(("port", config.port));
|
||||||
.merge(("address", config.address))
|
|
||||||
.merge(("port", config.port));
|
|
||||||
|
|
||||||
let mut rocket_builder = rocket::build()
|
let mut rocket_builder = rocket::build().configure(rocket_config).manage(state.clone());
|
||||||
.configure(rocket_config)
|
|
||||||
.manage(state.clone());
|
|
||||||
|
|
||||||
if !config.info.install {
|
if !config.init.sql {
|
||||||
rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::install]);
|
rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_sql]);
|
||||||
|
} else if !config.init.administrator {
|
||||||
|
rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_account]);
|
||||||
} else {
|
} else {
|
||||||
state.sql_link(&config.sql_config).await?;
|
state.sql_link(&config.sql_config).await?;
|
||||||
rocket_builder = rocket_builder
|
rocket_builder = rocket_builder.mount("/auth/token", api::jwt_routes()).mount("/config", api::configure_routes());
|
||||||
.mount("/auth/token", api::jwt_routes())
|
|
||||||
.mount("/config", api::configure_routes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let rocket = rocket_builder.ignite().await?;
|
let rocket = rocket_builder.ignite().await?;
|
||||||
|
|
||||||
rocket
|
rocket.state::<Arc<AppState>>().ok_or_else(|| "未能获取AppState".into_custom_error())?.set_shutdown(rocket.shutdown()).await;
|
||||||
.state::<Arc<AppState>>()
|
|
||||||
.ok_or_else(|| "未能获取AppState".into_custom_error())?
|
|
||||||
.set_shutdown(rocket.shutdown())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
rocket.launch().await?;
|
rocket.launch().await?;
|
||||||
|
|
||||||
if *state.restart_progress.lock().await {
|
if *state.restart_progress.lock().await {
|
||||||
if let Ok(current_exe) = std::env::current_exe() {
|
if let Ok(current_exe) = std::env::current_exe() {
|
||||||
let _ = std::process::Command::new(current_exe).spawn();
|
match std::process::Command::new(current_exe).spawn() {
|
||||||
|
Ok(_) => println!("成功启动新进程"),
|
||||||
|
Err(e) => eprintln!("启动新进程失败: {}", e),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
eprintln!("获取当前可执行文件路径失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
|
@ -2,16 +2,16 @@ use crate::common::error::{CustomErrorInto, CustomResult};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use uuid::Uuid;
|
use crate::sql::schema::DatabaseType;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize)]
|
||||||
pub enum ValidationLevel {
|
pub enum ValidationLevel {
|
||||||
Strict,
|
Strict,
|
||||||
Standard,
|
Standard,
|
||||||
Relaxed,
|
Relaxed,
|
||||||
|
Raw,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -28,6 +28,7 @@ impl Default for TextValidator {
|
|||||||
(ValidationLevel::Strict, 100),
|
(ValidationLevel::Strict, 100),
|
||||||
(ValidationLevel::Standard, 1000),
|
(ValidationLevel::Standard, 1000),
|
||||||
(ValidationLevel::Relaxed, 100000),
|
(ValidationLevel::Relaxed, 100000),
|
||||||
|
(ValidationLevel::Raw, usize::MAX),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let level_allowed_chars = HashMap::from([
|
let level_allowed_chars = HashMap::from([
|
||||||
@ -43,6 +44,7 @@ impl Default for TextValidator {
|
|||||||
'}', '@', '#', '$', '%', '^', '&', '*', '+', '=', '<', '>', '/', '\\',
|
'}', '@', '#', '$', '%', '^', '&', '*', '+', '=', '<', '>', '/', '\\',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
(ValidationLevel::Raw, vec![]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
TextValidator {
|
TextValidator {
|
||||||
@ -74,6 +76,9 @@ impl Default for TextValidator {
|
|||||||
|
|
||||||
impl TextValidator {
|
impl TextValidator {
|
||||||
pub fn validate(&self, text: &str, level: ValidationLevel) -> CustomResult<()> {
|
pub fn validate(&self, text: &str, level: ValidationLevel) -> CustomResult<()> {
|
||||||
|
if level == ValidationLevel::Raw {
|
||||||
|
return self.validate_sql_patterns(text);
|
||||||
|
}
|
||||||
let max_length = self
|
let max_length = self
|
||||||
.level_max_lengths
|
.level_max_lengths
|
||||||
.get(&level)
|
.get(&level)
|
||||||
@ -140,6 +145,9 @@ impl TextValidator {
|
|||||||
pub fn validate_strict(&self, text: &str) -> CustomResult<()> {
|
pub fn validate_strict(&self, text: &str) -> CustomResult<()> {
|
||||||
self.validate(text, ValidationLevel::Strict)
|
self.validate(text, ValidationLevel::Strict)
|
||||||
}
|
}
|
||||||
|
pub fn validate_raw(&self, text: &str) -> CustomResult<()> {
|
||||||
|
self.validate(text, ValidationLevel::Raw)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sanitize(&self, text: &str) -> CustomResult<String> {
|
pub fn sanitize(&self, text: &str) -> CustomResult<String> {
|
||||||
self.validate_relaxed(text)?;
|
self.validate_relaxed(text)?;
|
||||||
@ -155,77 +163,31 @@ pub enum SafeValue {
|
|||||||
Float(f64),
|
Float(f64),
|
||||||
Text(String, ValidationLevel),
|
Text(String, ValidationLevel),
|
||||||
DateTime(DateTime<Utc>),
|
DateTime(DateTime<Utc>),
|
||||||
Uuid(Uuid),
|
}
|
||||||
Binary(Vec<u8>),
|
|
||||||
Array(Vec<SafeValue>),
|
impl std::fmt::Display for SafeValue {
|
||||||
Json(JsonValue),
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
Enum(String, String, ValidationLevel),
|
match self {
|
||||||
|
SafeValue::Null => write!(f, "NULL"),
|
||||||
|
SafeValue::Bool(b) => write!(f, "{}", b),
|
||||||
|
SafeValue::Integer(i) => write!(f, "{}", i),
|
||||||
|
SafeValue::Float(f_val) => write!(f, "{}", f_val),
|
||||||
|
SafeValue::Text(s, _) => write!(f, "{}", s),
|
||||||
|
SafeValue::DateTime(dt) => write!(f, "{}", dt.to_rfc3339()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SafeValue {
|
impl SafeValue {
|
||||||
pub fn from_json(value: JsonValue, level: ValidationLevel) -> CustomResult<Self> {
|
|
||||||
match value {
|
|
||||||
JsonValue::Null => Ok(SafeValue::Null),
|
|
||||||
JsonValue::Bool(b) => Ok(SafeValue::Bool(b)),
|
|
||||||
JsonValue::Number(n) => {
|
|
||||||
if let Some(i) = n.as_i64() {
|
|
||||||
Ok(SafeValue::Integer(i))
|
|
||||||
} else if let Some(f) = n.as_f64() {
|
|
||||||
Ok(SafeValue::Float(f))
|
|
||||||
} else {
|
|
||||||
Err("Invalid number format".into_custom_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JsonValue::String(s) => {
|
|
||||||
TextValidator::default().validate(&s, level)?;
|
|
||||||
Ok(SafeValue::Text(s, level))
|
|
||||||
}
|
|
||||||
JsonValue::Array(arr) => Ok(SafeValue::Array(
|
|
||||||
arr.into_iter()
|
|
||||||
.map(|item| SafeValue::from_json(item, level))
|
|
||||||
.collect::<CustomResult<Vec<_>>>()?,
|
|
||||||
)),
|
|
||||||
JsonValue::Object(_) => {
|
|
||||||
Self::validate_json_structure(&value, level)?;
|
|
||||||
Ok(SafeValue::Json(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_json_structure(value: &JsonValue, level: ValidationLevel) -> CustomResult<()> {
|
|
||||||
let validator = TextValidator::default();
|
|
||||||
match value {
|
|
||||||
JsonValue::Object(map) => {
|
|
||||||
for (key, val) in map {
|
|
||||||
validator.validate(key, level)?;
|
|
||||||
Self::validate_json_structure(val, level)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JsonValue::Array(arr) => {
|
|
||||||
arr.iter()
|
|
||||||
.try_for_each(|item| Self::validate_json_structure(item, level))?;
|
|
||||||
}
|
|
||||||
JsonValue::String(s) => validator.validate(s, level)?,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sql_type(&self) -> CustomResult<String> {
|
fn get_sql_type(&self) -> CustomResult<String> {
|
||||||
let sql_type = match self {
|
let sql_type = match self {
|
||||||
SafeValue::Null => "NULL",
|
SafeValue::Null => "NULL",
|
||||||
SafeValue::Bool(_) => "boolean",
|
SafeValue::Bool(_) => "BOOLEAN",
|
||||||
SafeValue::Integer(_) => "bigint",
|
SafeValue::Integer(_) => "INTEGER",
|
||||||
SafeValue::Float(_) => "double precision",
|
SafeValue::Float(_) => "REAL",
|
||||||
SafeValue::Text(_, _) => "text",
|
SafeValue::Text(_, _) => "TEXT",
|
||||||
SafeValue::DateTime(_) => "timestamp with time zone",
|
SafeValue::DateTime(_) => "TEXT",
|
||||||
SafeValue::Uuid(_) => "uuid",
|
|
||||||
SafeValue::Binary(_) => "bytea",
|
|
||||||
SafeValue::Array(_) | SafeValue::Json(_) => "jsonb",
|
|
||||||
SafeValue::Enum(_, enum_type, level) => {
|
|
||||||
TextValidator::default().validate(enum_type, *level)?;
|
|
||||||
return Ok(enum_type.replace('\'', "''"));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Ok(sql_type.to_string())
|
Ok(sql_type.to_string())
|
||||||
}
|
}
|
||||||
@ -233,37 +195,27 @@ impl SafeValue {
|
|||||||
pub fn to_sql_string(&self) -> CustomResult<String> {
|
pub fn to_sql_string(&self) -> CustomResult<String> {
|
||||||
match self {
|
match self {
|
||||||
SafeValue::Null => Ok("NULL".to_string()),
|
SafeValue::Null => Ok("NULL".to_string()),
|
||||||
SafeValue::Bool(b) => Ok(b.to_string()),
|
SafeValue::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
|
||||||
SafeValue::Integer(i) => Ok(i.to_string()),
|
SafeValue::Integer(i) => Ok(i.to_string()),
|
||||||
SafeValue::Float(f) => Ok(f.to_string()),
|
SafeValue::Float(f) => Ok(f.to_string()),
|
||||||
SafeValue::Text(s, level) => {
|
SafeValue::Text(s, level) => {
|
||||||
TextValidator::default().validate(s, *level)?;
|
TextValidator::default().validate(s, *level)?;
|
||||||
Ok(s.replace('\'', "''"))
|
Ok(format!("{}", s.replace('\'', "''")))
|
||||||
}
|
|
||||||
SafeValue::DateTime(dt) => Ok(format!("'{}'", dt.to_rfc3339())),
|
|
||||||
SafeValue::Uuid(u) => Ok(format!("'{}'", u)),
|
|
||||||
SafeValue::Binary(b) => Ok(format!("'\\x{}'", hex::encode(b))),
|
|
||||||
SafeValue::Array(arr) => {
|
|
||||||
let values: CustomResult<Vec<_>> = arr.iter().map(|v| v.to_sql_string()).collect();
|
|
||||||
Ok(format!("ARRAY[{}]", values?.join(",")))
|
|
||||||
}
|
|
||||||
SafeValue::Json(j) => {
|
|
||||||
let json_str = serde_json::to_string(j)?;
|
|
||||||
TextValidator::default().validate(&json_str, ValidationLevel::Relaxed)?;
|
|
||||||
Ok(json_str.replace('\'', "''"))
|
|
||||||
}
|
|
||||||
SafeValue::Enum(s, _, level) => {
|
|
||||||
TextValidator::default().validate(s, *level)?;
|
|
||||||
Ok(s.to_string())
|
|
||||||
}
|
}
|
||||||
|
SafeValue::DateTime(dt) => Ok(format!("{}", dt.to_rfc3339())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_param_sql(&self, param_index: usize) -> CustomResult<String> {
|
fn to_param_sql(&self, param_index: usize, db_type: DatabaseType) -> CustomResult<String> {
|
||||||
if matches!(self, SafeValue::Null) {
|
if matches!(self, SafeValue::Null) {
|
||||||
Ok("NULL".to_string())
|
return Ok("NULL".to_string());
|
||||||
} else {
|
}
|
||||||
Ok(format!("${}::{}", param_index, self.get_sql_type()?))
|
|
||||||
|
// 根据数据库类型返回不同的参数占位符
|
||||||
|
match db_type {
|
||||||
|
DatabaseType::MySQL => Ok("?".to_string()),
|
||||||
|
DatabaseType::PostgreSQL => Ok(format!("${}", param_index)),
|
||||||
|
DatabaseType::SQLite => Ok("?".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,12 +257,10 @@ pub enum Operator {
|
|||||||
In,
|
In,
|
||||||
IsNull,
|
IsNull,
|
||||||
IsNotNull,
|
IsNotNull,
|
||||||
JsonContains,
|
|
||||||
JsonExists,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Operator::Eq => "=",
|
Operator::Eq => "=",
|
||||||
Operator::Ne => "!=",
|
Operator::Ne => "!=",
|
||||||
@ -322,17 +272,15 @@ impl Operator {
|
|||||||
Operator::In => "IN",
|
Operator::In => "IN",
|
||||||
Operator::IsNull => "IS NULL",
|
Operator::IsNull => "IS NULL",
|
||||||
Operator::IsNotNull => "IS NOT NULL",
|
Operator::IsNotNull => "IS NOT NULL",
|
||||||
Operator::JsonContains => "@>",
|
|
||||||
Operator::JsonExists => "?",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Condition {
|
pub struct Condition {
|
||||||
field: Identifier,
|
pub field: Identifier,
|
||||||
operator: Operator,
|
pub operator: Operator,
|
||||||
value: Option<SafeValue>,
|
pub value: Option<SafeValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Condition {
|
impl Condition {
|
||||||
@ -362,10 +310,11 @@ pub struct QueryBuilder {
|
|||||||
order_by: Option<Identifier>,
|
order_by: Option<Identifier>,
|
||||||
limit: Option<i32>,
|
limit: Option<i32>,
|
||||||
offset: Option<i32>,
|
offset: Option<i32>,
|
||||||
|
db_type: DatabaseType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueryBuilder {
|
impl QueryBuilder {
|
||||||
pub fn new(operation: SqlOperation, table: String) -> CustomResult<Self> {
|
pub fn new(operation: SqlOperation, table: String, db_type: DatabaseType) -> CustomResult<Self> {
|
||||||
Ok(QueryBuilder {
|
Ok(QueryBuilder {
|
||||||
operation,
|
operation,
|
||||||
table: Identifier::new(table)?,
|
table: Identifier::new(table)?,
|
||||||
@ -375,6 +324,7 @@ impl QueryBuilder {
|
|||||||
order_by: None,
|
order_by: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
offset: None,
|
offset: None,
|
||||||
|
db_type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,7 +388,7 @@ impl QueryBuilder {
|
|||||||
if matches!(value, SafeValue::Null) {
|
if matches!(value, SafeValue::Null) {
|
||||||
placeholders.push("NULL".to_string());
|
placeholders.push("NULL".to_string());
|
||||||
} else {
|
} else {
|
||||||
placeholders.push(format!("${}::{}", params.len() + 1, value.get_sql_type()?));
|
placeholders.push(value.to_param_sql(params.len() + 1, self.db_type)?);
|
||||||
params.push(value.clone());
|
params.push(value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -458,11 +408,13 @@ impl QueryBuilder {
|
|||||||
|
|
||||||
let mut updates = Vec::new();
|
let mut updates = Vec::new();
|
||||||
for (field, value) in &self.values {
|
for (field, value) in &self.values {
|
||||||
let set_sql = format!(
|
let placeholder = if matches!(value, SafeValue::Null) {
|
||||||
"{} = {}",
|
"NULL".to_string()
|
||||||
field.as_str(),
|
} else {
|
||||||
value.to_param_sql(params.len() + 1)?
|
value.to_param_sql(params.len() + 1, self.db_type)?
|
||||||
);
|
};
|
||||||
|
|
||||||
|
let set_sql = format!("{} = {}", field.as_str(), placeholder);
|
||||||
if !matches!(value, SafeValue::Null) {
|
if !matches!(value, SafeValue::Null) {
|
||||||
params.push(value.clone());
|
params.push(value.clone());
|
||||||
}
|
}
|
||||||
@ -542,11 +494,17 @@ impl QueryBuilder {
|
|||||||
) -> CustomResult<String> {
|
) -> CustomResult<String> {
|
||||||
match &condition.value {
|
match &condition.value {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
|
let placeholder = if matches!(value, SafeValue::Null) {
|
||||||
|
"NULL".to_string()
|
||||||
|
} else {
|
||||||
|
value.to_param_sql(param_index, self.db_type)?
|
||||||
|
};
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"{} {} {}",
|
"{} {} {}",
|
||||||
condition.field.as_str(),
|
condition.field.as_str(),
|
||||||
condition.operator.as_str(),
|
condition.operator.as_str(),
|
||||||
value.to_param_sql(param_index)?
|
placeholder
|
||||||
);
|
);
|
||||||
if !matches!(value, SafeValue::Null) {
|
if !matches!(value, SafeValue::Null) {
|
||||||
params.push(value.clone());
|
params.push(value.clone());
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
mod postgresql;
|
mod postgresql;
|
||||||
|
mod mysql;
|
||||||
|
mod sqllite;
|
||||||
|
pub mod builder;
|
||||||
|
mod schema;
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::common::error::{CustomErrorInto, CustomResult};
|
use crate::common::error::{CustomErrorInto, CustomResult};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
pub mod builder;
|
use schema::DatabaseType;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DatabaseTrait: Send + Sync {
|
pub trait DatabaseTrait: Send + Sync {
|
||||||
@ -22,6 +27,8 @@ pub trait DatabaseTrait: Send + Sync {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub db: Arc<Box<dyn DatabaseTrait>>,
|
pub db: Arc<Box<dyn DatabaseTrait>>,
|
||||||
|
pub prefix: Arc<String>,
|
||||||
|
pub db_type: Arc<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
@ -29,22 +36,45 @@ impl Database {
|
|||||||
&self.db
|
&self.db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_prefix(&self) -> &str {
|
||||||
|
&self.prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(&self) -> DatabaseType {
|
||||||
|
match self.db_type.as_str() {
|
||||||
|
"postgresql" => DatabaseType::PostgreSQL,
|
||||||
|
"mysql" => DatabaseType::MySQL,
|
||||||
|
_ => DatabaseType::SQLite,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn table_name(&self, name: &str) -> String {
|
||||||
|
format!("{}{}", self.prefix, name)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn link(database: &config::SqlConfig) -> CustomResult<Self> {
|
pub async fn link(database: &config::SqlConfig) -> CustomResult<Self> {
|
||||||
let db = match database.db_type.as_str() {
|
let db: Box<dyn DatabaseTrait> = match database.db_type.as_str() {
|
||||||
"postgresql" => postgresql::Postgresql::connect(database).await?,
|
"postgresql" => Box::new(postgresql::Postgresql::connect(database).await?),
|
||||||
|
"mysql" => Box::new(mysql::Mysql::connect(database).await?),
|
||||||
|
"sqllite" => Box::new(sqllite::Sqlite::connect(database).await?),
|
||||||
_ => return Err("unknown database type".into_custom_error()),
|
_ => return Err("unknown database type".into_custom_error()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: Arc::new(Box::new(db)),
|
db: Arc::new(db),
|
||||||
|
prefix: Arc::new(database.db_prefix.clone()),
|
||||||
|
db_type: Arc::new(database.db_type.clone())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initial_setup(database: config::SqlConfig) -> CustomResult<()> {
|
pub async fn initial_setup(database: config::SqlConfig) -> CustomResult<()> {
|
||||||
match database.db_type.as_str() {
|
match database.db_type.as_str() {
|
||||||
"postgresql" => postgresql::Postgresql::initialization(database).await?,
|
"postgresql" => postgresql::Postgresql::initialization(database).await?,
|
||||||
|
"mysql" => mysql::Mysql::initialization(database).await?,
|
||||||
|
"sqllite" => sqllite::Sqlite::initialization(database).await?,
|
||||||
_ => return Err("unknown database type".into_custom_error()),
|
_ => return Err("unknown database type".into_custom_error()),
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
110
backend/src/storage/sql/mysql.rs
Normal file
110
backend/src/storage/sql/mysql.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use super::{builder::{self, SafeValue}, schema, DatabaseTrait};
|
||||||
|
use crate::config;
|
||||||
|
use crate::common::error::CustomResult;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::mysql::MySqlPool;
|
||||||
|
use sqlx::{Column, Executor, Row, TypeInfo};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Mysql {
|
||||||
|
pool: MySqlPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DatabaseTrait for Mysql {
|
||||||
|
async fn initialization(db_config: config::SqlConfig) -> CustomResult<()> {
|
||||||
|
let db_prefix = SafeValue::Text(format!("{}",db_config.db_prefix), builder::ValidationLevel::Strict);
|
||||||
|
let grammar = schema::generate_schema(schema::DatabaseType::MySQL,db_prefix)?;
|
||||||
|
let connection_str = format!(
|
||||||
|
"mysql://{}:{}@{}:{}",
|
||||||
|
db_config.user, db_config.password, db_config.address, db_config.port
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool = MySqlPool::connect(&connection_str).await?;
|
||||||
|
|
||||||
|
pool.execute(format!("CREATE DATABASE `{}`", db_config.db_name).as_str()).await?;
|
||||||
|
pool.execute(format!(
|
||||||
|
"ALTER DATABASE `{}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||||
|
db_config.db_name
|
||||||
|
).as_str()).await?;
|
||||||
|
|
||||||
|
|
||||||
|
let new_connection_str = format!(
|
||||||
|
"mysql://{}:{}@{}:{}/{}",
|
||||||
|
db_config.user,
|
||||||
|
db_config.password,
|
||||||
|
db_config.address,
|
||||||
|
db_config.port,
|
||||||
|
db_config.db_name
|
||||||
|
);
|
||||||
|
let new_pool = MySqlPool::connect(&new_connection_str).await?;
|
||||||
|
|
||||||
|
new_pool.execute(grammar.as_str()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn connect(db_config: &config::SqlConfig) -> CustomResult<Self> {
|
||||||
|
let connection_str = format!(
|
||||||
|
"mysql://{}:{}@{}:{}/{}",
|
||||||
|
db_config.user,
|
||||||
|
db_config.password,
|
||||||
|
db_config.address,
|
||||||
|
db_config.port,
|
||||||
|
db_config.db_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool = MySqlPool::connect(&connection_str).await?;
|
||||||
|
|
||||||
|
Ok(Mysql { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_query<'a>(
|
||||||
|
&'a self,
|
||||||
|
builder: &builder::QueryBuilder,
|
||||||
|
) -> CustomResult<Vec<HashMap<String, Value>>> {
|
||||||
|
let (query, values) = builder.build()?;
|
||||||
|
|
||||||
|
let mut sqlx_query = sqlx::query(&query);
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
match value {
|
||||||
|
SafeValue::Null => sqlx_query = sqlx_query.bind(None::<String>),
|
||||||
|
SafeValue::Bool(b) => sqlx_query = sqlx_query.bind(b),
|
||||||
|
SafeValue::Integer(i) => sqlx_query = sqlx_query.bind(i),
|
||||||
|
SafeValue::Float(f) => sqlx_query = sqlx_query.bind(f),
|
||||||
|
SafeValue::Text(s, _) => sqlx_query = sqlx_query.bind(s),
|
||||||
|
SafeValue::DateTime(dt) => sqlx_query = sqlx_query.bind(dt.to_rfc3339()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = sqlx_query.fetch_all(&self.pool).await?;
|
||||||
|
|
||||||
|
Ok(rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
row.columns()
|
||||||
|
.iter()
|
||||||
|
.map(|col| {
|
||||||
|
let value = match col.type_info().name() {
|
||||||
|
"INT4" | "INT8" => Value::Number(
|
||||||
|
row.try_get::<i64, _>(col.name()).unwrap_or_default().into(),
|
||||||
|
),
|
||||||
|
"FLOAT4" | "FLOAT8" => Value::Number(
|
||||||
|
serde_json::Number::from_f64(
|
||||||
|
row.try_get::<f64, _>(col.name()).unwrap_or(0.0),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|| 0.into()),
|
||||||
|
),
|
||||||
|
"BOOL" => Value::Bool(row.try_get(col.name()).unwrap_or_default()),
|
||||||
|
_ => Value::String(row.try_get(col.name()).unwrap_or_default()),
|
||||||
|
};
|
||||||
|
(col.name().to_string(), value)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,15 @@
|
|||||||
use super::{builder, DatabaseTrait};
|
use super::{
|
||||||
|
builder::{self, SafeValue},
|
||||||
|
schema, DatabaseTrait,
|
||||||
|
};
|
||||||
|
use crate::common::error::{CustomError, CustomErrorInto, CustomResult};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::common::error::CustomResult;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sqlx::{Column, Executor, PgPool, Row, TypeInfo};
|
use sqlx::{Column, Executor, PgPool, Row, TypeInfo};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::{env, fs};
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Postgresql {
|
pub struct Postgresql {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
@ -14,13 +18,11 @@ pub struct Postgresql {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DatabaseTrait for Postgresql {
|
impl DatabaseTrait for Postgresql {
|
||||||
async fn initialization(db_config: config::SqlConfig) -> CustomResult<()> {
|
async fn initialization(db_config: config::SqlConfig) -> CustomResult<()> {
|
||||||
let path = env::current_dir()?
|
let db_prefix = SafeValue::Text(
|
||||||
.join("src")
|
format!("{}", db_config.db_prefix),
|
||||||
.join("storage")
|
builder::ValidationLevel::Strict,
|
||||||
.join("sql")
|
);
|
||||||
.join("postgresql")
|
let grammar = schema::generate_schema(schema::DatabaseType::PostgreSQL, db_prefix)?;
|
||||||
.join("schema.sql");
|
|
||||||
let grammar = fs::read_to_string(&path)?;
|
|
||||||
|
|
||||||
let connection_str = format!(
|
let connection_str = format!(
|
||||||
"postgres://{}:{}@{}:{}",
|
"postgres://{}:{}@{}:{}",
|
||||||
@ -70,7 +72,14 @@ impl DatabaseTrait for Postgresql {
|
|||||||
let mut sqlx_query = sqlx::query(&query);
|
let mut sqlx_query = sqlx::query(&query);
|
||||||
|
|
||||||
for value in values {
|
for value in values {
|
||||||
sqlx_query = sqlx_query.bind(value.to_sql_string()?);
|
match value {
|
||||||
|
SafeValue::Null => sqlx_query = sqlx_query.bind(None::<String>),
|
||||||
|
SafeValue::Bool(b) => sqlx_query = sqlx_query.bind(b),
|
||||||
|
SafeValue::Integer(i) => sqlx_query = sqlx_query.bind(i),
|
||||||
|
SafeValue::Float(f) => sqlx_query = sqlx_query.bind(f),
|
||||||
|
SafeValue::Text(s, _) => sqlx_query = sqlx_query.bind(s),
|
||||||
|
SafeValue::DateTime(dt) => sqlx_query = sqlx_query.bind(dt.to_rfc3339()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = sqlx_query.fetch_all(&self.pool).await?;
|
let rows = sqlx_query.fetch_all(&self.pool).await?;
|
||||||
@ -92,7 +101,6 @@ impl DatabaseTrait for Postgresql {
|
|||||||
.unwrap_or_else(|| 0.into()),
|
.unwrap_or_else(|| 0.into()),
|
||||||
),
|
),
|
||||||
"BOOL" => Value::Bool(row.try_get(col.name()).unwrap_or_default()),
|
"BOOL" => Value::Bool(row.try_get(col.name()).unwrap_or_default()),
|
||||||
"JSON" | "JSONB" => row.try_get(col.name()).unwrap_or(Value::Null),
|
|
||||||
_ => Value::String(row.try_get(col.name()).unwrap_or_default()),
|
_ => Value::String(row.try_get(col.name()).unwrap_or_default()),
|
||||||
};
|
};
|
||||||
(col.name().to_string(), value)
|
(col.name().to_string(), value)
|
||||||
@ -102,3 +110,9 @@ impl DatabaseTrait for Postgresql {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Postgresql {
|
||||||
|
fn get_sdb(&self){
|
||||||
|
let a=self.pool;
|
||||||
|
}
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
-- 自定义类型定义
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
CREATE TYPE user_role AS ENUM ('contributor', 'administrator');
|
|
||||||
CREATE TYPE content_status AS ENUM ('draft', 'published', 'private', 'hidden');
|
|
||||||
|
|
||||||
-- 用户表
|
|
||||||
CREATE TABLE users
|
|
||||||
(
|
|
||||||
username VARCHAR(100) PRIMARY KEY,
|
|
||||||
avatar_url VARCHAR(255),
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
profile_icon VARCHAR(255),
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
role user_role NOT NULL DEFAULT 'contributor',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 页面表
|
|
||||||
CREATE TABLE pages
|
|
||||||
(
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
meta_keywords VARCHAR(255) NOT NULL,
|
|
||||||
meta_description VARCHAR(255) NOT NULL,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
template VARCHAR(50),
|
|
||||||
custom_fields JSON,
|
|
||||||
status content_status DEFAULT 'draft'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 文章表
|
|
||||||
CREATE TABLE posts
|
|
||||||
(
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
author_id VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE,
|
|
||||||
cover_image VARCHAR(255),
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
meta_keywords VARCHAR(255) NOT NULL,
|
|
||||||
meta_description VARCHAR(255) NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
status content_status DEFAULT 'draft',
|
|
||||||
is_editor BOOLEAN DEFAULT FALSE,
|
|
||||||
draft_content TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
published_at TIMESTAMP,
|
|
||||||
CONSTRAINT check_update_time CHECK (updated_at >= created_at)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 标签表
|
|
||||||
CREATE TABLE tags
|
|
||||||
(
|
|
||||||
name VARCHAR(50) PRIMARY KEY CHECK (LOWER(name) = name),
|
|
||||||
icon VARCHAR(255)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 文章标签关联表
|
|
||||||
CREATE TABLE post_tags
|
|
||||||
(
|
|
||||||
post_id UUID REFERENCES posts (id) ON DELETE CASCADE,
|
|
||||||
tag_id VARCHAR(50) REFERENCES tags (name) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (post_id, tag_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 分类表
|
|
||||||
CREATE TABLE categories
|
|
||||||
(
|
|
||||||
name VARCHAR(50) PRIMARY KEY,
|
|
||||||
parent_id VARCHAR(50),
|
|
||||||
FOREIGN KEY (parent_id) REFERENCES categories (name)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 文章分类关联表
|
|
||||||
CREATE TABLE post_categories
|
|
||||||
(
|
|
||||||
post_id UUID REFERENCES posts (id) ON DELETE CASCADE,
|
|
||||||
category_id VARCHAR(50) REFERENCES categories (name) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (post_id, category_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 资源库表
|
|
||||||
CREATE TABLE resources
|
|
||||||
(
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
author_id VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
size_bytes BIGINT NOT NULL,
|
|
||||||
storage_path VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
file_type VARCHAR(50) NOT NULL,
|
|
||||||
category VARCHAR(50),
|
|
||||||
description VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
-- 配置表
|
|
||||||
CREATE TABLE settings
|
|
||||||
(
|
|
||||||
key VARCHAR(50) PRIMARY KEY CHECK (LOWER(key) = key),
|
|
||||||
data JSON
|
|
||||||
);
|
|
812
backend/src/storage/sql/schema.rs
Normal file
812
backend/src/storage/sql/schema.rs
Normal file
@ -0,0 +1,812 @@
|
|||||||
|
use super::builder::{Condition, Identifier, Operator, SafeValue, ValidationLevel, WhereClause};
|
||||||
|
use crate::common::error::{CustomErrorInto, CustomResult};
|
||||||
|
use std::{collections::HashMap, fmt::format};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum DatabaseType {
|
||||||
|
PostgreSQL,
|
||||||
|
MySQL,
|
||||||
|
SQLite,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum FieldType {
|
||||||
|
Integer(bool),
|
||||||
|
BigInt,
|
||||||
|
VarChar(usize),
|
||||||
|
Text,
|
||||||
|
Boolean,
|
||||||
|
Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FieldConstraint {
|
||||||
|
pub is_primary: bool,
|
||||||
|
pub is_unique: bool,
|
||||||
|
pub is_nullable: bool,
|
||||||
|
pub default_value: Option<SafeValue>,
|
||||||
|
pub check_constraint: Option<WhereClause>,
|
||||||
|
pub foreign_key: Option<ForeignKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ForeignKeyAction {
|
||||||
|
Cascade,
|
||||||
|
Restrict,
|
||||||
|
SetNull,
|
||||||
|
NoAction,
|
||||||
|
SetDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ForeignKey {
|
||||||
|
pub ref_table: String,
|
||||||
|
pub ref_column: String,
|
||||||
|
pub on_delete: Option<ForeignKeyAction>,
|
||||||
|
pub on_update: Option<ForeignKeyAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for ForeignKeyAction {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
ForeignKeyAction::Cascade => "CASCADE",
|
||||||
|
ForeignKeyAction::Restrict => "RESTRICT",
|
||||||
|
ForeignKeyAction::SetNull => "SET NULL",
|
||||||
|
ForeignKeyAction::NoAction => "NO ACTION",
|
||||||
|
ForeignKeyAction::SetDefault => "SET DEFAULT",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Field {
|
||||||
|
pub name: Identifier,
|
||||||
|
pub field_type: FieldType,
|
||||||
|
pub constraints: FieldConstraint,
|
||||||
|
pub validation_level: ValidationLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Table {
|
||||||
|
pub name: Identifier,
|
||||||
|
pub fields: Vec<Field>,
|
||||||
|
pub indexes: Vec<Index>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Index {
|
||||||
|
pub name: Identifier,
|
||||||
|
pub fields: Vec<Identifier>,
|
||||||
|
pub is_unique: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldConstraint {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
is_primary: false,
|
||||||
|
is_unique: false,
|
||||||
|
is_nullable: true,
|
||||||
|
default_value: None,
|
||||||
|
check_constraint: None,
|
||||||
|
foreign_key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn primary(mut self) -> Self {
|
||||||
|
self.is_primary = true;
|
||||||
|
self.is_nullable = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique(mut self) -> Self {
|
||||||
|
self.is_unique = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_null(mut self) -> Self {
|
||||||
|
self.is_nullable = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default(mut self, value: SafeValue) -> Self {
|
||||||
|
self.default_value = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(mut self, clause: WhereClause) -> Self {
|
||||||
|
self.check_constraint = Some(clause);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreign_key(mut self, ref_table: String, ref_column: String) -> Self {
|
||||||
|
self.foreign_key = Some(ForeignKey {
|
||||||
|
ref_table,
|
||||||
|
ref_column,
|
||||||
|
on_delete: None,
|
||||||
|
on_update: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_delete(mut self, action: ForeignKeyAction) -> Self {
|
||||||
|
if let Some(ref mut fk) = self.foreign_key {
|
||||||
|
fk.on_delete = Some(action);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_update(mut self, action: ForeignKeyAction) -> Self {
|
||||||
|
if let Some(ref mut fk) = self.foreign_key {
|
||||||
|
fk.on_update = Some(action);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Field {
|
||||||
|
pub fn new(
|
||||||
|
name: &str,
|
||||||
|
field_type: FieldType,
|
||||||
|
constraints: FieldConstraint,
|
||||||
|
validation_level: ValidationLevel,
|
||||||
|
) -> CustomResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: Identifier::new(name.to_string())?,
|
||||||
|
field_type,
|
||||||
|
constraints,
|
||||||
|
validation_level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_type_sql(&self, db_type: DatabaseType) -> CustomResult<String> {
|
||||||
|
Ok(match &self.field_type {
|
||||||
|
FieldType::Integer(auto_increment) => {
|
||||||
|
if *auto_increment && self.constraints.is_primary {
|
||||||
|
match db_type {
|
||||||
|
DatabaseType::MySQL => "INT AUTO_INCREMENT".to_string(),
|
||||||
|
DatabaseType::PostgreSQL => {
|
||||||
|
"INTEGER GENERATED ALWAYS AS IDENTITY".to_string()
|
||||||
|
}
|
||||||
|
DatabaseType::SQLite => "INTEGER".to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match db_type {
|
||||||
|
DatabaseType::MySQL => "INT".to_string(),
|
||||||
|
_ => "INTEGER".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FieldType::BigInt => "BIGINT".to_string(),
|
||||||
|
FieldType::VarChar(size) => format!("VARCHAR({})", size),
|
||||||
|
FieldType::Text => "TEXT".to_string(),
|
||||||
|
FieldType::Boolean => match db_type {
|
||||||
|
DatabaseType::PostgreSQL => "BOOLEAN".to_string(),
|
||||||
|
DatabaseType::MySQL => "BOOLEAN".to_string(),
|
||||||
|
DatabaseType::SQLite => "INTEGER".to_string(),
|
||||||
|
},
|
||||||
|
FieldType::Timestamp => match db_type {
|
||||||
|
DatabaseType::PostgreSQL => "TIMESTAMP WITH TIME ZONE".to_string(),
|
||||||
|
DatabaseType::MySQL => "TIMESTAMP".to_string(),
|
||||||
|
DatabaseType::SQLite => "TEXT".to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_check_constraint(check: &WhereClause) -> CustomResult<String> {
|
||||||
|
match check {
|
||||||
|
WhereClause::Condition(condition) => {
|
||||||
|
let field_name = condition.field.as_str();
|
||||||
|
match condition.operator {
|
||||||
|
Operator::In => {
|
||||||
|
if let Some(SafeValue::Text(values, _)) = &condition.value {
|
||||||
|
Ok(format!("{} IN {}", field_name, values))
|
||||||
|
} else {
|
||||||
|
Err("Invalid IN clause value".into_custom_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Operator::Eq
|
||||||
|
| Operator::Ne
|
||||||
|
| Operator::Gt
|
||||||
|
| Operator::Lt
|
||||||
|
| Operator::Gte
|
||||||
|
| Operator::Lte => {
|
||||||
|
if let Some(value) = &condition.value {
|
||||||
|
Ok(format!(
|
||||||
|
"{} {} {}",
|
||||||
|
field_name,
|
||||||
|
condition.operator.as_str(),
|
||||||
|
value.to_sql_string()?
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err("Missing value for comparison".into_custom_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err("Unsupported operator for CHECK constraint".into_custom_error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Err("Only simple conditions are supported for CHECK constraints"
|
||||||
|
.into_custom_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_sql(&self, db_type: DatabaseType) -> CustomResult<String> {
|
||||||
|
let mut sql = format!("{} {}", self.name.as_str(), self.field_type_sql(db_type)?);
|
||||||
|
|
||||||
|
if !self.constraints.is_nullable {
|
||||||
|
sql.push_str(" NOT NULL");
|
||||||
|
}
|
||||||
|
if self.constraints.is_unique {
|
||||||
|
sql.push_str(" UNIQUE");
|
||||||
|
}
|
||||||
|
if self.constraints.is_primary {
|
||||||
|
match (db_type, &self.field_type) {
|
||||||
|
(DatabaseType::SQLite, FieldType::Integer(true)) => {
|
||||||
|
sql.push_str(" PRIMARY KEY AUTOINCREMENT");
|
||||||
|
}
|
||||||
|
(DatabaseType::MySQL, FieldType::Integer(true)) => {
|
||||||
|
sql.push_str(" PRIMARY KEY");
|
||||||
|
}
|
||||||
|
(DatabaseType::PostgreSQL, FieldType::Integer(true)) => {
|
||||||
|
sql.push_str(" PRIMARY KEY");
|
||||||
|
}
|
||||||
|
_ => sql.push_str(" PRIMARY KEY"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(default) = &self.constraints.default_value {
|
||||||
|
sql.push_str(&format!(" DEFAULT {}", default.to_sql_string()?));
|
||||||
|
}
|
||||||
|
if let Some(check) = &self.constraints.check_constraint {
|
||||||
|
let check_sql = Self::build_check_constraint(check)?;
|
||||||
|
sql.push_str(&format!(" CHECK ({})", check_sql));
|
||||||
|
}
|
||||||
|
if let Some(fk) = &self.constraints.foreign_key {
|
||||||
|
sql.push_str(&format!(" REFERENCES {}({})", fk.ref_table, fk.ref_column));
|
||||||
|
|
||||||
|
if let Some(on_delete) = &fk.on_delete {
|
||||||
|
sql.push_str(&format!(" ON DELETE {}", on_delete.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(on_update) = &fk.on_update {
|
||||||
|
sql.push_str(&format!(" ON UPDATE {}", on_update.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
pub fn new(name: &str) -> CustomResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: Identifier::new(name.to_string())?,
|
||||||
|
fields: Vec::new(),
|
||||||
|
indexes: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_field(&mut self, field: Field) -> &mut Self {
|
||||||
|
self.fields.push(field);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_index(&mut self, index: Index) -> &mut Self {
|
||||||
|
self.indexes.push(index);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_sql(&self, db_type: DatabaseType) -> CustomResult<String> {
|
||||||
|
let fields_sql: CustomResult<Vec<String>> =
|
||||||
|
self.fields.iter().map(|f| f.to_sql(db_type)).collect();
|
||||||
|
let fields_sql = fields_sql?;
|
||||||
|
|
||||||
|
let mut sql = format!(
|
||||||
|
"CREATE TABLE {} (\n {}\n);",
|
||||||
|
self.name.as_str(),
|
||||||
|
fields_sql.join(",\n ")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加索引
|
||||||
|
for index in &self.indexes {
|
||||||
|
sql.push_str(&format!(
|
||||||
|
"\n\n{}",
|
||||||
|
index.to_sql(self.name.as_str(), db_type)?
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index {
|
||||||
|
pub fn new(name: &str, fields: Vec<String>, is_unique: bool) -> CustomResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: Identifier::new(name.to_string())?,
|
||||||
|
fields: fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| Identifier::new(f))
|
||||||
|
.collect::<CustomResult<Vec<_>>>()?,
|
||||||
|
is_unique,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_sql(&self, table_name: &str, db_type: DatabaseType) -> CustomResult<String> {
|
||||||
|
let unique = if self.is_unique { "UNIQUE " } else { "" };
|
||||||
|
Ok(format!(
|
||||||
|
"CREATE {}INDEX {} ON {} ({});",
|
||||||
|
unique,
|
||||||
|
self.name.as_str(),
|
||||||
|
table_name,
|
||||||
|
self.fields
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema构建器
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct SchemaBuilder {
|
||||||
|
tables: Vec<Table>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tables: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_table(&mut self, table: Table) -> CustomResult<&mut Self> {
|
||||||
|
self.tables.push(table);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&self, db_type: DatabaseType) -> CustomResult<String> {
|
||||||
|
let mut sql = String::new();
|
||||||
|
for table in &self.tables {
|
||||||
|
sql.push_str(&table.to_sql(db_type)?);
|
||||||
|
sql.push_str("\n\n");
|
||||||
|
}
|
||||||
|
Ok(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_schema(db_type: DatabaseType,db_prefix:SafeValue) -> CustomResult<String> {
|
||||||
|
let db_prefix=db_prefix.to_sql_string()?;
|
||||||
|
let mut schema = SchemaBuilder::new();
|
||||||
|
let user_level = "('contributor', 'administrator')";
|
||||||
|
let content_state = "('draft', 'published', 'private', 'hidden')";
|
||||||
|
|
||||||
|
// 用户表
|
||||||
|
let mut users_table = Table::new(&format!("{}users",db_prefix))?;
|
||||||
|
users_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"username",
|
||||||
|
FieldType::VarChar(100),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"avatar_url",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"email",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().unique().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"profile_icon",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"password_hash",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"role",
|
||||||
|
FieldType::VarChar(20),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(WhereClause::Condition(Condition::new(
|
||||||
|
"role".to_string(),
|
||||||
|
Operator::In,
|
||||||
|
Some(SafeValue::Text(user_level.to_string(), ValidationLevel::Relaxed)),
|
||||||
|
)?)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"created_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new().not_null().default(SafeValue::Text(
|
||||||
|
"CURRENT_TIMESTAMP".to_string(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"updated_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new().not_null().default(SafeValue::Text(
|
||||||
|
"CURRENT_TIMESTAMP".to_string(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"last_login_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new().default(SafeValue::Text(
|
||||||
|
"CURRENT_TIMESTAMP".to_string(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(users_table)?;
|
||||||
|
|
||||||
|
// 独立页面表
|
||||||
|
|
||||||
|
let mut pages_table = Table::new(&format!("{}pages",db_prefix))?;
|
||||||
|
pages_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"id",
|
||||||
|
FieldType::Integer(true),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"title",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"meta_keywords",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"meta_description",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"content",
|
||||||
|
FieldType::Text,
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"template",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"custom_fields",
|
||||||
|
FieldType::Text,
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"status",
|
||||||
|
FieldType::VarChar(20),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(WhereClause::Condition(Condition::new(
|
||||||
|
"status".to_string(),
|
||||||
|
Operator::In,
|
||||||
|
Some(SafeValue::Text(content_state.to_string(), ValidationLevel::Standard)),
|
||||||
|
)?)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(pages_table)?;
|
||||||
|
|
||||||
|
|
||||||
|
// posts 表
|
||||||
|
let mut posts_table = Table::new(&format!("{}posts",db_prefix))?;
|
||||||
|
posts_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"id",
|
||||||
|
FieldType::Integer(true),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"author_name",
|
||||||
|
FieldType::VarChar(100),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.foreign_key(format!("{}users",db_prefix), "username".to_string())
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"cover_image",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"title",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"meta_keywords",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"meta_description",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"content",
|
||||||
|
FieldType::Text,
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"status",
|
||||||
|
FieldType::VarChar(20),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(WhereClause::Condition(Condition::new(
|
||||||
|
"status".to_string(),
|
||||||
|
Operator::In,
|
||||||
|
Some(SafeValue::Text(content_state.to_string(), ValidationLevel::Standard)),
|
||||||
|
)?)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"is_editor",
|
||||||
|
FieldType::Boolean,
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.default(SafeValue::Bool(false)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"draft_content",
|
||||||
|
FieldType::Text,
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"created_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new().not_null().default(SafeValue::Text(
|
||||||
|
"CURRENT_TIMESTAMP".to_string(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"updated_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new().not_null().default(SafeValue::Text(
|
||||||
|
"CURRENT_TIMESTAMP".to_string(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"published_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(posts_table)?;
|
||||||
|
|
||||||
|
// 标签表
|
||||||
|
let mut tags_tables = Table::new(&format!("{}tags",db_prefix))?;
|
||||||
|
tags_tables
|
||||||
|
.add_field(Field::new(
|
||||||
|
"name",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"icon",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(tags_tables)?;
|
||||||
|
|
||||||
|
|
||||||
|
// 文章标签
|
||||||
|
let mut post_tags_tables = Table::new(&format!("{}post_tags",db_prefix))?;
|
||||||
|
post_tags_tables
|
||||||
|
.add_field(Field::new(
|
||||||
|
"post_id",
|
||||||
|
FieldType::Integer(false),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.foreign_key(format!("{}posts",db_prefix), "id".to_string())
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?).add_field(Field::new(
|
||||||
|
"tag_id",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.foreign_key(format!("{}tags",db_prefix), "name".to_string())
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
post_tags_tables.add_index(Index::new(
|
||||||
|
"pk_post_tags",
|
||||||
|
vec!["post_id".to_string(), "tag_id".to_string()],
|
||||||
|
true,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(post_tags_tables)?;
|
||||||
|
|
||||||
|
// 分类表
|
||||||
|
|
||||||
|
let mut categories_table = Table::new(&format!("{}categories",db_prefix))?;
|
||||||
|
categories_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"name",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"parent_id",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.foreign_key(format!("{}categories",db_prefix), "name".to_string()),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(categories_table)?;
|
||||||
|
|
||||||
|
// 文章分类关联表
|
||||||
|
let mut post_categories_table = Table::new(&format!("{}post_categories",db_prefix))?;
|
||||||
|
post_categories_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"post_id",
|
||||||
|
FieldType::Integer(false),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.foreign_key(format!("{}posts",db_prefix), "id".to_string())
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"category_id",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.foreign_key(format!("{}categories",db_prefix), "name".to_string())
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
post_categories_table.add_index(Index::new(
|
||||||
|
"pk_post_categories",
|
||||||
|
vec!["post_id".to_string(), "category_id".to_string()],
|
||||||
|
true,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(post_categories_table)?;
|
||||||
|
|
||||||
|
// 资源库表
|
||||||
|
let mut resources_table = Table::new(&format!("{}resources",db_prefix))?;
|
||||||
|
resources_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"id",
|
||||||
|
FieldType::Integer(true),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"author_id",
|
||||||
|
FieldType::VarChar(100),
|
||||||
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.foreign_key(format!("{}users",db_prefix), "username".to_string())
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"name",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"size_bytes",
|
||||||
|
FieldType::BigInt,
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"storage_path",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new().not_null().unique(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"file_type",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new().not_null(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"category",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"description",
|
||||||
|
FieldType::VarChar(255),
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"created_at",
|
||||||
|
FieldType::Timestamp,
|
||||||
|
FieldConstraint::new().not_null().default(SafeValue::Text(
|
||||||
|
"CURRENT_TIMESTAMP".to_string(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(resources_table)?;
|
||||||
|
|
||||||
|
// 配置表
|
||||||
|
let mut settings_table = Table::new(&format!("{}settings",db_prefix))?;
|
||||||
|
settings_table
|
||||||
|
.add_field(Field::new(
|
||||||
|
"name",
|
||||||
|
FieldType::VarChar(50),
|
||||||
|
FieldConstraint::new().primary(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?)
|
||||||
|
.add_field(Field::new(
|
||||||
|
"data",
|
||||||
|
FieldType::Text,
|
||||||
|
FieldConstraint::new(),
|
||||||
|
ValidationLevel::Strict,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
schema.add_table(settings_table)?;
|
||||||
|
|
||||||
|
schema.build(db_type)
|
||||||
|
}
|
107
backend/src/storage/sql/sqllite.rs
Normal file
107
backend/src/storage/sql/sqllite.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
use super::{
|
||||||
|
builder::{self, SafeValue},
|
||||||
|
schema, DatabaseTrait,
|
||||||
|
};
|
||||||
|
use crate::common::error::{CustomError, CustomErrorInto, CustomResult};
|
||||||
|
use crate::config;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::{Column, Executor, SqlitePool, Row, TypeInfo};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Sqlite {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DatabaseTrait for Sqlite {
|
||||||
|
async fn initialization(db_config: config::SqlConfig) -> CustomResult<()> {
|
||||||
|
let db_prefix = SafeValue::Text(
|
||||||
|
format!("{}", db_config.db_prefix),
|
||||||
|
builder::ValidationLevel::Strict,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sqlite_dir = env::current_dir()?.join("assets").join("sqllite");
|
||||||
|
std::fs::create_dir_all(&sqlite_dir)?;
|
||||||
|
|
||||||
|
let db_file = sqlite_dir.join(&db_config.db_name);
|
||||||
|
std::fs::File::create(&db_file)?;
|
||||||
|
|
||||||
|
let path = db_file.to_str().ok_or("Unable to get sqllite path".into_custom_error())?;
|
||||||
|
let grammar = schema::generate_schema(schema::DatabaseType::SQLite, db_prefix)?;
|
||||||
|
|
||||||
|
let connection_str = format!("sqlite:///{}", path);
|
||||||
|
let pool = SqlitePool::connect(&connection_str).await?;
|
||||||
|
|
||||||
|
pool.execute(grammar.as_str()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(db_config: &config::SqlConfig) -> CustomResult<Self> {
|
||||||
|
let db_file = env::current_dir()?
|
||||||
|
.join("assets")
|
||||||
|
.join("sqllite")
|
||||||
|
.join(&db_config.db_name);
|
||||||
|
|
||||||
|
if !db_file.exists() {
|
||||||
|
return Err("SQLite database file does not exist".into_custom_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = db_file.to_str().ok_or("Unable to get sqllite path".into_custom_error())?;
|
||||||
|
let connection_str = format!("sqlite:///{}", path);
|
||||||
|
let pool = SqlitePool::connect(&connection_str).await?;
|
||||||
|
|
||||||
|
Ok(Sqlite { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_query<'a>(
|
||||||
|
&'a self,
|
||||||
|
builder: &builder::QueryBuilder,
|
||||||
|
) -> CustomResult<Vec<HashMap<String, Value>>> {
|
||||||
|
let (query, values) = builder.build()?;
|
||||||
|
|
||||||
|
let mut sqlx_query = sqlx::query(&query);
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
match value {
|
||||||
|
SafeValue::Null => sqlx_query = sqlx_query.bind(None::<String>),
|
||||||
|
SafeValue::Bool(b) => sqlx_query = sqlx_query.bind(b),
|
||||||
|
SafeValue::Integer(i) => sqlx_query = sqlx_query.bind(i),
|
||||||
|
SafeValue::Float(f) => sqlx_query = sqlx_query.bind(f),
|
||||||
|
SafeValue::Text(s, _) => sqlx_query = sqlx_query.bind(s),
|
||||||
|
SafeValue::DateTime(dt) => sqlx_query = sqlx_query.bind(dt.to_rfc3339()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = sqlx_query.fetch_all(&self.pool).await?;
|
||||||
|
|
||||||
|
Ok(rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
row.columns()
|
||||||
|
.iter()
|
||||||
|
.map(|col| {
|
||||||
|
let value = match col.type_info().name() {
|
||||||
|
"INTEGER" => Value::Number(
|
||||||
|
row.try_get::<i64, _>(col.name()).unwrap_or_default().into(),
|
||||||
|
),
|
||||||
|
"REAL" => Value::Number(
|
||||||
|
serde_json::Number::from_f64(
|
||||||
|
row.try_get::<f64, _>(col.name()).unwrap_or(0.0),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|| 0.into()),
|
||||||
|
),
|
||||||
|
"BOOLEAN" => Value::Bool(row.try_get(col.name()).unwrap_or_default()),
|
||||||
|
_ => Value::String(row.try_get(col.name()).unwrap_or_default()),
|
||||||
|
};
|
||||||
|
(col.name().to_string(), value)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
10
frontend/app/env.d.ts
vendored
10
frontend/app/env.d.ts
vendored
@ -7,12 +7,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_SERVER_API: string; // 用于访问API的基础URL
|
readonly VITE_INIT_STATUS: string;
|
||||||
readonly VITE_ADDRESS: string; // 前端地址
|
readonly VITE_SERVER_API: string;
|
||||||
readonly VITE_PORT: number; // 前端系统端口
|
readonly VITE_PORT: string;
|
||||||
VITE_USERNAME: string; // 前端账号名称
|
readonly VITE_ADDRESS: string;
|
||||||
VITE_PASSWORD: string; // 前端账号密码
|
|
||||||
VITE_INIT_STATUS: boolean; // 系统是否进行安装
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useContext, createContext, useState } from "react";
|
import React, { createContext, useState, useEffect } from "react";
|
||||||
|
import { useApi } from "hooks/servicesProvider";
|
||||||
interface SetupContextType {
|
interface SetupContextType {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
setCurrentStep: (step: number) => void;
|
setCurrentStep: (step: number) => void;
|
||||||
@ -21,30 +21,22 @@ const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({
|
|||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
<h2 className="text-2xl font-semibold text-custom-title-light dark:text-custom-title-dark mb-5">
|
<h2 className="text-xl font-medium text-custom-title-light dark:text-custom-title-dark mb-6 px-4">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-custom-box-light dark:bg-custom-box-dark rounded-lg shadow-lg p-8">
|
<div className="space-y-6 px-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 通用的导航按钮组件
|
// 通用的导航按钮组件
|
||||||
const NavigationButtons: React.FC<StepProps> = ({ onNext, onPrev }) => (
|
const NavigationButtons: React.FC<StepProps> = ({ onNext }) => (
|
||||||
<div className="flex gap-4 mt-6">
|
<div className="flex justify-end mt-4">
|
||||||
{onPrev && (
|
|
||||||
<button
|
|
||||||
onClick={onPrev}
|
|
||||||
className="px-6 py-2 rounded-lg bg-gray-500 hover:bg-gray-600 text-white transition-colors"
|
|
||||||
>
|
|
||||||
上一步
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="px-6 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white transition-colors"
|
className="px-6 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white transition-colors font-medium text-sm"
|
||||||
>
|
>
|
||||||
下一步
|
下一步
|
||||||
</button>
|
</button>
|
||||||
@ -58,17 +50,17 @@ const InputField: React.FC<{
|
|||||||
defaultValue?: string | number;
|
defaultValue?: string | number;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
}> = ({ label, name, defaultValue, hint }) => (
|
}> = ({ label, name, defaultValue, hint }) => (
|
||||||
<div className="mb-4">
|
<div className="mb-6">
|
||||||
<h3 className="text-xl text-custom-title-light dark:text-custom-title-dark mb-2">
|
<h3 className="text-base font-medium text-custom-title-light dark:text-custom-title-dark mb-2">
|
||||||
{label}
|
{label}
|
||||||
</h3>
|
</h3>
|
||||||
<input
|
<input
|
||||||
name={name}
|
name={name}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"
|
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
{hint && (
|
{hint && (
|
||||||
<p className="text-xs text-custom-p-light dark:text-custom-p-dark mt-1">
|
<p className="text-xs text-custom-p-light dark:text-custom-p-dark mt-1.5">
|
||||||
{hint}
|
{hint}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -78,7 +70,7 @@ const InputField: React.FC<{
|
|||||||
const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
||||||
<StepContainer title="安装说明">
|
<StepContainer title="安装说明">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<p className="text-xl text-custom-p-light dark:text-custom-p-dark">
|
<p className="text-base text-custom-p-light dark:text-custom-p-dark">
|
||||||
欢迎使用 Echoes
|
欢迎使用 Echoes
|
||||||
</p>
|
</p>
|
||||||
<NavigationButtons onNext={onNext} />
|
<NavigationButtons onNext={onNext} />
|
||||||
@ -86,22 +78,24 @@ const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
|||||||
</StepContainer>
|
</StepContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DatabaseConfig: React.FC<StepProps> = ({ onNext, onPrev }) => {
|
const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||||
const [dbType, setDbType] = useState("postgresql");
|
const [dbType, setDbType] = useState("postgresql");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepContainer title="数据库配置">
|
<StepContainer title="数据库配置">
|
||||||
<div className="space-y-6">
|
<div>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-xl text-custom-title-light dark:text-custom-title-dark mb-2">
|
<h3 className="text-base font-medium text-custom-title-light dark:text-custom-title-dark mb-1.5">
|
||||||
数据库类型
|
数据库类型
|
||||||
</h3>
|
</h3>
|
||||||
<select
|
<select
|
||||||
value={dbType}
|
value={dbType}
|
||||||
onChange={(e) => setDbType(e.target.value)}
|
onChange={(e) => setDbType(e.target.value)}
|
||||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"
|
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||||
>
|
>
|
||||||
<option value="postgresql">PostgreSQL</option>
|
<option value="postgresql">PostgreSQL</option>
|
||||||
|
<option value="mysql">MySQL</option>
|
||||||
|
<option value="sqllite">SQLite</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -113,6 +107,12 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext, onPrev }) => {
|
|||||||
defaultValue="localhost"
|
defaultValue="localhost"
|
||||||
hint="通常使用 localhost"
|
hint="通常使用 localhost"
|
||||||
/>
|
/>
|
||||||
|
<InputField
|
||||||
|
label="数据库前缀"
|
||||||
|
name="db_prefix"
|
||||||
|
defaultValue="echoec_"
|
||||||
|
hint="通常使用 echoec_"
|
||||||
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label="端口"
|
label="端口"
|
||||||
name="db_port"
|
name="db_port"
|
||||||
@ -136,19 +136,71 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext, onPrev }) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<NavigationButtons onNext={onNext} onPrev={onPrev} />
|
{dbType === "mysql" && (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
label="数据库地址"
|
||||||
|
name="db_host"
|
||||||
|
defaultValue="localhost"
|
||||||
|
hint="通常使用 localhost"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="数据库前缀"
|
||||||
|
name="db_prefix"
|
||||||
|
defaultValue="echoec_"
|
||||||
|
hint="通常使用 echoec_"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="端口"
|
||||||
|
name="db_port"
|
||||||
|
defaultValue={3306}
|
||||||
|
hint="mysql 默认端口为 3306"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="用户名"
|
||||||
|
name="db_user"
|
||||||
|
defaultValue="root"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="密码"
|
||||||
|
name="db_password"
|
||||||
|
defaultValue="mysql"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="数据库名"
|
||||||
|
name="db_name"
|
||||||
|
defaultValue="echoes"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{dbType === "sqllite" && (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
label="数据库前缀"
|
||||||
|
name="db_prefix"
|
||||||
|
defaultValue="echoec_"
|
||||||
|
hint="通常使用 echoec_"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="数据库名"
|
||||||
|
name="db_name"
|
||||||
|
defaultValue="echoes.db"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<NavigationButtons onNext={onNext} />
|
||||||
</div>
|
</div>
|
||||||
</StepContainer>
|
</StepContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AdminConfig: React.FC<StepProps> = ({ onNext, onPrev }) => (
|
const AdminConfig: React.FC<StepProps> = ({ onNext }) => (
|
||||||
<StepContainer title="创建管理员账号">
|
<StepContainer title="创建管理员账号">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<InputField label="用户名" name="admin_username" />
|
<InputField label="用户名" name="admin_username" />
|
||||||
<InputField label="密码" name="admin_password" />
|
<InputField label="密码" name="admin_password" />
|
||||||
<InputField label="邮箱" name="admin_email" />
|
<InputField label="邮箱" name="admin_email" />
|
||||||
<NavigationButtons onNext={onNext} onPrev={onPrev} />
|
<NavigationButtons onNext={onNext} />
|
||||||
</div>
|
</div>
|
||||||
</StepContainer>
|
</StepContainer>
|
||||||
);
|
);
|
||||||
@ -163,17 +215,59 @@ const SetupComplete: React.FC = () => (
|
|||||||
</StepContainer>
|
</StepContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function SetupPage() {
|
// 修改主题切换按钮组件
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const ThemeToggle: React.FC = () => {
|
||||||
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
setIsDark(isDarkMode);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const currentScrollPos = window.scrollY;
|
||||||
|
setIsVisible(currentScrollPos < 100); // 滚动超过100px就隐藏
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const newIsDark = !isDark;
|
||||||
|
setIsDark(newIsDark);
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-custom-bg-light dark:bg-custom-bg-dark">
|
<button
|
||||||
<div className="container mx-auto px-4 py-4">
|
onClick={toggleTheme}
|
||||||
<div className="text-center mb-8">
|
className={`absolute top-4 right-4 p-2.5 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-all duration-300 ${
|
||||||
<h1 className="text-4xl font-bold text-custom-title-light dark:text-custom-title-dark mb-4">
|
isVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||||
Echoes
|
}`}
|
||||||
</h1>
|
>
|
||||||
</div>
|
{isDark ? (
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupPage() {
|
||||||
|
let step = Number(import.meta.env.VITE_INIT_STATUS);
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState(step);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen w-full bg-custom-bg-light dark:bg-custom-bg-dark">
|
||||||
|
<ThemeToggle />
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
<SetupContext.Provider value={{ currentStep, setCurrentStep }}>
|
<SetupContext.Provider value={{ currentStep, setCurrentStep }}>
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<Introduction onNext={() => setCurrentStep(currentStep + 1)} />
|
<Introduction onNext={() => setCurrentStep(currentStep + 1)} />
|
||||||
@ -181,13 +275,11 @@ export default function SetupPage() {
|
|||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<DatabaseConfig
|
<DatabaseConfig
|
||||||
onNext={() => setCurrentStep(currentStep + 1)}
|
onNext={() => setCurrentStep(currentStep + 1)}
|
||||||
onPrev={() => setCurrentStep(currentStep - 1)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === 3 && (
|
{currentStep === 3 && (
|
||||||
<AdminConfig
|
<AdminConfig
|
||||||
onNext={() => setCurrentStep(currentStep + 1)}
|
onNext={() => setCurrentStep(currentStep + 1)}
|
||||||
onPrev={() => setCurrentStep(currentStep - 1)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === 4 && <SetupComplete />}
|
{currentStep === 4 && <SetupComplete />}
|
||||||
|
12
frontend/core/template.ts
Normal file
12
frontend/core/template.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface Template {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
config: {
|
||||||
|
layout?: string;
|
||||||
|
styles?: string[];
|
||||||
|
scripts?: string[];
|
||||||
|
};
|
||||||
|
loader: () => Promise<void>;
|
||||||
|
element: () => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import { Configuration, PathDescription } from "common/serializableType";
|
import { Configuration, PathDescription } from "common/serializableType";
|
||||||
import { ApiService } from "./api";
|
import { ApiService } from "core/api";
|
||||||
|
import { Template } from "core/template";
|
||||||
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
name: string;
|
name: string;
|
||||||
@ -25,17 +26,6 @@ export interface ThemeConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Template {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
config: {
|
|
||||||
layout?: string;
|
|
||||||
styles?: string[];
|
|
||||||
scripts?: string[];
|
|
||||||
};
|
|
||||||
loader: () => Promise<void>;
|
|
||||||
element: () => React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
private static instance: ThemeService;
|
private static instance: ThemeService;
|
||||||
@ -56,7 +46,7 @@ export class ThemeService {
|
|||||||
public async getCurrentTheme(): Promise<void> {
|
public async getCurrentTheme(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const themeConfig = await this.api.request<ThemeConfig>(
|
const themeConfig = await this.api.request<ThemeConfig>(
|
||||||
"/theme/current",
|
"/theme",
|
||||||
{ method: "GET" },
|
{ method: "GET" },
|
||||||
);
|
);
|
||||||
this.currentTheme = themeConfig;
|
this.currentTheme = themeConfig;
|
||||||
@ -70,10 +60,10 @@ export class ThemeService {
|
|||||||
return this.currentTheme;
|
return this.currentTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateThemeConfig(config: Partial<ThemeConfig>): Promise<void> {
|
public async updateThemeConfig(config: Partial<ThemeConfig>,name:string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const updatedConfig = await this.api.request<ThemeConfig>(
|
const updatedConfig = await this.api.request<ThemeConfig>(
|
||||||
"/theme/config",
|
`/theme/`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -17,7 +17,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
routes: (defineRoutes) => {
|
routes: (defineRoutes) => {
|
||||||
return defineRoutes((route) => {
|
return defineRoutes((route) => {
|
||||||
if (!env.VITE_INIT_STATUS) {
|
if (Number(env.VITE_INIT_STATUS??1)<4) {
|
||||||
route("/", "init.tsx", { id: "index-route" });
|
route("/", "init.tsx", { id: "index-route" });
|
||||||
route("*", "init.tsx", { id: "catch-all-route" });
|
route("*", "init.tsx", { id: "catch-all-route" });
|
||||||
} else {
|
} else {
|
||||||
@ -30,7 +30,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
"import.meta.env.VITE_INIT_STATUS": JSON.stringify(false),
|
"import.meta.env.VITE_INIT_STATUS": JSON.stringify(1),
|
||||||
"import.meta.env.VITE_SERVER_API": JSON.stringify("localhost:22000"),
|
"import.meta.env.VITE_SERVER_API": JSON.stringify("localhost:22000"),
|
||||||
"import.meta.env.VITE_PORT": JSON.stringify(22100),
|
"import.meta.env.VITE_PORT": JSON.stringify(22100),
|
||||||
"import.meta.env.VITE_ADDRESS": JSON.stringify("localhost"),
|
"import.meta.env.VITE_ADDRESS": JSON.stringify("localhost"),
|
||||||
|
Loading…
Reference in New Issue
Block a user