diff --git a/Dockerfile b/Dockerfile index feed0db..f708535 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1-bullseye as builder +FROM rust:1.64-bullseye as builder WORKDIR /usr/src/ RUN cargo new myapp --vcs none WORKDIR /usr/src/myapp diff --git a/src/api/mod.rs b/src/api/mod.rs index 80ad830..e19ac09 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -61,20 +61,23 @@ pub struct CurrentUser { is_admin: bool, is_candidate: bool, custom_title: String, + title_secret: String, pub auto_block_rank: u8, } impl CurrentUser { pub async fn from_hash(rconn: &RdsConn, namehash: String) -> Self { + let (custom_title, title_secret) = CustomTitle::get(rconn, &namehash) + .await + .ok() + .flatten() + .unwrap_or_default(); Self { id: None, is_admin: false, is_candidate: false, - custom_title: CustomTitle::get(rconn, &namehash) - .await - .ok() - .flatten() - .unwrap_or_default(), + custom_title, + title_secret, auto_block_rank: AutoBlockRank::get(rconn, &namehash).await.unwrap_or(2), namehash, } @@ -129,6 +132,8 @@ pub enum PolicyError { IsDeleted, NotAllowed, TitleUsed, + TitleProtected, + InvalidTitle, YouAreTmp, NoReason, OldApi, @@ -158,6 +163,8 @@ impl<'r> Responder<'r, 'static> for ApiError { PolicyError::IsDeleted => "内容被删除", PolicyError::NotAllowed => "不允许的操作", PolicyError::TitleUsed => "头衔已被使用", + PolicyError::TitleProtected => "头衔处于保护期", + PolicyError::InvalidTitle => "头衔包含不允许的符号", PolicyError::YouAreTmp => "临时用户只可发布内容和进入单个洞", PolicyError::NoReason => "未填写理由", PolicyError::OldApi => "请使用最新版前端地址并检查更新", diff --git a/src/api/operation.rs b/src/api/operation.rs index 5cc448a..2c4b515 100644 --- a/src/api/operation.rs +++ b/src/api/operation.rs @@ -1,4 +1,8 @@ -use crate::api::{ApiError, CurrentUser, JsonApi, PolicyError::*, Ugc}; +use crate::api::{ + ApiError, CurrentUser, JsonApi, + PolicyError::{self, *}, + Ugc, +}; use crate::cache::*; use crate::db_conn::Db; use crate::libs::diesel_logger::LoggingConnection; @@ -204,20 +208,21 @@ pub async fn block(bi: Form, user: CurrentUser, db: Db, rconn: RdsCo pub struct TitleInput { #[field(validate = len(1..31))] title: String, + secret: String, } -#[post("/title", data = "")] +#[post("/set-title", data = "")] pub async fn set_title(ti: Form, user: CurrentUser, rconn: RdsConn) -> JsonApi { - let title: String = ti.title.chars().filter(|c| c.is_alphanumeric()).collect(); - if title.is_empty() { - Err(TitleUsed)? + if ti.title.is_empty() { + Err(InvalidTitle)? } + ti.title + .chars() + .map(|c| c.is_alphanumeric().then_some(()).ok_or(InvalidTitle)) + .collect::, PolicyError>>()?; - if CustomTitle::set(&rconn, &user.namehash, &title).await? { - code0!() - } else { - Err(TitleUsed)? - } + let secret = CustomTitle::set(&rconn, &user.namehash, &ti.title, &ti.secret).await?; + code0!(secret) } #[derive(FromForm)] diff --git a/src/api/post.rs b/src/api/post.rs index f600499..c65e6ed 100644 --- a/src/api/post.rs +++ b/src/api/post.rs @@ -176,6 +176,7 @@ pub async fn get_list( "data": ps_data, "count": ps_data.len(), "custom_title": user.custom_title, + "title_secret": user.title_secret, "auto_block_rank": user.auto_block_rank, "announcement": get_announcement(&rconn).await?, "code": 0 diff --git a/src/main.rs b/src/main.rs index 61bb536..a31952a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,6 @@ async fn main() { api::systemlog::get_systemlog, api::operation::delete, api::operation::report, - api::operation::set_title, api::operation::block, api::operation::set_auto_block, api::vote::vote, @@ -90,10 +89,11 @@ async fn main() { .mount( "/_api/v2", routes![ + api::attention::set_notification, api::comment::add_comment, + api::operation::set_title, api::upload::local_upload, cors::options_handler, - api::attention::set_notification, ], ) .mount( diff --git a/src/rds_models.rs b/src/rds_models.rs index 2a343eb..488eda4 100644 --- a/src/rds_models.rs +++ b/src/rds_models.rs @@ -1,4 +1,5 @@ -use crate::api::CurrentUser; +use crate::api::{Api, CurrentUser, PolicyError}; +use crate::random_hasher::random_string; use crate::rds_conn::RdsConn; use chrono::{offset::Local, DateTime}; use redis::{AsyncCommands, RedisResult}; @@ -51,6 +52,12 @@ const KEY_SYSTEMLOG: &str = "hole_v2:systemlog_list"; const KEY_BANNED_USERS: &str = "hole_v2:banned_user_hash_list"; const KEY_BLOCKED_COUNTER: &str = "hole_v2:blocked_counter"; const KEY_CUSTOM_TITLE: &str = "hole_v2:title"; +const CUSTOM_TITLE_KEEP_TIME: usize = 7 * 24 * 60 * 60; +macro_rules! KEY_TITLE_SECRET { + ($title: expr) => { + format!("hole_v2:title_secret:{}", $title) + }; +} const KEY_AUTO_BLOCK_RANK: &str = "hole_v2:auto_block_rank"; // rank * 5: 自动过滤的拉黑数阈值 const KEY_ANNOUNCEMENT: &str = "hole_v2:announcement"; const KEY_CANDIDATE: &str = "hole_v2:candidate"; @@ -210,20 +217,48 @@ impl BlockCounter { pub struct CustomTitle; impl CustomTitle { + async fn gen_and_set_secret(rconn: &RdsConn, title: &str) -> RedisResult { + let secret = random_string(8); + rconn + .clone() + .set_ex(KEY_TITLE_SECRET!(&title), &secret, CUSTOM_TITLE_KEEP_TIME) + .await?; + Ok(secret) + } + // return false if title exits - pub async fn set(rconn: &RdsConn, namehash: &str, title: &str) -> RedisResult { + pub async fn set(rconn: &RdsConn, namehash: &str, title: &str, secret: &str) -> Api { let mut rconn = rconn.clone(); if rconn.hexists(KEY_CUSTOM_TITLE, title).await? { - Ok(false) + Err(PolicyError::TitleUsed)? } else { + let ori_secret: Option = rconn.get(KEY_TITLE_SECRET!(title)).await?; + ori_secret + .map_or(Some(()), |s| (s.eq(&secret).then_some(()))) + .ok_or(PolicyError::TitleProtected)?; rconn.hset(KEY_CUSTOM_TITLE, namehash, title).await?; rconn.hset(KEY_CUSTOM_TITLE, title, namehash).await?; - Ok(true) + Ok(Self::gen_and_set_secret(&rconn, title).await?) } } - pub async fn get(rconn: &RdsConn, namehash: &str) -> RedisResult> { - rconn.clone().hget(KEY_CUSTOM_TITLE, namehash).await + pub async fn get(rconn: &RdsConn, namehash: &str) -> RedisResult> { + let t: Option = rconn.clone().hget(KEY_CUSTOM_TITLE, namehash).await?; + Ok(if let Some(title) = t { + let s: Option = rconn.clone().get(KEY_TITLE_SECRET!(title)).await?; + let secret = if let Some(ss) = s { + rconn + .clone() + .expire(KEY_TITLE_SECRET!(title), CUSTOM_TITLE_KEEP_TIME) + .await?; + ss + } else { + Self::gen_and_set_secret(rconn, &title).await? + }; + Some((title, secret)) + } else { + None + }) } pub async fn clear(rconn: &RdsConn) -> RedisResult<()> {