style: format rust code
This commit is contained in:
parent
71ac08c373
commit
294d60afab
@ -4,11 +4,11 @@ use dioxus::prelude::*;
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use http::header::{HeaderValue, SET_COOKIE};
|
use http::header::{HeaderValue, SET_COOKIE};
|
||||||
|
|
||||||
use crate::auth::{password, session};
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use crate::auth::session::get_session_from_ctx;
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use crate::auth::session::get_session_from_ctx;
|
||||||
|
use crate::auth::{password, session};
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::models::user::{PublicUser, User, UserRole};
|
use crate::models::user::{PublicUser, User, UserRole};
|
||||||
|
|
||||||
@ -106,7 +106,8 @@ pub async fn register(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let password_hash = password::hash_password(&password).map_err(|_| AppError::Internal("密码处理失败"))?;
|
let password_hash =
|
||||||
|
password::hash_password(&password).map_err(|_| AppError::Internal("密码处理失败"))?;
|
||||||
|
|
||||||
let result = client
|
let result = client
|
||||||
.query_one(
|
.query_one(
|
||||||
@ -176,7 +177,8 @@ pub async fn login(username: String, password: String) -> Result<AuthResponse, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
let password_hash: String = row.get("password_hash");
|
let password_hash: String = row.get("password_hash");
|
||||||
let valid = password::verify_password(&password, &password_hash).map_err(|_| AppError::Internal("密码处理失败"))?;
|
let valid = password::verify_password(&password, &password_hash)
|
||||||
|
.map_err(|_| AppError::Internal("密码处理失败"))?;
|
||||||
|
|
||||||
if !valid {
|
if !valid {
|
||||||
return Ok(AuthResponse {
|
return Ok(AuthResponse {
|
||||||
|
|||||||
@ -10,8 +10,8 @@ pub struct PendingStatusItem {
|
|||||||
pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem>, ServerFnError> {
|
pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem>, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
use crate::db::pool::get_conn;
|
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
@ -35,7 +35,10 @@ pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem
|
|||||||
let result: Vec<PendingStatusItem> = ids
|
let result: Vec<PendingStatusItem> = ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| {
|
.map(|id| {
|
||||||
let status = found.get(&id).cloned().unwrap_or_else(|| "gone".to_string());
|
let status = found
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "gone".to_string());
|
||||||
PendingStatusItem { id, status }
|
PendingStatusItem { id, status }
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::api::comments::types::*;
|
use crate::api::comments::types::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[server(CreateComment, "/api")]
|
#[server(CreateComment, "/api")]
|
||||||
pub async fn create_comment(
|
pub async fn create_comment(
|
||||||
@ -12,13 +12,13 @@ pub async fn create_comment(
|
|||||||
) -> Result<CommentResponse, ServerFnError> {
|
) -> Result<CommentResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::comments::helpers::{
|
||||||
|
compute_content_hash, validate_comment_content, validate_comment_email,
|
||||||
|
validate_comment_name, validate_comment_url,
|
||||||
|
};
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::comments::helpers::{
|
|
||||||
validate_comment_name, validate_comment_email, validate_comment_url,
|
|
||||||
validate_comment_content, compute_content_hash,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
||||||
let parts = ctx.parts_mut();
|
let parts = ctx.parts_mut();
|
||||||
@ -177,12 +177,7 @@ pub async fn create_comment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_hash = compute_content_hash(
|
let content_hash = compute_content_hash(post_id, parent_id, &author_name, &content_md);
|
||||||
post_id,
|
|
||||||
parent_id,
|
|
||||||
&author_name,
|
|
||||||
&content_md,
|
|
||||||
);
|
|
||||||
|
|
||||||
let dup: Option<i64> = client
|
let dup: Option<i64> = client
|
||||||
.query_opt(
|
.query_opt(
|
||||||
@ -215,7 +210,8 @@ pub async fn create_comment(
|
|||||||
|
|
||||||
let user_agent = if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
let user_agent = if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
||||||
let parts = ctx.parts_mut();
|
let parts = ctx.parts_mut();
|
||||||
parts.headers
|
parts
|
||||||
|
.headers
|
||||||
.get("user-agent")
|
.get("user-agent")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@ -236,7 +232,10 @@ pub async fn create_comment(
|
|||||||
&depth,
|
&depth,
|
||||||
&author_name.trim(),
|
&author_name.trim(),
|
||||||
&author_email.trim(),
|
&author_email.trim(),
|
||||||
&author_url.as_ref().map(|u| u.trim()).filter(|u| !u.is_empty()),
|
&author_url
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| u.trim())
|
||||||
|
.filter(|u| !u.is_empty()),
|
||||||
&content_md,
|
&content_md,
|
||||||
&content_html,
|
&content_html,
|
||||||
&content_hash,
|
&content_hash,
|
||||||
|
|||||||
@ -89,8 +89,7 @@ pub fn validate_comment_name(name: &str) -> Result<(), String> {
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn validate_comment_email(email: &str) -> Result<(), String> {
|
pub fn validate_comment_email(email: &str) -> Result<(), String> {
|
||||||
let re =
|
let re = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
|
||||||
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
|
|
||||||
if !re.is_match(email.trim()) {
|
if !re.is_match(email.trim()) {
|
||||||
return Err("邮箱格式不正确".to_string());
|
return Err("邮箱格式不正确".to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::api::comments::types::*;
|
use crate::api::comments::types::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[server(GetPendingComments, "/api")]
|
#[server(GetPendingComments, "/api")]
|
||||||
pub async fn get_pending_comments(
|
pub async fn get_pending_comments(page: i32) -> Result<PendingCommentsResponse, ServerFnError> {
|
||||||
page: i32,
|
|
||||||
) -> Result<PendingCommentsResponse, ServerFnError> {
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
use crate::db::pool::get_conn;
|
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::comments::helpers::row_to_admin_comment;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::comments::helpers::row_to_admin_comment;
|
||||||
|
use crate::api::error::AppError;
|
||||||
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
@ -54,10 +52,10 @@ pub async fn get_pending_comments(
|
|||||||
pub async fn get_pending_count() -> Result<PendingCountResponse, ServerFnError> {
|
pub async fn get_pending_count() -> Result<PendingCountResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
@ -91,10 +89,10 @@ pub async fn get_all_comments(
|
|||||||
) -> Result<AllCommentsResponse, ServerFnError> {
|
) -> Result<AllCommentsResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
use crate::db::pool::get_conn;
|
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::comments::helpers::row_to_admin_comment;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::comments::helpers::row_to_admin_comment;
|
||||||
|
use crate::api::error::AppError;
|
||||||
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,7 @@ pub fn render_comment_markdown(md: &str) -> String {
|
|||||||
Event::Start(Tag::CodeBlock(kind)) => {
|
Event::Start(Tag::CodeBlock(kind)) => {
|
||||||
in_codeblock = true;
|
in_codeblock = true;
|
||||||
code_lang = match kind {
|
code_lang = match kind {
|
||||||
CodeBlockKind::Fenced(lang) if !lang.is_empty() => {
|
CodeBlockKind::Fenced(lang) if !lang.is_empty() => Some(lang.to_string()),
|
||||||
Some(lang.to_string())
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
code_buffer.clear();
|
code_buffer.clear();
|
||||||
@ -83,10 +81,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn render_comment_heading_all_levels() {
|
fn render_comment_heading_all_levels() {
|
||||||
for md in &[
|
for md in &[
|
||||||
"# H1", "## H2", "### H3", "#### H4", "##### H5", "###### H6",
|
"# H1",
|
||||||
|
"## H2",
|
||||||
|
"### H3",
|
||||||
|
"#### H4",
|
||||||
|
"##### H5",
|
||||||
|
"###### H6",
|
||||||
] {
|
] {
|
||||||
let result = render_comment_markdown(md);
|
let result = render_comment_markdown(md);
|
||||||
assert!(result.contains("<strong>"), "heading not converted for: {}", md);
|
assert!(
|
||||||
|
result.contains("<strong>"),
|
||||||
|
"heading not converted for: {}",
|
||||||
|
md
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +169,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clean_comment_html_removes_details_summary() {
|
fn clean_comment_html_removes_details_summary() {
|
||||||
let result = clean_comment_html("<details><summary>Click</summary><p>Content</p></details>");
|
let result =
|
||||||
|
clean_comment_html("<details><summary>Click</summary><p>Content</p></details>");
|
||||||
assert!(!result.contains("details"));
|
assert!(!result.contains("details"));
|
||||||
assert!(!result.contains("summary"));
|
assert!(!result.contains("summary"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,25 @@
|
|||||||
#![allow(clippy::unused_unit, deprecated, unused_imports, clippy::too_many_arguments)]
|
#![allow(
|
||||||
|
clippy::unused_unit,
|
||||||
|
deprecated,
|
||||||
|
unused_imports,
|
||||||
|
clippy::too_many_arguments
|
||||||
|
)]
|
||||||
|
|
||||||
mod types;
|
|
||||||
mod helpers;
|
|
||||||
mod markdown;
|
|
||||||
mod create;
|
|
||||||
mod read;
|
|
||||||
mod update;
|
|
||||||
mod list;
|
|
||||||
mod check;
|
mod check;
|
||||||
|
mod create;
|
||||||
|
mod helpers;
|
||||||
|
mod list;
|
||||||
|
mod markdown;
|
||||||
|
mod read;
|
||||||
|
mod types;
|
||||||
|
mod update;
|
||||||
|
|
||||||
pub use types::*;
|
|
||||||
pub use create::create_comment;
|
|
||||||
pub use read::{get_comments, get_comment_count};
|
|
||||||
pub use update::{approve_comment, spam_comment, trash_comment, batch_update_comment_status};
|
|
||||||
pub use list::{get_pending_comments, get_pending_count, get_all_comments};
|
|
||||||
pub use check::check_pending_status;
|
pub use check::check_pending_status;
|
||||||
|
pub use create::create_comment;
|
||||||
|
pub use list::{get_all_comments, get_pending_comments, get_pending_count};
|
||||||
|
pub use read::{get_comment_count, get_comments};
|
||||||
|
pub use types::*;
|
||||||
|
pub use update::{approve_comment, batch_update_comment_status, spam_comment, trash_comment};
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub use markdown::render_comment_markdown;
|
pub use markdown::render_comment_markdown;
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::api::comments::types::*;
|
use crate::api::comments::types::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[server(GetComments, "/api")]
|
#[server(GetComments, "/api")]
|
||||||
pub async fn get_comments(
|
pub async fn get_comments(post_id: i32) -> Result<CommentTreeResponse, ServerFnError> {
|
||||||
post_id: i32,
|
|
||||||
) -> Result<CommentTreeResponse, ServerFnError> {
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
use crate::cache;
|
|
||||||
use crate::db::pool::get_conn;
|
|
||||||
use crate::api::comments::helpers::row_to_public_comment;
|
use crate::api::comments::helpers::row_to_public_comment;
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
use crate::cache;
|
||||||
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
if let Some(cached) = cache::get_comments_by_post(post_id).await {
|
if let Some(cached) = cache::get_comments_by_post(post_id).await {
|
||||||
let count = cached.len() as i64;
|
let count = cached.len() as i64;
|
||||||
@ -45,14 +43,12 @@ pub async fn get_comments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[server(GetCommentCount, "/api")]
|
#[server(GetCommentCount, "/api")]
|
||||||
pub async fn get_comment_count(
|
pub async fn get_comment_count(post_id: i32) -> Result<CommentCountResponse, ServerFnError> {
|
||||||
post_id: i32,
|
|
||||||
) -> Result<CommentCountResponse, ServerFnError> {
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
|
|
||||||
if let Some(cached) = cache::get_comment_count(post_id).await {
|
if let Some(cached) = cache::get_comment_count(post_id).await {
|
||||||
return Ok(CommentCountResponse { count: cached });
|
return Ok(CommentCountResponse { count: cached });
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::models::comment::{AdminComment, PublicComment};
|
use crate::models::comment::{AdminComment, PublicComment};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::api::comments::types::*;
|
use crate::api::comments::types::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[server(ApproveComment, "/api")]
|
#[server(ApproveComment, "/api")]
|
||||||
pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
@ -79,10 +79,10 @@ pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError>
|
|||||||
pub async fn spam_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
pub async fn spam_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
@ -132,10 +132,10 @@ pub async fn spam_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
|||||||
pub async fn trash_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
pub async fn trash_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
@ -185,10 +185,10 @@ pub async fn batch_update_comment_status(
|
|||||||
) -> Result<BatchStatusResponse, ServerFnError> {
|
) -> Result<BatchStatusResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
use crate::api::error::AppError;
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
use crate::api::error::AppError;
|
|
||||||
use crate::api::auth::get_current_admin_user;
|
|
||||||
|
|
||||||
let _admin = get_current_admin_user().await?;
|
let _admin = get_current_admin_user().await?;
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,10 @@ mod tests {
|
|||||||
!msg.contains("5432"),
|
!msg.contains("5432"),
|
||||||
"should not leak internal details: {msg}"
|
"should not leak internal details: {msg}"
|
||||||
);
|
);
|
||||||
assert!(msg.contains("服务暂时不可用"), "expected generic message: {msg}");
|
assert!(
|
||||||
|
msg.contains("服务暂时不可用"),
|
||||||
|
"expected generic message: {msg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
118
src/api/image.rs
118
src/api/image.rs
@ -266,7 +266,10 @@ async fn write_disk_cache(cache_key: &str, cached: &CachedImage) {
|
|||||||
tracing::warn!("Failed to create cache dir: {:?}", e);
|
tracing::warn!("Failed to create cache dir: {:?}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ct_str = cached.content_type.to_str().unwrap_or("application/octet-stream");
|
let ct_str = cached
|
||||||
|
.content_type
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or("application/octet-stream");
|
||||||
if let Err(e) = tokio::fs::write(format!("{}.dat", base), &cached.data).await {
|
if let Err(e) = tokio::fs::write(format!("{}.dat", base), &cached.data).await {
|
||||||
tracing::warn!("Failed to write disk cache data: {:?}", e);
|
tracing::warn!("Failed to write disk cache data: {:?}", e);
|
||||||
}
|
}
|
||||||
@ -319,10 +322,15 @@ pub async fn serve_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cached) = read_disk_cache(&cache_key).await {
|
if let Some(cached) = read_disk_cache(&cache_key).await {
|
||||||
let _ = IMAGE_CACHE.insert(cache_key.clone(), CachedImage {
|
let _ = IMAGE_CACHE
|
||||||
data: cached.data.clone(),
|
.insert(
|
||||||
content_type: cached.content_type.clone(),
|
cache_key.clone(),
|
||||||
}).await;
|
CachedImage {
|
||||||
|
data: cached.data.clone(),
|
||||||
|
content_type: cached.content_type.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
return (
|
return (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[(header::CONTENT_TYPE, cached.content_type)],
|
[(header::CONTENT_TYPE, cached.content_type)],
|
||||||
@ -388,77 +396,113 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_valid_width() {
|
fn image_params_validate_valid_width() {
|
||||||
let params = ImageParams { w: Some(100), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
w: Some(100),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_ok());
|
assert!(params.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_zero_width_rejected() {
|
fn image_params_validate_zero_width_rejected() {
|
||||||
let params = ImageParams { w: Some(0), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
w: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_oversized_width_rejected() {
|
fn image_params_validate_oversized_width_rejected() {
|
||||||
let params = ImageParams { w: Some(5000), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
w: Some(5000),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_valid_rotation() {
|
fn image_params_validate_valid_rotation() {
|
||||||
for angle in [0, 90, 180, 270] {
|
for angle in [0, 90, 180, 270] {
|
||||||
let params = ImageParams { rotate: Some(angle), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
rotate: Some(angle),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_ok(), "angle {} should be valid", angle);
|
assert!(params.validate().is_ok(), "angle {} should be valid", angle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_invalid_rotation_rejected() {
|
fn image_params_validate_invalid_rotation_rejected() {
|
||||||
let params = ImageParams { rotate: Some(45), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
rotate: Some(45),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_valid_format() {
|
fn image_params_validate_valid_format() {
|
||||||
for fmt in &["jpeg", "jpg", "png", "webp", "JPEG", "PNG"] {
|
for fmt in &["jpeg", "jpg", "png", "webp", "JPEG", "PNG"] {
|
||||||
let params = ImageParams { format: Some(fmt.to_string()), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
format: Some(fmt.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_ok(), "format {} should be valid", fmt);
|
assert!(params.validate().is_ok(), "format {} should be valid", fmt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_invalid_format_rejected() {
|
fn image_params_validate_invalid_format_rejected() {
|
||||||
let params = ImageParams { format: Some("gif".to_string()), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
format: Some("gif".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_valid_thumbnail() {
|
fn image_params_validate_valid_thumbnail() {
|
||||||
let params = ImageParams { thumb: Some("200x150".to_string()), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
thumb: Some("200x150".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_ok());
|
assert!(params.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_invalid_thumbnail_rejected() {
|
fn image_params_validate_invalid_thumbnail_rejected() {
|
||||||
let params = ImageParams { thumb: Some("200".to_string()), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
thumb: Some("200".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_valid_quality() {
|
fn image_params_validate_valid_quality() {
|
||||||
let params = ImageParams { quality: Some(85), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
quality: Some(85),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_ok());
|
assert!(params.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_zero_quality_rejected() {
|
fn image_params_validate_zero_quality_rejected() {
|
||||||
let params = ImageParams { quality: Some(0), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
quality: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_params_validate_over_100_quality_rejected() {
|
fn image_params_validate_over_100_quality_rejected() {
|
||||||
let params = ImageParams { quality: Some(101), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
quality: Some(101),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(params.validate().is_err());
|
assert!(params.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,9 +530,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detect_format_jpeg() {
|
fn detect_format_jpeg() {
|
||||||
assert!(matches!(detect_format("photo.jpg"), image::ImageFormat::Jpeg));
|
assert!(matches!(
|
||||||
assert!(matches!(detect_format("photo.jpeg"), image::ImageFormat::Jpeg));
|
detect_format("photo.jpg"),
|
||||||
assert!(matches!(detect_format("PHOTO.JPG"), image::ImageFormat::Jpeg));
|
image::ImageFormat::Jpeg
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
detect_format("photo.jpeg"),
|
||||||
|
image::ImageFormat::Jpeg
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
detect_format("PHOTO.JPG"),
|
||||||
|
image::ImageFormat::Jpeg
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -498,18 +551,30 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detect_format_webp() {
|
fn detect_format_webp() {
|
||||||
assert!(matches!(detect_format("anim.webp"), image::ImageFormat::WebP));
|
assert!(matches!(
|
||||||
|
detect_format("anim.webp"),
|
||||||
|
image::ImageFormat::WebP
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detect_format_defaults_to_jpeg() {
|
fn detect_format_defaults_to_jpeg() {
|
||||||
assert!(matches!(detect_format("file.xyz"), image::ImageFormat::Jpeg));
|
assert!(matches!(
|
||||||
|
detect_format("file.xyz"),
|
||||||
|
image::ImageFormat::Jpeg
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cache_key_differs_for_different_params() {
|
fn cache_key_differs_for_different_params() {
|
||||||
let p1 = ImageParams { w: Some(100), ..Default::default() };
|
let p1 = ImageParams {
|
||||||
let p2 = ImageParams { w: Some(200), ..Default::default() };
|
w: Some(100),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let p2 = ImageParams {
|
||||||
|
w: Some(200),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_ne!(p1.cache_key("img.jpg"), p2.cache_key("img.jpg"));
|
assert_ne!(p1.cache_key("img.jpg"), p2.cache_key("img.jpg"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,7 +586,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_empty_false_when_any_set() {
|
fn is_empty_false_when_any_set() {
|
||||||
let params = ImageParams { w: Some(100), ..Default::default() };
|
let params = ImageParams {
|
||||||
|
w: Some(100),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(!params.is_empty());
|
assert!(!params.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -204,9 +204,7 @@ fn generate_toc_html(headings: &[(u8, String, String)]) -> String {
|
|||||||
let clean_text = clean_html(text);
|
let clean_text = clean_html(text);
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<li><a href=\"#{}\" aria-label=\"{}\">{}</a>",
|
"<li><a href=\"#{}\" aria-label=\"{}\">{}</a>",
|
||||||
id,
|
id, clean_text, clean_text
|
||||||
clean_text,
|
|
||||||
clean_text
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,7 +365,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn render_markdown_data_uri_image() {
|
fn render_markdown_data_uri_image() {
|
||||||
let result = render_markdown_enhanced("");
|
let result = render_markdown_enhanced("");
|
||||||
assert!(result.html.contains("data:image/svg+xml"), "data URI should be preserved in img src, got: {}", result.html);
|
assert!(
|
||||||
|
result.html.contains("data:image/svg+xml"),
|
||||||
|
"data URI should be preserved in img src, got: {}",
|
||||||
|
result.html
|
||||||
|
);
|
||||||
assert!(result.html.contains("alt=\"alt\""));
|
assert!(result.html.contains("alt=\"alt\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,10 @@ pub async fn list_published_posts(
|
|||||||
{
|
{
|
||||||
let cache_key = crate::cache::CacheKey::PublishedPosts { page, per_page };
|
let cache_key = crate::cache::CacheKey::PublishedPosts { page, per_page };
|
||||||
if let Some((cached_posts, cached_total)) = crate::cache::get_post_list(&cache_key).await {
|
if let Some((cached_posts, cached_total)) = crate::cache::get_post_list(&cache_key).await {
|
||||||
return Ok(PostListResponse { posts: cached_posts, total: cached_total });
|
return Ok(PostListResponse {
|
||||||
|
posts: cached_posts,
|
||||||
|
total: cached_total,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||||
@ -68,15 +71,15 @@ pub async fn list_published_posts(
|
|||||||
|
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
{
|
{
|
||||||
Ok(PostListResponse { posts: Vec::new(), total: 0 })
|
Ok(PostListResponse {
|
||||||
|
posts: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(ListPosts, "/api")]
|
#[server(ListPosts, "/api")]
|
||||||
pub async fn list_posts(
|
pub async fn list_posts(page: i32, per_page: i32) -> Result<PostListResponse, ServerFnError> {
|
||||||
page: i32,
|
|
||||||
per_page: i32,
|
|
||||||
) -> Result<PostListResponse, ServerFnError> {
|
|
||||||
let _user = get_current_admin_user().await?;
|
let _user = get_current_admin_user().await?;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
@ -84,10 +87,7 @@ pub async fn list_posts(
|
|||||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||||
|
|
||||||
let count_row = client
|
let count_row = client
|
||||||
.query_one(
|
.query_one("SELECT COUNT(*) FROM posts WHERE deleted_at IS NULL", &[])
|
||||||
"SELECT COUNT(*) FROM posts WHERE deleted_at IS NULL",
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::query)?;
|
.map_err(AppError::query)?;
|
||||||
let total: i64 = count_row.get(0);
|
let total: i64 = count_row.get(0);
|
||||||
@ -122,7 +122,10 @@ pub async fn list_posts(
|
|||||||
|
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
{
|
{
|
||||||
Ok(PostListResponse { posts: Vec::new(), total: 0 })
|
Ok(PostListResponse {
|
||||||
|
posts: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,8 +133,12 @@ pub async fn list_posts(
|
|||||||
pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, ServerFnError> {
|
pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, ServerFnError> {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
if let Some((cached_posts, cached_total)) = crate::cache::get_posts_by_tag(&tag_name).await {
|
if let Some((cached_posts, cached_total)) = crate::cache::get_posts_by_tag(&tag_name).await
|
||||||
return Ok(PostListResponse { posts: cached_posts, total: cached_total });
|
{
|
||||||
|
return Ok(PostListResponse {
|
||||||
|
posts: cached_posts,
|
||||||
|
total: cached_total,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||||
@ -170,6 +177,9 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, Serv
|
|||||||
|
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
{
|
{
|
||||||
Ok(PostListResponse { posts: Vec::new(), total: 0 })
|
Ok(PostListResponse {
|
||||||
|
posts: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,32 @@
|
|||||||
#![allow(clippy::unused_unit, deprecated, unused_imports, clippy::too_many_arguments)]
|
#![allow(
|
||||||
|
clippy::unused_unit,
|
||||||
|
deprecated,
|
||||||
|
unused_imports,
|
||||||
|
clippy::too_many_arguments
|
||||||
|
)]
|
||||||
|
|
||||||
mod types;
|
|
||||||
mod helpers;
|
|
||||||
mod create;
|
mod create;
|
||||||
mod update;
|
|
||||||
mod delete;
|
mod delete;
|
||||||
mod read;
|
mod helpers;
|
||||||
mod list;
|
mod list;
|
||||||
mod search;
|
mod read;
|
||||||
mod tags;
|
|
||||||
mod stats;
|
|
||||||
mod rebuild;
|
mod rebuild;
|
||||||
|
mod search;
|
||||||
|
mod stats;
|
||||||
|
mod tags;
|
||||||
|
mod types;
|
||||||
|
mod update;
|
||||||
|
|
||||||
pub use types::*;
|
|
||||||
pub use create::create_post;
|
pub use create::create_post;
|
||||||
pub use update::update_post;
|
|
||||||
pub use delete::delete_post;
|
pub use delete::delete_post;
|
||||||
|
pub use list::{get_posts_by_tag, list_posts, list_published_posts};
|
||||||
pub use read::{get_post_by_id, get_post_by_slug};
|
pub use read::{get_post_by_id, get_post_by_slug};
|
||||||
pub use list::{list_published_posts, list_posts, get_posts_by_tag};
|
|
||||||
pub use search::search_posts;
|
|
||||||
pub use tags::list_tags;
|
|
||||||
pub use stats::get_post_stats;
|
|
||||||
pub use rebuild::rebuild_content_html;
|
pub use rebuild::rebuild_content_html;
|
||||||
|
pub use search::search_posts;
|
||||||
|
pub use stats::get_post_stats;
|
||||||
|
pub use tags::list_tags;
|
||||||
|
pub use types::*;
|
||||||
|
pub use update::update_post;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub use crate::api::markdown::render_markdown_enhanced;
|
pub use crate::api::markdown::render_markdown_enhanced;
|
||||||
|
|||||||
@ -15,7 +15,10 @@ pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnErr
|
|||||||
|
|
||||||
let q = query.trim();
|
let q = query.trim();
|
||||||
if q.is_empty() {
|
if q.is_empty() {
|
||||||
return Ok(PostListResponse { posts: Vec::new(), total: 0 });
|
return Ok(PostListResponse {
|
||||||
|
posts: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = client
|
let rows = client
|
||||||
@ -49,6 +52,9 @@ pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnErr
|
|||||||
|
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
{
|
{
|
||||||
Ok(PostListResponse { posts: Vec::new(), total: 0 })
|
Ok(PostListResponse {
|
||||||
|
posts: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use super::types::TagListResponse;
|
use super::types::TagListResponse;
|
||||||
use crate::db::pool::get_conn;
|
|
||||||
use crate::models::post::Tag;
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
use crate::db::pool::get_conn;
|
||||||
|
use crate::models::post::Tag;
|
||||||
|
|
||||||
#[server(ListTags, "/api")]
|
#[server(ListTags, "/api")]
|
||||||
pub async fn list_tags() -> Result<TagListResponse, ServerFnError> {
|
pub async fn list_tags() -> Result<TagListResponse, ServerFnError> {
|
||||||
|
|||||||
@ -65,7 +65,8 @@ pub async fn update_post(
|
|||||||
_ => crate::api::slug::slugify(&title),
|
_ => crate::api::slug::slugify(&title),
|
||||||
};
|
};
|
||||||
|
|
||||||
let final_slug = crate::api::slug::ensure_unique_slug(&client, &base_slug, Some(post_id)).await?;
|
let final_slug =
|
||||||
|
crate::api::slug::ensure_unique_slug(&client, &base_slug, Some(post_id)).await?;
|
||||||
let rendered = crate::api::markdown::render_markdown_enhanced(&content_md);
|
let rendered = crate::api::markdown::render_markdown_enhanced(&content_md);
|
||||||
let content_html = rendered.html;
|
let content_html = rendered.html;
|
||||||
let toc_html = if rendered.toc_html.is_empty() {
|
let toc_html = if rendered.toc_html.is_empty() {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use std::sync::LazyLock;
|
use axum::http::StatusCode;
|
||||||
#[cfg(feature = "server")]
|
|
||||||
use std::num::NonZeroU32;
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use axum::http::StatusCode;
|
use std::num::NonZeroU32;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
fn env_or(key: &str, default: u32) -> NonZeroU32 {
|
fn env_or(key: &str, default: u32) -> NonZeroU32 {
|
||||||
@ -90,10 +90,7 @@ fn ip_from_x_forwarded_for(value: &str, trusted_proxy_count: usize) -> Option<St
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn get_client_ip_with_trusted(
|
pub fn get_client_ip_with_trusted(headers: &http::HeaderMap, trusted_proxy_count: usize) -> String {
|
||||||
headers: &http::HeaderMap,
|
|
||||||
trusted_proxy_count: usize,
|
|
||||||
) -> String {
|
|
||||||
if let Some(value) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
|
if let Some(value) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
|
||||||
if let Some(ip) = ip_from_x_forwarded_for(value, trusted_proxy_count) {
|
if let Some(ip) = ip_from_x_forwarded_for(value, trusted_proxy_count) {
|
||||||
return ip;
|
return ip;
|
||||||
@ -204,7 +201,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn get_client_ip_ignores_empty_x_forwarded_for_entries() {
|
fn get_client_ip_ignores_empty_x_forwarded_for_entries() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert("x-forwarded-for", " , 1.2.3.4 , 5.6.7.8 , ".parse().unwrap());
|
headers.insert(
|
||||||
|
"x-forwarded-for",
|
||||||
|
" , 1.2.3.4 , 5.6.7.8 , ".parse().unwrap(),
|
||||||
|
);
|
||||||
assert_eq!(get_client_ip_with_trusted(&headers, 1), "1.2.3.4");
|
assert_eq!(get_client_ip_with_trusted(&headers, 1), "1.2.3.4");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,15 +7,81 @@ use std::collections::HashSet;
|
|||||||
fn default_allowed_tags() -> HashSet<&'static str> {
|
fn default_allowed_tags() -> HashSet<&'static str> {
|
||||||
let mut set = HashSet::new();
|
let mut set = HashSet::new();
|
||||||
for tag in [
|
for tag in [
|
||||||
"a", "abbr", "acronym", "area", "article", "aside", "b", "bdi",
|
"a",
|
||||||
"bdo", "blockquote", "br", "caption", "center", "cite", "code",
|
"abbr",
|
||||||
"col", "colgroup", "data", "dd", "del", "details", "dfn", "div",
|
"acronym",
|
||||||
"dl", "dt", "em", "figcaption", "figure", "footer", "h1", "h2",
|
"area",
|
||||||
"h3", "h4", "h5", "h6", "header", "hgroup", "hr", "i", "img",
|
"article",
|
||||||
"ins", "kbd", "li", "map", "mark", "nav", "ol", "p", "pre",
|
"aside",
|
||||||
"q", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "span",
|
"b",
|
||||||
"strike", "strong", "sub", "summary", "sup", "table", "tbody",
|
"bdi",
|
||||||
"td", "th", "thead", "time", "tr", "tt", "u", "ul", "var", "wbr",
|
"bdo",
|
||||||
|
"blockquote",
|
||||||
|
"br",
|
||||||
|
"caption",
|
||||||
|
"center",
|
||||||
|
"cite",
|
||||||
|
"code",
|
||||||
|
"col",
|
||||||
|
"colgroup",
|
||||||
|
"data",
|
||||||
|
"dd",
|
||||||
|
"del",
|
||||||
|
"details",
|
||||||
|
"dfn",
|
||||||
|
"div",
|
||||||
|
"dl",
|
||||||
|
"dt",
|
||||||
|
"em",
|
||||||
|
"figcaption",
|
||||||
|
"figure",
|
||||||
|
"footer",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"header",
|
||||||
|
"hgroup",
|
||||||
|
"hr",
|
||||||
|
"i",
|
||||||
|
"img",
|
||||||
|
"ins",
|
||||||
|
"kbd",
|
||||||
|
"li",
|
||||||
|
"map",
|
||||||
|
"mark",
|
||||||
|
"nav",
|
||||||
|
"ol",
|
||||||
|
"p",
|
||||||
|
"pre",
|
||||||
|
"q",
|
||||||
|
"rp",
|
||||||
|
"rt",
|
||||||
|
"rtc",
|
||||||
|
"ruby",
|
||||||
|
"s",
|
||||||
|
"samp",
|
||||||
|
"small",
|
||||||
|
"span",
|
||||||
|
"strike",
|
||||||
|
"strong",
|
||||||
|
"sub",
|
||||||
|
"summary",
|
||||||
|
"sup",
|
||||||
|
"table",
|
||||||
|
"tbody",
|
||||||
|
"td",
|
||||||
|
"th",
|
||||||
|
"thead",
|
||||||
|
"time",
|
||||||
|
"tr",
|
||||||
|
"tt",
|
||||||
|
"u",
|
||||||
|
"ul",
|
||||||
|
"var",
|
||||||
|
"wbr",
|
||||||
] {
|
] {
|
||||||
set.insert(tag);
|
set.insert(tag);
|
||||||
}
|
}
|
||||||
@ -34,10 +100,31 @@ fn clean_content_tags() -> HashSet<&'static str> {
|
|||||||
fn default_allowed_schemes() -> HashSet<&'static str> {
|
fn default_allowed_schemes() -> HashSet<&'static str> {
|
||||||
let mut set = HashSet::new();
|
let mut set = HashSet::new();
|
||||||
for scheme in [
|
for scheme in [
|
||||||
"bitcoin", "ftp", "ftps", "geo", "http", "https", "im", "irc",
|
"bitcoin",
|
||||||
"ircs", "magnet", "mailto", "mms", "mx", "news", "nntp",
|
"ftp",
|
||||||
"openpgp4fpr", "sip", "sms", "smsto", "ssh", "tel", "url",
|
"ftps",
|
||||||
"webcal", "wtai", "xmpp",
|
"geo",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"im",
|
||||||
|
"irc",
|
||||||
|
"ircs",
|
||||||
|
"magnet",
|
||||||
|
"mailto",
|
||||||
|
"mms",
|
||||||
|
"mx",
|
||||||
|
"news",
|
||||||
|
"nntp",
|
||||||
|
"openpgp4fpr",
|
||||||
|
"sip",
|
||||||
|
"sms",
|
||||||
|
"smsto",
|
||||||
|
"ssh",
|
||||||
|
"tel",
|
||||||
|
"url",
|
||||||
|
"webcal",
|
||||||
|
"wtai",
|
||||||
|
"xmpp",
|
||||||
] {
|
] {
|
||||||
set.insert(scheme);
|
set.insert(scheme);
|
||||||
}
|
}
|
||||||
@ -109,9 +196,17 @@ fn sanitize(input: &str, config: &SanitizerConfig) -> String {
|
|||||||
("q", vec!["cite"]),
|
("q", vec!["cite"]),
|
||||||
("table", vec!["align", "char", "charoff", "summary"]),
|
("table", vec!["align", "char", "charoff", "summary"]),
|
||||||
("tbody", vec!["align", "char", "charoff"]),
|
("tbody", vec!["align", "char", "charoff"]),
|
||||||
("td", vec!["align", "char", "charoff", "colspan", "headers", "rowspan"]),
|
(
|
||||||
|
"td",
|
||||||
|
vec!["align", "char", "charoff", "colspan", "headers", "rowspan"],
|
||||||
|
),
|
||||||
("tfoot", vec!["align", "char", "charoff"]),
|
("tfoot", vec!["align", "char", "charoff"]),
|
||||||
("th", vec!["align", "char", "charoff", "colspan", "headers", "rowspan", "scope"]),
|
(
|
||||||
|
"th",
|
||||||
|
vec![
|
||||||
|
"align", "char", "charoff", "colspan", "headers", "rowspan", "scope",
|
||||||
|
],
|
||||||
|
),
|
||||||
("thead", vec!["align", "char", "charoff"]),
|
("thead", vec!["align", "char", "charoff"]),
|
||||||
("tr", vec!["align", "char", "charoff"]),
|
("tr", vec!["align", "char", "charoff"]),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -261,7 +261,11 @@ mod tests {
|
|||||||
let uuid = "abc-123";
|
let uuid = "abc-123";
|
||||||
let ext = "jpg";
|
let ext = "jpg";
|
||||||
let file_name = format!("{}.{}.{}", now_str, uuid, ext);
|
let file_name = format!("{}.{}.{}", now_str, uuid, ext);
|
||||||
assert!(!file_name.contains(' '), "filename should not contain spaces: got '{}'", file_name);
|
assert!(
|
||||||
|
!file_name.contains(' '),
|
||||||
|
"filename should not contain spaces: got '{}'",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -297,5 +301,3 @@ mod tests {
|
|||||||
assert!(loaded.is_ok());
|
assert!(loaded.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -36,8 +36,7 @@ fn main() {
|
|||||||
output.push_str(&mocha_rewritten);
|
output.push_str(&mocha_rewritten);
|
||||||
|
|
||||||
std::fs::create_dir_all("public").expect("Failed to create public/");
|
std::fs::create_dir_all("public").expect("Failed to create public/");
|
||||||
std::fs::write("public/highlight.css", output)
|
std::fs::write("public/highlight.css", output).expect("Failed to write public/highlight.css");
|
||||||
.expect("Failed to write public/highlight.css");
|
|
||||||
|
|
||||||
println!("Generated public/highlight.css");
|
println!("Generated public/highlight.css");
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/cache.rs
65
src/cache.rs
@ -49,8 +49,6 @@ pub enum CacheKey {
|
|||||||
PendingCommentCount,
|
PendingCommentCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Cache Instances
|
// Cache Instances
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -153,12 +151,17 @@ pub async fn set_post_list(key: &CacheKey, posts: Vec<Post>, total: i64) {
|
|||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub async fn get_total_published_posts() -> Option<i64> {
|
pub async fn get_total_published_posts() -> Option<i64> {
|
||||||
POST_LIST_CACHE.get(&CacheKey::TotalPublishedPosts).await.map(|(_, total)| total)
|
POST_LIST_CACHE
|
||||||
|
.get(&CacheKey::TotalPublishedPosts)
|
||||||
|
.await
|
||||||
|
.map(|(_, total)| total)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub async fn set_total_published_posts(total: i64) {
|
pub async fn set_total_published_posts(total: i64) {
|
||||||
let _ = POST_LIST_CACHE.insert(CacheKey::TotalPublishedPosts, (vec![], total)).await;
|
let _ = POST_LIST_CACHE
|
||||||
|
.insert(CacheKey::TotalPublishedPosts, (vec![], total))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
@ -330,21 +333,33 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cache_key_equality() {
|
fn cache_key_equality() {
|
||||||
let k1 = CacheKey::PublishedPosts { page: 1, per_page: 10 };
|
let k1 = CacheKey::PublishedPosts {
|
||||||
let k2 = CacheKey::PublishedPosts { page: 1, per_page: 10 };
|
page: 1,
|
||||||
let k3 = CacheKey::PublishedPosts { page: 2, per_page: 10 };
|
per_page: 10,
|
||||||
|
};
|
||||||
|
let k2 = CacheKey::PublishedPosts {
|
||||||
|
page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
};
|
||||||
|
let k3 = CacheKey::PublishedPosts {
|
||||||
|
page: 2,
|
||||||
|
per_page: 10,
|
||||||
|
};
|
||||||
assert_eq!(k1, k2);
|
assert_eq!(k1, k2);
|
||||||
assert_ne!(k1, k3);
|
assert_ne!(k1, k3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn post_list_cache_roundtrip() {
|
async fn post_list_cache_roundtrip() {
|
||||||
let key = CacheKey::PublishedPosts { page: 999, per_page: 99 };
|
let key = CacheKey::PublishedPosts {
|
||||||
|
page: 999,
|
||||||
|
per_page: 99,
|
||||||
|
};
|
||||||
let posts = vec![];
|
let posts = vec![];
|
||||||
|
|
||||||
set_post_list(&key, posts.clone(), 0).await;
|
set_post_list(&key, posts.clone(), 0).await;
|
||||||
let cached = get_post_list(&key).await;
|
let cached = get_post_list(&key).await;
|
||||||
|
|
||||||
assert!(cached.is_some());
|
assert!(cached.is_some());
|
||||||
let (cached_posts, cached_total) = cached.unwrap();
|
let (cached_posts, cached_total) = cached.unwrap();
|
||||||
assert_eq!(cached_posts.len(), 0);
|
assert_eq!(cached_posts.len(), 0);
|
||||||
@ -353,11 +368,15 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn tag_list_cache_roundtrip() {
|
async fn tag_list_cache_roundtrip() {
|
||||||
let tags = vec![Tag { id: 1, name: "rust".to_string(), post_count: 5 }];
|
let tags = vec![Tag {
|
||||||
|
id: 1,
|
||||||
|
name: "rust".to_string(),
|
||||||
|
post_count: 5,
|
||||||
|
}];
|
||||||
|
|
||||||
set_tag_list(tags.clone()).await;
|
set_tag_list(tags.clone()).await;
|
||||||
let cached = get_tag_list().await;
|
let cached = get_tag_list().await;
|
||||||
|
|
||||||
assert!(cached.is_some());
|
assert!(cached.is_some());
|
||||||
assert_eq!(cached.unwrap()[0].name, "rust");
|
assert_eq!(cached.unwrap()[0].name, "rust");
|
||||||
}
|
}
|
||||||
@ -384,21 +403,25 @@ mod tests {
|
|||||||
prev_post: None,
|
prev_post: None,
|
||||||
next_post: None,
|
next_post: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
set_post_by_slug("test", post.clone()).await;
|
set_post_by_slug("test", post.clone()).await;
|
||||||
let cached = get_post_by_slug("test").await;
|
let cached = get_post_by_slug("test").await;
|
||||||
|
|
||||||
assert!(cached.is_some());
|
assert!(cached.is_some());
|
||||||
assert_eq!(cached.unwrap().unwrap().title, "Test");
|
assert_eq!(cached.unwrap().unwrap().title, "Test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn post_stats_cache_roundtrip() {
|
async fn post_stats_cache_roundtrip() {
|
||||||
let stats = PostStats { total: 10, drafts: 3, published: 7 };
|
let stats = PostStats {
|
||||||
|
total: 10,
|
||||||
|
drafts: 3,
|
||||||
|
published: 7,
|
||||||
|
};
|
||||||
|
|
||||||
set_post_stats(stats.clone()).await;
|
set_post_stats(stats.clone()).await;
|
||||||
let cached = get_post_stats().await;
|
let cached = get_post_stats().await;
|
||||||
|
|
||||||
assert!(cached.is_some());
|
assert!(cached.is_some());
|
||||||
assert_eq!(cached.unwrap().total, 10);
|
assert_eq!(cached.unwrap().total, 10);
|
||||||
}
|
}
|
||||||
@ -425,13 +448,13 @@ mod tests {
|
|||||||
prev_post: None,
|
prev_post: None,
|
||||||
next_post: None,
|
next_post: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
set_post_by_slug("invalidation-test", post.clone()).await;
|
set_post_by_slug("invalidation-test", post.clone()).await;
|
||||||
let cached_before = get_post_by_slug("invalidation-test").await;
|
let cached_before = get_post_by_slug("invalidation-test").await;
|
||||||
assert!(cached_before.is_some());
|
assert!(cached_before.is_some());
|
||||||
|
|
||||||
invalidate_post_by_slug("invalidation-test").await;
|
invalidate_post_by_slug("invalidation-test").await;
|
||||||
|
|
||||||
let cached_after = get_post_by_slug("invalidation-test").await;
|
let cached_after = get_post_by_slug("invalidation-test").await;
|
||||||
assert!(cached_after.is_none());
|
assert!(cached_after.is_none());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,8 @@ pub fn AdminLayout() -> Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_write_route = matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
|
let is_write_route =
|
||||||
|
matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
|
||||||
let main_class = if is_write_route {
|
let main_class = if is_write_route {
|
||||||
"flex-1 w-full max-w-5xl mx-auto px-6 flex flex-col overflow-hidden"
|
"flex-1 w-full max-w-5xl mx-auto px-6 flex flex-col overflow-hidden"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -38,5 +38,3 @@ pub fn AdminDashboardSkeleton() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use dioxus::prelude::*;
|
|||||||
|
|
||||||
use crate::api::comments::create_comment;
|
use crate::api::comments::create_comment;
|
||||||
use crate::components::comments::section::CommentContext;
|
use crate::components::comments::section::CommentContext;
|
||||||
use crate::components::forms::{INPUT_CLASS, BUTTON_PRIMARY_CLASS, AlertBox};
|
use crate::components::forms::{AlertBox, BUTTON_PRIMARY_CLASS, INPUT_CLASS};
|
||||||
use crate::hooks::comment_storage::{self, PendingComment};
|
use crate::hooks::comment_storage::{self, PendingComment};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::models::comment::PublicComment;
|
|
||||||
use crate::components::comments::section::CommentContext;
|
|
||||||
use crate::components::comments::form::CommentForm;
|
use crate::components::comments::form::CommentForm;
|
||||||
|
use crate::components::comments::section::CommentContext;
|
||||||
|
use crate::models::comment::PublicComment;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
|
pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::models::comment::PublicComment;
|
|
||||||
use crate::hooks::comment_storage::PendingComment;
|
|
||||||
use crate::components::comments::item::CommentItem;
|
use crate::components::comments::item::CommentItem;
|
||||||
use crate::components::comments::pending_item::PendingCommentItem;
|
use crate::components::comments::pending_item::PendingCommentItem;
|
||||||
|
use crate::hooks::comment_storage::PendingComment;
|
||||||
|
use crate::models::comment::PublicComment;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum MergedComment {
|
enum MergedComment {
|
||||||
@ -23,10 +23,13 @@ fn merge_and_treeify(
|
|||||||
.chain(pending.into_iter().map(MergedComment::Pending))
|
.chain(pending.into_iter().map(MergedComment::Pending))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let all_ids: HashSet<i64> = all.iter().map(|c| match c {
|
let all_ids: HashSet<i64> = all
|
||||||
MergedComment::Approved(c) => c.id,
|
.iter()
|
||||||
MergedComment::Pending(c) => c.id,
|
.map(|c| match c {
|
||||||
}).collect();
|
MergedComment::Approved(c) => c.id,
|
||||||
|
MergedComment::Pending(c) => c.id,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut children_map: HashMap<Option<i64>, Vec<MergedComment>> = HashMap::new();
|
let mut children_map: HashMap<Option<i64>, Vec<MergedComment>> = HashMap::new();
|
||||||
for comment in all {
|
for comment in all {
|
||||||
@ -38,7 +41,10 @@ fn merge_and_treeify(
|
|||||||
Some(pid) if !all_ids.contains(&pid) => None,
|
Some(pid) if !all_ids.contains(&pid) => None,
|
||||||
_ => parent_id,
|
_ => parent_id,
|
||||||
};
|
};
|
||||||
children_map.entry(effective_parent).or_default().push(comment);
|
children_map
|
||||||
|
.entry(effective_parent)
|
||||||
|
.or_default()
|
||||||
|
.push(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
for children in children_map.values_mut() {
|
for children in children_map.values_mut() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
pub mod section;
|
|
||||||
pub mod form;
|
|
||||||
pub mod list;
|
|
||||||
pub mod item;
|
|
||||||
pub mod pending_item;
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
|
pub mod form;
|
||||||
|
pub mod item;
|
||||||
|
pub mod list;
|
||||||
|
pub mod pending_item;
|
||||||
|
pub mod section;
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::hooks::comment_storage::{PendingComment, render_pending_content};
|
use crate::hooks::comment_storage::{render_pending_content, PendingComment};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element {
|
pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element {
|
||||||
|
|
||||||
let depth = if comment.parent_id.is_none() && comment.depth > 0 {
|
let depth = if comment.parent_id.is_none() && comment.depth > 0 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -13,7 +13,11 @@ pub fn FormInput(
|
|||||||
oninput: EventHandler<String>,
|
oninput: EventHandler<String>,
|
||||||
onkeydown: Option<EventHandler<KeyboardEvent>>,
|
onkeydown: Option<EventHandler<KeyboardEvent>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let disabled_class = if disabled { "opacity-60 cursor-not-allowed" } else { "" };
|
let disabled_class = if disabled {
|
||||||
|
"opacity-60 cursor-not-allowed"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
rsx! {
|
rsx! {
|
||||||
input {
|
input {
|
||||||
class: "{INPUT_CLASS} {disabled_class}",
|
class: "{INPUT_CLASS} {disabled_class}",
|
||||||
@ -43,8 +47,14 @@ pub fn FormLabel(label: &'static str) -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn AlertBox(message: String, variant: &'static str) -> Element {
|
pub fn AlertBox(message: String, variant: &'static str) -> Element {
|
||||||
let (bg_class, text_class) = match variant {
|
let (bg_class, text_class) = match variant {
|
||||||
"error" => ("bg-red-100 dark:bg-red-900/30", "text-red-700 dark:text-red-300"),
|
"error" => (
|
||||||
"success" => ("bg-green-100 dark:bg-green-900/30", "text-green-700 dark:text-green-300"),
|
"bg-red-100 dark:bg-red-900/30",
|
||||||
|
"text-red-700 dark:text-red-300",
|
||||||
|
),
|
||||||
|
"success" => (
|
||||||
|
"bg-green-100 dark:bg-green-900/30",
|
||||||
|
"text-green-700 dark:text-green-300",
|
||||||
|
),
|
||||||
_ => ("bg-paper-code-bg", "text-paper-secondary"),
|
_ => ("bg-paper-code-bg", "text-paper-secondary"),
|
||||||
};
|
};
|
||||||
rsx! {
|
rsx! {
|
||||||
|
|||||||
@ -3,17 +3,18 @@ use dioxus::prelude::*;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn ImageViewer(
|
pub fn ImageViewer(
|
||||||
src: String,
|
src: String,
|
||||||
#[props(default = "?w=800".to_string())]
|
#[props(default = "?w=800".to_string())] thumb_params: String,
|
||||||
thumb_params: String,
|
#[props(default = "图片".to_string())] alt: String,
|
||||||
#[props(default = "图片".to_string())]
|
#[props(default = false)] lazy_load: bool,
|
||||||
alt: String,
|
|
||||||
#[props(default = false)]
|
|
||||||
lazy_load: bool,
|
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let mut is_open = use_signal(|| false);
|
let mut is_open = use_signal(|| false);
|
||||||
|
|
||||||
let thumb_src = if src.contains('?') {
|
let thumb_src = if src.contains('?') {
|
||||||
format!("{}&{}", src.split('?').next().unwrap_or(&src), thumb_params.trim_start_matches('?'))
|
format!(
|
||||||
|
"{}&{}",
|
||||||
|
src.split('?').next().unwrap_or(&src),
|
||||||
|
thumb_params.trim_start_matches('?')
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("{}{}", src, thumb_params)
|
format!("{}{}", src, thumb_params)
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,7 +9,7 @@ pub fn PostHeader(post: Post) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
header { class: "post-header",
|
header { class: "post-header",
|
||||||
Breadcrumbs { title: post.title.clone() }
|
Breadcrumbs { title: post.title.clone() }
|
||||||
|
|
||||||
h1 { class: "post-title",
|
h1 { class: "post-title",
|
||||||
"{post.title}"
|
"{post.title}"
|
||||||
if post.status == PostStatus::Draft {
|
if post.status == PostStatus::Draft {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::components::skeletons::atoms::*;
|
use crate::components::skeletons::atoms::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
/// 归档页骨架屏
|
/// 归档页骨架屏
|
||||||
/// 结构:统计行("共 N 篇文章") + 年份标题 + 月份标题 + 文章条目列表
|
/// 结构:统计行("共 N 篇文章") + 年份标题 + 月份标题 + 文章条目列表
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::components::skeletons::atoms::*;
|
use crate::components::skeletons::atoms::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CommentListSkeleton() -> Element {
|
pub fn CommentListSkeleton() -> Element {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::utils::time::sleep_ms;
|
use crate::utils::time::sleep_ms;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
/// 骨架屏 pulse 动画延迟(毫秒)
|
/// 骨架屏 pulse 动画延迟(毫秒)
|
||||||
/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过
|
/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
pub mod atoms;
|
|
||||||
pub mod archive_skeleton;
|
pub mod archive_skeleton;
|
||||||
|
pub mod atoms;
|
||||||
pub mod comment_skeleton;
|
pub mod comment_skeleton;
|
||||||
pub mod delayed_skeleton;
|
pub mod delayed_skeleton;
|
||||||
pub mod home_skeleton;
|
pub mod home_skeleton;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::components::skeletons::atoms::*;
|
use crate::components::skeletons::atoms::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
/// 文章详情页骨架屏
|
/// 文章详情页骨架屏
|
||||||
/// 结构:面包屑 + 标题(h1) + 摘要 + 元信息 + 封面图 + 正文(多段) + Footer
|
/// 结构:面包屑 + 标题(h1) + 摘要 + 元信息 + 封面图 + 正文(多段) + Footer
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::components::skeletons::atoms::*;
|
use crate::components::skeletons::atoms::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn WriteSkeleton() -> Element {
|
pub fn WriteSkeleton() -> Element {
|
||||||
|
|||||||
@ -16,10 +16,12 @@ pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
|
|||||||
let mgr = Manager::from_config(pg_cfg, NoTls, mgr_cfg);
|
let mgr = Manager::from_config(pg_cfg, NoTls, mgr_cfg);
|
||||||
|
|
||||||
Pool::builder(mgr)
|
Pool::builder(mgr)
|
||||||
.max_size(std::env::var("DB_POOL_SIZE")
|
.max_size(
|
||||||
.ok()
|
std::env::var("DB_POOL_SIZE")
|
||||||
.and_then(|s| s.parse().ok())
|
.ok()
|
||||||
.unwrap_or(20))
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(20),
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to create database connection pool")
|
.expect("Failed to create database connection pool")
|
||||||
});
|
});
|
||||||
@ -34,7 +36,12 @@ pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::
|
|||||||
Ok(conn) => return Ok(conn),
|
Ok(conn) => return Ok(conn),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if attempt < MAX_RETRIES {
|
if attempt < MAX_RETRIES {
|
||||||
tracing::warn!("DB connection attempt {} failed, retrying in {:?}: {:?}", attempt + 1, RETRY_DELAY, e);
|
tracing::warn!(
|
||||||
|
"DB connection attempt {} failed, retrying in {:?}: {:?}",
|
||||||
|
attempt + 1,
|
||||||
|
RETRY_DELAY,
|
||||||
|
e
|
||||||
|
);
|
||||||
tokio::time::sleep(RETRY_DELAY).await;
|
tokio::time::sleep(RETRY_DELAY).await;
|
||||||
}
|
}
|
||||||
last_err = Some(e);
|
last_err = Some(e);
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
pub mod server {
|
pub mod server {
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use syntect::html::{ClassedHTMLGenerator, ClassStyle};
|
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
use syntect::util::LinesWithEndings;
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::utils::time::sleep_ms;
|
use crate::utils::time::sleep_ms;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
/// 骨架屏最小显示延迟(毫秒)。加载时间低于此值时不会显示骨架屏,避免闪烁。
|
/// 骨架屏最小显示延迟(毫秒)。加载时间低于此值时不会显示骨架屏,避免闪烁。
|
||||||
pub const MIN_SKELETON_DELAY_MS: u32 = 200;
|
pub const MIN_SKELETON_DELAY_MS: u32 = 200;
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
pub mod delayed_loading;
|
|
||||||
pub mod comment_storage;
|
pub mod comment_storage;
|
||||||
|
pub mod delayed_loading;
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@ -1,6 +1,6 @@
|
|||||||
mod api;
|
mod api;
|
||||||
mod cache;
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod cache;
|
||||||
mod components;
|
mod components;
|
||||||
mod context;
|
mod context;
|
||||||
mod db;
|
mod db;
|
||||||
@ -44,13 +44,14 @@ fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let config = ServeConfig::builder().incremental(
|
let config = ServeConfig::builder().incremental(
|
||||||
dioxus::server::IncrementalRendererConfig::default()
|
dioxus::server::IncrementalRendererConfig::default().invalidate_after(
|
||||||
.invalidate_after(std::time::Duration::from_secs(
|
std::time::Duration::from_secs(
|
||||||
std::env::var("SSR_CACHE_SECS")
|
std::env::var("SSR_CACHE_SECS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.unwrap_or(3600),
|
.unwrap_or(3600),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
let api_routes = axum::Router::new().route(
|
let api_routes = axum::Router::new().route(
|
||||||
"/api/upload",
|
"/api/upload",
|
||||||
@ -66,9 +67,7 @@ fn main() {
|
|||||||
let dioxus_app =
|
let dioxus_app =
|
||||||
axum::Router::new().serve_dioxus_application(config, router::AppRouter);
|
axum::Router::new().serve_dioxus_application(config, router::AppRouter);
|
||||||
|
|
||||||
let router = api_routes
|
let router = api_routes.merge(static_routes).merge(dioxus_app);
|
||||||
.merge(static_routes)
|
|
||||||
.merge(dioxus_app);
|
|
||||||
|
|
||||||
Ok(router)
|
Ok(router)
|
||||||
});
|
});
|
||||||
|
|||||||
@ -71,7 +71,9 @@ impl Post {
|
|||||||
|
|
||||||
pub fn status_badge_class(&self) -> &'static str {
|
pub fn status_badge_class(&self) -> &'static str {
|
||||||
match self.status {
|
match self.status {
|
||||||
PostStatus::Published => "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300",
|
PostStatus::Published => {
|
||||||
|
"bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300"
|
||||||
|
}
|
||||||
PostStatus::Draft => "bg-gray-100 dark:bg-[#333] text-gray-600 dark:text-[#9b9c9d]",
|
PostStatus::Draft => "bg-gray-100 dark:bg-[#333] text-gray-600 dark:text-[#9b9c9d]",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,7 +130,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn post_status_from_str() {
|
fn post_status_from_str() {
|
||||||
assert_eq!(PostStatus::from_str("draft"), Some(PostStatus::Draft));
|
assert_eq!(PostStatus::from_str("draft"), Some(PostStatus::Draft));
|
||||||
assert_eq!(PostStatus::from_str("published"), Some(PostStatus::Published));
|
assert_eq!(
|
||||||
|
PostStatus::from_str("published"),
|
||||||
|
Some(PostStatus::Published)
|
||||||
|
);
|
||||||
assert_eq!(PostStatus::from_str("unknown"), None);
|
assert_eq!(PostStatus::from_str("unknown"), None);
|
||||||
assert_eq!(PostStatus::from_str(""), None);
|
assert_eq!(PostStatus::from_str(""), None);
|
||||||
}
|
}
|
||||||
@ -186,6 +191,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn post_status_serde_roundtrip() {
|
fn post_status_serde_roundtrip() {
|
||||||
let json = serde_json::to_string(&PostStatus::Draft).unwrap();
|
let json = serde_json::to_string(&PostStatus::Draft).unwrap();
|
||||||
assert_eq!(serde_json::from_str::<PostStatus>(&json).unwrap(), PostStatus::Draft);
|
assert_eq!(
|
||||||
|
serde_json::from_str::<PostStatus>(&json).unwrap(),
|
||||||
|
PostStatus::Draft
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn user_role_serde_roundtrip() {
|
fn user_role_serde_roundtrip() {
|
||||||
let json = serde_json::to_string(&UserRole::Admin).unwrap();
|
let json = serde_json::to_string(&UserRole::Admin).unwrap();
|
||||||
assert_eq!(serde_json::from_str::<UserRole>(&json).unwrap(), UserRole::Admin);
|
assert_eq!(
|
||||||
|
serde_json::from_str::<UserRole>(&json).unwrap(),
|
||||||
|
UserRole::Admin
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,11 @@ use std::collections::HashSet;
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus::router::components::Link;
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::comments::{
|
|
||||||
approve_comment, batch_update_comment_status, spam_comment,
|
|
||||||
};
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use crate::api::comments::{get_all_comments, AllCommentsResponse};
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::api::comments::trash_comment;
|
use crate::api::comments::trash_comment;
|
||||||
|
use crate::api::comments::{approve_comment, batch_update_comment_status, spam_comment};
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use crate::api::comments::{get_all_comments, AllCommentsResponse};
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::models::comment::{AdminComment, CommentStatus};
|
use crate::models::comment::{AdminComment, CommentStatus};
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
@ -54,7 +52,11 @@ pub fn AdminCommentsPage(page: i32) -> Element {
|
|||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
let filter_status = move || {
|
let filter_status = move || {
|
||||||
let f = active_filter();
|
let f = active_filter();
|
||||||
if f.is_empty() { None } else { Some(f) }
|
if f.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 客户端(CSR)加载数据
|
// 客户端(CSR)加载数据
|
||||||
@ -313,14 +315,29 @@ fn CommentRow(
|
|||||||
on_trash: EventHandler,
|
on_trash: EventHandler,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let (badge_class, status_label) = match &comment.status {
|
let (badge_class, status_label) = match &comment.status {
|
||||||
CommentStatus::Pending => ("bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", "待审核"),
|
CommentStatus::Pending => (
|
||||||
CommentStatus::Approved => ("bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", "已通过"),
|
"bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
|
||||||
CommentStatus::Spam => ("bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", "垃圾"),
|
"待审核",
|
||||||
CommentStatus::Trash => ("bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400", "已删除"),
|
),
|
||||||
|
CommentStatus::Approved => (
|
||||||
|
"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
|
||||||
|
"已通过",
|
||||||
|
),
|
||||||
|
CommentStatus::Spam => (
|
||||||
|
"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
||||||
|
"垃圾",
|
||||||
|
),
|
||||||
|
CommentStatus::Trash => (
|
||||||
|
"bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400",
|
||||||
|
"已删除",
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let date_str = comment.created_at.format("%Y-%m-%d").to_string();
|
let date_str = comment.created_at.format("%Y-%m-%d").to_string();
|
||||||
let preview = if comment.content_md.len() > 100 {
|
let preview = if comment.content_md.len() > 100 {
|
||||||
format!("{}...", &comment.content_md[..comment.content_md.ceil_char_boundary(100)])
|
format!(
|
||||||
|
"{}...",
|
||||||
|
&comment.content_md[..comment.content_md.ceil_char_boundary(100)]
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
comment.content_md.clone()
|
comment.content_md.clone()
|
||||||
};
|
};
|
||||||
@ -411,9 +428,13 @@ fn CommentsPagination(current_page: i32, total: i64) -> Element {
|
|||||||
let prev_route = if current_page - 1 <= 1 {
|
let prev_route = if current_page - 1 <= 1 {
|
||||||
Route::AdminComments {}
|
Route::AdminComments {}
|
||||||
} else {
|
} else {
|
||||||
Route::AdminCommentsPage { page: current_page - 1 }
|
Route::AdminCommentsPage {
|
||||||
|
page: current_page - 1,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let next_route = Route::AdminCommentsPage {
|
||||||
|
page: current_page + 1,
|
||||||
};
|
};
|
||||||
let next_route = Route::AdminCommentsPage { page: current_page + 1 };
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
nav { class: "flex mt-6 justify-between",
|
nav { class: "flex mt-6 justify-between",
|
||||||
|
|||||||
@ -37,7 +37,10 @@ pub fn PostsPage(page: i32) -> Element {
|
|||||||
let p = current_page;
|
let p = current_page;
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match list_posts(p, POSTS_PER_PAGE).await {
|
match list_posts(p, POSTS_PER_PAGE).await {
|
||||||
Ok(PostListResponse { posts: list, total: t }) => {
|
Ok(PostListResponse {
|
||||||
|
posts: list,
|
||||||
|
total: t,
|
||||||
|
}) => {
|
||||||
posts.set(list);
|
posts.set(list);
|
||||||
total.set(t);
|
total.set(t);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ use dioxus::prelude::*;
|
|||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::api::posts::{create_post, get_post_by_id, update_post, CreatePostResponse, SinglePostResponse};
|
use crate::api::posts::{
|
||||||
|
create_post, get_post_by_id, update_post, CreatePostResponse, SinglePostResponse,
|
||||||
|
};
|
||||||
use crate::components::write_skeleton::WriteSkeleton;
|
use crate::components::write_skeleton::WriteSkeleton;
|
||||||
use crate::models::post::Post;
|
use crate::models::post::Post;
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
@ -63,21 +65,21 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
if is_edit {
|
if is_edit {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
if let Some(id) = post_id {
|
if let Some(id) = post_id {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match get_post_by_id(id).await {
|
match get_post_by_id(id).await {
|
||||||
Ok(SinglePostResponse { post: Some(post) }) => {
|
Ok(SinglePostResponse { post: Some(post) }) => {
|
||||||
edit_post.set(Some(post));
|
edit_post.set(Some(post));
|
||||||
|
}
|
||||||
|
Ok(SinglePostResponse { post: None }) => {
|
||||||
|
load_error.set(Some("文章不存在".to_string()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
load_error.set(Some(format!("加载失败: {}", e)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(SinglePostResponse { post: None }) => {
|
});
|
||||||
load_error.set(Some("文章不存在".to_string()));
|
}
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
load_error.set(Some(format!("加载失败: {}", e)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use dioxus::prelude::*;
|
|||||||
use dioxus::router::components::Link;
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{list_published_posts, PostListResponse};
|
use crate::api::posts::{list_published_posts, PostListResponse};
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
|
||||||
use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
|
use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
|
||||||
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::models::post::Post;
|
use crate::models::post::Post;
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use dioxus::router::components::Link;
|
|||||||
use crate::api::posts::{get_posts_by_tag, list_tags, PostListResponse, TagListResponse};
|
use crate::api::posts::{get_posts_by_tag, list_tags, PostListResponse, TagListResponse};
|
||||||
use crate::components::post_card::PostCard;
|
use crate::components::post_card::PostCard;
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::components::skeletons::tags_skeleton::{TagsSkeleton, TagDetailSkeleton};
|
use crate::components::skeletons::tags_skeleton::{TagDetailSkeleton, TagsSkeleton};
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|||||||
@ -5,7 +5,9 @@ use crate::components::admin_layout::AdminLayout;
|
|||||||
use crate::components::frontend_layout::FrontendLayout;
|
use crate::components::frontend_layout::FrontendLayout;
|
||||||
use crate::context::UserContext;
|
use crate::context::UserContext;
|
||||||
use crate::pages::about::About;
|
use crate::pages::about::About;
|
||||||
use crate::pages::admin::{Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, Write, WriteEdit};
|
use crate::pages::admin::{
|
||||||
|
Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, Write, WriteEdit,
|
||||||
|
};
|
||||||
use crate::pages::archives::Archives;
|
use crate::pages::archives::Archives;
|
||||||
use crate::pages::home::{Home, HomePage};
|
use crate::pages::home::{Home, HomePage};
|
||||||
use crate::pages::login::Login;
|
use crate::pages::login::Login;
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
pub mod time;
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
pub mod time;
|
||||||
|
|||||||
@ -1,28 +1,21 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
static CODE_BLOCK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
static CODE_BLOCK_RE: LazyLock<regex::Regex> =
|
||||||
regex::Regex::new(r"```[\s\S]*?```").unwrap()
|
LazyLock::new(|| regex::Regex::new(r"```[\s\S]*?```").unwrap());
|
||||||
});
|
|
||||||
|
|
||||||
static INLINE_CODE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
static INLINE_CODE_RE: LazyLock<regex::Regex> =
|
||||||
regex::Regex::new(r"`[^`]*`").unwrap()
|
LazyLock::new(|| regex::Regex::new(r"`[^`]*`").unwrap());
|
||||||
});
|
|
||||||
|
|
||||||
static LINK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
static LINK_RE: LazyLock<regex::Regex> =
|
||||||
regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap()
|
LazyLock::new(|| regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||||
});
|
|
||||||
|
|
||||||
static HEADING_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
static HEADING_RE: LazyLock<regex::Regex> =
|
||||||
regex::Regex::new(r"^#{1,6}\s*").unwrap()
|
LazyLock::new(|| regex::Regex::new(r"^#{1,6}\s*").unwrap());
|
||||||
});
|
|
||||||
|
|
||||||
static IMAGE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
static IMAGE_RE: LazyLock<regex::Regex> =
|
||||||
regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap()
|
LazyLock::new(|| regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||||
});
|
|
||||||
|
|
||||||
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\s+").unwrap());
|
||||||
regex::Regex::new(r"\s+").unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
pub fn strip_markdown(md: &str) -> String {
|
pub fn strip_markdown(md: &str) -> String {
|
||||||
let mut plain = CODE_BLOCK_RE.replace_all(md, "").to_string();
|
let mut plain = CODE_BLOCK_RE.replace_all(md, "").to_string();
|
||||||
@ -88,7 +81,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strip_markdown_keeps_link_text() {
|
fn strip_markdown_keeps_link_text() {
|
||||||
assert_eq!(strip_markdown("[click me](https://example.com)"), "click me");
|
assert_eq!(
|
||||||
|
strip_markdown("[click me](https://example.com)"),
|
||||||
|
"click me"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -98,7 +94,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strip_markdown_removes_bold_and_italic() {
|
fn strip_markdown_removes_bold_and_italic() {
|
||||||
assert_eq!(strip_markdown("**bold** *italic* __bold__ _italic_"), "bold italic bold italic");
|
assert_eq!(
|
||||||
|
strip_markdown("**bold** *italic* __bold__ _italic_"),
|
||||||
|
"bold italic bold italic"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -178,7 +178,10 @@ mod tests {
|
|||||||
// Note: LazyLock initializes once, so we can't easily test
|
// Note: LazyLock initializes once, so we can't easily test
|
||||||
// different env values in the same process.
|
// different env values in the same process.
|
||||||
// Test that the default config is reasonable
|
// Test that the default config is reasonable
|
||||||
let config = WebpConfig { quality: 85.0, method: 2 };
|
let config = WebpConfig {
|
||||||
|
quality: 85.0,
|
||||||
|
method: 2,
|
||||||
|
};
|
||||||
assert!(config.quality >= 0.0 && config.quality <= 100.0);
|
assert!(config.quality >= 0.0 && config.quality <= 100.0);
|
||||||
assert!(config.method <= 6);
|
assert!(config.method <= 6);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user