This reverts commit 8a086c8240
.
This commit is contained in:
parent
dce79b83bf
commit
1f1c0ad17a
16 changed files with 20 additions and 300 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2526,7 +2526,6 @@ dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"captcha",
|
"captcha",
|
||||||
"chrono",
|
|
||||||
"lemmy_api_common",
|
"lemmy_api_common",
|
||||||
"lemmy_db_schema",
|
"lemmy_db_schema",
|
||||||
"lemmy_db_views",
|
"lemmy_db_views",
|
||||||
|
@ -2577,7 +2576,6 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
|
||||||
"lemmy_api_common",
|
"lemmy_api_common",
|
||||||
"lemmy_db_schema",
|
"lemmy_db_schema",
|
||||||
"lemmy_db_views",
|
"lemmy_db_views",
|
||||||
|
|
|
@ -29,7 +29,6 @@ async-trait = { workspace = true }
|
||||||
captcha = { workspace = true }
|
captcha = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
chrono = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use captcha::Captcha;
|
|
||||||
use lemmy_api_common::{context::LemmyContext, utils::local_site_to_slur_regex};
|
use lemmy_api_common::{context::LemmyContext, utils::local_site_to_slur_regex};
|
||||||
use lemmy_db_schema::source::local_site::LocalSite;
|
use lemmy_db_schema::source::local_site::LocalSite;
|
||||||
use lemmy_utils::{error::LemmyError, utils::slurs::check_slurs};
|
use lemmy_utils::{error::LemmyError, utils::slurs::check_slurs};
|
||||||
|
@ -21,21 +20,6 @@ pub trait Perform {
|
||||||
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError>;
|
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts the captcha to a base64 encoded wav audio file
|
|
||||||
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> String {
|
|
||||||
let letters = captcha.as_wav();
|
|
||||||
|
|
||||||
let mut concat_letters: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
for letter in letters {
|
|
||||||
let bytes = letter.unwrap_or_default();
|
|
||||||
concat_letters.extend(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to base64
|
|
||||||
base64::encode(concat_letters)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check size of report and remove whitespace
|
/// Check size of report and remove whitespace
|
||||||
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
|
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
|
||||||
let slur_regex = &local_site_to_slur_regex(local_site);
|
let slur_regex = &local_site_to_slur_regex(local_site);
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
use crate::{captcha_as_wav_base64, Perform};
|
|
||||||
use actix_web::web::Data;
|
|
||||||
use captcha::{gen, Difficulty};
|
|
||||||
use chrono::Duration;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{CaptchaResponse, GetCaptcha, GetCaptchaResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{captcha_answer::CaptchaAnswer, local_site::LocalSite},
|
|
||||||
utils::naive_now,
|
|
||||||
};
|
|
||||||
use lemmy_utils::error::LemmyError;
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl Perform for GetCaptcha {
|
|
||||||
type Response = GetCaptchaResponse;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError> {
|
|
||||||
let local_site = LocalSite::read(context.pool()).await?;
|
|
||||||
|
|
||||||
if !local_site.captcha_enabled {
|
|
||||||
return Ok(GetCaptchaResponse { ok: None });
|
|
||||||
}
|
|
||||||
|
|
||||||
let captcha = gen(match local_site.captcha_difficulty.as_str() {
|
|
||||||
"easy" => Difficulty::Easy,
|
|
||||||
"hard" => Difficulty::Hard,
|
|
||||||
_ => Difficulty::Medium,
|
|
||||||
});
|
|
||||||
|
|
||||||
let answer = captcha.chars_as_string();
|
|
||||||
|
|
||||||
let png = captcha.as_base64().expect("failed to generate captcha");
|
|
||||||
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
let wav = captcha_as_wav_base64(&captcha);
|
|
||||||
|
|
||||||
let captcha: CaptchaAnswer = CaptchaAnswer {
|
|
||||||
answer,
|
|
||||||
uuid: uuid.clone(),
|
|
||||||
expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
|
|
||||||
};
|
|
||||||
// Stores the captcha item in the db
|
|
||||||
CaptchaAnswer::insert(context.pool(), &captcha).await?;
|
|
||||||
|
|
||||||
Ok(GetCaptchaResponse {
|
|
||||||
ok: Some(CaptchaResponse { png, wav, uuid }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ mod ban_person;
|
||||||
mod block;
|
mod block;
|
||||||
mod change_password;
|
mod change_password;
|
||||||
mod change_password_after_reset;
|
mod change_password_after_reset;
|
||||||
mod get_captcha;
|
|
||||||
mod list_banned;
|
mod list_banned;
|
||||||
mod login;
|
mod login;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
|
|
@ -22,4 +22,3 @@ tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
webmention = "0.4.0"
|
webmention = "0.4.0"
|
||||||
chrono = { worspace = true }
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::PerformCrud;
|
use crate::PerformCrud;
|
||||||
use activitypub_federation::http_signatures::generate_actor_keypair;
|
use activitypub_federation::http_signatures::generate_actor_keypair;
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
person::{LoginResponse, Register},
|
person::{LoginResponse, Register},
|
||||||
|
@ -20,7 +19,6 @@ use lemmy_api_common::{
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
aggregates::structs::PersonAggregates,
|
aggregates::structs::PersonAggregates,
|
||||||
source::{
|
source::{
|
||||||
captcha_answer::CaptchaAnswer,
|
|
||||||
local_user::{LocalUser, LocalUserInsertForm},
|
local_user::{LocalUser, LocalUserInsertForm},
|
||||||
person::{Person, PersonInsertForm},
|
person::{Person, PersonInsertForm},
|
||||||
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
|
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
|
||||||
|
@ -73,22 +71,6 @@ impl PerformCrud for Register {
|
||||||
return Err(LemmyError::from_message("passwords_dont_match"));
|
return Err(LemmyError::from_message("passwords_dont_match"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if local_site.site_setup && local_site.captcha_enabled {
|
|
||||||
let check = CaptchaAnswer::check_captcha(
|
|
||||||
context.pool(),
|
|
||||||
CaptchaAnswer {
|
|
||||||
uuid: data.captcha_uuid.clone().unwrap_or_default(),
|
|
||||||
answer: data.captcha_answer.clone().unwrap_or_default(),
|
|
||||||
// not used when checking
|
|
||||||
expires: NaiveDateTime::MIN,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if !check {
|
|
||||||
return Err(LemmyError::from_message("captcha_incorrect"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let slur_regex = local_site_to_slur_regex(&local_site);
|
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||||
check_slurs(&data.username, &slur_regex)?;
|
check_slurs(&data.username, &slur_regex)?;
|
||||||
check_slurs_opt(&data.answer, &slur_regex)?;
|
check_slurs_opt(&data.answer, &slur_regex)?;
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
--- schema.rs 2023-06-21 22:25:50.252384233 +0100
|
diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs
|
||||||
+++ "schema copy.rs" 2023-06-21 22:26:50.452378651 +0100
|
index 255c6422..f2ccf5e2 100644
|
||||||
@@ -6,10 +6,6 @@
|
--- a/crates/db_schema/src/schema.rs
|
||||||
|
+++ b/crates/db_schema/src/schema.rs
|
||||||
|
@@ -2,16 +2,12 @@
|
||||||
|
|
||||||
|
pub mod sql_types {
|
||||||
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "listing_type_enum"))]
|
||||||
pub struct ListingTypeEnum;
|
pub struct ListingTypeEnum;
|
||||||
|
|
||||||
#[derive(diesel::sql_types::SqlType)]
|
- #[derive(diesel::sql_types::SqlType)]
|
||||||
- #[diesel(postgres_type(name = "ltree"))]
|
- #[diesel(postgres_type(name = "ltree"))]
|
||||||
- pub struct Ltree;
|
- pub struct Ltree;
|
||||||
-
|
-
|
||||||
- #[derive(diesel::sql_types::SqlType)]
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
#[diesel(postgres_type(name = "registration_mode_enum"))]
|
#[diesel(postgres_type(name = "registration_mode_enum"))]
|
||||||
pub struct RegistrationModeEnum;
|
pub struct RegistrationModeEnum;
|
||||||
|
|
||||||
@@ -78,7 +74,7 @@
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "sort_type_enum"))]
|
||||||
|
@@ -67,13 +63,13 @@ diesel::table! {
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
use diesel::sql_types::*;
|
use diesel::sql_types::*;
|
||||||
|
@ -20,3 +31,6 @@
|
||||||
|
|
||||||
comment (id) {
|
comment (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
content -> Text,
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
use crate::{
|
|
||||||
schema::captcha_answer,
|
|
||||||
source::captcha_answer::CaptchaAnswer,
|
|
||||||
utils::{functions::lower, get_conn, naive_now, DbPool},
|
|
||||||
};
|
|
||||||
use diesel::{
|
|
||||||
delete,
|
|
||||||
dsl::exists,
|
|
||||||
insert_into,
|
|
||||||
result::Error,
|
|
||||||
select,
|
|
||||||
ExpressionMethods,
|
|
||||||
QueryDsl,
|
|
||||||
};
|
|
||||||
use diesel_async::RunQueryDsl;
|
|
||||||
|
|
||||||
impl CaptchaAnswer {
|
|
||||||
pub async fn insert(pool: &DbPool, captcha: &CaptchaAnswer) -> Result<Self, Error> {
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
|
||||||
|
|
||||||
insert_into(captcha_answer::table)
|
|
||||||
.values(captcha)
|
|
||||||
.get_result::<Self>(conn)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_captcha(pool: &DbPool, to_check: CaptchaAnswer) -> Result<bool, Error> {
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
|
||||||
|
|
||||||
// delete any expired captchas
|
|
||||||
delete(captcha_answer::table.filter(captcha_answer::expires.lt(&naive_now())))
|
|
||||||
.execute(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// fetch requested captcha
|
|
||||||
let captcha_exists = select(exists(
|
|
||||||
captcha_answer::dsl::captcha_answer
|
|
||||||
.filter((captcha_answer::dsl::uuid).eq(to_check.uuid.clone()))
|
|
||||||
.filter(lower(captcha_answer::dsl::answer).eq(to_check.answer.to_lowercase().clone())),
|
|
||||||
))
|
|
||||||
.get_result::<bool>(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// delete checked captcha
|
|
||||||
delete(captcha_answer::table.filter(captcha_answer::uuid.eq(to_check.uuid.clone())))
|
|
||||||
.execute(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(captcha_exists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{
|
|
||||||
source::captcha_answer::CaptchaAnswer,
|
|
||||||
utils::{build_db_pool_for_tests, naive_now},
|
|
||||||
};
|
|
||||||
use chrono::Duration;
|
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_captcha_happy_path() {
|
|
||||||
let pool = &build_db_pool_for_tests().await;
|
|
||||||
|
|
||||||
let captcha_a_id = "a".to_string();
|
|
||||||
|
|
||||||
let _ = CaptchaAnswer::insert(
|
|
||||||
pool,
|
|
||||||
&CaptchaAnswer {
|
|
||||||
uuid: captcha_a_id.clone(),
|
|
||||||
answer: "XYZ".to_string(),
|
|
||||||
expires: naive_now() + Duration::minutes(10),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let result = CaptchaAnswer::check_captcha(
|
|
||||||
pool,
|
|
||||||
CaptchaAnswer {
|
|
||||||
uuid: captcha_a_id.clone(),
|
|
||||||
answer: "xyz".to_string(),
|
|
||||||
expires: chrono::NaiveDateTime::MIN,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_captcha_repeat_answer_fails() {
|
|
||||||
let pool = &build_db_pool_for_tests().await;
|
|
||||||
|
|
||||||
let captcha_a_id = "a".to_string();
|
|
||||||
|
|
||||||
let _ = CaptchaAnswer::insert(
|
|
||||||
pool,
|
|
||||||
&CaptchaAnswer {
|
|
||||||
uuid: captcha_a_id.clone(),
|
|
||||||
answer: "XYZ".to_string(),
|
|
||||||
expires: naive_now() + Duration::minutes(10),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let result = CaptchaAnswer::check_captcha(
|
|
||||||
pool,
|
|
||||||
CaptchaAnswer {
|
|
||||||
uuid: captcha_a_id.clone(),
|
|
||||||
answer: "xyz".to_string(),
|
|
||||||
expires: chrono::NaiveDateTime::MIN,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let result_repeat = CaptchaAnswer::check_captcha(
|
|
||||||
pool,
|
|
||||||
CaptchaAnswer {
|
|
||||||
uuid: captcha_a_id.clone(),
|
|
||||||
answer: "xyz".to_string(),
|
|
||||||
expires: chrono::NaiveDateTime::MIN,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result_repeat.is_ok());
|
|
||||||
assert!(!result_repeat.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_captcha_expired_fails() {
|
|
||||||
let pool = &build_db_pool_for_tests().await;
|
|
||||||
|
|
||||||
let expired_id = "already_expired".to_string();
|
|
||||||
|
|
||||||
let _ = CaptchaAnswer::insert(
|
|
||||||
pool,
|
|
||||||
&CaptchaAnswer {
|
|
||||||
uuid: expired_id.clone(),
|
|
||||||
answer: "xyz".to_string(),
|
|
||||||
expires: naive_now() - Duration::seconds(1),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let expired_result = CaptchaAnswer::check_captcha(
|
|
||||||
pool,
|
|
||||||
CaptchaAnswer {
|
|
||||||
uuid: expired_id.clone(),
|
|
||||||
answer: "xyz".to_string(),
|
|
||||||
expires: chrono::NaiveDateTime::MIN,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(expired_result.is_ok());
|
|
||||||
assert!(!expired_result.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
pub mod activity;
|
pub mod activity;
|
||||||
pub mod actor_language;
|
pub mod actor_language;
|
||||||
pub mod captcha_answer;
|
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod comment_reply;
|
pub mod comment_reply;
|
||||||
pub mod comment_report;
|
pub mod comment_report;
|
||||||
|
|
|
@ -64,14 +64,6 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
captcha_answer (uuid) {
|
|
||||||
uuid -> Text,
|
|
||||||
answer -> Text,
|
|
||||||
expires -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
use diesel::sql_types::{Bool, Int4, Nullable, Text, Timestamp, Varchar};
|
use diesel::sql_types::{Bool, Int4, Nullable, Text, Timestamp, Varchar};
|
||||||
use diesel_ltree::sql_types::Ltree;
|
use diesel_ltree::sql_types::Ltree;
|
||||||
|
@ -924,7 +916,6 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
admin_purge_community,
|
admin_purge_community,
|
||||||
admin_purge_person,
|
admin_purge_person,
|
||||||
admin_purge_post,
|
admin_purge_post,
|
||||||
captcha_answer,
|
|
||||||
comment,
|
comment,
|
||||||
comment_aggregates,
|
comment_aggregates,
|
||||||
comment_like,
|
comment_like,
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
#[cfg(feature = "full")]
|
|
||||||
use crate::schema::captcha_answer;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "full", derive(Queryable, Insertable, AsChangeset))]
|
|
||||||
#[cfg_attr(feature = "full", diesel(table_name = captcha_answer))]
|
|
||||||
pub struct CaptchaAnswer {
|
|
||||||
pub uuid: String,
|
|
||||||
pub answer: String,
|
|
||||||
pub expires: chrono::NaiveDateTime,
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod activity;
|
pub mod activity;
|
||||||
pub mod actor_language;
|
pub mod actor_language;
|
||||||
pub mod captcha_answer;
|
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod comment_reply;
|
pub mod comment_reply;
|
||||||
pub mod comment_report;
|
pub mod comment_report;
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
drop table captcha_answer;
|
|
|
@ -1,5 +0,0 @@
|
||||||
create table captcha_answer (
|
|
||||||
uuid text not null primary key,
|
|
||||||
answer text not null,
|
|
||||||
expires timestamp not null
|
|
||||||
);
|
|
|
@ -38,7 +38,6 @@ use lemmy_api_common::{
|
||||||
ChangePassword,
|
ChangePassword,
|
||||||
DeleteAccount,
|
DeleteAccount,
|
||||||
GetBannedPersons,
|
GetBannedPersons,
|
||||||
GetCaptcha,
|
|
||||||
GetPersonDetails,
|
GetPersonDetails,
|
||||||
GetPersonMentions,
|
GetPersonMentions,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
|
@ -273,12 +272,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.wrap(rate_limit.register())
|
.wrap(rate_limit.register())
|
||||||
.route(web::post().to(route_post_crud::<Register>)),
|
.route(web::post().to(route_post_crud::<Register>)),
|
||||||
)
|
)
|
||||||
.service(
|
|
||||||
// Handle captcha separately
|
|
||||||
web::resource("/user/get_captcha")
|
|
||||||
.wrap(rate_limit.post())
|
|
||||||
.route(web::get().to(route_get::<GetCaptcha>)),
|
|
||||||
)
|
|
||||||
// User actions
|
// User actions
|
||||||
.service(
|
.service(
|
||||||
web::scope("/user")
|
web::scope("/user")
|
||||||
|
|
Loading…
Reference in a new issue