mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-25 13:51:19 +00:00
Merge branch 'main' into add_federation_worker_index
This commit is contained in:
commit
2dbaf67b7c
43 changed files with 672 additions and 125 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2627,6 +2627,7 @@ dependencies = [
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"sitemap-rs",
|
"sitemap-rs",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"totp-rs",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -2937,7 +2938,6 @@ dependencies = [
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"tokio",
|
"tokio",
|
||||||
"totp-rs",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"lemmy-js-client": "0.19.0-rc.3",
|
"lemmy-js-client": "0.19.0-rc.5",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
getComments,
|
getComments,
|
||||||
createComment,
|
createComment,
|
||||||
getCommunityByName,
|
getCommunityByName,
|
||||||
|
blockInstance,
|
||||||
waitUntil,
|
waitUntil,
|
||||||
delay,
|
delay,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
@ -338,3 +339,39 @@ test("Get community for different casing on domain", async () => {
|
||||||
.community_view;
|
.community_view;
|
||||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User blocks instance, communities are hidden", async () => {
|
||||||
|
// create community and post on beta
|
||||||
|
let communityRes = await createCommunity(beta);
|
||||||
|
expect(communityRes.community_view.community.name).toBeDefined();
|
||||||
|
let postRes = await createPost(
|
||||||
|
beta,
|
||||||
|
communityRes.community_view.community.id,
|
||||||
|
);
|
||||||
|
expect(postRes.post_view.post.id).toBeDefined();
|
||||||
|
|
||||||
|
// fetch post to alpha
|
||||||
|
let alphaPost = await resolvePost(alpha, postRes.post_view.post);
|
||||||
|
expect(alphaPost.post?.post).toBeDefined();
|
||||||
|
|
||||||
|
// post should be included in listing
|
||||||
|
let listing = await getPosts(alpha, "All");
|
||||||
|
let listing_ids = listing.posts.map(p => p.post.ap_id);
|
||||||
|
expect(listing_ids).toContain(postRes.post_view.post.ap_id);
|
||||||
|
|
||||||
|
// block the beta instance
|
||||||
|
await blockInstance(alpha, alphaPost.post!.community.instance_id, true);
|
||||||
|
|
||||||
|
// after blocking, post should not be in listing
|
||||||
|
let listing2 = await getPosts(alpha, "All");
|
||||||
|
let listing_ids2 = listing2.posts.map(p => p.post.ap_id);
|
||||||
|
expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1);
|
||||||
|
|
||||||
|
// unblock instance again
|
||||||
|
await blockInstance(alpha, alphaPost.post!.community.instance_id, false);
|
||||||
|
|
||||||
|
// post should be included in listing
|
||||||
|
let listing3 = await getPosts(alpha, "All");
|
||||||
|
let listing_ids3 = listing3.posts.map(p => p.post.ap_id);
|
||||||
|
expect(listing_ids3).toContain(postRes.post_view.post.ap_id);
|
||||||
|
});
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import {
|
import {
|
||||||
|
BlockInstance,
|
||||||
|
BlockInstanceResponse,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
GetRepliesResponse,
|
GetRepliesResponse,
|
||||||
GetUnreadCount,
|
GetUnreadCount,
|
||||||
GetUnreadCountResponse,
|
GetUnreadCountResponse,
|
||||||
|
InstanceId,
|
||||||
LemmyHttp,
|
LemmyHttp,
|
||||||
PostView,
|
PostView,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
|
@ -838,6 +841,19 @@ export function getPosts(
|
||||||
return api.client.getPosts(form);
|
return api.client.getPosts(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function blockInstance(
|
||||||
|
api: API,
|
||||||
|
instance_id: InstanceId,
|
||||||
|
block: boolean,
|
||||||
|
): Promise<BlockInstanceResponse> {
|
||||||
|
let form: BlockInstance = {
|
||||||
|
instance_id,
|
||||||
|
block,
|
||||||
|
auth: api.auth,
|
||||||
|
};
|
||||||
|
return api.client.blockInstance(form);
|
||||||
|
}
|
||||||
|
|
||||||
export function delay(millis = 500) {
|
export function delay(millis = 500) {
|
||||||
return new Promise(resolve => setTimeout(resolve, millis));
|
return new Promise(resolve => setTimeout(resolve, millis));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2174,10 +2174,10 @@ kleur@^3.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
||||||
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||||
|
|
||||||
lemmy-js-client@0.19.0-rc.3:
|
lemmy-js-client@0.19.0-rc.5:
|
||||||
version "0.19.0-rc.3"
|
version "0.19.0-rc.5"
|
||||||
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-rc.3.tgz#1efbfd5ce492319227a41cb020fc1cf9b2e7c075"
|
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-rc.5.tgz#a0d3e0ac829b1e46124edcf3a0343ae04317d51a"
|
||||||
integrity sha512-RmibQ3+YTvqsQ89II2I29pfPmVAWiSObGAU9Nc/AGYfyvaCya7f5+TirKwHdKA2eWDWLOTnD4rm6WgcgAwvhWw==
|
integrity sha512-Z1T95Ht1VZNvWlLH9XpVnO2oC7LhMT81jTiU5BhYPIkEtGhOwN91jk5uI1oyI6/d4v9lwbrsyzFYPsiuMmXTFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-fetch "^3.1.5"
|
cross-fetch "^3.1.5"
|
||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
|
|
|
@ -34,6 +34,7 @@ chrono = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
wav = "1.0.0"
|
wav = "1.0.0"
|
||||||
sitemap-rs = "0.2.0"
|
sitemap-rs = "0.2.0"
|
||||||
|
totp-rs = { version = "5.0.2", features = ["gen_secret", "otpauth"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
|
|
|
@ -2,11 +2,13 @@ use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine};
|
||||||
use captcha::Captcha;
|
use captcha::Captcha;
|
||||||
use lemmy_api_common::utils::local_site_to_slur_regex;
|
use lemmy_api_common::utils::local_site_to_slur_regex;
|
||||||
use lemmy_db_schema::source::local_site::LocalSite;
|
use lemmy_db_schema::source::local_site::LocalSite;
|
||||||
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||||
utils::slurs::check_slurs,
|
utils::slurs::check_slurs,
|
||||||
};
|
};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use totp_rs::{Secret, TOTP};
|
||||||
|
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod comment_report;
|
pub mod comment_report;
|
||||||
|
@ -67,11 +69,63 @@ pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Resul
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_totp_2fa_valid(
|
||||||
|
local_user_view: &LocalUserView,
|
||||||
|
totp_token: &Option<String>,
|
||||||
|
site_name: &str,
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
// Throw an error if their token is missing
|
||||||
|
let token = totp_token
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(LemmyErrorType::MissingTotpToken)?;
|
||||||
|
let secret = local_user_view
|
||||||
|
.local_user
|
||||||
|
.totp_2fa_secret
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(LemmyErrorType::MissingTotpSecret)?;
|
||||||
|
|
||||||
|
let totp = build_totp_2fa(site_name, &local_user_view.person.name, secret)?;
|
||||||
|
|
||||||
|
let check_passed = totp.check_current(token)?;
|
||||||
|
if !check_passed {
|
||||||
|
return Err(LemmyErrorType::IncorrectTotpToken.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generate_totp_2fa_secret() -> String {
|
||||||
|
Secret::generate_secret().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_totp_2fa(
|
||||||
|
site_name: &str,
|
||||||
|
username: &str,
|
||||||
|
secret: &str,
|
||||||
|
) -> Result<TOTP, LemmyError> {
|
||||||
|
let sec = Secret::Raw(secret.as_bytes().to_vec());
|
||||||
|
let sec_bytes = sec
|
||||||
|
.to_bytes()
|
||||||
|
.map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?;
|
||||||
|
|
||||||
|
TOTP::new(
|
||||||
|
totp_rs::Algorithm::SHA1,
|
||||||
|
6,
|
||||||
|
1,
|
||||||
|
30,
|
||||||
|
sec_bytes,
|
||||||
|
Some(site_name.to_string()),
|
||||||
|
username.to_string(),
|
||||||
|
)
|
||||||
|
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
use lemmy_api_common::utils::check_validator_time;
|
use lemmy_api_common::utils::check_validator_time;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
|
@ -134,4 +188,11 @@ mod tests {
|
||||||
let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
|
let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
|
||||||
assert_eq!(1, num_deleted);
|
assert_eq!(1, num_deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_totp() {
|
||||||
|
let generated_secret = generate_totp_2fa_secret();
|
||||||
|
let totp = build_totp_2fa("lemmy", "my_name", &generated_secret);
|
||||||
|
assert!(totp.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
47
crates/api/src/local_user/generate_totp_secret.rs
Normal file
47
crates/api/src/local_user/generate_totp_secret.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use crate::{build_totp_2fa, generate_totp_2fa_secret};
|
||||||
|
use activitypub_federation::config::Data;
|
||||||
|
use actix_web::web::Json;
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
person::GenerateTotpSecretResponse,
|
||||||
|
sensitive::Sensitive,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::local_user::{LocalUser, LocalUserUpdateForm},
|
||||||
|
traits::Crud,
|
||||||
|
};
|
||||||
|
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||||
|
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||||
|
|
||||||
|
/// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp]
|
||||||
|
/// to enable it. This can only be called if 2FA is currently disabled.
|
||||||
|
#[tracing::instrument(skip(context))]
|
||||||
|
pub async fn generate_totp_secret(
|
||||||
|
local_user_view: LocalUserView,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
) -> Result<Json<GenerateTotpSecretResponse>, LemmyError> {
|
||||||
|
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||||
|
|
||||||
|
if local_user_view.local_user.totp_2fa_enabled {
|
||||||
|
return Err(LemmyErrorType::TotpAlreadyEnabled)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret = generate_totp_2fa_secret();
|
||||||
|
let secret_url =
|
||||||
|
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
|
||||||
|
|
||||||
|
let local_user_form = LocalUserUpdateForm {
|
||||||
|
totp_2fa_secret: Some(Some(secret)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
LocalUser::update(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.local_user.id,
|
||||||
|
&local_user_form,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(GenerateTotpSecretResponse {
|
||||||
|
totp_secret_url: Sensitive::new(secret_url),
|
||||||
|
}))
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::check_totp_2fa_valid;
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::{Data, Json};
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
|
@ -9,7 +10,6 @@ use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
claims::Claims,
|
claims::Claims,
|
||||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
||||||
utils::validation::check_totp_2fa_valid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
|
@ -53,13 +53,10 @@ pub async fn login(
|
||||||
check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool())
|
check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Check the totp
|
// Check the totp if enabled
|
||||||
check_totp_2fa_valid(
|
if local_user_view.local_user.totp_2fa_enabled {
|
||||||
&local_user_view.local_user.totp_2fa_secret,
|
check_totp_2fa_valid(&local_user_view, &data.totp_2fa_token, &site_view.site.name)?;
|
||||||
&data.totp_2fa_token,
|
}
|
||||||
&site_view.site.name,
|
|
||||||
&local_user_view.person.name,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(Json(LoginResponse {
|
Ok(Json(LoginResponse {
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod ban_person;
|
||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod change_password;
|
pub mod change_password;
|
||||||
pub mod change_password_after_reset;
|
pub mod change_password_after_reset;
|
||||||
|
pub mod generate_totp_secret;
|
||||||
pub mod get_captcha;
|
pub mod get_captcha;
|
||||||
pub mod list_banned;
|
pub mod list_banned;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
@ -10,4 +11,5 @@ pub mod notifications;
|
||||||
pub mod report_count;
|
pub mod report_count;
|
||||||
pub mod reset_password;
|
pub mod reset_password;
|
||||||
pub mod save_settings;
|
pub mod save_settings;
|
||||||
|
pub mod update_totp;
|
||||||
pub mod verify_email;
|
pub mod verify_email;
|
||||||
|
|
|
@ -17,13 +17,7 @@ use lemmy_db_views::structs::SiteView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
claims::Claims,
|
claims::Claims,
|
||||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
||||||
utils::validation::{
|
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
|
||||||
build_totp_2fa,
|
|
||||||
generate_totp_2fa_secret,
|
|
||||||
is_valid_bio_field,
|
|
||||||
is_valid_display_name,
|
|
||||||
is_valid_matrix_id,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
|
@ -105,20 +99,6 @@ pub async fn save_user_settings(
|
||||||
LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?;
|
LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If generate_totp is Some(false), this will clear it out from the database.
|
|
||||||
let (totp_2fa_secret, totp_2fa_url) = if let Some(generate) = data.generate_totp_2fa {
|
|
||||||
if generate {
|
|
||||||
let secret = generate_totp_2fa_secret();
|
|
||||||
let url =
|
|
||||||
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
|
|
||||||
(Some(Some(secret)), Some(Some(url)))
|
|
||||||
} else {
|
|
||||||
(Some(None), Some(None))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let local_user_form = LocalUserUpdateForm {
|
let local_user_form = LocalUserUpdateForm {
|
||||||
email,
|
email,
|
||||||
show_avatars: data.show_avatars,
|
show_avatars: data.show_avatars,
|
||||||
|
@ -134,8 +114,6 @@ pub async fn save_user_settings(
|
||||||
default_listing_type,
|
default_listing_type,
|
||||||
theme,
|
theme,
|
||||||
interface_language: data.interface_language.clone(),
|
interface_language: data.interface_language.clone(),
|
||||||
totp_2fa_secret,
|
|
||||||
totp_2fa_url,
|
|
||||||
open_links_in_new_tab: data.open_links_in_new_tab,
|
open_links_in_new_tab: data.open_links_in_new_tab,
|
||||||
infinite_scroll_enabled: data.infinite_scroll_enabled,
|
infinite_scroll_enabled: data.infinite_scroll_enabled,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
54
crates/api/src/local_user/update_totp.rs
Normal file
54
crates/api/src/local_user/update_totp.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crate::check_totp_2fa_valid;
|
||||||
|
use actix_web::web::{Data, Json};
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
person::{UpdateTotp, UpdateTotpResponse},
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::local_user::{LocalUser, LocalUserUpdateForm},
|
||||||
|
traits::Crud,
|
||||||
|
};
|
||||||
|
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||||
|
use lemmy_utils::error::LemmyError;
|
||||||
|
|
||||||
|
/// Enable or disable two-factor-authentication. The current setting is determined from
|
||||||
|
/// [LocalUser.totp_2fa_enabled].
|
||||||
|
///
|
||||||
|
/// To enable, you need to first call [generate_totp_secret] and then pass a valid token to this
|
||||||
|
/// function.
|
||||||
|
///
|
||||||
|
/// Disabling is only possible if 2FA was previously enabled. Again it is necessary to pass a valid
|
||||||
|
/// token.
|
||||||
|
#[tracing::instrument(skip(context))]
|
||||||
|
pub async fn update_totp(
|
||||||
|
data: Json<UpdateTotp>,
|
||||||
|
local_user_view: LocalUserView,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
) -> Result<Json<UpdateTotpResponse>, LemmyError> {
|
||||||
|
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||||
|
|
||||||
|
check_totp_2fa_valid(
|
||||||
|
&local_user_view,
|
||||||
|
&Some(data.totp_token.clone()),
|
||||||
|
&site_view.site.name,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// toggle the 2fa setting
|
||||||
|
let local_user_form = LocalUserUpdateForm {
|
||||||
|
totp_2fa_enabled: Some(data.enabled),
|
||||||
|
// if totp is enabled, leave unchanged. otherwise clear secret
|
||||||
|
totp_2fa_secret: if data.enabled { None } else { Some(None) },
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
LocalUser::update(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.local_user.id,
|
||||||
|
&local_user_form,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(UpdateTotpResponse {
|
||||||
|
enabled: data.enabled,
|
||||||
|
}))
|
||||||
|
}
|
41
crates/api/src/site/block.rs
Normal file
41
crates/api/src/site/block.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use activitypub_federation::config::Data;
|
||||||
|
use actix_web::web::Json;
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
site::{BlockInstance, BlockInstanceResponse},
|
||||||
|
utils::local_user_view_from_jwt,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::instance_block::{InstanceBlock, InstanceBlockForm},
|
||||||
|
traits::Blockable,
|
||||||
|
};
|
||||||
|
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(context))]
|
||||||
|
pub async fn block_instance(
|
||||||
|
data: Json<BlockInstance>,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
) -> Result<Json<BlockInstanceResponse>, LemmyError> {
|
||||||
|
let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
|
||||||
|
|
||||||
|
let instance_id = data.instance_id;
|
||||||
|
let person_id = local_user_view.person.id;
|
||||||
|
let instance_block_form = InstanceBlockForm {
|
||||||
|
person_id,
|
||||||
|
instance_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if data.block {
|
||||||
|
InstanceBlock::block(&mut context.pool(), &instance_block_form)
|
||||||
|
.await
|
||||||
|
.with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?;
|
||||||
|
} else {
|
||||||
|
InstanceBlock::unblock(&mut context.pool(), &instance_block_form)
|
||||||
|
.await
|
||||||
|
.with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(BlockInstanceResponse {
|
||||||
|
blocked: data.block,
|
||||||
|
}))
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod block;
|
||||||
pub mod federated_instances;
|
pub mod federated_instances;
|
||||||
pub mod leave_admin;
|
pub mod leave_admin;
|
||||||
pub mod mod_log;
|
pub mod mod_log;
|
||||||
|
|
|
@ -119,10 +119,6 @@ pub struct SaveUserSettings {
|
||||||
pub show_new_post_notifs: Option<bool>,
|
pub show_new_post_notifs: Option<bool>,
|
||||||
/// A list of languages you are able to see discussion in.
|
/// A list of languages you are able to see discussion in.
|
||||||
pub discussion_languages: Option<Vec<LanguageId>>,
|
pub discussion_languages: Option<Vec<LanguageId>>,
|
||||||
/// Generates a TOTP / 2-factor authentication token.
|
|
||||||
///
|
|
||||||
/// None leaves it as is, true will generate or regenerate it, false clears it out.
|
|
||||||
pub generate_totp_2fa: Option<bool>,
|
|
||||||
pub auth: Sensitive<String>,
|
pub auth: Sensitive<String>,
|
||||||
/// Open links in a new tab
|
/// Open links in a new tab
|
||||||
pub open_links_in_new_tab: Option<bool>,
|
pub open_links_in_new_tab: Option<bool>,
|
||||||
|
@ -443,3 +439,25 @@ pub struct VerifyEmail {
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
/// A response to verifying your email.
|
/// A response to verifying your email.
|
||||||
pub struct VerifyEmailResponse {}
|
pub struct VerifyEmailResponse {}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct GenerateTotpSecretResponse {
|
||||||
|
pub totp_secret_url: Sensitive<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct UpdateTotp {
|
||||||
|
pub totp_token: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct UpdateTotpResponse {
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ pub struct GetPosts {
|
||||||
pub page_cursor: Option<PaginationCursor>,
|
pub page_cursor: Option<PaginationCursor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[cfg_attr(feature = "full", derive(TS))]
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::sensitive::Sensitive;
|
use crate::sensitive::Sensitive;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::{CommentId, CommunityId, LanguageId, PersonId, PostId},
|
newtypes::{CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId},
|
||||||
source::{instance::Instance, language::Language, tagline::Tagline},
|
source::{instance::Instance, language::Language, tagline::Tagline},
|
||||||
ListingType,
|
ListingType,
|
||||||
ModlogActionType,
|
ModlogActionType,
|
||||||
|
@ -21,6 +21,7 @@ use lemmy_db_views_actor::structs::{
|
||||||
CommunityFollowerView,
|
CommunityFollowerView,
|
||||||
CommunityModeratorView,
|
CommunityModeratorView,
|
||||||
CommunityView,
|
CommunityView,
|
||||||
|
InstanceBlockView,
|
||||||
PersonBlockView,
|
PersonBlockView,
|
||||||
PersonView,
|
PersonView,
|
||||||
};
|
};
|
||||||
|
@ -320,6 +321,7 @@ pub struct MyUserInfo {
|
||||||
pub follows: Vec<CommunityFollowerView>,
|
pub follows: Vec<CommunityFollowerView>,
|
||||||
pub moderates: Vec<CommunityModeratorView>,
|
pub moderates: Vec<CommunityModeratorView>,
|
||||||
pub community_blocks: Vec<CommunityBlockView>,
|
pub community_blocks: Vec<CommunityBlockView>,
|
||||||
|
pub instance_blocks: Vec<InstanceBlockView>,
|
||||||
pub person_blocks: Vec<PersonBlockView>,
|
pub person_blocks: Vec<PersonBlockView>,
|
||||||
pub discussion_languages: Vec<LanguageId>,
|
pub discussion_languages: Vec<LanguageId>,
|
||||||
}
|
}
|
||||||
|
@ -450,3 +452,21 @@ pub struct GetUnreadRegistrationApplicationCount {
|
||||||
pub struct GetUnreadRegistrationApplicationCountResponse {
|
pub struct GetUnreadRegistrationApplicationCountResponse {
|
||||||
pub registration_applications: i64,
|
pub registration_applications: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
/// Block an instance as user
|
||||||
|
pub struct BlockInstance {
|
||||||
|
pub instance_id: InstanceId,
|
||||||
|
pub block: bool,
|
||||||
|
pub auth: Sensitive<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct BlockInstanceResponse {
|
||||||
|
pub blocked: bool,
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ use lemmy_db_views_actor::structs::{
|
||||||
CommunityBlockView,
|
CommunityBlockView,
|
||||||
CommunityFollowerView,
|
CommunityFollowerView,
|
||||||
CommunityModeratorView,
|
CommunityModeratorView,
|
||||||
|
InstanceBlockView,
|
||||||
PersonBlockView,
|
PersonBlockView,
|
||||||
PersonView,
|
PersonView,
|
||||||
};
|
};
|
||||||
|
@ -52,6 +53,10 @@ pub async fn get_site(
|
||||||
.await
|
.await
|
||||||
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
||||||
|
|
||||||
|
let instance_blocks = InstanceBlockView::for_person(&mut context.pool(), person_id)
|
||||||
|
.await
|
||||||
|
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
||||||
|
|
||||||
let person_id = local_user_view.person.id;
|
let person_id = local_user_view.person.id;
|
||||||
let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
|
let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
|
||||||
.await
|
.await
|
||||||
|
@ -70,6 +75,7 @@ pub async fn get_site(
|
||||||
follows,
|
follows,
|
||||||
moderates,
|
moderates,
|
||||||
community_blocks,
|
community_blocks,
|
||||||
|
instance_blocks,
|
||||||
person_blocks,
|
person_blocks,
|
||||||
discussion_languages,
|
discussion_languages,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::newtypes::{CommentId, CommunityId, PersonId, PostId, SiteId};
|
use crate::newtypes::{CommentId, CommunityId, InstanceId, PersonId, PostId, SiteId};
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use crate::schema::{
|
use crate::schema::{
|
||||||
comment_aggregates,
|
comment_aggregates,
|
||||||
|
@ -100,6 +100,7 @@ pub struct PostAggregates {
|
||||||
pub community_id: CommunityId,
|
pub community_id: CommunityId,
|
||||||
pub creator_id: PersonId,
|
pub creator_id: PersonId,
|
||||||
pub controversy_rank: f64,
|
pub controversy_rank: f64,
|
||||||
|
pub instance_id: InstanceId,
|
||||||
/// A rank that amplifies smaller communities
|
/// A rank that amplifies smaller communities
|
||||||
pub scaled_rank: f64,
|
pub scaled_rank: f64,
|
||||||
}
|
}
|
||||||
|
|
36
crates/db_schema/src/impls/instance_block.rs
Normal file
36
crates/db_schema/src/impls/instance_block.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::{
|
||||||
|
schema::instance_block::dsl::{instance_block, instance_id, person_id},
|
||||||
|
source::instance_block::{InstanceBlock, InstanceBlockForm},
|
||||||
|
traits::Blockable,
|
||||||
|
utils::{get_conn, DbPool},
|
||||||
|
};
|
||||||
|
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Blockable for InstanceBlock {
|
||||||
|
type Form = InstanceBlockForm;
|
||||||
|
async fn block(pool: &mut DbPool<'_>, instance_block_form: &Self::Form) -> Result<Self, Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
insert_into(instance_block)
|
||||||
|
.values(instance_block_form)
|
||||||
|
.on_conflict((person_id, instance_id))
|
||||||
|
.do_update()
|
||||||
|
.set(instance_block_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
async fn unblock(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
instance_block_form: &Self::Form,
|
||||||
|
) -> Result<usize, Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
diesel::delete(
|
||||||
|
instance_block
|
||||||
|
.filter(person_id.eq(instance_block_form.person_id))
|
||||||
|
.filter(instance_id.eq(instance_block_form.instance_id)),
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ pub mod federation_allowlist;
|
||||||
pub mod federation_blocklist;
|
pub mod federation_blocklist;
|
||||||
pub mod image_upload;
|
pub mod image_upload;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
|
pub mod instance_block;
|
||||||
pub mod language;
|
pub mod language;
|
||||||
pub mod local_site;
|
pub mod local_site;
|
||||||
pub mod local_site_rate_limit;
|
pub mod local_site_rate_limit;
|
||||||
|
|
|
@ -337,6 +337,15 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
instance_block (id) {
|
||||||
|
id -> Int4,
|
||||||
|
person_id -> Int4,
|
||||||
|
instance_id -> Int4,
|
||||||
|
published -> Timestamptz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
language (id) {
|
language (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
@ -427,13 +436,13 @@ diesel::table! {
|
||||||
email_verified -> Bool,
|
email_verified -> Bool,
|
||||||
accepted_application -> Bool,
|
accepted_application -> Bool,
|
||||||
totp_2fa_secret -> Nullable<Text>,
|
totp_2fa_secret -> Nullable<Text>,
|
||||||
totp_2fa_url -> Nullable<Text>,
|
|
||||||
open_links_in_new_tab -> Bool,
|
open_links_in_new_tab -> Bool,
|
||||||
blur_nsfw -> Bool,
|
blur_nsfw -> Bool,
|
||||||
auto_expand -> Bool,
|
auto_expand -> Bool,
|
||||||
infinite_scroll_enabled -> Bool,
|
infinite_scroll_enabled -> Bool,
|
||||||
admin -> Bool,
|
admin -> Bool,
|
||||||
post_listing_mode -> PostListingModeEnum,
|
post_listing_mode -> PostListingModeEnum,
|
||||||
|
totp_2fa_enabled -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -712,6 +721,7 @@ diesel::table! {
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
creator_id -> Int4,
|
creator_id -> Int4,
|
||||||
controversy_rank -> Float8,
|
controversy_rank -> Float8,
|
||||||
|
instance_id -> Int4,
|
||||||
scaled_rank -> Float8,
|
scaled_rank -> Float8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -928,6 +938,8 @@ diesel::joinable!(federation_allowlist -> instance (instance_id));
|
||||||
diesel::joinable!(federation_blocklist -> instance (instance_id));
|
diesel::joinable!(federation_blocklist -> instance (instance_id));
|
||||||
diesel::joinable!(federation_queue_state -> instance (instance_id));
|
diesel::joinable!(federation_queue_state -> instance (instance_id));
|
||||||
diesel::joinable!(image_upload -> local_user (local_user_id));
|
diesel::joinable!(image_upload -> local_user (local_user_id));
|
||||||
|
diesel::joinable!(instance_block -> instance (instance_id));
|
||||||
|
diesel::joinable!(instance_block -> person (person_id));
|
||||||
diesel::joinable!(local_site -> site (site_id));
|
diesel::joinable!(local_site -> site (site_id));
|
||||||
diesel::joinable!(local_site_rate_limit -> local_site (local_site_id));
|
diesel::joinable!(local_site_rate_limit -> local_site (local_site_id));
|
||||||
diesel::joinable!(local_user -> person (person_id));
|
diesel::joinable!(local_user -> person (person_id));
|
||||||
|
@ -960,6 +972,7 @@ diesel::joinable!(post -> community (community_id));
|
||||||
diesel::joinable!(post -> language (language_id));
|
diesel::joinable!(post -> language (language_id));
|
||||||
diesel::joinable!(post -> person (creator_id));
|
diesel::joinable!(post -> person (creator_id));
|
||||||
diesel::joinable!(post_aggregates -> community (community_id));
|
diesel::joinable!(post_aggregates -> community (community_id));
|
||||||
|
diesel::joinable!(post_aggregates -> instance (instance_id));
|
||||||
diesel::joinable!(post_aggregates -> person (creator_id));
|
diesel::joinable!(post_aggregates -> person (creator_id));
|
||||||
diesel::joinable!(post_aggregates -> post (post_id));
|
diesel::joinable!(post_aggregates -> post (post_id));
|
||||||
diesel::joinable!(post_like -> person (person_id));
|
diesel::joinable!(post_like -> person (person_id));
|
||||||
|
@ -1005,6 +1018,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
federation_queue_state,
|
federation_queue_state,
|
||||||
image_upload,
|
image_upload,
|
||||||
instance,
|
instance,
|
||||||
|
instance_block,
|
||||||
language,
|
language,
|
||||||
local_site,
|
local_site,
|
||||||
local_site_rate_limit,
|
local_site_rate_limit,
|
||||||
|
|
26
crates/db_schema/src/source/instance_block.rs
Normal file
26
crates/db_schema/src/source/instance_block.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::newtypes::{InstanceId, PersonId};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
use crate::schema::instance_block;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "full",
|
||||||
|
diesel(belongs_to(crate::source::instance::Instance))
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = instance_block))]
|
||||||
|
pub struct InstanceBlock {
|
||||||
|
pub id: i32,
|
||||||
|
pub person_id: PersonId,
|
||||||
|
pub instance_id: InstanceId,
|
||||||
|
pub published: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = instance_block))]
|
||||||
|
pub struct InstanceBlockForm {
|
||||||
|
pub person_id: PersonId,
|
||||||
|
pub instance_id: InstanceId,
|
||||||
|
}
|
|
@ -51,8 +51,6 @@ pub struct LocalUser {
|
||||||
pub accepted_application: bool,
|
pub accepted_application: bool,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub totp_2fa_secret: Option<String>,
|
pub totp_2fa_secret: Option<String>,
|
||||||
/// A URL to add their 2-factor auth.
|
|
||||||
pub totp_2fa_url: Option<String>,
|
|
||||||
/// Open links in a new tab.
|
/// Open links in a new tab.
|
||||||
pub open_links_in_new_tab: bool,
|
pub open_links_in_new_tab: bool,
|
||||||
pub blur_nsfw: bool,
|
pub blur_nsfw: bool,
|
||||||
|
@ -62,6 +60,7 @@ pub struct LocalUser {
|
||||||
/// Whether the person is an admin.
|
/// Whether the person is an admin.
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub post_listing_mode: PostListingMode,
|
pub post_listing_mode: PostListingMode,
|
||||||
|
pub totp_2fa_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, TypedBuilder)]
|
#[derive(Clone, TypedBuilder)]
|
||||||
|
@ -88,13 +87,13 @@ pub struct LocalUserInsertForm {
|
||||||
pub email_verified: Option<bool>,
|
pub email_verified: Option<bool>,
|
||||||
pub accepted_application: Option<bool>,
|
pub accepted_application: Option<bool>,
|
||||||
pub totp_2fa_secret: Option<Option<String>>,
|
pub totp_2fa_secret: Option<Option<String>>,
|
||||||
pub totp_2fa_url: Option<Option<String>>,
|
|
||||||
pub open_links_in_new_tab: Option<bool>,
|
pub open_links_in_new_tab: Option<bool>,
|
||||||
pub blur_nsfw: Option<bool>,
|
pub blur_nsfw: Option<bool>,
|
||||||
pub auto_expand: Option<bool>,
|
pub auto_expand: Option<bool>,
|
||||||
pub infinite_scroll_enabled: Option<bool>,
|
pub infinite_scroll_enabled: Option<bool>,
|
||||||
pub admin: Option<bool>,
|
pub admin: Option<bool>,
|
||||||
pub post_listing_mode: Option<PostListingMode>,
|
pub post_listing_mode: Option<PostListingMode>,
|
||||||
|
pub totp_2fa_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
@ -117,11 +116,11 @@ pub struct LocalUserUpdateForm {
|
||||||
pub email_verified: Option<bool>,
|
pub email_verified: Option<bool>,
|
||||||
pub accepted_application: Option<bool>,
|
pub accepted_application: Option<bool>,
|
||||||
pub totp_2fa_secret: Option<Option<String>>,
|
pub totp_2fa_secret: Option<Option<String>>,
|
||||||
pub totp_2fa_url: Option<Option<String>>,
|
|
||||||
pub open_links_in_new_tab: Option<bool>,
|
pub open_links_in_new_tab: Option<bool>,
|
||||||
pub blur_nsfw: Option<bool>,
|
pub blur_nsfw: Option<bool>,
|
||||||
pub auto_expand: Option<bool>,
|
pub auto_expand: Option<bool>,
|
||||||
pub infinite_scroll_enabled: Option<bool>,
|
pub infinite_scroll_enabled: Option<bool>,
|
||||||
pub admin: Option<bool>,
|
pub admin: Option<bool>,
|
||||||
pub post_listing_mode: Option<PostListingMode>,
|
pub post_listing_mode: Option<PostListingMode>,
|
||||||
|
pub totp_2fa_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub mod federation_allowlist;
|
||||||
pub mod federation_blocklist;
|
pub mod federation_blocklist;
|
||||||
pub mod image_upload;
|
pub mod image_upload;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
|
pub mod instance_block;
|
||||||
pub mod language;
|
pub mod language;
|
||||||
pub mod local_site;
|
pub mod local_site;
|
||||||
pub mod local_site_rate_limit;
|
pub mod local_site_rate_limit;
|
||||||
|
|
|
@ -23,6 +23,7 @@ use lemmy_db_schema::{
|
||||||
community_follower,
|
community_follower,
|
||||||
community_moderator,
|
community_moderator,
|
||||||
community_person_ban,
|
community_person_ban,
|
||||||
|
instance_block,
|
||||||
local_user_language,
|
local_user_language,
|
||||||
person,
|
person,
|
||||||
person_block,
|
person_block,
|
||||||
|
@ -121,6 +122,13 @@ fn queries<'a>() -> Queries<
|
||||||
let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
|
let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
|
||||||
|
|
||||||
let mut query = all_joins(comment::table.into_boxed(), person_id)
|
let mut query = all_joins(comment::table.into_boxed(), person_id)
|
||||||
|
.left_join(
|
||||||
|
instance_block::table.on(
|
||||||
|
community::instance_id
|
||||||
|
.eq(instance_block::instance_id)
|
||||||
|
.and(instance_block::person_id.eq(person_id_join)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.left_join(
|
.left_join(
|
||||||
community_block::table.on(
|
community_block::table.on(
|
||||||
community::id
|
community::id
|
||||||
|
@ -221,6 +229,7 @@ fn queries<'a>() -> Queries<
|
||||||
|
|
||||||
// Don't show blocked communities or persons
|
// Don't show blocked communities or persons
|
||||||
if options.post_id.is_none() {
|
if options.post_id.is_none() {
|
||||||
|
query = query.filter(instance_block::person_id.is_null());
|
||||||
query = query.filter(community_block::person_id.is_null());
|
query = query.filter(community_block::person_id.is_null());
|
||||||
}
|
}
|
||||||
query = query.filter(person_block::person_id.is_null());
|
query = query.filter(person_block::person_id.is_null());
|
||||||
|
|
|
@ -28,6 +28,7 @@ use lemmy_db_schema::{
|
||||||
community_follower,
|
community_follower,
|
||||||
community_moderator,
|
community_moderator,
|
||||||
community_person_ban,
|
community_person_ban,
|
||||||
|
instance_block,
|
||||||
local_user_language,
|
local_user_language,
|
||||||
person,
|
person,
|
||||||
person_block,
|
person_block,
|
||||||
|
@ -420,7 +421,7 @@ fn queries<'a>() -> Queries<
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Don't show blocked communities or persons
|
// Don't show blocked instances, communities or persons
|
||||||
query = query.filter(not(exists(
|
query = query.filter(not(exists(
|
||||||
community_block::table.filter(
|
community_block::table.filter(
|
||||||
post_aggregates::community_id
|
post_aggregates::community_id
|
||||||
|
@ -428,6 +429,13 @@ fn queries<'a>() -> Queries<
|
||||||
.and(community_block::person_id.eq(person_id_join)),
|
.and(community_block::person_id.eq(person_id_join)),
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
|
query = query.filter(not(exists(
|
||||||
|
instance_block::table.filter(
|
||||||
|
post_aggregates::instance_id
|
||||||
|
.eq(instance_block::instance_id)
|
||||||
|
.and(instance_block::person_id.eq(person_id_join)),
|
||||||
|
),
|
||||||
|
)));
|
||||||
query = query.filter(not(is_creator_blocked(person_id)));
|
query = query.filter(not(is_creator_blocked(person_id)));
|
||||||
}
|
}
|
||||||
let now = diesel::dsl::now.into_sql::<Timestamptz>();
|
let now = diesel::dsl::now.into_sql::<Timestamptz>();
|
||||||
|
@ -706,6 +714,7 @@ mod tests {
|
||||||
community::{Community, CommunityInsertForm},
|
community::{Community, CommunityInsertForm},
|
||||||
community_block::{CommunityBlock, CommunityBlockForm},
|
community_block::{CommunityBlock, CommunityBlockForm},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
|
instance_block::{InstanceBlock, InstanceBlockForm},
|
||||||
language::Language,
|
language::Language,
|
||||||
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
|
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
|
||||||
person::{Person, PersonInsertForm},
|
person::{Person, PersonInsertForm},
|
||||||
|
@ -1239,6 +1248,84 @@ mod tests {
|
||||||
cleanup(data, pool).await;
|
cleanup(data, pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn post_listing_instance_block() {
|
||||||
|
let pool = &build_db_pool_for_tests().await;
|
||||||
|
let pool = &mut pool.into();
|
||||||
|
let data = init_data(pool).await;
|
||||||
|
|
||||||
|
let blocked_instance = Instance::read_or_create(pool, "another_domain.tld".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let community_form = CommunityInsertForm::builder()
|
||||||
|
.name("test_community_4".to_string())
|
||||||
|
.title("none".to_owned())
|
||||||
|
.public_key("pubkey".to_string())
|
||||||
|
.instance_id(blocked_instance.id)
|
||||||
|
.build();
|
||||||
|
let inserted_community = Community::create(pool, &community_form).await.unwrap();
|
||||||
|
|
||||||
|
let post_form = PostInsertForm::builder()
|
||||||
|
.name("blocked instance post".to_string())
|
||||||
|
.creator_id(data.inserted_bot.id)
|
||||||
|
.community_id(inserted_community.id)
|
||||||
|
.language_id(Some(LanguageId(1)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let post_from_blocked_instance = Post::create(pool, &post_form).await.unwrap();
|
||||||
|
|
||||||
|
// no instance block, should return all posts
|
||||||
|
let post_listings_all = PostQuery {
|
||||||
|
local_user: Some(&data.local_user_view),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(post_listings_all.len(), 3);
|
||||||
|
|
||||||
|
// block the instance
|
||||||
|
let block_form = InstanceBlockForm {
|
||||||
|
person_id: data.local_user_view.person.id,
|
||||||
|
instance_id: blocked_instance.id,
|
||||||
|
};
|
||||||
|
InstanceBlock::block(pool, &block_form).await.unwrap();
|
||||||
|
|
||||||
|
// now posts from communities on that instance should be hidden
|
||||||
|
let post_listings_blocked = PostQuery {
|
||||||
|
local_user: Some(&data.local_user_view),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(post_listings_blocked.len(), 2);
|
||||||
|
assert_ne!(
|
||||||
|
post_listings_blocked[0].post.id,
|
||||||
|
post_from_blocked_instance.id
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
post_listings_blocked[1].post.id,
|
||||||
|
post_from_blocked_instance.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// after unblocking it should return all posts again
|
||||||
|
InstanceBlock::unblock(pool, &block_form).await.unwrap();
|
||||||
|
let post_listings_blocked = PostQuery {
|
||||||
|
local_user: Some(&data.local_user_view),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(post_listings_blocked.len(), 3);
|
||||||
|
|
||||||
|
Instance::delete(pool, blocked_instance.id).await.unwrap();
|
||||||
|
cleanup(data, pool).await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
|
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
|
||||||
let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
|
let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
|
||||||
Community::delete(pool, data.inserted_community.id)
|
Community::delete(pool, data.inserted_community.id)
|
||||||
|
@ -1359,6 +1446,7 @@ mod tests {
|
||||||
scaled_rank: 0.3621,
|
scaled_rank: 0.3621,
|
||||||
community_id: inserted_post.community_id,
|
community_id: inserted_post.community_id,
|
||||||
creator_id: inserted_post.creator_id,
|
creator_id: inserted_post.creator_id,
|
||||||
|
instance_id: data.inserted_instance.id,
|
||||||
},
|
},
|
||||||
subscribed: SubscribedType::NotSubscribed,
|
subscribed: SubscribedType::NotSubscribed,
|
||||||
read: false,
|
read: false,
|
||||||
|
|
|
@ -262,12 +262,12 @@ mod tests {
|
||||||
email_verified: inserted_sara_local_user.email_verified,
|
email_verified: inserted_sara_local_user.email_verified,
|
||||||
accepted_application: inserted_sara_local_user.accepted_application,
|
accepted_application: inserted_sara_local_user.accepted_application,
|
||||||
totp_2fa_secret: inserted_sara_local_user.totp_2fa_secret,
|
totp_2fa_secret: inserted_sara_local_user.totp_2fa_secret,
|
||||||
totp_2fa_url: inserted_sara_local_user.totp_2fa_url,
|
|
||||||
password_encrypted: inserted_sara_local_user.password_encrypted,
|
password_encrypted: inserted_sara_local_user.password_encrypted,
|
||||||
open_links_in_new_tab: inserted_sara_local_user.open_links_in_new_tab,
|
open_links_in_new_tab: inserted_sara_local_user.open_links_in_new_tab,
|
||||||
infinite_scroll_enabled: inserted_sara_local_user.infinite_scroll_enabled,
|
infinite_scroll_enabled: inserted_sara_local_user.infinite_scroll_enabled,
|
||||||
admin: false,
|
admin: false,
|
||||||
post_listing_mode: inserted_sara_local_user.post_listing_mode,
|
post_listing_mode: inserted_sara_local_user.post_listing_mode,
|
||||||
|
totp_2fa_enabled: inserted_sara_local_user.totp_2fa_enabled,
|
||||||
},
|
},
|
||||||
creator: Person {
|
creator: Person {
|
||||||
id: inserted_sara_person.id,
|
id: inserted_sara_person.id,
|
||||||
|
|
|
@ -94,6 +94,7 @@ pub struct PostReportView {
|
||||||
/// (api users love to make assumptions (e.g. parse stuff that looks like numbers as numbers) about apis that aren't part of the spec
|
/// (api users love to make assumptions (e.g. parse stuff that looks like numbers as numbers) about apis that aren't part of the spec
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
pub struct PaginationCursor(pub(crate) String);
|
pub struct PaginationCursor(pub(crate) String);
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
|
|
|
@ -12,7 +12,14 @@ use diesel::{
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::{CommunityId, PersonId},
|
newtypes::{CommunityId, PersonId},
|
||||||
schema::{community, community_aggregates, community_block, community_follower, local_user},
|
schema::{
|
||||||
|
community,
|
||||||
|
community_aggregates,
|
||||||
|
community_block,
|
||||||
|
community_follower,
|
||||||
|
instance_block,
|
||||||
|
local_user,
|
||||||
|
},
|
||||||
source::{community::CommunityFollower, local_user::LocalUser},
|
source::{community::CommunityFollower, local_user::LocalUser},
|
||||||
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||||
ListingType,
|
ListingType,
|
||||||
|
@ -36,6 +43,13 @@ fn queries<'a>() -> Queries<
|
||||||
.and(community_follower::person_id.eq(person_id_join)),
|
.and(community_follower::person_id.eq(person_id_join)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.left_join(
|
||||||
|
instance_block::table.on(
|
||||||
|
community::instance_id
|
||||||
|
.eq(instance_block::instance_id)
|
||||||
|
.and(instance_block::person_id.eq(person_id_join)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.left_join(
|
.left_join(
|
||||||
community_block::table.on(
|
community_block::table.on(
|
||||||
community::id
|
community::id
|
||||||
|
@ -131,8 +145,10 @@ fn queries<'a>() -> Queries<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show blocked communities or nsfw communities if not enabled in profile
|
// Don't show blocked communities and communities on blocked instances. nsfw communities are
|
||||||
|
// also hidden (based on profile setting)
|
||||||
if options.local_user.is_some() {
|
if options.local_user.is_some() {
|
||||||
|
query = query.filter(instance_block::person_id.is_null());
|
||||||
query = query.filter(community_block::person_id.is_null());
|
query = query.filter(community_block::person_id.is_null());
|
||||||
query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true)));
|
query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true)));
|
||||||
} else {
|
} else {
|
||||||
|
|
27
crates/db_views_actor/src/instance_block_view.rs
Normal file
27
crates/db_views_actor/src/instance_block_view.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::structs::InstanceBlockView;
|
||||||
|
use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
newtypes::PersonId,
|
||||||
|
schema::{instance, instance_block, person, site},
|
||||||
|
utils::{get_conn, DbPool},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl InstanceBlockView {
|
||||||
|
pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result<Vec<Self>, Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
instance_block::table
|
||||||
|
.inner_join(person::table)
|
||||||
|
.inner_join(instance::table)
|
||||||
|
.left_join(site::table.on(site::instance_id.eq(instance::id)))
|
||||||
|
.select((
|
||||||
|
person::all_columns,
|
||||||
|
instance::all_columns,
|
||||||
|
site::all_columns.nullable(),
|
||||||
|
))
|
||||||
|
.filter(instance_block::person_id.eq(person_id))
|
||||||
|
.order_by(instance_block::published)
|
||||||
|
.load::<InstanceBlockView>(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ pub mod community_person_ban_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod community_view;
|
pub mod community_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
pub mod instance_block_view;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
pub mod person_block_view;
|
pub mod person_block_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod person_mention_view;
|
pub mod person_mention_view;
|
||||||
|
|
|
@ -6,9 +6,11 @@ use lemmy_db_schema::{
|
||||||
comment::Comment,
|
comment::Comment,
|
||||||
comment_reply::CommentReply,
|
comment_reply::CommentReply,
|
||||||
community::Community,
|
community::Community,
|
||||||
|
instance::Instance,
|
||||||
person::Person,
|
person::Person,
|
||||||
person_mention::PersonMention,
|
person_mention::PersonMention,
|
||||||
post::Post,
|
post::Post,
|
||||||
|
site::Site,
|
||||||
},
|
},
|
||||||
SubscribedType,
|
SubscribedType,
|
||||||
};
|
};
|
||||||
|
@ -26,6 +28,17 @@ pub struct CommunityBlockView {
|
||||||
pub community: Community,
|
pub community: Community,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS, Queryable))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
/// An instance block by a user.
|
||||||
|
pub struct InstanceBlockView {
|
||||||
|
pub person: Person,
|
||||||
|
pub instance: Instance,
|
||||||
|
pub site: Option<Site>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[cfg_attr(feature = "full", derive(TS, Queryable))]
|
#[cfg_attr(feature = "full", derive(TS, Queryable))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
|
|
@ -47,7 +47,6 @@ smart-default = "0.7.1"
|
||||||
jsonwebtoken = "8.3.0"
|
jsonwebtoken = "8.3.0"
|
||||||
lettre = { version = "0.10.4", features = ["tokio1", "tokio1-native-tls"] }
|
lettre = { version = "0.10.4", features = ["tokio1", "tokio1-native-tls"] }
|
||||||
markdown-it = "0.5.1"
|
markdown-it = "0.5.1"
|
||||||
totp-rs = { version = "5.0.2", features = ["gen_secret", "otpauth"] }
|
|
||||||
ts-rs = { workspace = true, optional = true }
|
ts-rs = { workspace = true, optional = true }
|
||||||
enum-map = "2.6"
|
enum-map = "2.6"
|
||||||
|
|
||||||
|
|
|
@ -159,8 +159,11 @@ pub enum LemmyErrorType {
|
||||||
InvalidBodyField,
|
InvalidBodyField,
|
||||||
BioLengthOverflow,
|
BioLengthOverflow,
|
||||||
MissingTotpToken,
|
MissingTotpToken,
|
||||||
|
MissingTotpSecret,
|
||||||
IncorrectTotpToken,
|
IncorrectTotpToken,
|
||||||
CouldntParseTotpSecret,
|
CouldntParseTotpSecret,
|
||||||
|
CouldntGenerateTotp,
|
||||||
|
TotpAlreadyEnabled,
|
||||||
CouldntLikeComment,
|
CouldntLikeComment,
|
||||||
CouldntSaveComment,
|
CouldntSaveComment,
|
||||||
CouldntCreateReport,
|
CouldntCreateReport,
|
||||||
|
@ -192,7 +195,6 @@ pub enum LemmyErrorType {
|
||||||
InvalidUrl,
|
InvalidUrl,
|
||||||
EmailSendFailed,
|
EmailSendFailed,
|
||||||
Slurs,
|
Slurs,
|
||||||
CouldntGenerateTotp,
|
|
||||||
CouldntFindObject,
|
CouldntFindObject,
|
||||||
RegistrationDenied(String),
|
RegistrationDenied(String),
|
||||||
FederationDisabled,
|
FederationDisabled,
|
||||||
|
@ -209,6 +211,7 @@ pub enum LemmyErrorType {
|
||||||
InvalidUrlScheme,
|
InvalidUrlScheme,
|
||||||
CouldntSendWebmention,
|
CouldntSendWebmention,
|
||||||
ContradictingFilters,
|
ContradictingFilters,
|
||||||
|
InstanceBlockAlreadyExists,
|
||||||
AuthCookieInsecure,
|
AuthCookieInsecure,
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult};
|
use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
use totp_rs::{Secret, TOTP};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
static VALID_ACTOR_NAME_REGEX: Lazy<Regex> =
|
static VALID_ACTOR_NAME_REGEX: Lazy<Regex> =
|
||||||
|
@ -238,54 +237,6 @@ pub fn clean_url_params(url: &Url) -> Url {
|
||||||
url_out
|
url_out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_totp_2fa_valid(
|
|
||||||
totp_secret: &Option<String>,
|
|
||||||
totp_token: &Option<String>,
|
|
||||||
site_name: &str,
|
|
||||||
username: &str,
|
|
||||||
) -> LemmyResult<()> {
|
|
||||||
// Check only if they have a totp_secret in the DB
|
|
||||||
if let Some(totp_secret) = totp_secret {
|
|
||||||
// Throw an error if their token is missing
|
|
||||||
let token = totp_token
|
|
||||||
.as_deref()
|
|
||||||
.ok_or(LemmyErrorType::MissingTotpToken)?;
|
|
||||||
|
|
||||||
let totp = build_totp_2fa(site_name, username, totp_secret)?;
|
|
||||||
|
|
||||||
let check_passed = totp.check_current(token)?;
|
|
||||||
if !check_passed {
|
|
||||||
Err(LemmyErrorType::IncorrectTotpToken.into())
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_totp_2fa_secret() -> String {
|
|
||||||
Secret::generate_secret().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_totp_2fa(site_name: &str, username: &str, secret: &str) -> Result<TOTP, LemmyError> {
|
|
||||||
let sec = Secret::Raw(secret.as_bytes().to_vec());
|
|
||||||
let sec_bytes = sec
|
|
||||||
.to_bytes()
|
|
||||||
.map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?;
|
|
||||||
|
|
||||||
TOTP::new(
|
|
||||||
totp_rs::Algorithm::SHA256,
|
|
||||||
6,
|
|
||||||
1,
|
|
||||||
30,
|
|
||||||
sec_bytes,
|
|
||||||
Some(site_name.to_string()),
|
|
||||||
username.to_string(),
|
|
||||||
)
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_site_visibility_valid(
|
pub fn check_site_visibility_valid(
|
||||||
current_private_instance: bool,
|
current_private_instance: bool,
|
||||||
current_federation_enabled: bool,
|
current_federation_enabled: bool,
|
||||||
|
@ -319,7 +270,6 @@ mod tests {
|
||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
use super::build_totp_2fa;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::LemmyErrorType,
|
error::LemmyErrorType,
|
||||||
utils::validation::{
|
utils::validation::{
|
||||||
|
@ -327,7 +277,6 @@ mod tests {
|
||||||
check_site_visibility_valid,
|
check_site_visibility_valid,
|
||||||
check_url_scheme,
|
check_url_scheme,
|
||||||
clean_url_params,
|
clean_url_params,
|
||||||
generate_totp_2fa_secret,
|
|
||||||
is_valid_actor_name,
|
is_valid_actor_name,
|
||||||
is_valid_bio_field,
|
is_valid_bio_field,
|
||||||
is_valid_display_name,
|
is_valid_display_name,
|
||||||
|
@ -400,13 +349,6 @@ mod tests {
|
||||||
assert!(is_valid_matrix_id("@dess:matrix.org t").is_err());
|
assert!(is_valid_matrix_id("@dess:matrix.org t").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_build_totp() {
|
|
||||||
let generated_secret = generate_totp_2fa_secret();
|
|
||||||
let totp = build_totp_2fa("lemmy", "my_name", &generated_secret);
|
|
||||||
assert!(totp.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_site_name() {
|
fn test_valid_site_name() {
|
||||||
let valid_names = [
|
let valid_names = [
|
||||||
|
|
|
@ -2,16 +2,18 @@ version: "3.7"
|
||||||
|
|
||||||
x-ui-default: &ui-default
|
x-ui-default: &ui-default
|
||||||
init: true
|
init: true
|
||||||
image: dessalines/lemmy-ui:0.18.1
|
image: dessalines/lemmy-ui:0.18.4
|
||||||
# assuming lemmy-ui is cloned besides lemmy directory
|
# assuming lemmy-ui is cloned besides lemmy directory
|
||||||
# build:
|
# build:
|
||||||
# context: ../../../lemmy-ui
|
# context: ../../../lemmy-ui
|
||||||
# dockerfile: dev.dockerfile
|
# dockerfile: dev.dockerfile
|
||||||
environment:
|
environment:
|
||||||
- LEMMY_UI_HTTPS=false
|
- LEMMY_UI_HTTPS=false
|
||||||
|
|
||||||
x-lemmy-default: &lemmy-default
|
x-lemmy-default: &lemmy-default
|
||||||
image: lemmy-federation:latest
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- RUST_BACKTRACE=1
|
- RUST_BACKTRACE=1
|
||||||
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
|
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
sudo docker-compose down
|
sudo docker compose down
|
||||||
|
|
||||||
sudo docker build ../../ --file ../Dockerfile -t lemmy-federation:latest
|
|
||||||
|
|
||||||
for Item in alpha beta gamma delta epsilon ; do
|
for Item in alpha beta gamma delta epsilon ; do
|
||||||
sudo mkdir -p volumes/pictrs_$Item
|
sudo mkdir -p volumes/pictrs_$Item
|
||||||
sudo chown -R 991:991 volumes/pictrs_$Item
|
sudo chown -R 991:991 volumes/pictrs_$Item
|
||||||
done
|
done
|
||||||
|
|
||||||
#sudo docker-compose pull --ignore-pull-failures || true
|
sudo docker compose up
|
||||||
sudo docker-compose up
|
|
||||||
|
|
21
migrations/2023-08-09-101305_user_instance_block/down.sql
Normal file
21
migrations/2023-08-09-101305_user_instance_block/down.sql
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
DROP TABLE instance_block;
|
||||||
|
|
||||||
|
ALTER TABLE post_aggregates
|
||||||
|
DROP COLUMN instance_id;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION post_aggregates_post ()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'INSERT') THEN
|
||||||
|
INSERT INTO post_aggregates (post_id, published, newest_comment_time, newest_comment_time_necro, community_id, creator_id)
|
||||||
|
VALUES (NEW.id, NEW.published, NEW.published, NEW.published, NEW.community_id, NEW.creator_id);
|
||||||
|
ELSIF (TG_OP = 'DELETE') THEN
|
||||||
|
DELETE FROM post_aggregates
|
||||||
|
WHERE post_id = OLD.id;
|
||||||
|
END IF;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
51
migrations/2023-08-09-101305_user_instance_block/up.sql
Normal file
51
migrations/2023-08-09-101305_user_instance_block/up.sql
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
CREATE TABLE instance_block (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
|
instance_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
|
published timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (person_id, instance_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE post_aggregates
|
||||||
|
ADD COLUMN instance_id integer REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION post_aggregates_post ()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'INSERT') THEN
|
||||||
|
INSERT INTO post_aggregates (post_id, published, newest_comment_time, newest_comment_time_necro, community_id, creator_id, instance_id)
|
||||||
|
SELECT
|
||||||
|
NEW.id,
|
||||||
|
NEW.published,
|
||||||
|
NEW.published,
|
||||||
|
NEW.published,
|
||||||
|
NEW.community_id,
|
||||||
|
NEW.creator_id,
|
||||||
|
community.instance_id
|
||||||
|
FROM
|
||||||
|
community
|
||||||
|
WHERE
|
||||||
|
NEW.community_id = community.id;
|
||||||
|
ELSIF (TG_OP = 'DELETE') THEN
|
||||||
|
DELETE FROM post_aggregates
|
||||||
|
WHERE post_id = OLD.id;
|
||||||
|
END IF;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
post_aggregates
|
||||||
|
SET
|
||||||
|
instance_id = community.instance_id
|
||||||
|
FROM
|
||||||
|
post
|
||||||
|
JOIN community ON post.community_id = community.id
|
||||||
|
WHERE
|
||||||
|
post.id = post_aggregates.post_id;
|
||||||
|
|
||||||
|
ALTER TABLE post_aggregates
|
||||||
|
ALTER COLUMN instance_id SET NOT NULL;
|
||||||
|
|
6
migrations/2023-09-11-110040_rework-2fa-setup/down.sql
Normal file
6
migrations/2023-09-11-110040_rework-2fa-setup/down.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
ALTER TABLE local_user
|
||||||
|
ADD COLUMN totp_2fa_url text;
|
||||||
|
|
||||||
|
ALTER TABLE local_user
|
||||||
|
DROP COLUMN totp_2fa_enabled;
|
||||||
|
|
6
migrations/2023-09-11-110040_rework-2fa-setup/up.sql
Normal file
6
migrations/2023-09-11-110040_rework-2fa-setup/up.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
ALTER TABLE local_user
|
||||||
|
DROP COLUMN totp_2fa_url;
|
||||||
|
|
||||||
|
ALTER TABLE local_user
|
||||||
|
ADD COLUMN totp_2fa_enabled boolean NOT NULL DEFAULT FALSE;
|
||||||
|
|
|
@ -20,6 +20,7 @@ use lemmy_api::{
|
||||||
block::block_person,
|
block::block_person,
|
||||||
change_password::change_password,
|
change_password::change_password,
|
||||||
change_password_after_reset::change_password_after_reset,
|
change_password_after_reset::change_password_after_reset,
|
||||||
|
generate_totp_secret::generate_totp_secret,
|
||||||
get_captcha::get_captcha,
|
get_captcha::get_captcha,
|
||||||
list_banned::list_banned_users,
|
list_banned::list_banned_users,
|
||||||
login::login,
|
login::login,
|
||||||
|
@ -34,6 +35,7 @@ use lemmy_api::{
|
||||||
report_count::report_count,
|
report_count::report_count,
|
||||||
reset_password::reset_password,
|
reset_password::reset_password,
|
||||||
save_settings::save_user_settings,
|
save_settings::save_user_settings,
|
||||||
|
update_totp::update_totp,
|
||||||
verify_email::verify_email,
|
verify_email::verify_email,
|
||||||
},
|
},
|
||||||
post::{
|
post::{
|
||||||
|
@ -56,6 +58,7 @@ use lemmy_api::{
|
||||||
resolve::resolve_pm_report,
|
resolve::resolve_pm_report,
|
||||||
},
|
},
|
||||||
site::{
|
site::{
|
||||||
|
block::block_instance,
|
||||||
federated_instances::get_federated_instances,
|
federated_instances::get_federated_instances,
|
||||||
leave_admin::leave_admin,
|
leave_admin::leave_admin,
|
||||||
mod_log::get_mod_log,
|
mod_log::get_mod_log,
|
||||||
|
@ -129,7 +132,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.route("", web::get().to(get_site))
|
.route("", web::get().to(get_site))
|
||||||
// Admin Actions
|
// Admin Actions
|
||||||
.route("", web::post().to(create_site))
|
.route("", web::post().to(create_site))
|
||||||
.route("", web::put().to(update_site)),
|
.route("", web::put().to(update_site))
|
||||||
|
.route("/block", web::post().to(block_instance)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/modlog")
|
web::resource("/modlog")
|
||||||
|
@ -285,7 +289,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.route("/report_count", web::get().to(report_count))
|
.route("/report_count", web::get().to(report_count))
|
||||||
.route("/unread_count", web::get().to(unread_count))
|
.route("/unread_count", web::get().to(unread_count))
|
||||||
.route("/verify_email", web::post().to(verify_email))
|
.route("/verify_email", web::post().to(verify_email))
|
||||||
.route("/leave_admin", web::post().to(leave_admin)),
|
.route("/leave_admin", web::post().to(leave_admin))
|
||||||
|
.route("/totp/generate", web::post().to(generate_totp_secret))
|
||||||
|
.route("/totp/update", web::post().to(update_totp)),
|
||||||
)
|
)
|
||||||
// Admin Actions
|
// Admin Actions
|
||||||
.service(
|
.service(
|
||||||
|
|
Loading…
Reference in a new issue