diff --git a/Cargo.toml b/Cargo.toml index 7ac36cf9dc55648368d6e593d18556b697e5d9d0..ce4280e89a96e70ad78a4b32530be575434d812a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum={version = "0.7.5", features=["multipart"]} +axum={version = "0.7.5", features=["multipart","form"]} tokio={version="1.37.0",features=["macros","rt-multi-thread","time"]} askama = {version="0.12.1"} askama_axum="0.4.0" diff --git a/README.md b/README.md index 2a282b2641e7ac552f13ae5864a93c0a62a43e47..7fe5f34bc9aa75c9dd9321fc1a64fd2ae0c38442 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,17 @@ 日志:tklog 定时任务:job_scheduler 模板渲染:aksama, askama-axum -页面:html,css,js, VUE, Bootstrap +页面:html,css,js, VUE, Bootstrap, marked.umd.js +配置: config + +### 解析Markdown +在前段解析Markdown文档,使用的lib为marked.umd.js ### 服务器端口 -端口 3000, 硬编码在代码中,需要改进 +端口 3000, 写入到配置文件中application.toml ### 数据库 -数据库信息硬编码在数据库中,需要改进,用配置文件或者.env。 +写入到配置文件中 ### 技术细节 diff --git a/resource/application.toml b/resource/application.toml index 758304b9efe6bc332cd2d204a25fee730834b38d..58bf5efddb98306665c30bb61664bc6511165893 100644 --- a/resource/application.toml +++ b/resource/application.toml @@ -3,13 +3,20 @@ host = "0.0.0.0" port = 3000 [database] -host = "xxxx" -port = xxxx +host = "xx.xx.xx.xx" +port = xxx username = "xxx" -password = "xxxx" -database = "xxxx" +password = "xx" +database = "Blog" [file_location] blog_home_dir = "/home/owen/blog/" upload_location = "markdown" archive_location = "archive" + +[session] +timeout = 1800 + +[admin] +username = "admin" +password = "admin" diff --git a/src/configuration/app_config.rs b/src/configuration/app_config.rs index 7f315300bf82aff9287ba53c743350c448a566de..221200d512343ac6aefb98966e4ecbd8b70925c3 100644 --- a/src/configuration/app_config.rs +++ b/src/configuration/app_config.rs @@ -8,6 +8,8 @@ pub struct AppConfig { database: DatabaseSettings, server: ServerSettings, file_location: FileSettings, + session: SessionSettings, + admin: AdminSettings, } #[derive(Debug, Clone, Deserialize)] @@ -32,6 +34,17 @@ struct FileSettings { blog_home_dir: String, } +#[derive(Debug, Clone, Deserialize)] +struct SessionSettings { + timeout: u64, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdminSettings { + pub username: String, + pub password: String, +} + // 使用 OnceLock 实现配置的单例缓存 static CONFIG: OnceLock = OnceLock::new(); @@ -95,4 +108,12 @@ impl AppConfig { self.file_location.blog_home_dir.clone() + self.file_location.archive_location.clone().as_str() } + + pub fn get_session_timeout(&self) -> Option { + Some(self.session.timeout) + } + + pub fn get_admin_settings(&self) -> &AdminSettings { + &self.admin + } } diff --git a/src/entity/login_form.rs b/src/entity/login_form.rs new file mode 100644 index 0000000000000000000000000000000000000000..645e5071c01aa9aa9e5aa28dd81d69c18f836c78 --- /dev/null +++ b/src/entity/login_form.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct LoginForm { + pub username: String, + pub password: String, +} diff --git a/src/entity/mod.rs b/src/entity/mod.rs index f38666ae4badbb6659562d90cef62034764a773a..95017a8c61298f804f1fbce826b543351af6f892 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -4,3 +4,4 @@ pub mod article_detail; pub mod article_list_view; pub mod article_summary; pub mod article_summary_vec; +pub mod login_form; diff --git a/src/layer/mod.rs b/src/layer/mod.rs index f9f79cd26e5dced94569a7599de5f35eac3fb586..4286ccdfdb82a6db54e9706196bbe17d7d409624 100644 --- a/src/layer/mod.rs +++ b/src/layer/mod.rs @@ -1,2 +1,3 @@ pub mod log_layer; +pub mod session_layer; pub mod url_layer; diff --git a/src/layer/session_layer.rs b/src/layer/session_layer.rs new file mode 100644 index 0000000000000000000000000000000000000000..505d6e9faeb930ebdbbfa5273a38ad986c0da4ff --- /dev/null +++ b/src/layer/session_layer.rs @@ -0,0 +1,103 @@ +use axum::body::Body; +use axum::http::{Request, Response}; +use axum::response::IntoResponse; +use axum::response::Redirect; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use tklog::info; + +use crate::session::session_manage::SessionManager; + +/// a demo https://tower-rs.github.io/tower/tower_layer/trait.Layer.html +use tower_layer::Layer; +use tower_service::Service; + +#[derive(Clone)] +pub struct SessionLayer<'a> { + session_manager: &'a SessionManager, +} + +impl<'a> SessionLayer<'a> { + pub fn new(session_manager: &'a SessionManager) -> Self { + SessionLayer { session_manager } + } +} + +impl Layer for SessionLayer<'_> { + type Service = SessionService; + + fn layer(&self, service: S) -> Self::Service { + SessionService { + service, + session_manager: self.session_manager.clone(), + } + } +} + +// This service implements the Log behavior +#[derive(Debug, Clone)] +pub struct SessionService { + service: S, + session_manager: SessionManager, +} + +impl Service> for SessionService +where + S: Service, Response = Response>, + S::Future: Send + 'static, + S::Response: Send + 'static, +{ + type Response = Response; + type Error = S::Error; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let uri = req.uri().path(); + info!("Request URI: {}", uri); + if uri == "/toupload" { + let cookies = req.headers().get("cookie").unwrap(); + println!("the cookies:{:?}", cookies.to_str()); + let cookies = cookies.to_str().unwrap(); + let should_redirect = if cookies.contains("sessionid") { + let session_id = cookies + .split(';') + .map(|s| s.trim()) + .find(|s| s.starts_with("sessionid=")) + .and_then(|s| s.split_once('=')) + .map(|(_, v)| v) + .unwrap(); + info!("the sessionid:{:?}", session_id); + + if self.session_manager.is_session_exist(&session_id) { + if self.session_manager.is_expired(&session_id) { + let _ = self.session_manager.remove_session(&session_id); + true + } else { + info!("Session is valid"); + false + } + } else { + info!("Session does not exist"); + true + } + } else { + info!("No session found"); + true + }; + if should_redirect { + info!("Returning response"); + let response = Redirect::temporary("/tologin"); + return Box::pin(async { Ok(response.into_response()) }); + }; + } + Box::pin(self.service.call(req)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 75326556e945d0b76fe78687683c7520e87b0a3d..a211fc6e6534d24c7f3ceea1442a2f49cc0bcb88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,6 @@ -//pub mod service; -//pub mod traits; -//pub mod common; pub mod configuration; pub mod entity; pub mod job; pub mod layer; pub mod scheduler_job; +pub mod session; diff --git a/src/main.rs b/src/main.rs index e2958a0e76c8d274c14d677f1598fb0397cb1b7e..6fe0ad63b50fbe3dec723fc27bb489a03fc12368 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,36 +2,52 @@ use std::fs; use std::sync::atomic::AtomicU16; use std::sync::{Arc, RwLock}; +use axum::body::Body; use axum::extract::Multipart; -use axum::http::HeaderValue; +use axum::http::{Request, Response}; use axum::response::Html; +use axum::response::IntoResponse; +use axum::Form; + +use axum::response::Redirect; use axum::{ extract::Path, extract::State, + http::{StatusCode, Uri}, routing::{get, post}, Json, Router, }; use axum_hello::entity::article_list_view::ArticlesListView; use axum_hello::layer::log_layer::LogLayer; +use axum_hello::layer::session_layer::SessionLayer; use axum_hello::layer::url_layer::UrlLayer; use axum_hello::scheduler_job::article_read_time_scheduler_job::ArticleReadTimeSchedulerJob; +use axum_hello::session::session::Session; +use axum_hello::session::session_manage::SessionManager; +use axum::http::HeaderValue; use sqlx::mysql::MySqlPoolOptions; use sqlx::MySqlPool; use tower_http::cors::CorsLayer; +use job_scheduler::Uuid; + use tklog::{error, info, warn}; +use chrono::{DateTime, Duration, Utc}; + pub mod configuration; pub mod entity; pub mod job; pub mod person; +pub mod session; use crate::configuration::app_config::AppConfig; use crate::entity::article::Article; use crate::entity::article_detail::ArticleDetail; use crate::entity::article_summary::ArticleSummary; use crate::entity::article_summary_vec::ArticleSummaryVec; +use crate::entity::login_form::LoginForm; use crate::person::Person; use crate::job::file_extractor::Extractor; @@ -47,15 +63,7 @@ async fn main() { let workspace = std::env::current_dir().unwrap(); println!("{:?}", &workspace); info!("Current workspace:: {:?}", &workspace.to_string_lossy()); - /* - let args: Vec = std::env::args().collect(); - if (args).capacity() < 2 { - //第一个参数是程序名称,第二个参数才是我们输入的参数 - eprintln!("You need to entry the basic_path"); - panic!("Lost the param basic_path"); - } - let basic_path = Arc::new(args[1].clone()); - */ + let pool = MySqlPoolOptions::new() .connect(&app_config.get_database_connection_url()) .await @@ -71,21 +79,28 @@ async fn main() { let log_layer = LogLayer { target: "hello layer", }; + let timeout = AppConfig::get().get_session_timeout(); + let _ = SessionManager::init(timeout).expect("Initialized SessionManager failed!"); + let session_layer = SessionLayer::new(SessionManager::get()); let router = Router::new() .route("/blog/new", post(upload_file)) - .route("/blog/uploadpage", get(upload_page)) + .route("/toupload", get(upload_page)) .route("/", get(articles_view)) //只返回试图,路由作用 .route("/articles", get(articles_view)) //只返回试图,路由作用 .route("/articles/:page_num", get(article_list)) //返回具体信息 .route("/article/:id", get(article_detail)) .route("/me", get(me)) + .route("/tologin", get(to_login)) + .route("/login", post(login)) .route("/article/new", get(extract_article)) + .fallback(status_sode_404) .layer( //设置同源策略 - CorsLayer::new().allow_origin("*".parse::().unwrap()), + CorsLayer::new().allow_origin("*".parse::().unwrap()), //need to fix ) .layer(log_layer) .layer(url_layer) + .layer(session_layer) .with_state(pool.clone()) .nest_service("/assets", tower_http::services::ServeDir::new("assets")); @@ -109,6 +124,10 @@ async fn say_hello(State(pool): State) -> String { row.0 } +async fn status_sode_404(uri: Uri) -> (StatusCode, String) { + (StatusCode::NOT_FOUND, format!("No route for {uri}")) +} + async fn articles_view() -> ArticlesListView { ArticlesListView::new() } @@ -188,37 +207,7 @@ async fn upload_page() -> Html { let upload_page_html = fs::read_to_string("./templates/html/blog_upload.html").unwrap(); Html(upload_page_html) } -/* -async fn upload_file1(mut multipart: Multipart) -> &'static str { - let mut response = "上传成功"; - while let Some(field) = multipart.next_field().await.map_err(|e| { - error!("upload file failed {}", e); - return "上传文件失败!"; - })? { - let name = field.file_name().unwrap().to_string(); - let data = field.bytes().await.unwrap(); - println!("Length of `{}` is {} bytes", name, data.len()); - //保存文件 - //let file_path = std::env::current_dir().unwrap().join("markdown").join(name); - let file_path_string = AppConfig::get().get_file_upload_location() + "/" + &name; - - let file_path = std::path::Path::new(&file_path_string); - info!(&file_path_string); - //检查文件是否存在 - if file_path.exists() { - //存在则更新文件,同时更新数据库信息 - fs::write(file_path, data).expect("更新文件失败!"); - //todo db update - } else { - fs::write(file_path, data).expect("保存文件失败!"); - } - //上传单文件,所以这里直接返回成功 - return response; - } - response = "没有接受到上传的文件,请检查是否上传文件!"; - return response; -} -*/ + async fn upload_file(mut multipart: Multipart) -> &'static str { // 循环读取每一个字段 while let Some(field) = match multipart.next_field().await { @@ -270,6 +259,43 @@ async fn upload_file(mut multipart: Multipart) -> &'static str { "没有接收到上传的文件,请检查是否选择文件!" } +async fn to_login() -> Html { + let login_page = fs::read_to_string("./templates/html/login.html").unwrap(); + Html(login_page) +} + +async fn login(Form(form): Form) -> Result, StatusCode> { + println!("login"); + let app_config = AppConfig::get(); + println!("app_config: {:?}", app_config); + let admin = app_config.get_admin_settings(); + if admin.username == form.username && admin.password == form.password { + let mut response = Redirect::to("/").into_response(); //redirect to home page + let now: DateTime = Utc::now(); + let thirty_minutes_later = now + Duration::minutes(30); + let expire_time = thirty_minutes_later.format("%a, %d %b %Y %T GMT"); + let session_id = Uuid::new_v4().to_string(); + let cookies = format!("sessionid={};Expires={}", session_id, expire_time); + + let session = Session::new( + session_id.clone(), + admin.username.clone(), + admin.password.clone(), + String::from("Admin"), + ); + + SessionManager::get() + .add_session(session_id.clone(), session) + .unwrap(); + response.headers_mut().insert( + "Set-Cookie", + HeaderValue::from_str(cookies.as_str()).unwrap(), + ); + return Ok(response); + } + return Err(StatusCode::UNAUTHORIZED); +} + #[cfg(test)] mod test { use super::*; diff --git a/src/session/mod.rs b/src/session/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..4de486cd226523d3aedf15dca6878812c45d71f3 --- /dev/null +++ b/src/session/mod.rs @@ -0,0 +1,2 @@ +pub mod session; +pub mod session_manage; diff --git a/src/session/session.rs b/src/session/session.rs new file mode 100644 index 0000000000000000000000000000000000000000..651070994781c0c9bb885563152cafbb64c5fc63 --- /dev/null +++ b/src/session/session.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use std::time::SystemTime; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + id: String, + username: String, + email: String, + role: String, + time: SystemTime, +} + +impl Session { + pub fn new(id: String, username: String, email: String, role: String) -> Self { + Session { + id, + username, + email, + role, + time: SystemTime::now(), + } + } + + pub fn is_expired(&self, duration: u64) -> bool { + self.time.elapsed().unwrap().as_secs() >= duration + } +} diff --git a/src/session/session_manage.rs b/src/session/session_manage.rs new file mode 100644 index 0000000000000000000000000000000000000000..16c0fdf90805ece4e3a973bef0693a83c00f8b48 --- /dev/null +++ b/src/session/session_manage.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; +use std::sync::RwLock; + +use tklog::error; + +use crate::session::session::Session; + +#[derive(Clone, Debug)] +pub struct SessionManager { + timeout: u64, + sessions: Arc>>, //hashmap线程不安全, Arc 允许多线程同时访问, 但需要使用RwLock来保证线程安全 +} + +const DEFAULT_TIMEOUT: u64 = 1800; //默认时间为1800秒 +const DEFAULT_SESSION_COUNT: usize = 1; + +static SESSION_MANAGER: OnceLock = OnceLock::new(); + +impl SessionManager { + pub fn init(timeout: Option) -> Result<(), Box> { + let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); + let manager = SessionManager { + timeout, + sessions: Arc::new(RwLock::new(HashMap::new())), + }; + + SESSION_MANAGER + .set(manager) + .map_err(|_| "Already initialized".into()) + } + + pub fn get() -> &'static SessionManager { + SESSION_MANAGER + .get() + .expect("SessionManager not initialized") + } + + pub fn add_session( + &self, + id: String, + session: Session, + ) -> Result<(), Box> { + let mut sessions = self.sessions.write().map_err(|e| { + Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to acquire write lock: {}", e), + )) + })?; + + sessions.insert(id, session); + Ok(()) + } + + pub fn remove_session(&self, id: &str) -> Result<(), Box> { + let mut sessions = self.sessions.write().map_err(|e| { + Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to acquire write lock: {}", e), + )) + })?; + + sessions.remove(id); + Ok(()) + } + + pub fn is_session_exist(&self, id: &str) -> bool { + self.sessions.read().map_or(false, |s| s.contains_key(id)) + } + + pub fn is_expired(&self, id: &str) -> bool { + self.sessions + .read() + .unwrap() + .get(id) + .map_or(false, |session| session.is_expired(self.timeout)) + } +} diff --git a/src/session/session_manage_bk.rs b/src/session/session_manage_bk.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7a3f9f953aefc2489f0a8ac7254180a2a6c9bd7 --- /dev/null +++ b/src/session/session_manage_bk.rs @@ -0,0 +1,48 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; +use std::sync::RwLock; + +use tklog::error; + +use crate::session::session::Session; + +#[derive(Clone, Debug)] +pub struct SessionManager { + timeout: u64, + sessions: Arc>>, //hashmap线程不安全, Arc 允许多线程同时访问, 但需要使用RwLock来保证线程安全 +} + +const DEFAULT_TIMEOUT: u64 = 1800; //默认时间为1800秒 +const DEFAULT_SESSION_COUNT: usize = 1; + +impl SessionManager { + pub fn init(timeout: Option) -> Result> { + let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); + let session_manager = SessionManager { + timeout, + sessions: Arc::new(RwLock::new(HashMap::with_capacity(DEFAULT_SESSION_COUNT))), //只需要管理员需要登陆 + }; + Ok(session_manager) + } + + pub fn add_session(&mut self, id: String, session: Session) { + self.sessions.write().unwrap().insert(id, session); + } + + pub fn remove_session(&mut self, id: &str) { + self.sessions.write().unwrap().remove(id); + } + + pub fn is_session_exist(&self, id: &str) -> bool { + self.sessions.read().unwrap().contains_key(id) + } + + pub fn is_expired(&self, id: &str) -> bool { + self.sessions + .read() + .unwrap() + .get(id) + .map_or(false, |session| session.is_expired(self.timeout)) + } +} diff --git a/templates/html/blog.html b/templates/html/blog.html index 4ce5227e26ba2b28a07e379ab4295353eb85adbd..be65f7cdd61e6591b3e89b856633451e9fe2c639 100644 --- a/templates/html/blog.html +++ b/templates/html/blog.html @@ -5,8 +5,15 @@ RustU - - + + @@ -85,19 +91,28 @@
-
+

- 粤公网安备44011802000978号 ICP备案: 粤ICP备2024315825号-1 © 2024 你我同锈. All rights reserved. + 粤公网安备44011802000978号 ICP备案: 粤ICP备2024315825号-1 + © 2024 你我同锈. All rights reserved.

- + + diff --git a/templates/html/blogs.html b/templates/html/blogs.html index 5fd7aa731e7cd216a134f95fbce75b0135821865..ba5df438eee402e83e86dabd4b02481f82a780fa 100644 --- a/templates/html/blogs.html +++ b/templates/html/blogs.html @@ -31,6 +31,17 @@ Click me! + + Upload Markdown! + +
+ + + + + + + + + + RustU + + +
+
+
+
Master,快登陆吧!
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +