后端:添加sql构建器防止sql注入,config增加写入,获取功能,将路由提取出来
This commit is contained in:
parent
7e4d9c1b48
commit
8071a16510
@ -11,8 +11,8 @@ 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"] }
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
once_cell = "1.10.0"
|
|
||||||
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"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
regex = "1.11.1"
|
@ -1,16 +1,11 @@
|
|||||||
# config/config.toml
|
|
||||||
# 配置文件
|
|
||||||
|
|
||||||
# 信息
|
|
||||||
[info]
|
[info]
|
||||||
install = false # 是否为第一次安装
|
install = true
|
||||||
non_relational = true # 是否使用非关系型数据库
|
non_relational = false
|
||||||
|
|
||||||
# 关系型数据库
|
|
||||||
[sql_config]
|
[sql_config]
|
||||||
db_type = "postgresql" # 数据库类型
|
db_type = "postgresql"
|
||||||
address = "localhost" # 地址
|
address = "localhost"
|
||||||
port = 5432 # 端口
|
port = 5432
|
||||||
user = "postgres" # 用户名
|
user = "postgres"
|
||||||
password = "postgres" # 密码
|
password = "postgres"
|
||||||
db_name = "echoes" # 数据库
|
db_name = "echoes"
|
||||||
|
1
backend/src/auth/mod.rs
Normal file
1
backend/src/auth/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod jwt;
|
@ -1,19 +1,20 @@
|
|||||||
use serde::Deserialize;
|
use serde::{Deserialize,Serialize};
|
||||||
use std::{ env, fs};
|
use std::{ env, fs};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Deserialize,Debug,Clone)]
|
#[derive(Deserialize,Serialize,Debug,Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub info: Info,
|
pub info: Info,
|
||||||
pub sql_config: SqlConfig,
|
pub sql_config: SqlConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize,Debug,Clone)]
|
#[derive(Deserialize,Serialize,Debug,Clone,)]
|
||||||
pub struct Info {
|
pub struct Info {
|
||||||
pub install: bool,
|
pub install: bool,
|
||||||
pub non_relational: bool,
|
pub non_relational: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize,Debug,Clone)]
|
#[derive(Deserialize,Serialize,Debug,Clone)]
|
||||||
pub struct SqlConfig {
|
pub struct SqlConfig {
|
||||||
pub db_type: String,
|
pub db_type: String,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@ -23,7 +24,7 @@ pub struct SqlConfig {
|
|||||||
pub db_name: String,
|
pub db_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize,Debug,Clone)]
|
#[derive(Deserialize,Serialize,Debug,Clone)]
|
||||||
pub struct NoSqlConfig {
|
pub struct NoSqlConfig {
|
||||||
pub db_type: String,
|
pub db_type: String,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@ -35,9 +36,18 @@ pub struct NoSqlConfig {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn read() -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn read() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let path = env::current_dir()?
|
let path=Self::get_path()?;
|
||||||
.join("assets")
|
|
||||||
.join("config.toml");
|
|
||||||
Ok(toml::from_str(&fs::read_to_string(path)?)?)
|
Ok(toml::from_str(&fs::read_to_string(path)?)?)
|
||||||
}
|
}
|
||||||
|
pub fn write(config:Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let path=Self::get_path()?;
|
||||||
|
fs::write(path, toml::to_string(&config)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
Ok(env::current_dir()?
|
||||||
|
.join("assets")
|
||||||
|
.join("config.toml"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
279
backend/src/database/relational/builder.rs
Normal file
279
backend/src/database/relational/builder.rs
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use super::DatabaseError;
|
||||||
|
|
||||||
|
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ValidatedValue {
|
||||||
|
Identifier(String),
|
||||||
|
RichText(String),
|
||||||
|
PlainText(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidatedValue {
|
||||||
|
pub fn new_identifier(value: String) -> Result<Self, DatabaseError> {
|
||||||
|
let valid_pattern = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]{0,63}$").unwrap();
|
||||||
|
if !valid_pattern.is_match(&value) {
|
||||||
|
return Err(DatabaseError::ValidationError(
|
||||||
|
"Invalid identifier format".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(ValidatedValue::Identifier(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_rich_text(value: String) -> Result<Self, DatabaseError> {
|
||||||
|
let dangerous_patterns = [
|
||||||
|
"UNION ALL SELECT",
|
||||||
|
"UNION SELECT",
|
||||||
|
"OR 1=1",
|
||||||
|
"OR '1'='1",
|
||||||
|
"DROP TABLE",
|
||||||
|
"DELETE FROM",
|
||||||
|
"UPDATE ",
|
||||||
|
"INSERT INTO",
|
||||||
|
"--",
|
||||||
|
"/*",
|
||||||
|
"*/",
|
||||||
|
"@@",
|
||||||
|
];
|
||||||
|
|
||||||
|
let value_upper = value.to_uppercase();
|
||||||
|
for pattern in dangerous_patterns.iter() {
|
||||||
|
if value_upper.contains(&pattern.to_uppercase()) {
|
||||||
|
return Err(DatabaseError::SqlInjectionAttempt(
|
||||||
|
format!("Dangerous SQL pattern detected: {}", pattern)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ValidatedValue::RichText(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_plain_text(value: String) -> Result<Self, DatabaseError> {
|
||||||
|
if value.contains(';') || value.contains("--") {
|
||||||
|
return Err(DatabaseError::ValidationError("Invalid characters in text".to_string()));
|
||||||
|
}
|
||||||
|
Ok(ValidatedValue::PlainText(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ValidatedValue::Identifier(s) | ValidatedValue::RichText(s) | ValidatedValue::PlainText(s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum SqlOperation {
|
||||||
|
Select,
|
||||||
|
Insert,
|
||||||
|
Update,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Operator {
|
||||||
|
Eq,
|
||||||
|
Ne,
|
||||||
|
Gt,
|
||||||
|
Lt,
|
||||||
|
Gte,
|
||||||
|
Lte,
|
||||||
|
Like,
|
||||||
|
In,
|
||||||
|
IsNull,
|
||||||
|
IsNotNull,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operator {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Operator::Eq => "=",
|
||||||
|
Operator::Ne => "!=",
|
||||||
|
Operator::Gt => ">",
|
||||||
|
Operator::Lt => "<",
|
||||||
|
Operator::Gte => ">=",
|
||||||
|
Operator::Lte => "<=",
|
||||||
|
Operator::Like => "LIKE",
|
||||||
|
Operator::In => "IN",
|
||||||
|
Operator::IsNull => "IS NULL",
|
||||||
|
Operator::IsNotNull => "IS NOT NULL",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WhereCondition {
|
||||||
|
field: ValidatedValue,
|
||||||
|
operator: Operator,
|
||||||
|
value: Option<ValidatedValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WhereCondition {
|
||||||
|
pub fn new(
|
||||||
|
field: String,
|
||||||
|
operator: Operator,
|
||||||
|
value: Option<String>,
|
||||||
|
) -> Result<Self, DatabaseError> {
|
||||||
|
let field = ValidatedValue::new_identifier(field)?;
|
||||||
|
|
||||||
|
let value = match value {
|
||||||
|
Some(v) => Some(match operator {
|
||||||
|
Operator::Like => ValidatedValue::new_plain_text(v)?,
|
||||||
|
_ => ValidatedValue::new_plain_text(v)?,
|
||||||
|
}),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(WhereCondition {
|
||||||
|
field,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum WhereClause {
|
||||||
|
And(Vec<WhereClause>),
|
||||||
|
Or(Vec<WhereClause>),
|
||||||
|
Condition(WhereCondition),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QueryBuilder {
|
||||||
|
operation: SqlOperation,
|
||||||
|
table: ValidatedValue,
|
||||||
|
fields: Vec<ValidatedValue>,
|
||||||
|
params: HashMap<ValidatedValue, ValidatedValue>,
|
||||||
|
where_clause: Option<WhereClause>,
|
||||||
|
order_by: Option<ValidatedValue>,
|
||||||
|
limit: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryBuilder {
|
||||||
|
pub fn new(operation: SqlOperation, table: String) -> Result<Self, DatabaseError> {
|
||||||
|
Ok(QueryBuilder {
|
||||||
|
operation,
|
||||||
|
table: ValidatedValue::new_identifier(table)?,
|
||||||
|
fields: Vec::new(),
|
||||||
|
params: HashMap::new(),
|
||||||
|
where_clause: None,
|
||||||
|
order_by: None,
|
||||||
|
limit: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&self) -> Result<(String, Vec<String>), DatabaseError> {
|
||||||
|
let mut query = String::new();
|
||||||
|
let mut values = Vec::new();
|
||||||
|
let mut param_counter = 1;
|
||||||
|
|
||||||
|
match self.operation {
|
||||||
|
SqlOperation::Select => {
|
||||||
|
let fields = if self.fields.is_empty() {
|
||||||
|
"*".to_string()
|
||||||
|
} else {
|
||||||
|
self.fields.iter()
|
||||||
|
.map(|f| f.get().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
};
|
||||||
|
query.push_str(&format!("SELECT {} FROM {}", fields, self.table.get()));
|
||||||
|
}
|
||||||
|
SqlOperation::Insert => {
|
||||||
|
let fields: Vec<String> = self.params.keys()
|
||||||
|
.map(|k| k.get().to_string())
|
||||||
|
.collect();
|
||||||
|
let placeholders: Vec<String> = (1..=self.params.len())
|
||||||
|
.map(|i| format!("${}", i))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
query.push_str(&format!(
|
||||||
|
"INSERT INTO {} ({}) VALUES ({})",
|
||||||
|
self.table.get(),
|
||||||
|
fields.join(", "),
|
||||||
|
placeholders.join(", ")
|
||||||
|
));
|
||||||
|
|
||||||
|
values.extend(self.params.values().map(|v| v.get().to_string()));
|
||||||
|
return Ok((query, values));
|
||||||
|
}
|
||||||
|
SqlOperation::Update => {
|
||||||
|
query.push_str(&format!("UPDATE {} SET ", self.table.get()));
|
||||||
|
let set_clauses: Vec<String> = self.params
|
||||||
|
.iter()
|
||||||
|
.map(|(key, _)| {
|
||||||
|
let placeholder = format!("${}", param_counter);
|
||||||
|
values.push(self.params[key].get().to_string());
|
||||||
|
param_counter += 1;
|
||||||
|
format!("{} = {}", key.get(), placeholder)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
query.push_str(&set_clauses.join(", "));
|
||||||
|
}
|
||||||
|
SqlOperation::Delete => {
|
||||||
|
query.push_str(&format!("DELETE FROM {}", self.table.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(where_clause) = &self.where_clause {
|
||||||
|
query.push_str(" WHERE ");
|
||||||
|
let (where_sql, where_values) = self.build_where_clause(where_clause, param_counter)?;
|
||||||
|
query.push_str(&where_sql);
|
||||||
|
values.extend(where_values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(order) = &self.order_by {
|
||||||
|
query.push_str(&format!(" ORDER BY {}", order.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(limit) = self.limit {
|
||||||
|
query.push_str(&format!(" LIMIT {}", limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((query, values))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_where_clause(
|
||||||
|
&self,
|
||||||
|
clause: &WhereClause,
|
||||||
|
mut param_counter: i32,
|
||||||
|
) -> Result<(String, Vec<String>), DatabaseError> {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
|
||||||
|
let sql = match clause {
|
||||||
|
WhereClause::And(conditions) => {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
for condition in conditions {
|
||||||
|
let (sql, mut vals) = self.build_where_clause(condition, param_counter)?;
|
||||||
|
param_counter += vals.len() as i32;
|
||||||
|
parts.push(sql);
|
||||||
|
values.append(&mut vals);
|
||||||
|
}
|
||||||
|
format!("({})", parts.join(" AND "))
|
||||||
|
}
|
||||||
|
WhereClause::Or(conditions) => {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
for condition in conditions {
|
||||||
|
let (sql, mut vals) = self.build_where_clause(condition, param_counter)?;
|
||||||
|
param_counter += vals.len() as i32;
|
||||||
|
parts.push(sql);
|
||||||
|
values.append(&mut vals);
|
||||||
|
}
|
||||||
|
format!("({})", parts.join(" OR "))
|
||||||
|
}
|
||||||
|
WhereClause::Condition(cond) => {
|
||||||
|
if let Some(value) = &cond.value {
|
||||||
|
let placeholder = format!("${}", param_counter);
|
||||||
|
values.push(value.get().to_string());
|
||||||
|
format!("{} {} {}", cond.field.get(), cond.operator.as_str(), placeholder)
|
||||||
|
} else {
|
||||||
|
format!("{} {}", cond.field.get(), cond.operator.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((sql, values))
|
||||||
|
}
|
||||||
|
}
|
@ -1,36 +1,46 @@
|
|||||||
mod postgresql;
|
mod postgresql;
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::fmt;
|
||||||
|
pub mod builder;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub enum SqlOperation {
|
pub enum DatabaseError {
|
||||||
Select,
|
ValidationError(String),
|
||||||
Insert,
|
SqlInjectionAttempt(String),
|
||||||
Update,
|
InvalidParameter(String),
|
||||||
Delete,
|
ExecutionError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct QueryBuilder {
|
impl fmt::Display for DatabaseError {
|
||||||
operation: SqlOperation,
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
table: String,
|
match self {
|
||||||
fields: Vec<String>,
|
DatabaseError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
|
||||||
params: HashMap<String, String>,
|
DatabaseError::SqlInjectionAttempt(msg) => write!(f, "SQL injection attempt: {}", msg),
|
||||||
where_conditions: HashMap<String, String>,
|
DatabaseError::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
|
||||||
order_by: Option<String>,
|
DatabaseError::ExecutionError(msg) => write!(f, "Execution error: {}", msg),
|
||||||
limit: Option<i32>,
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Error for DatabaseError {}
|
||||||
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DatabaseTrait: Send + Sync {
|
pub trait DatabaseTrait: Send + Sync {
|
||||||
async fn connect(database: config::SqlConfig) -> Result<Self, Box<dyn Error>> where Self: Sized;
|
async fn connect(database: config::SqlConfig) -> Result<Self, Box<dyn Error>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
async fn execute_query<'a>(
|
async fn execute_query<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
builder: &QueryBuilder,
|
builder: &builder::QueryBuilder,
|
||||||
) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>>;
|
) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>>;
|
||||||
async fn initialization(database: config::SqlConfig) -> Result<(), Box<dyn Error>> where Self: Sized;
|
async fn initialization(database: config::SqlConfig) -> Result<(), Box<dyn Error>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -49,7 +59,9 @@ impl Database {
|
|||||||
_ => return Err("unknown database type".into()),
|
_ => return Err("unknown database type".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self { db: Arc::new(Box::new(db)) })
|
Ok(Self {
|
||||||
|
db: Arc::new(Box::new(db)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initial_setup(database: config::SqlConfig) -> Result<(), Box<dyn Error>> {
|
pub async fn initial_setup(database: config::SqlConfig) -> Result<(), Box<dyn Error>> {
|
||||||
@ -60,148 +72,3 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueryBuilder {
|
|
||||||
pub fn new(operation: SqlOperation, table: &str) -> Self {
|
|
||||||
QueryBuilder {
|
|
||||||
operation,
|
|
||||||
table: table.to_string(),
|
|
||||||
fields: Vec::new(),
|
|
||||||
params: HashMap::new(),
|
|
||||||
where_conditions: HashMap::new(),
|
|
||||||
order_by: None,
|
|
||||||
limit: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(&self) -> (String, Vec<String>) {
|
|
||||||
let mut query = String::new();
|
|
||||||
let mut values = Vec::new();
|
|
||||||
let mut param_counter = 1;
|
|
||||||
|
|
||||||
match self.operation {
|
|
||||||
SqlOperation::Select => {
|
|
||||||
let fields = if self.fields.is_empty() {
|
|
||||||
"*".to_string()
|
|
||||||
} else {
|
|
||||||
self.fields.join(", ")
|
|
||||||
};
|
|
||||||
|
|
||||||
query.push_str(&format!("SELECT {} FROM {}", fields, self.table));
|
|
||||||
|
|
||||||
if !self.where_conditions.is_empty() {
|
|
||||||
let conditions: Vec<String> = self.where_conditions
|
|
||||||
.iter()
|
|
||||||
.map(|(key, _)| {
|
|
||||||
let placeholder = format!("${}", param_counter);
|
|
||||||
values.push(self.where_conditions[key].clone());
|
|
||||||
param_counter += 1;
|
|
||||||
format!("{} = {}", key, placeholder)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
query.push_str(" WHERE ");
|
|
||||||
query.push_str(&conditions.join(" AND "));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SqlOperation::Insert => {
|
|
||||||
let fields: Vec<String> = self.params.keys().cloned().collect();
|
|
||||||
let placeholders: Vec<String> = (1..=self.params.len())
|
|
||||||
.map(|i| format!("${}", i))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
query.push_str(&format!(
|
|
||||||
"INSERT INTO {} ({}) VALUES ({})",
|
|
||||||
self.table,
|
|
||||||
fields.join(", "),
|
|
||||||
placeholders.join(", ")
|
|
||||||
));
|
|
||||||
|
|
||||||
for field in fields {
|
|
||||||
values.push(self.params[&field].clone());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SqlOperation::Update => {
|
|
||||||
query.push_str(&format!("UPDATE {}", self.table));
|
|
||||||
|
|
||||||
let set_clauses: Vec<String> = self.params
|
|
||||||
.keys()
|
|
||||||
.map(|key| {
|
|
||||||
let placeholder = format!("${}", param_counter);
|
|
||||||
values.push(self.params[key].clone());
|
|
||||||
param_counter += 1;
|
|
||||||
format!("{} = {}", key, placeholder)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
query.push_str(" SET ");
|
|
||||||
query.push_str(&set_clauses.join(", "));
|
|
||||||
|
|
||||||
if !self.where_conditions.is_empty() {
|
|
||||||
let conditions: Vec<String> = self.where_conditions
|
|
||||||
.iter()
|
|
||||||
.map(|(key, _)| {
|
|
||||||
let placeholder = format!("${}", param_counter);
|
|
||||||
values.push(self.where_conditions[key].clone());
|
|
||||||
param_counter += 1;
|
|
||||||
format!("{} = {}", key, placeholder)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
query.push_str(" WHERE ");
|
|
||||||
query.push_str(&conditions.join(" AND "));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SqlOperation::Delete => {
|
|
||||||
query.push_str(&format!("DELETE FROM {}", self.table));
|
|
||||||
|
|
||||||
if !self.where_conditions.is_empty() {
|
|
||||||
let conditions: Vec<String> = self.where_conditions
|
|
||||||
.iter()
|
|
||||||
.map(|(key, _)| {
|
|
||||||
let placeholder = format!("${}", param_counter);
|
|
||||||
values.push(self.where_conditions[key].clone());
|
|
||||||
param_counter += 1;
|
|
||||||
format!("{} = {}", key, placeholder)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
query.push_str(" WHERE ");
|
|
||||||
query.push_str(&conditions.join(" AND "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(order) = &self.order_by {
|
|
||||||
query.push_str(&format!(" ORDER BY {}", order));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(limit) = self.limit {
|
|
||||||
query.push_str(&format!(" LIMIT {}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
(query, values)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fields(&mut self, fields: Vec<String>) -> &mut Self {
|
|
||||||
self.fields = fields;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn params(&mut self, params: HashMap<String, String>) -> &mut Self {
|
|
||||||
self.params = params;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn where_conditions(&mut self, conditions: HashMap<String, String>) -> &mut Self {
|
|
||||||
self.where_conditions = conditions;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn order_by(&mut self, order: &str) -> &mut Self {
|
|
||||||
self.order_by = Some(order.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn limit(&mut self, limit: i32) -> &mut Self {
|
|
||||||
self.limit = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,12 +3,12 @@ CREATE TYPE privilege_level AS ENUM ( 'contributor', 'administrators');
|
|||||||
CREATE TABLE persons
|
CREATE TABLE persons
|
||||||
(
|
(
|
||||||
person_name VARCHAR(100) PRIMARY KEY,
|
person_name VARCHAR(100) PRIMARY KEY,
|
||||||
|
person_avatar VARCHAR(255),
|
||||||
person_email VARCHAR(255) UNIQUE NOT NULL,
|
person_email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
person_icon VARCHAR(255),
|
person_icon VARCHAR(255),
|
||||||
person_password VARCHAR(255) NOT NULL,
|
person_password VARCHAR(255) NOT NULL,
|
||||||
person_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
person_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
person_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
person_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
person_avatar VARCHAR(255),
|
|
||||||
person_role VARCHAR(50),
|
person_role VARCHAR(50),
|
||||||
person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
person_level privilege_level NOT NULL DEFAULT 'contributor'
|
person_level privilege_level NOT NULL DEFAULT 'contributor'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::{DatabaseTrait, QueryBuilder};
|
use super::{DatabaseTrait,builder};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{Column, PgPool, Row, Executor};
|
use sqlx::{Column, PgPool, Row, Executor};
|
||||||
@ -49,9 +49,9 @@ impl DatabaseTrait for Postgresql {
|
|||||||
|
|
||||||
async fn execute_query<'a>(
|
async fn execute_query<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
builder: &QueryBuilder,
|
builder: &builder::QueryBuilder,
|
||||||
) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>> {
|
) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>> {
|
||||||
let (query, values) = builder.build();
|
let (query, values) = builder.build()?;
|
||||||
|
|
||||||
let mut sqlx_query = sqlx::query(&query);
|
let mut sqlx_query = sqlx::query(&query);
|
||||||
|
|
||||||
|
@ -1,73 +1,104 @@
|
|||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod secret;
|
mod auth;
|
||||||
|
mod utils;
|
||||||
|
mod routes;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use database::relational;
|
use database::relational;
|
||||||
use once_cell::sync::Lazy;
|
use rocket::{
|
||||||
use rocket::{get, post, http::Status, launch, response::status, routes, serde::json::Json};
|
get, post,
|
||||||
|
http::Status,
|
||||||
|
launch,
|
||||||
|
response::status,
|
||||||
|
State,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
static SQL: Lazy<Arc<Mutex<Option<relational::Database>>>> =
|
#[derive(Debug)]
|
||||||
Lazy::new(|| Arc::new(Mutex::new(None)));
|
pub enum AppError {
|
||||||
|
Database(String),
|
||||||
async fn init_sql(database: config::SqlConfig) -> Result<(), Box<dyn std::error::Error>> {
|
Config(String),
|
||||||
let database = relational::Database::link(database).await?;
|
Auth(String),
|
||||||
*SQL.lock().await = Some(database);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_sql() -> Result<relational::Database, Box<dyn std::error::Error>> {
|
impl From<AppError> for status::Custom<String> {
|
||||||
SQL.lock()
|
fn from(error: AppError) -> Self {
|
||||||
.await
|
match error {
|
||||||
.clone()
|
AppError::Database(msg) => status::Custom(Status::InternalServerError, format!("Database error: {}", msg)),
|
||||||
.ok_or_else(|| "Database not initialized".into())
|
AppError::Config(msg) => status::Custom(Status::InternalServerError, format!("Config error: {}", msg)),
|
||||||
|
AppError::Auth(msg) => status::Custom(Status::InternalServerError, format!("Auth error: {}", msg)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/install", format = "json", data = "<data>")]
|
type AppResult<T> = Result<T, AppError>;
|
||||||
async fn install(data: Json<config::SqlConfig>) -> Result<status::Custom<String>, status::Custom<String>> {
|
|
||||||
relational::Database::initial_setup(data.into_inner()).await.map_err(|e| {
|
|
||||||
status::Custom(
|
|
||||||
Status::InternalServerError,
|
|
||||||
format!("Database initialization failed: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(status::Custom(
|
struct AppState {
|
||||||
Status::Ok,
|
db: Arc<Mutex<Option<relational::Database>>>,
|
||||||
format!("Initialization successful"),
|
configure: Arc<Mutex<config::Config>>,
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
async fn get_sql(&self) -> AppResult<relational::Database> {
|
||||||
|
self.db
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| AppError::Database("Database not initialized".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn link_sql(&self, config: config::SqlConfig) -> AppResult<()> {
|
||||||
|
let database = relational::Database::link(config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
*self.db.lock().await = Some(database);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[get("/system")]
|
#[get("/system")]
|
||||||
async fn token_system() -> Result<status::Custom<String>, status::Custom<String>> {
|
async fn token_system(_state: &State<AppState>) -> Result<status::Custom<String>, status::Custom<String>> {
|
||||||
let claims = secret::CustomClaims {
|
let claims = auth::jwt::CustomClaims {
|
||||||
user_id: String::from("system"),
|
user_id: "system".into(),
|
||||||
device_ua: String::from("system"),
|
device_ua: "system".into(),
|
||||||
};
|
};
|
||||||
let token = secret::generate_jwt(claims, Duration::seconds(1)).map_err(|e| {
|
|
||||||
status::Custom(
|
|
||||||
Status::InternalServerError,
|
|
||||||
format!("JWT generation failed: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(status::Custom(Status::Ok, token))
|
auth::jwt::generate_jwt(claims, Duration::seconds(1))
|
||||||
|
.map(|token| status::Custom(Status::Ok, token))
|
||||||
|
.map_err(|e| AppError::Auth(e.to_string()).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
let config = config::Config::read().expect("Failed to read config");
|
let config = config::Config::read().expect("Failed to read config");
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
db: Arc::new(Mutex::new(None)),
|
||||||
|
configure: Arc::new(Mutex::new(config.clone())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rocket_builder = rocket::build().manage(state);
|
||||||
|
|
||||||
if config.info.install {
|
if config.info.install {
|
||||||
init_sql(config.sql_config)
|
if let Some(state) = rocket_builder.state::<AppState>() {
|
||||||
.await
|
state.link_sql(config.sql_config.clone())
|
||||||
.expect("Failed to connect to database");
|
.await
|
||||||
rocket::build()
|
.expect("Failed to connect to database");
|
||||||
.mount("/auth/token", routes![token_system])
|
}
|
||||||
} else {
|
|
||||||
rocket::build()
|
|
||||||
.mount("/", routes![install])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ! config.info.install {
|
||||||
|
rocket_builder = rocket_builder
|
||||||
|
.mount("/", rocket::routes![routes::install]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rocket_builder = rocket_builder
|
||||||
|
.mount("/auth/token", routes![token_system]);
|
||||||
|
|
||||||
|
rocket_builder
|
||||||
}
|
}
|
71
backend/src/routes/intsall.rs
Normal file
71
backend/src/routes/intsall.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use serde::{Deserialize,Serialize};
|
||||||
|
use crate::{config,utils};
|
||||||
|
use crate::database::{relational,relational::builder};
|
||||||
|
use crate::{AppState,AppError,AppResult};
|
||||||
|
use rocket::{
|
||||||
|
get, post,
|
||||||
|
http::Status,
|
||||||
|
response::status,
|
||||||
|
serde::json::Json,
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct InstallData{
|
||||||
|
name:String,
|
||||||
|
email:String,
|
||||||
|
password:String,
|
||||||
|
sql_config: config::SqlConfig
|
||||||
|
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct InstallReplyData{
|
||||||
|
token:String,
|
||||||
|
name:String,
|
||||||
|
password:String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[post("/install", format = "application/json", data = "<data>")]
|
||||||
|
async fn install(
|
||||||
|
data: Json<InstallData>,
|
||||||
|
state: &State<AppState>
|
||||||
|
) -> AppResult<status::Custom<Json<InstallReplyData>>, AppError> {
|
||||||
|
let mut config = state.configure.lock().await;
|
||||||
|
if config.info.install {
|
||||||
|
return Err(AppError::Database("Database already initialized".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data=data.into_inner();
|
||||||
|
|
||||||
|
relational::Database::initial_setup(data.sql_config.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
config.info.install = true;
|
||||||
|
|
||||||
|
state.link_sql(data.sql_config.clone());
|
||||||
|
let sql= state.get_sql()
|
||||||
|
.await?
|
||||||
|
.get_db();
|
||||||
|
|
||||||
|
|
||||||
|
let system_name=utils::generate_random_string(20);
|
||||||
|
let system_password=utils::generate_random_string(20);
|
||||||
|
|
||||||
|
let mut builder = builder::QueryBuilder::new(builder::SqlOperation::Insert,String::from("persons"))?;
|
||||||
|
|
||||||
|
let user_params=HashMap::new();
|
||||||
|
user_params.insert("person_name", data.name);
|
||||||
|
user_params.insert("person_email", data.email);
|
||||||
|
user_params.insert("person_password", data.password);
|
||||||
|
user_params.insert("person_role", data.name);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
config::Config::write(config.clone())
|
||||||
|
.map_err(|e| AppError::Config(e.to_string()))?;
|
||||||
|
Ok()
|
||||||
|
}
|
6
backend/src/routes/mod.rs
Normal file
6
backend/src/routes/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mod intsall;
|
||||||
|
use rocket::routes;
|
||||||
|
|
||||||
|
pub fn create_routes() -> routes {
|
||||||
|
routes!["/", intsall::install]
|
||||||
|
}
|
9
backend/src/utils.rs
Normal file
9
backend/src/utils.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
|
pub fn generate_random_string(length: usize) -> String {
|
||||||
|
let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
(0..length)
|
||||||
|
.map(|_| *charset.choose(&mut rng).unwrap() as char)
|
||||||
|
.collect()
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
||||||
import { ThemeService } from '../services/themeService';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hasError: boolean;
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ErrorBoundary extends Component<Props, State> {
|
|
||||||
public state: State = {
|
|
||||||
hasError: false
|
|
||||||
};
|
|
||||||
|
|
||||||
public static getDerivedStateFromError(error: Error): State {
|
|
||||||
return { hasError: true, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
||||||
console.error('Uncaught error:', error, errorInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
const themeService = ThemeService.getInstance();
|
|
||||||
try {
|
|
||||||
const errorTemplate = themeService.getTemplate('error');
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: errorTemplate }} />;
|
|
||||||
} catch (e) {
|
|
||||||
return (
|
|
||||||
<div className="error-page">
|
|
||||||
<h1>Something went wrong</h1>
|
|
||||||
<p>{this.state.error?.message}</p>
|
|
||||||
<button onClick={() => this.setState({ hasError: false })}>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import React, { Suspense } from 'react';
|
|
||||||
|
|
||||||
interface LoadingBoundaryProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
fallback?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoadingBoundary: React.FC<LoadingBoundaryProps> = ({
|
|
||||||
children,
|
|
||||||
fallback = <div>Loading...</div>
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={fallback}>
|
|
||||||
{children}
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
@ -18,6 +18,7 @@ export interface ThemeConfig {
|
|||||||
tag: string;
|
tag: string;
|
||||||
category: string;
|
category: string;
|
||||||
error: string;
|
error: string;
|
||||||
|
loding: string;
|
||||||
page: Map<string, string>;
|
page: Map<string, string>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react'; // Import React
|
import React from 'react'; // Import React
|
||||||
import { useEffect } from 'react';
|
import { LoaderFunction, RouteObject } from 'react-router-dom';
|
||||||
import { LoaderFunction, RouteObject } from 'react-router-dom';
|
|
||||||
|
|
||||||
export class RouteManager {
|
export class RouteManager {
|
||||||
private static instance: RouteManager;
|
private static instance: RouteManager;
|
||||||
@ -15,30 +14,18 @@ export class RouteManager {
|
|||||||
return RouteManager.instance;
|
return RouteManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private register(path:string, element: React.ReactNode) {
|
|
||||||
|
private createRouteElement(path: string,element:React.ReactNode,loader?:LoaderFunction,children?:RouteObject[]) {
|
||||||
this.routes.push({
|
this.routes.push({
|
||||||
path,
|
path,
|
||||||
element,
|
element,
|
||||||
});
|
loader,
|
||||||
}
|
children,
|
||||||
|
|
||||||
|
|
||||||
private createRouteElement(path: string,element:React.ReactNode,loader?:LoaderFunction) {
|
|
||||||
this.routes.push({
|
|
||||||
path,
|
|
||||||
element,
|
|
||||||
loader?: loader
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRoutes(): RouteObject[] {
|
private getRoutes(): RouteObject[] {
|
||||||
return this.routes;
|
return this.routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRoute(path: string, templateName: string): void {
|
|
||||||
this.routes.push({
|
|
||||||
path,
|
|
||||||
element: this.createRouteElement(templateName),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -4,7 +4,6 @@ import { ApiService } from "./apiService";
|
|||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
private static instance: ThemeService;
|
private static instance: ThemeService;
|
||||||
private currentTheme?: ThemeConfig;
|
private currentTheme?: ThemeConfig;
|
||||||
private templates: Map<string, string> = new Map();
|
|
||||||
private api: ApiService;
|
private api: ApiService;
|
||||||
|
|
||||||
private constructor(api: ApiService) {
|
private constructor(api: ApiService) {
|
||||||
|
Loading…
Reference in New Issue
Block a user