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