数据库:重构结构

后端:重构结构
This commit is contained in:
lsy 2024-11-26 12:19:57 +08:00
parent 2c1923da07
commit dbdfbf5d8a
22 changed files with 249 additions and 198 deletions

View File

@ -1,6 +1,6 @@
use crate::auth;
use crate::database::relational::builder;
use crate::error::{AppResult, AppResultInto};
use crate::security;
use crate::storage::sql::builder;
use crate::common::error::{AppResult, AppResultInto};
use crate::AppState;
use chrono::Duration;
use rocket::{
@ -15,7 +15,7 @@ use serde_json::json;
use std::sync::Arc;
#[derive(Deserialize, Serialize)]
pub struct TokenSystemData {
name: String,
username: String,
password: String,
}
#[post("/system", format = "application/json", data = "<data>")]
@ -24,18 +24,18 @@ pub async fn token_system(
data: Json<TokenSystemData>,
) -> AppResult<String> {
let mut builder =
builder::QueryBuilder::new(builder::SqlOperation::Select, "persons".to_string())
builder::QueryBuilder::new(builder::SqlOperation::Select, "users".to_string())
.into_app_result()?;
builder
.add_field("person_password".to_string())
.add_field("password_hash".to_string())
.into_app_result()?
.add_condition(builder::WhereClause::And(vec![
builder::WhereClause::Condition(
builder::Condition::new(
"person_name".to_string(),
"username".to_string(),
builder::Operator::Eq,
Some(builder::SafeValue::Text(
data.name.clone(),
data.username.clone(),
builder::ValidationLevel::Relaxed,
)),
)
@ -43,7 +43,7 @@ pub async fn token_system(
),
builder::WhereClause::Condition(
builder::Condition::new(
"person_email".to_string(),
"email".to_string(),
builder::Operator::Eq,
Some(builder::SafeValue::Text(
"author@lsy22.com".into(),
@ -54,11 +54,11 @@ pub async fn token_system(
),
builder::WhereClause::Condition(
builder::Condition::new(
"person_level".to_string(),
"role".to_string(),
builder::Operator::Eq,
Some(builder::SafeValue::Enum(
"administrators".into(),
"privilege_level".into(),
"administrator".into(),
"user_role".into(),
builder::ValidationLevel::Standard,
)),
)
@ -77,17 +77,17 @@ pub async fn token_system(
let password = values
.first()
.and_then(|row| row.get("person_password"))
.and_then(|row| row.get("password_hash"))
.and_then(|val| val.as_str())
.ok_or_else(|| {
status::Custom(Status::NotFound, "Invalid system user or password".into())
})?;
auth::bcrypt::verify_hash(&data.password, password)
security::bcrypt::verify_hash(&data.password, password)
.map_err(|_| status::Custom(Status::Forbidden, "Invalid password".into()))?;
Ok(auth::jwt::generate_jwt(
auth::jwt::CustomClaims {
Ok(security::jwt::generate_jwt(
security::jwt::CustomClaims {
name: "system".into(),
},
Duration::minutes(1),

View File

@ -1,8 +1,8 @@
pub mod auth;
pub mod configure;
pub mod install;
pub mod person;
use crate::auth::jwt;
pub mod settings;
pub mod setup;
pub mod users;
use crate::security::jwt;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use rocket::routes;
@ -48,5 +48,5 @@ pub fn jwt_routes() -> Vec<rocket::Route> {
}
pub fn configure_routes() -> Vec<rocket::Route> {
routes![configure::system_config_get]
routes![settings::system_config_get]
}

View File

@ -1,6 +1,6 @@
use super::SystemToken;
use crate::database::{relational, relational::builder};
use crate::error::{AppResult, AppResultInto, CustomResult};
use crate::storage::{sql, sql::builder};
use crate::common::error::{AppResult, AppResultInto, CustomResult};
use crate::AppState;
use rocket::{
get,
@ -34,13 +34,13 @@ impl Default for SystemConfigure {
}
}
pub async fn get_configure(
sql: &relational::Database,
pub async fn get_setting(
sql: &sql::Database,
comfig_type: String,
name: String,
) -> CustomResult<Json<Value>> {
let name_condition = builder::Condition::new(
"config_name".to_string(),
"key".to_string(),
builder::Operator::Eq,
Some(builder::SafeValue::Text(
format!("{}_{}", comfig_type, name),
@ -48,40 +48,37 @@ pub async fn get_configure(
)),
)?;
println!(
"Searching for config_name: {}",
format!("{}_{}", comfig_type, name)
);
let where_clause = builder::WhereClause::Condition(name_condition);
let mut sql_builder =
builder::QueryBuilder::new(builder::SqlOperation::Select, "config".to_string())?;
builder::QueryBuilder::new(builder::SqlOperation::Select, "settings".to_string())?;
sql_builder
.add_condition(where_clause)
.add_field("config_data".to_string())?;
.add_field("data".to_string())?;
println!("{:?}", sql_builder.build());
let result = sql.get_db().execute_query(&sql_builder).await?;
Ok(Json(json!(result)))
}
pub async fn insert_configure(
sql: &relational::Database,
pub async fn insert_setting(
sql: &sql::Database,
comfig_type: String,
name: String,
data: Json<Value>,
) -> CustomResult<()> {
let mut builder =
builder::QueryBuilder::new(builder::SqlOperation::Insert, "config".to_string())?;
builder::QueryBuilder::new(builder::SqlOperation::Insert, "settings".to_string())?;
builder.set_value(
"config_name".to_string(),
"key".to_string(),
builder::SafeValue::Text(
format!("{}_{}", comfig_type, name).to_string(),
builder::ValidationLevel::Strict,
),
)?;
builder.set_value(
"config_data".to_string(),
"data".to_string(),
builder::SafeValue::Json(data.into_inner()),
)?;
sql.get_db().execute_query(&builder).await?;
@ -94,7 +91,7 @@ pub async fn system_config_get(
_token: SystemToken,
) -> AppResult<Json<Value>> {
let sql = state.sql_get().await.into_app_result()?;
let configure = get_configure(&sql, "system".to_string(), "config".to_string())
let configure = get_setting(&sql, "system".to_string(), "settings".to_string())
.await
.into_app_result()?;
Ok(configure)

View File

@ -1,26 +1,27 @@
use super::{configure, person};
use crate::auth;
use crate::database::relational;
use crate::error::{AppResult, AppResultInto};
use super::{settings, users};
use crate::security;
use crate::storage::sql;
use crate::common::error::{AppResult, AppResultInto};
use crate::AppState;
use crate::{config, utils};
use crate::common::config;
use crate::common::helpers;
use chrono::Duration;
use rocket::{http::Status, post, response::status, serde::json::Json, State};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize,Debug)]
pub struct InstallData {
name: String,
username: String,
email: String,
password: String,
sql_config: config::SqlConfig,
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize,Debug)]
pub struct InstallReplyData {
token: String,
name: String,
username: String,
password: String,
}
@ -36,64 +37,63 @@ pub async fn install(
"Database already initialized".to_string(),
));
}
let data = data.into_inner();
let sql = {
config.info.install = true;
config.sql_config = data.sql_config.clone();
relational::Database::initial_setup(data.sql_config.clone())
sql::Database::initial_setup(data.sql_config.clone())
.await
.into_app_result()?;
auth::jwt::generate_key().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()?
};
let system_credentials = (
utils::generate_random_string(20),
utils::generate_random_string(20),
helpers::generate_random_string(20),
helpers::generate_random_string(20),
);
person::insert(
users::insert_user(
&sql,
person::RegisterData {
name: data.name.clone(),
users::RegisterData {
username: data.username.clone(),
email: data.email,
password: data.password,
level: "administrators".to_string(),
role: "administrator".to_string(),
},
)
.await
.into_app_result()?;
person::insert(
users::insert_user(
&sql,
person::RegisterData {
name: system_credentials.0.clone(),
users::RegisterData {
username: system_credentials.0.clone(),
email: "author@lsy22.com".to_string(),
password: system_credentials.1.clone(),
level: "administrators".to_string(),
role: "administrator".to_string(),
},
)
.await
.into_app_result()?;
configure::insert_configure(
settings::insert_setting(
&sql,
"system".to_string(),
"config".to_string(),
Json(json!(configure::SystemConfigure {
author_name: data.name.clone(),
..configure::SystemConfigure::default()
"settings".to_string(),
Json(json!(settings::SystemConfigure {
author_name: data.username.clone(),
..settings::SystemConfigure::default()
})),
)
.await
.into_app_result()?;
let token = auth::jwt::generate_jwt(
auth::jwt::CustomClaims { name: data.name },
let token = security::jwt::generate_jwt(
security::jwt::CustomClaims { name: data.username },
Duration::days(7),
)
.into_app_result()?;
@ -105,7 +105,7 @@ pub async fn install(
Status::Ok,
Json(InstallReplyData {
token,
name: system_credentials.0,
username: system_credentials.0,
password: system_credentials.1,
}),
))

View File

@ -1,49 +1,54 @@
use crate::auth;
use crate::auth::bcrypt;
use crate::database::{relational, relational::builder};
use crate::error::{CustomErrorInto, CustomResult};
use crate::{config, utils};
use crate::security;
use crate::security::bcrypt;
use crate::storage::{sql, sql::builder};
use crate::common::error::{CustomErrorInto, CustomResult};
use rocket::{get, http::Status, post, response::status, serde::json::Json, State};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Serialize)]
pub struct LoginData {
pub name: String,
pub username: String,
pub password: String,
}
#[derive(Debug)]
pub struct RegisterData {
pub name: String,
pub username: String,
pub email: String,
pub password: String,
pub level: String,
pub role: String,
}
pub async fn insert(sql: &relational::Database, data: RegisterData) -> CustomResult<()> {
pub async fn insert_user(sql: &sql::Database, data: RegisterData) -> CustomResult<()> {
let role = match data.role.as_str() {
"administrator" | "contributor" => data.role,
_ => return Err("Invalid role. Must be either 'administrator' or 'contributor'".into_custom_error()),
};
let mut builder =
builder::QueryBuilder::new(builder::SqlOperation::Insert, "persons".to_string())?;
builder::QueryBuilder::new(builder::SqlOperation::Insert, "users".to_string())?;
builder
.set_value(
"person_name".to_string(),
builder::SafeValue::Text(data.name, builder::ValidationLevel::Relaxed),
"username".to_string(),
builder::SafeValue::Text(data.username, builder::ValidationLevel::Relaxed),
)?
.set_value(
"person_email".to_string(),
"email".to_string(),
builder::SafeValue::Text(data.email, builder::ValidationLevel::Relaxed),
)?
.set_value(
"person_password".to_string(),
"password_hash".to_string(),
builder::SafeValue::Text(
bcrypt::generate_hash(&data.password)?,
builder::ValidationLevel::Relaxed,
),
)?
.set_value(
"person_level".to_string(),
"role".to_string(),
builder::SafeValue::Enum(
data.level,
"privilege_level".to_string(),
role,
"user_role".to_string(),
builder::ValidationLevel::Standard,
),
)?;

View File

@ -1,4 +1,4 @@
use crate::error::CustomResult;
use crate::common::error::CustomResult;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{env, fs};

View File

@ -0,0 +1,3 @@
pub mod error;
pub mod helpers;
pub mod config;

View File

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

View File

@ -1,84 +0,0 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TYPE privilege_level AS ENUM ( 'contributor', 'administrators');
CREATE TABLE persons
(
person_name VARCHAR(100) PRIMARY KEY,
person_avatar VARCHAR(255),
person_email VARCHAR(255) UNIQUE NOT NULL,
person_icon VARCHAR(255),
person_password VARCHAR(255) NOT NULL,
person_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
person_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
person_role VARCHAR(50),
person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
person_level privilege_level NOT NULL DEFAULT 'contributor'
);
CREATE TYPE publication_status AS ENUM ('draft', 'published', 'private','hide');
CREATE TABLE pages
(
page_id SERIAL PRIMARY KEY,
page_meta_keywords VARCHAR(255) NOT NULL,
page_meta_description VARCHAR(255) NOT NULL,
page_title VARCHAR(255) NOT NULL,
page_content TEXT NOT NULL,
page_mould VARCHAR(50),
page_fields JSON,
page_status publication_status DEFAULT 'draft'
);
CREATE TABLE posts
(
post_author VARCHAR(100) NOT NULL REFERENCES persons (person_name) ON DELETE CASCADE,
post_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_picture VARCHAR(255),
post_title VARCHAR(255) NOT NULL,
post_meta_keywords VARCHAR(255) NOT NULL,
post_meta_description VARCHAR(255) NOT NULL,
post_content TEXT NOT NULL,
post_status publication_status DEFAULT 'draft',
post_editor BOOLEAN DEFAULT FALSE,
post_unsaved_content TEXT,
post_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
post_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
post_published_at TIMESTAMP,
CONSTRAINT post_updated_at_check CHECK (post_updated_at >= post_created_at)
);
CREATE TABLE tags
(
tag_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(tag_name) = tag_name),
tag_icon VARCHAR(255)
);
CREATE TABLE post_tags
(
post_id UUID REFERENCES posts (post_id) ON DELETE CASCADE,
tag_name VARCHAR(50) REFERENCES tags (tag_name) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_name)
);
CREATE TABLE categories
(
category_name VARCHAR(50) PRIMARY KEY,
parent_category_name VARCHAR(50),
FOREIGN KEY (parent_category_name) REFERENCES categories (category_name)
);
CREATE TABLE post_categories
(
post_id UUID REFERENCES posts (post_id) ON DELETE CASCADE,
category_id VARCHAR(50) REFERENCES categories (category_name) ON DELETE CASCADE,
PRIMARY KEY (post_id, category_id)
);
CREATE TABLE library
(
library_author VARCHAR(100) NOT NULL REFERENCES persons (person_name) ON DELETE CASCADE,
library_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
library_name VARCHAR(255) NOT NULL,
library_size BIGINT NOT NULL,
library_storage_path VARCHAR(255) NOT NULL UNIQUE,
library_type VARCHAR(50) NOT NULL,
library_class VARCHAR(50),
library_description VARCHAR(255),
library_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE config
(
config_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(config_name) = config_name),
config_data JSON
);

View File

@ -1,18 +1,16 @@
mod auth;
mod config;
mod database;
mod error;
mod routes;
mod utils;
mod security;
mod common;
mod storage;
mod api;
use database::relational;
use error::{CustomErrorInto, CustomResult};
use storage::sql;
use common::error::{CustomErrorInto, CustomResult};
use rocket::Shutdown;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::common::config;
pub struct AppState {
db: Arc<Mutex<Option<relational::Database>>>,
db: Arc<Mutex<Option<sql::Database>>>,
shutdown: Arc<Mutex<Option<Shutdown>>>,
restart_progress: Arc<Mutex<bool>>,
}
@ -26,7 +24,7 @@ impl AppState {
}
}
pub async fn sql_get(&self) -> CustomResult<relational::Database> {
pub async fn sql_get(&self) -> CustomResult<sql::Database> {
self.db
.lock()
.await
@ -35,7 +33,7 @@ impl AppState {
}
pub async fn sql_link(&self, config: &config::SqlConfig) -> CustomResult<()> {
*self.db.lock().await = Some(relational::Database::link(config).await?);
*self.db.lock().await = Some(sql::Database::link(config).await?);
Ok(())
}
@ -69,12 +67,12 @@ async fn main() -> CustomResult<()> {
.manage(state.clone());
if !config.info.install {
rocket_builder = rocket_builder.mount("/", rocket::routes![routes::install::install]);
rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::install]);
} else {
state.sql_link(&config.sql_config).await?;
rocket_builder = rocket_builder
.mount("/auth/token", routes::jwt_routes())
.mount("/config", routes::configure_routes());
.mount("/auth/token", api::jwt_routes())
.mount("/config", api::configure_routes());
}
let rocket = rocket_builder.ignite().await?;

View File

@ -1,4 +1,4 @@
use crate::error::{CustomErrorInto, CustomResult};
use crate::common::error::{CustomErrorInto, CustomResult};
use bcrypt::{hash, verify, DEFAULT_COST};
pub fn generate_hash(s: &str) -> CustomResult<String> {

View File

@ -1,4 +1,4 @@
use crate::error::CustomResult;
use crate::common::error::CustomResult;
use chrono::{Duration, Utc};
use ed25519_dalek::{SigningKey, VerifyingKey};
use jwt_compact::{alg::Ed25519, AlgorithmExt, Header, TimeOptions, Token, UntrustedToken};

View File

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

View File

@ -1,4 +1,4 @@
use crate::error::{CustomErrorInto, CustomResult};
use crate::common::error::{CustomErrorInto, CustomResult};
use chrono::{DateTime, Utc};
use regex::Regex;
use serde::Serialize;

View File

@ -1,6 +1,6 @@
mod postgresql;
use crate::config;
use crate::error::{CustomErrorInto, CustomResult};
use crate::common::error::{CustomErrorInto, CustomResult};
use async_trait::async_trait;
use std::{collections::HashMap, sync::Arc};
pub mod builder;

View File

@ -1,7 +1,6 @@
use super::{builder, DatabaseTrait};
use crate::config;
use crate::error::CustomErrorInto;
use crate::error::CustomResult;
use crate::common::error::CustomResult;
use async_trait::async_trait;
use serde_json::Value;
use sqlx::{Column, Executor, PgPool, Row, TypeInfo};
@ -17,10 +16,10 @@ impl DatabaseTrait for Postgresql {
async fn initialization(db_config: config::SqlConfig) -> CustomResult<()> {
let path = env::current_dir()?
.join("src")
.join("database")
.join("relational")
.join("storage")
.join("sql")
.join("postgresql")
.join("init.sql");
.join("schema.sql");
let grammar = fs::read_to_string(&path)?;
let connection_str = format!(

View File

@ -0,0 +1,102 @@
-- 自定义类型定义
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
);

View File

@ -53,6 +53,37 @@ export class ApiService {
}
}
private async getToken(username: string, password: string): Promise<string> {
if (username.split(" ").length === 0 || password.split(" ").length === 0) {
throw new Error(
"Username or password cannot be empty",
);
}
try {
const response = await fetch(`${this.baseURL}/auth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
});
if (!response.ok) {
throw new Error("Failed to get system token");
}
const data = await response.text();
return data;
} catch (error) {
console.error("Error getting system token:", error);
throw error;
}
}
public async request<T>(
endpoint: string,
options: RequestInit = {},