diff --git a/backend/src/routes/auth/mod.rs b/backend/src/api/auth/mod.rs similarity index 100% rename from backend/src/routes/auth/mod.rs rename to backend/src/api/auth/mod.rs diff --git a/backend/src/routes/auth/token.rs b/backend/src/api/auth/token.rs similarity index 78% rename from backend/src/routes/auth/token.rs rename to backend/src/api/auth/token.rs index 6e74998..b93fe5f 100644 --- a/backend/src/routes/auth/token.rs +++ b/backend/src/api/auth/token.rs @@ -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 = "")] @@ -24,18 +24,18 @@ pub async fn token_system( data: Json, ) -> AppResult { 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), diff --git a/backend/src/routes/mod.rs b/backend/src/api/mod.rs similarity index 92% rename from backend/src/routes/mod.rs rename to backend/src/api/mod.rs index e012625..72419a0 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/api/mod.rs @@ -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 { } pub fn configure_routes() -> Vec { - routes![configure::system_config_get] + routes![settings::system_config_get] } diff --git a/backend/src/routes/configure.rs b/backend/src/api/settings.rs similarity index 77% rename from backend/src/routes/configure.rs rename to backend/src/api/settings.rs index 39c7fad..0b2081f 100644 --- a/backend/src/routes/configure.rs +++ b/backend/src/api/settings.rs @@ -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> { 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, ) -> 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> { 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) diff --git a/backend/src/routes/install.rs b/backend/src/api/setup.rs similarity index 61% rename from backend/src/routes/install.rs rename to backend/src/api/setup.rs index ab9b5b1..f0a0973 100644 --- a/backend/src/routes/install.rs +++ b/backend/src/api/setup.rs @@ -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, }), )) diff --git a/backend/src/routes/person.rs b/backend/src/api/users.rs similarity index 54% rename from backend/src/routes/person.rs rename to backend/src/api/users.rs index 73849ae..47f70d4 100644 --- a/backend/src/routes/person.rs +++ b/backend/src/api/users.rs @@ -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, ), )?; diff --git a/backend/src/config.rs b/backend/src/common/config.rs similarity index 98% rename from backend/src/config.rs rename to backend/src/common/config.rs index d11a321..f75f8eb 100644 --- a/backend/src/config.rs +++ b/backend/src/common/config.rs @@ -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}; diff --git a/backend/src/error.rs b/backend/src/common/error.rs similarity index 100% rename from backend/src/error.rs rename to backend/src/common/error.rs diff --git a/backend/src/utils.rs b/backend/src/common/helpers.rs similarity index 100% rename from backend/src/utils.rs rename to backend/src/common/helpers.rs diff --git a/backend/src/common/mod.rs b/backend/src/common/mod.rs new file mode 100644 index 0000000..d8458fe --- /dev/null +++ b/backend/src/common/mod.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod helpers; +pub mod config; diff --git a/backend/src/database/mod.rs b/backend/src/database/mod.rs deleted file mode 100644 index 81aa43e..0000000 --- a/backend/src/database/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod relational; diff --git a/backend/src/database/relational/postgresql/init.sql b/backend/src/database/relational/postgresql/init.sql deleted file mode 100644 index 5b42ad4..0000000 --- a/backend/src/database/relational/postgresql/init.sql +++ /dev/null @@ -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 -); diff --git a/backend/src/main.rs b/backend/src/main.rs index a4ab47e..ee70f49 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -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>>, + db: Arc>>, shutdown: Arc>>, restart_progress: Arc>, } @@ -26,7 +24,7 @@ impl AppState { } } - pub async fn sql_get(&self) -> CustomResult { + pub async fn sql_get(&self) -> CustomResult { 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?; diff --git a/backend/src/auth/bcrypt.rs b/backend/src/security/bcrypt.rs similarity index 83% rename from backend/src/auth/bcrypt.rs rename to backend/src/security/bcrypt.rs index c0f1fb8..c618f50 100644 --- a/backend/src/auth/bcrypt.rs +++ b/backend/src/security/bcrypt.rs @@ -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 { diff --git a/backend/src/auth/jwt.rs b/backend/src/security/jwt.rs similarity index 98% rename from backend/src/auth/jwt.rs rename to backend/src/security/jwt.rs index aa655b4..eb04fb9 100644 --- a/backend/src/auth/jwt.rs +++ b/backend/src/security/jwt.rs @@ -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}; diff --git a/backend/src/auth/mod.rs b/backend/src/security/mod.rs similarity index 100% rename from backend/src/auth/mod.rs rename to backend/src/security/mod.rs diff --git a/backend/src/storage/mod.rs b/backend/src/storage/mod.rs new file mode 100644 index 0000000..07c4d2f --- /dev/null +++ b/backend/src/storage/mod.rs @@ -0,0 +1 @@ +pub mod sql; \ No newline at end of file diff --git a/backend/src/database/relational/builder.rs b/backend/src/storage/sql/builder.rs similarity index 99% rename from backend/src/database/relational/builder.rs rename to backend/src/storage/sql/builder.rs index d0cabab..46fae24 100644 --- a/backend/src/database/relational/builder.rs +++ b/backend/src/storage/sql/builder.rs @@ -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; diff --git a/backend/src/database/relational/mod.rs b/backend/src/storage/sql/mod.rs similarity index 95% rename from backend/src/database/relational/mod.rs rename to backend/src/storage/sql/mod.rs index 0de5052..f4e987c 100644 --- a/backend/src/database/relational/mod.rs +++ b/backend/src/storage/sql/mod.rs @@ -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; diff --git a/backend/src/database/relational/postgresql/mod.rs b/backend/src/storage/sql/postgresql/mod.rs similarity index 95% rename from backend/src/database/relational/postgresql/mod.rs rename to backend/src/storage/sql/postgresql/mod.rs index 2067ebd..6206e59 100644 --- a/backend/src/database/relational/postgresql/mod.rs +++ b/backend/src/storage/sql/postgresql/mod.rs @@ -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!( diff --git a/backend/src/storage/sql/postgresql/schema.sql b/backend/src/storage/sql/postgresql/schema.sql new file mode 100644 index 0000000..53462d4 --- /dev/null +++ b/backend/src/storage/sql/postgresql/schema.sql @@ -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 +); \ No newline at end of file diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 041d38e..574a475 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -53,6 +53,37 @@ export class ApiService { } } + private async getToken(username: string, password: string): Promise { + 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( endpoint: string, options: RequestInit = {},