mirror of
https://github.com/LemmyNet/lemmy
synced 2024-11-10 06:54:12 +00:00
Merge remote-tracking branch 'origin/main' into fix_is_community_mod_check
This commit is contained in:
commit
92240b6b6f
85 changed files with 3229 additions and 4401 deletions
2078
Cargo.lock
generated
2078
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -36,7 +36,6 @@ opt-level = "z" # Optimize for size.
|
|||
debug = 0
|
||||
|
||||
[features]
|
||||
embed-pictrs = ["pict-rs"]
|
||||
json-log = ["tracing-subscriber/json"]
|
||||
default = []
|
||||
|
||||
|
@ -182,7 +181,6 @@ reqwest-middleware = { workspace = true }
|
|||
reqwest-tracing = { workspace = true }
|
||||
clokwerk = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
pict-rs = { version = "0.5.16", optional = true }
|
||||
rustls = { workspace = true }
|
||||
tokio.workspace = true
|
||||
actix-cors = "0.7.0"
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.5.0",
|
||||
"lemmy-js-client": "0.19.5-alpha.1",
|
||||
"lemmy-js-client": "0.20.0-alpha.4",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.5.4",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -502,10 +502,17 @@ test("Enforce site ban federation for local user", async () => {
|
|||
alpha,
|
||||
alphaPerson.person.id,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
expect(unBanAlpha.banned).toBe(false);
|
||||
|
||||
// existing alpha post should be restored on beta
|
||||
betaBanRes = await waitUntil(
|
||||
() => getPost(beta, searchBeta1.post.id),
|
||||
s => !s.post_view.post.removed,
|
||||
);
|
||||
expect(betaBanRes.post_view.post.removed).toBe(false);
|
||||
|
||||
// Login gets invalidated by ban, need to login again
|
||||
if (!alphaUserPerson) {
|
||||
throw "Missing alpha person";
|
||||
|
|
|
@ -419,13 +419,13 @@ export async function banPersonFromSite(
|
|||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
ban: boolean,
|
||||
remove_data: boolean,
|
||||
remove_or_restore_data: boolean,
|
||||
): Promise<BanPersonResponse> {
|
||||
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
|
||||
let form: BanPerson = {
|
||||
person_id,
|
||||
ban,
|
||||
remove_data,
|
||||
remove_or_restore_data,
|
||||
};
|
||||
return api.banPerson(form);
|
||||
}
|
||||
|
@ -434,13 +434,13 @@ export async function banPersonFromCommunity(
|
|||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
community_id: number,
|
||||
remove_data: boolean,
|
||||
remove_or_restore_data: boolean,
|
||||
ban: boolean,
|
||||
): Promise<BanFromCommunityResponse> {
|
||||
let form: BanFromCommunity = {
|
||||
person_id,
|
||||
community_id,
|
||||
remove_data: remove_data,
|
||||
remove_or_restore_data,
|
||||
ban,
|
||||
};
|
||||
return api.banFromCommunity(form);
|
||||
|
|
|
@ -4,7 +4,11 @@ use lemmy_api_common::{
|
|||
community::{BanFromCommunity, BanFromCommunityResponse},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community},
|
||||
utils::{
|
||||
check_community_mod_action,
|
||||
check_expire_time,
|
||||
remove_or_restore_user_data_in_community,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -33,7 +37,6 @@ pub async fn ban_from_community(
|
|||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<BanFromCommunityResponse>> {
|
||||
let banned_person_id = data.person_id;
|
||||
let remove_data = data.remove_data.unwrap_or(false);
|
||||
let expires = check_expire_time(data.expires)?;
|
||||
|
||||
// Verify that only mods or admins can ban
|
||||
|
@ -85,9 +88,16 @@ pub async fn ban_from_community(
|
|||
}
|
||||
|
||||
// Remove/Restore their data if that's desired
|
||||
if remove_data {
|
||||
remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?;
|
||||
}
|
||||
if data.remove_or_restore_data.unwrap_or(false) {
|
||||
let remove_data = data.ban;
|
||||
remove_or_restore_user_data_in_community(
|
||||
data.community_id,
|
||||
banned_person_id,
|
||||
remove_data,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanFromCommunityForm {
|
||||
|
|
|
@ -172,7 +172,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
|
|||
target: &Person,
|
||||
ban: bool,
|
||||
reason: &Option<String>,
|
||||
remove_data: &Option<bool>,
|
||||
remove_or_restore_data: &Option<bool>,
|
||||
expires: &Option<i64>,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
|
@ -230,7 +230,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
|
|||
person_id: target.id,
|
||||
ban,
|
||||
reason: reason.clone(),
|
||||
remove_data: *remove_data,
|
||||
remove_or_restore_data: *remove_or_restore_data,
|
||||
expires: *expires,
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use lemmy_api_common::{
|
|||
context::LemmyContext,
|
||||
person::{BanPerson, BanPersonResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_expire_time, is_admin, remove_user_data},
|
||||
utils::{check_expire_time, is_admin, remove_user_data, restore_user_data},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -65,10 +65,13 @@ pub async fn ban_from_site(
|
|||
}
|
||||
|
||||
// Remove their data if that's desired
|
||||
let remove_data = data.remove_data.unwrap_or(false);
|
||||
if remove_data {
|
||||
remove_user_data(person.id, &context).await?;
|
||||
}
|
||||
if data.remove_or_restore_data.unwrap_or(false) {
|
||||
if data.ban {
|
||||
remove_user_data(person.id, &context).await?;
|
||||
} else {
|
||||
restore_user_data(person.id, &context).await?;
|
||||
}
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanForm {
|
||||
|
@ -90,7 +93,7 @@ pub async fn ban_from_site(
|
|||
&person,
|
||||
data.ban,
|
||||
&data.reason,
|
||||
&data.remove_data,
|
||||
&data.remove_or_restore_data,
|
||||
&data.expires,
|
||||
&context,
|
||||
)
|
||||
|
@ -101,7 +104,7 @@ pub async fn ban_from_site(
|
|||
moderator: local_user_view.person,
|
||||
banned_user: person_view.person.clone(),
|
||||
reason: data.reason.clone(),
|
||||
remove_data: data.remove_data,
|
||||
remove_or_restore_data: data.remove_or_restore_data,
|
||||
ban: data.ban,
|
||||
expires: data.expires,
|
||||
},
|
||||
|
|
|
@ -28,11 +28,13 @@ pub async fn change_password(
|
|||
}
|
||||
|
||||
// Check the old password
|
||||
let valid: bool = verify(
|
||||
&data.old_password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted
|
||||
{
|
||||
verify(&data.old_password, password_encrypted).unwrap_or(false)
|
||||
} else {
|
||||
data.old_password.is_empty()
|
||||
};
|
||||
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@ 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};
|
||||
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_db_schema::source::{
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
site::Site,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp]
|
||||
|
@ -13,17 +16,14 @@ pub async fn generate_totp_secret(
|
|||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site = Site::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 secret_url = build_totp_2fa(&site.name, &local_user_view.person.name, &secret)?.get_url();
|
||||
|
||||
let local_user_form = LocalUserUpdateForm {
|
||||
totp_2fa_secret: Some(Some(secret)),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{check_totp_2fa_valid, local_user::check_email_verified};
|
||||
use crate::check_totp_2fa_valid;
|
||||
use actix_web::{
|
||||
web::{Data, Json},
|
||||
HttpRequest,
|
||||
|
@ -8,12 +8,7 @@ use lemmy_api_common::{
|
|||
claims::Claims,
|
||||
context::LemmyContext,
|
||||
person::{Login, LoginResponse},
|
||||
utils::check_user_valid,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{local_site::LocalSite, registration_application::RegistrationApplication},
|
||||
utils::DbPool,
|
||||
RegistrationMode,
|
||||
utils::{check_email_verified, check_registration_application, check_user_valid},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
@ -24,9 +19,7 @@ pub async fn login(
|
|||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
// Fetch that username / email
|
||||
let username_or_email = data.username_or_email.clone();
|
||||
|
@ -36,11 +29,12 @@ pub async fn login(
|
|||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
// Verify the password
|
||||
let valid: bool = verify(
|
||||
&data.password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
let valid: bool = local_user_view
|
||||
.local_user
|
||||
.password_encrypted
|
||||
.as_ref()
|
||||
.and_then(|password_encrypted| verify(&data.password, password_encrypted).ok())
|
||||
.unwrap_or(false);
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
|
@ -67,28 +61,3 @@ pub async fn login(
|
|||
registration_created: false,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn check_registration_application(
|
||||
local_user_view: &LocalUserView,
|
||||
local_site: &LocalSite,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<()> {
|
||||
if (local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
|| local_site.registration_mode == RegistrationMode::Closed)
|
||||
&& !local_user_view.local_user.accepted_application
|
||||
&& !local_user_view.local_user.admin
|
||||
{
|
||||
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
|
||||
// was processed (either accepted or denied).
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
|
||||
if registration.admin_id.is_some() {
|
||||
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
|
||||
} else {
|
||||
Err(LemmyErrorType::RegistrationApplicationIsPending)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
pub mod add_admin;
|
||||
pub mod ban_person;
|
||||
pub mod block;
|
||||
|
@ -20,15 +17,3 @@ pub mod save_settings;
|
|||
pub mod update_totp;
|
||||
pub mod validate_auth;
|
||||
pub mod verify_email;
|
||||
|
||||
/// Check if the user's email is verified if email verification is turned on
|
||||
/// However, skip checking verification if the user is an admin
|
||||
fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> {
|
||||
if !local_user_view.local_user.admin
|
||||
&& site_view.local_site.require_email_verification
|
||||
&& !local_user_view.local_user.email_verified
|
||||
{
|
||||
Err(LemmyErrorType::EmailNotVerified)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::local_user::check_email_verified;
|
||||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::PasswordReset,
|
||||
utils::send_password_reset_email,
|
||||
utils::{check_email_verified, send_password_reset_email},
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
|
@ -20,9 +19,7 @@ pub async fn reset_password(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
check_email_verified(&local_user_view, &site_view)?;
|
||||
|
||||
// Email the pure token to the user.
|
||||
|
|
|
@ -36,9 +36,7 @@ pub async fn save_user_settings(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
|
|
|
@ -16,9 +16,7 @@ pub async fn verify_email(
|
|||
data: Json<VerifyEmail>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let token = data.token.clone();
|
||||
let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
|
||||
.await?
|
||||
|
|
|
@ -5,15 +5,13 @@ use lemmy_api_common::{
|
|||
utils::build_federated_instances,
|
||||
};
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_federated_instances(
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<GetFederatedInstancesResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let federated_instances =
|
||||
build_federated_instances(&site_view.local_site, &mut context.pool()).await?;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ use lemmy_db_schema::{
|
|||
local_site_url_blocklist::LocalSiteUrlBlocklist,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
moderator::{ModAdd, ModAddForm},
|
||||
oauth_provider::OAuthProvider,
|
||||
tagline::Tagline,
|
||||
},
|
||||
traits::Crud,
|
||||
|
@ -55,9 +56,7 @@ pub async fn leave_admin(
|
|||
ModAdd::create(&mut context.pool(), &form).await?;
|
||||
|
||||
// Reread site and admins
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let admins = PersonView::admins(&mut context.pool()).await?;
|
||||
|
||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||
|
@ -65,6 +64,7 @@ pub async fn leave_admin(
|
|||
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
|
||||
let custom_emojis =
|
||||
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
|
||||
let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?;
|
||||
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
||||
|
||||
Ok(Json(GetSiteResponse {
|
||||
|
@ -76,6 +76,8 @@ pub async fn leave_admin(
|
|||
discussion_languages,
|
||||
taglines,
|
||||
custom_emojis,
|
||||
oauth_providers: Some(oauth_providers),
|
||||
admin_oauth_providers: None,
|
||||
blocked_urls,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ pub async fn purge_person(
|
|||
moderator: local_user_view.person,
|
||||
banned_user: person,
|
||||
reason: data.reason.clone(),
|
||||
remove_data: Some(true),
|
||||
remove_or_restore_data: Some(true),
|
||||
ban: true,
|
||||
expires: None,
|
||||
},
|
||||
|
|
|
@ -97,7 +97,9 @@ pub struct BanFromCommunity {
|
|||
pub community_id: CommunityId,
|
||||
pub person_id: PersonId,
|
||||
pub ban: bool,
|
||||
pub remove_data: Option<bool>,
|
||||
/// Optionally remove or restore all their data. Useful for new troll accounts.
|
||||
/// If ban is true, then this means remove. If ban is false, it means restore.
|
||||
pub remove_or_restore_data: Option<bool>,
|
||||
pub reason: Option<String>,
|
||||
/// A time that the ban will expire, in unix epoch seconds.
|
||||
///
|
||||
|
|
|
@ -7,6 +7,7 @@ pub mod community;
|
|||
#[cfg(feature = "full")]
|
||||
pub mod context;
|
||||
pub mod custom_emoji;
|
||||
pub mod oauth_provider;
|
||||
pub mod person;
|
||||
pub mod post;
|
||||
pub mod private_message;
|
||||
|
|
69
crates/api_common/src/oauth_provider.rs
Normal file
69
crates/api_common/src/oauth_provider.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use lemmy_db_schema::newtypes::OAuthProviderId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Create an external auth method.
|
||||
pub struct CreateOAuthProvider {
|
||||
pub display_name: String,
|
||||
pub issuer: String,
|
||||
pub authorization_endpoint: String,
|
||||
pub token_endpoint: String,
|
||||
pub userinfo_endpoint: String,
|
||||
pub id_claim: String,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub scopes: String,
|
||||
pub auto_verify_email: bool,
|
||||
pub account_linking_enabled: bool,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Edit an external auth method.
|
||||
pub struct EditOAuthProvider {
|
||||
pub id: OAuthProviderId,
|
||||
pub display_name: Option<String>,
|
||||
pub authorization_endpoint: Option<String>,
|
||||
pub token_endpoint: Option<String>,
|
||||
pub userinfo_endpoint: Option<String>,
|
||||
pub id_claim: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
pub scopes: Option<String>,
|
||||
pub auto_verify_email: Option<bool>,
|
||||
pub account_linking_enabled: Option<bool>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Delete an external auth method.
|
||||
pub struct DeleteOAuthProvider {
|
||||
pub id: OAuthProviderId,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Logging in with an OAuth 2.0 authorization
|
||||
pub struct AuthenticateWithOauth {
|
||||
pub code: String,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub oauth_provider_id: OAuthProviderId,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub redirect_uri: Url,
|
||||
pub show_nsfw: Option<bool>,
|
||||
/// Username is mandatory at registration time
|
||||
pub username: Option<String>,
|
||||
/// An answer is mandatory if require application is enabled on the server
|
||||
pub answer: Option<String>,
|
||||
}
|
|
@ -217,8 +217,9 @@ pub struct AddAdminResponse {
|
|||
pub struct BanPerson {
|
||||
pub person_id: PersonId,
|
||||
pub ban: bool,
|
||||
/// Optionally remove all their data. Useful for new troll accounts.
|
||||
pub remove_data: Option<bool>,
|
||||
/// Optionally remove or restore all their data. Useful for new troll accounts.
|
||||
/// If ban is true, then this means remove. If ban is false, it means restore.
|
||||
pub remove_or_restore_data: Option<bool>,
|
||||
pub reason: Option<String>,
|
||||
/// A time that the ban will expire, in unix epoch seconds.
|
||||
///
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
lemmy_db_schema::traits::Crud,
|
||||
post::{LinkMetadata, OpenGraphData},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{local_site_opt_to_sensitive, proxy_image_link},
|
||||
utils::proxy_image_link,
|
||||
};
|
||||
use activitypub_federation::config::Data;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
@ -13,8 +13,8 @@ use lemmy_db_schema::{
|
|||
newtypes::DbUrl,
|
||||
source::{
|
||||
images::{ImageDetailsForm, LocalImage, LocalImageForm},
|
||||
local_site::LocalSite,
|
||||
post::{Post, PostUpdateForm},
|
||||
site::Site,
|
||||
},
|
||||
};
|
||||
use lemmy_utils::{
|
||||
|
@ -44,6 +44,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
|
|||
.user_agent(user_agent.clone())
|
||||
.timeout(REQWEST_TIMEOUT)
|
||||
.connect_timeout(REQWEST_TIMEOUT)
|
||||
.use_rustls_tls()
|
||||
}
|
||||
|
||||
/// Fetches metadata for the given link and optionally generates thumbnail.
|
||||
|
@ -130,7 +131,6 @@ pub async fn generate_post_link_metadata(
|
|||
post: Post,
|
||||
custom_thumbnail: Option<Url>,
|
||||
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
|
||||
local_site: Option<LocalSite>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
let metadata = match &post.url {
|
||||
|
@ -144,7 +144,8 @@ pub async fn generate_post_link_metadata(
|
|||
.is_some_and(|content_type| content_type.starts_with("image"));
|
||||
|
||||
// Decide if we are allowed to generate local thumbnail
|
||||
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
|
||||
let site = Site::read_local(&mut context.pool()).await?;
|
||||
let allow_sensitive = site.content_warning.is_some();
|
||||
let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
|
||||
|
||||
let image_url = if is_image_post {
|
||||
|
|
|
@ -83,7 +83,7 @@ pub enum SendActivityData {
|
|||
moderator: Person,
|
||||
banned_user: Person,
|
||||
reason: Option<String>,
|
||||
remove_data: Option<bool>,
|
||||
remove_or_restore_data: Option<bool>,
|
||||
ban: bool,
|
||||
expires: Option<i64>,
|
||||
},
|
||||
|
|
|
@ -16,6 +16,7 @@ use lemmy_db_schema::{
|
|||
instance::Instance,
|
||||
language::Language,
|
||||
local_site_url_blocklist::LocalSiteUrlBlocklist,
|
||||
oauth_provider::{OAuthProvider, PublicOAuthProvider},
|
||||
person::Person,
|
||||
tagline::Tagline,
|
||||
},
|
||||
|
@ -77,6 +78,7 @@ pub struct Search {
|
|||
pub listing_type: Option<ListingType>,
|
||||
pub page: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
pub post_title_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -199,6 +201,7 @@ pub struct CreateSite {
|
|||
pub blocked_instances: Option<Vec<String>>,
|
||||
pub taglines: Option<Vec<String>>,
|
||||
pub registration_mode: Option<RegistrationMode>,
|
||||
pub oauth_registration: Option<bool>,
|
||||
pub content_warning: Option<String>,
|
||||
pub default_post_listing_mode: Option<PostListingMode>,
|
||||
}
|
||||
|
@ -281,6 +284,8 @@ pub struct EditSite {
|
|||
/// A list of taglines shown at the top of the front page.
|
||||
pub taglines: Option<Vec<String>>,
|
||||
pub registration_mode: Option<RegistrationMode>,
|
||||
/// Whether or not external auth methods can auto-register users.
|
||||
pub oauth_registration: Option<bool>,
|
||||
/// Whether to email admins for new reports.
|
||||
pub reports_email_admins: Option<bool>,
|
||||
/// If present, nsfw content is visible by default. Should be displayed by frontends/clients
|
||||
|
@ -315,6 +320,9 @@ pub struct GetSiteResponse {
|
|||
pub taglines: Vec<Tagline>,
|
||||
/// A list of custom emojis your site supports.
|
||||
pub custom_emojis: Vec<CustomEmojiView>,
|
||||
/// A list of external auth methods your site supports.
|
||||
pub oauth_providers: Option<Vec<PublicOAuthProvider>>,
|
||||
pub admin_oauth_providers: Option<Vec<OAuthProvider>>,
|
||||
pub blocked_urls: Vec<LocalSiteUrlBlocklist>,
|
||||
}
|
||||
|
||||
|
|
|
@ -23,18 +23,21 @@ use lemmy_db_schema::{
|
|||
local_site::LocalSite,
|
||||
local_site_rate_limit::LocalSiteRateLimit,
|
||||
local_site_url_blocklist::LocalSiteUrlBlocklist,
|
||||
oauth_account::OAuthAccount,
|
||||
password_reset_request::PasswordResetRequest,
|
||||
person::{Person, PersonUpdateForm},
|
||||
person_block::PersonBlock,
|
||||
post::{Post, PostRead},
|
||||
registration_application::RegistrationApplication,
|
||||
site::Site,
|
||||
},
|
||||
traits::Crud,
|
||||
utils::DbPool,
|
||||
RegistrationMode,
|
||||
};
|
||||
use lemmy_db_views::{
|
||||
comment_view::CommentQuery,
|
||||
structs::{LocalImageView, LocalUserView},
|
||||
structs::{LocalImageView, LocalUserView, SiteView},
|
||||
};
|
||||
use lemmy_db_views_actor::structs::{
|
||||
CommunityModeratorView,
|
||||
|
@ -49,6 +52,7 @@ use lemmy_utils::{
|
|||
utils::{
|
||||
markdown::{markdown_check_for_blocked_urls, markdown_rewrite_image_links},
|
||||
slurs::{build_slur_regex, remove_slurs},
|
||||
validation::clean_urls_in_text,
|
||||
},
|
||||
CACHE_DURATION_FEDERATION,
|
||||
};
|
||||
|
@ -179,6 +183,46 @@ pub fn check_user_valid(person: &Person) -> LemmyResult<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if the user's email is verified if email verification is turned on
|
||||
/// However, skip checking verification if the user is an admin
|
||||
pub fn check_email_verified(
|
||||
local_user_view: &LocalUserView,
|
||||
site_view: &SiteView,
|
||||
) -> LemmyResult<()> {
|
||||
if !local_user_view.local_user.admin
|
||||
&& site_view.local_site.require_email_verification
|
||||
&& !local_user_view.local_user.email_verified
|
||||
{
|
||||
Err(LemmyErrorType::EmailNotVerified)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_registration_application(
|
||||
local_user_view: &LocalUserView,
|
||||
local_site: &LocalSite,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<()> {
|
||||
if (local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
|| local_site.registration_mode == RegistrationMode::Closed)
|
||||
&& !local_user_view.local_user.accepted_application
|
||||
&& !local_user_view.local_user.admin
|
||||
{
|
||||
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
|
||||
// was processed (either accepted or denied).
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
|
||||
if registration.admin_id.is_some() {
|
||||
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
|
||||
} else {
|
||||
Err(LemmyErrorType::RegistrationApplicationIsPending)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks that a normal user action (eg posting or voting) is allowed in a given community.
|
||||
///
|
||||
/// In particular it checks that neither the user nor community are banned or deleted, and that
|
||||
|
@ -467,13 +511,6 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Re
|
|||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn local_site_opt_to_sensitive(local_site: &Option<LocalSite>) -> bool {
|
||||
local_site
|
||||
.as_ref()
|
||||
.map(|site| site.enable_nsfw)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
|
||||
static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
|
@ -720,13 +757,30 @@ pub async fn remove_user_data(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_user_data_in_community(
|
||||
/// We can't restore their images, but we can unremove their posts and comments
|
||||
pub async fn restore_user_data(
|
||||
banned_person_id: PersonId,
|
||||
context: &LemmyContext,
|
||||
) -> LemmyResult<()> {
|
||||
let pool = &mut context.pool();
|
||||
|
||||
// Posts
|
||||
Post::update_removed_for_creator(pool, banned_person_id, None, false).await?;
|
||||
|
||||
// Comments
|
||||
Comment::update_removed_for_creator(pool, banned_person_id, false).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_or_restore_user_data_in_community(
|
||||
community_id: CommunityId,
|
||||
banned_person_id: PersonId,
|
||||
remove: bool,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<()> {
|
||||
// Posts
|
||||
Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?;
|
||||
Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?;
|
||||
|
||||
// Comments
|
||||
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
|
||||
|
@ -744,7 +798,7 @@ pub async fn remove_user_data_in_community(
|
|||
pool,
|
||||
comment_id,
|
||||
&CommentUpdateForm {
|
||||
removed: Some(true),
|
||||
removed: Some(remove),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
@ -788,6 +842,11 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) ->
|
|||
// Leave communities they mod
|
||||
CommunityModerator::leave_all_communities(pool, person_id).await?;
|
||||
|
||||
// Delete the oauth accounts linked to the local user
|
||||
if let Ok(Some(local_user)) = LocalUserView::read_person(pool, person_id).await {
|
||||
OAuthAccount::delete_user_accounts(pool, local_user.local_user.id).await?;
|
||||
}
|
||||
|
||||
Person::delete_account(pool, person_id).await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -877,6 +936,7 @@ pub async fn process_markdown(
|
|||
context: &LemmyContext,
|
||||
) -> LemmyResult<String> {
|
||||
let text = remove_slurs(text, slur_regex);
|
||||
let text = clean_urls_in_text(&text);
|
||||
|
||||
markdown_check_for_blocked_urls(&text, url_blocklist)?;
|
||||
|
||||
|
|
|
@ -27,8 +27,11 @@ futures.workspace = true
|
|||
uuid = { workspace = true }
|
||||
moka.workspace = true
|
||||
anyhow.workspace = true
|
||||
webmention = "0.5.0"
|
||||
webmention = "0.6.0"
|
||||
accept-language = "3.1.0"
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["futures"]
|
||||
|
|
|
@ -47,9 +47,7 @@ pub async fn create_community(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommunityResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let local_site = site_view.local_site;
|
||||
|
||||
if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
|
||||
|
|
|
@ -6,7 +6,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_db_views_actor::community_view::CommunityQuery;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_communities(
|
||||
|
@ -14,9 +14,7 @@ pub async fn list_communities(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
) -> LemmyResult<Json<ListCommunitiesResponse>> {
|
||||
let local_site = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
||||
let is_admin = local_user_view
|
||||
.as_ref()
|
||||
.map(|luv| is_admin(luv).is_ok())
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
pub mod comment;
|
||||
pub mod community;
|
||||
pub mod custom_emoji;
|
||||
pub mod oauth_provider;
|
||||
pub mod post;
|
||||
pub mod private_message;
|
||||
pub mod site;
|
||||
|
|
42
crates/api_crud/src/oauth_provider/create.rs
Normal file
42
crates/api_crud/src/oauth_provider/create.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
oauth_provider::CreateOAuthProvider,
|
||||
utils::is_admin,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::oauth_provider::{OAuthProvider, OAuthProviderInsertForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use url::Url;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn create_oauth_provider(
|
||||
data: Json<CreateOAuthProvider>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<OAuthProvider>, LemmyError> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let cloned_data = data.clone();
|
||||
let oauth_provider_form = OAuthProviderInsertForm {
|
||||
display_name: cloned_data.display_name,
|
||||
issuer: Url::parse(&cloned_data.issuer)?.into(),
|
||||
authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(),
|
||||
token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(),
|
||||
userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(),
|
||||
id_claim: cloned_data.id_claim,
|
||||
client_id: data.client_id.to_string(),
|
||||
client_secret: data.client_secret.to_string(),
|
||||
scopes: data.scopes.to_string(),
|
||||
auto_verify_email: data.auto_verify_email,
|
||||
account_linking_enabled: data.account_linking_enabled,
|
||||
enabled: data.enabled,
|
||||
};
|
||||
let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?;
|
||||
Ok(Json(oauth_provider))
|
||||
}
|
25
crates/api_crud/src/oauth_provider/delete.rs
Normal file
25
crates/api_crud/src/oauth_provider/delete.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
oauth_provider::DeleteOAuthProvider,
|
||||
utils::is_admin,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{source::oauth_provider::OAuthProvider, traits::Crud};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn delete_oauth_provider(
|
||||
data: Json<DeleteOAuthProvider>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
OAuthProvider::delete(&mut context.pool(), data.id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntDeleteOauthProvider)?;
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
3
crates/api_crud/src/oauth_provider/mod.rs
Normal file
3
crates/api_crud/src/oauth_provider/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod update;
|
44
crates/api_crud/src/oauth_provider/update.rs
Normal file
44
crates/api_crud/src/oauth_provider/update.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin};
|
||||
use lemmy_db_schema::{
|
||||
source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm},
|
||||
traits::Crud,
|
||||
utils::{diesel_required_string_update, diesel_required_url_update, naive_now},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{error::LemmyError, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn update_oauth_provider(
|
||||
data: Json<EditOAuthProvider>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<OAuthProvider>, LemmyError> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let cloned_data = data.clone();
|
||||
let oauth_provider_form = OAuthProviderUpdateForm {
|
||||
display_name: diesel_required_string_update(cloned_data.display_name.as_deref()),
|
||||
authorization_endpoint: diesel_required_url_update(
|
||||
cloned_data.authorization_endpoint.as_deref(),
|
||||
)?,
|
||||
token_endpoint: diesel_required_url_update(cloned_data.token_endpoint.as_deref())?,
|
||||
userinfo_endpoint: diesel_required_url_update(cloned_data.userinfo_endpoint.as_deref())?,
|
||||
id_claim: diesel_required_string_update(data.id_claim.as_deref()),
|
||||
client_secret: diesel_required_string_update(data.client_secret.as_deref()),
|
||||
scopes: diesel_required_string_update(data.scopes.as_deref()),
|
||||
auto_verify_email: data.auto_verify_email,
|
||||
account_linking_enabled: data.account_linking_enabled,
|
||||
enabled: data.enabled,
|
||||
updated: Some(Some(naive_now())),
|
||||
};
|
||||
|
||||
let update_result =
|
||||
OAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form).await?;
|
||||
let oauth_provider = OAuthProvider::read(&mut context.pool(), update_result.id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindOauthProvider)?;
|
||||
Ok(Json(oauth_provider))
|
||||
}
|
|
@ -146,7 +146,6 @@ pub async fn create_post(
|
|||
inserted_post.clone(),
|
||||
custom_thumbnail.map(Into::into),
|
||||
|post| Some(SendActivityData::CreatePost(post)),
|
||||
Some(local_site),
|
||||
context.reset_request_count(),
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -21,9 +21,7 @@ pub async fn get_post(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
) -> LemmyResult<Json<GetPostResponse>> {
|
||||
let local_site = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site.local_site)?;
|
||||
|
||||
|
|
|
@ -129,7 +129,6 @@ pub async fn update_post(
|
|||
updated_post.clone(),
|
||||
custom_thumbnail.flatten().map(Into::into),
|
||||
|post| Some(SendActivityData::UpdatePost(post)),
|
||||
Some(local_site),
|
||||
context.reset_request_count(),
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -92,7 +92,6 @@ pub async fn create_site(
|
|||
site_setup: Some(true),
|
||||
enable_downvotes: data.enable_downvotes,
|
||||
registration_mode: data.registration_mode,
|
||||
enable_nsfw: data.enable_nsfw,
|
||||
community_creation_admin_only: data.community_creation_admin_only,
|
||||
require_email_verification: data.require_email_verification,
|
||||
application_question: diesel_string_update(data.application_question.as_deref()),
|
||||
|
@ -133,9 +132,7 @@ pub async fn create_site(
|
|||
|
||||
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
|
||||
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
let new_taglines = data.taglines.clone();
|
||||
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
|
||||
|
@ -594,6 +591,7 @@ mod tests {
|
|||
blocked_instances: None,
|
||||
taglines: None,
|
||||
registration_mode: site_registration_mode,
|
||||
oauth_registration: None,
|
||||
content_warning: None,
|
||||
default_post_listing_mode: None,
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_db_schema::source::{
|
|||
instance_block::InstanceBlock,
|
||||
language::Language,
|
||||
local_site_url_blocklist::LocalSiteUrlBlocklist,
|
||||
oauth_provider::OAuthProvider,
|
||||
person_block::PersonBlock,
|
||||
tagline::Tagline,
|
||||
};
|
||||
|
@ -37,9 +38,7 @@ pub async fn get_site(
|
|||
// This data is independent from the user account so we can cache it across requests
|
||||
let mut site_response = CACHE
|
||||
.try_get_with::<_, LemmyError>((), async {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let admins = PersonView::admins(&mut context.pool()).await?;
|
||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
|
||||
|
@ -47,6 +46,10 @@ pub async fn get_site(
|
|||
let custom_emojis =
|
||||
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
|
||||
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
||||
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
|
||||
let oauth_providers =
|
||||
OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());
|
||||
|
||||
Ok(GetSiteResponse {
|
||||
site_view,
|
||||
admins,
|
||||
|
@ -57,13 +60,15 @@ pub async fn get_site(
|
|||
taglines,
|
||||
custom_emojis,
|
||||
blocked_urls,
|
||||
oauth_providers: Some(oauth_providers),
|
||||
admin_oauth_providers: Some(admin_oauth_providers),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?;
|
||||
|
||||
// Build the local user with parallel queries and add it to site response
|
||||
site_response.my_user = if let Some(local_user_view) = local_user_view {
|
||||
site_response.my_user = if let Some(ref local_user_view) = local_user_view {
|
||||
let person_id = local_user_view.person.id;
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let pool = &mut context.pool();
|
||||
|
@ -86,7 +91,7 @@ pub async fn get_site(
|
|||
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
||||
|
||||
Some(MyUserInfo {
|
||||
local_user_view,
|
||||
local_user_view: local_user_view.clone(),
|
||||
follows,
|
||||
moderates,
|
||||
community_blocks,
|
||||
|
@ -98,5 +103,13 @@ pub async fn get_site(
|
|||
None
|
||||
};
|
||||
|
||||
// filter oauth_providers for public access
|
||||
if !local_user_view
|
||||
.map(|l| l.local_user.admin)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
site_response.admin_oauth_providers = None;
|
||||
}
|
||||
|
||||
Ok(Json(site_response))
|
||||
}
|
||||
|
|
|
@ -52,9 +52,7 @@ pub async fn update_site(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SiteResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let local_site = site_view.local_site;
|
||||
let site = site_view.site;
|
||||
|
||||
|
@ -103,7 +101,6 @@ pub async fn update_site(
|
|||
let local_site_form = LocalSiteUpdateForm {
|
||||
enable_downvotes: data.enable_downvotes,
|
||||
registration_mode: data.registration_mode,
|
||||
enable_nsfw: data.enable_nsfw,
|
||||
community_creation_admin_only: data.community_creation_admin_only,
|
||||
require_email_verification: data.require_email_verification,
|
||||
application_question: diesel_string_update(data.application_question.as_deref()),
|
||||
|
@ -122,6 +119,7 @@ pub async fn update_site(
|
|||
captcha_difficulty: data.captcha_difficulty.clone(),
|
||||
reports_email_admins: data.reports_email_admins,
|
||||
default_post_listing_mode: data.default_post_listing_mode,
|
||||
oauth_registration: data.oauth_registration,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
@ -191,9 +189,7 @@ pub async fn update_site(
|
|||
let new_taglines = data.taglines.clone();
|
||||
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
|
||||
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
let rate_limit_config =
|
||||
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
|
||||
|
@ -283,6 +279,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -306,6 +303,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -329,6 +327,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -352,6 +351,7 @@ mod tests {
|
|||
Some(true),
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -375,6 +375,7 @@ mod tests {
|
|||
Some(true),
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -398,6 +399,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
Some(RegistrationMode::RequireApplication),
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
@ -452,6 +454,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -474,6 +477,7 @@ mod tests {
|
|||
Some(true),
|
||||
Some(String::new()),
|
||||
Some(RegistrationMode::Open),
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -496,6 +500,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
None::<RegistrationMode>,
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -518,6 +523,7 @@ mod tests {
|
|||
None::<bool>,
|
||||
None::<String>,
|
||||
Some(RegistrationMode::RequireApplication),
|
||||
None::<bool>,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
@ -566,6 +572,7 @@ mod tests {
|
|||
site_is_federated: Option<bool>,
|
||||
site_application_question: Option<String>,
|
||||
site_registration_mode: Option<RegistrationMode>,
|
||||
site_oauth_registration: Option<bool>,
|
||||
) -> EditSite {
|
||||
EditSite {
|
||||
name: site_name,
|
||||
|
@ -612,6 +619,7 @@ mod tests {
|
|||
reports_email_admins: None,
|
||||
content_warning: None,
|
||||
default_post_listing_mode: None,
|
||||
oauth_registration: site_oauth_registration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,12 @@ use actix_web::{web::Json, HttpRequest};
|
|||
use lemmy_api_common::{
|
||||
claims::Claims,
|
||||
context::LemmyContext,
|
||||
oauth_provider::AuthenticateWithOauth,
|
||||
person::{LoginResponse, Register},
|
||||
utils::{
|
||||
check_email_verified,
|
||||
check_registration_application,
|
||||
check_user_valid,
|
||||
generate_inbox_url,
|
||||
generate_local_apub_endpoint,
|
||||
generate_shared_inbox_url,
|
||||
|
@ -18,11 +22,15 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
aggregates::structs::PersonAggregates,
|
||||
newtypes::{InstanceId, OAuthProviderId},
|
||||
source::{
|
||||
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
|
||||
language::Language,
|
||||
local_site::LocalSite,
|
||||
local_user::{LocalUser, LocalUserInsertForm},
|
||||
local_user_vote_display_mode::LocalUserVoteDisplayMode,
|
||||
oauth_account::{OAuthAccount, OAuthAccountInsertForm},
|
||||
oauth_provider::OAuthProvider,
|
||||
person::{Person, PersonInsertForm},
|
||||
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
|
||||
},
|
||||
|
@ -31,23 +39,33 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::{
|
||||
slurs::{check_slurs, check_slurs_opt},
|
||||
validation::is_valid_actor_name,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
/// Response from OAuth token endpoint
|
||||
struct TokenResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub expires_in: Option<i64>,
|
||||
pub refresh_token: Option<String>,
|
||||
pub scope: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
data: Json<Register>,
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let local_site = site_view.local_site;
|
||||
let require_registration_application =
|
||||
local_site.registration_mode == RegistrationMode::RequireApplication;
|
||||
|
@ -63,8 +81,9 @@ pub async fn register(
|
|||
Err(LemmyErrorType::EmailRequired)?
|
||||
}
|
||||
|
||||
if local_site.site_setup && require_registration_application && data.answer.is_none() {
|
||||
Err(LemmyErrorType::RegistrationApplicationAnswerRequired)?
|
||||
// make sure the registration answer is provided when the registration application is required
|
||||
if local_site.site_setup {
|
||||
validate_registration_answer(require_registration_application, &data.answer)?;
|
||||
}
|
||||
|
||||
// Make sure passwords match
|
||||
|
@ -88,62 +107,37 @@ pub async fn register(
|
|||
check_slurs(&data.username, &slur_regex)?;
|
||||
check_slurs_opt(&data.answer, &slur_regex)?;
|
||||
|
||||
let actor_keypair = generate_actor_keypair()?;
|
||||
is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?;
|
||||
let actor_id = generate_local_apub_endpoint(
|
||||
EndpointType::Person,
|
||||
&data.username,
|
||||
&context.settings().get_protocol_and_hostname(),
|
||||
)?;
|
||||
if Person::is_username_taken(&mut context.pool(), &data.username).await? {
|
||||
return Err(LemmyErrorType::UsernameAlreadyExists)?;
|
||||
}
|
||||
|
||||
if let Some(email) = &data.email {
|
||||
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
|
||||
}
|
||||
|
||||
// We have to create both a person, and local_user
|
||||
|
||||
// Register the new person
|
||||
let person_form = PersonInsertForm {
|
||||
actor_id: Some(actor_id.clone()),
|
||||
inbox_url: Some(generate_inbox_url(&actor_id)?),
|
||||
shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
|
||||
private_key: Some(actor_keypair.private_key),
|
||||
..PersonInsertForm::new(
|
||||
data.username.clone(),
|
||||
actor_keypair.public_key,
|
||||
site_view.site.instance_id,
|
||||
)
|
||||
};
|
||||
|
||||
// insert the person
|
||||
let inserted_person = Person::create(&mut context.pool(), &person_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::UserAlreadyExists)?;
|
||||
let inserted_person = create_person(
|
||||
data.username.clone(),
|
||||
&local_site,
|
||||
site_view.site.instance_id,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Automatically set their application as accepted, if they created this with open registration.
|
||||
// Also fixes a bug which allows users to log in when registrations are changed to closed.
|
||||
let accepted_application = Some(!require_registration_application);
|
||||
|
||||
// Get the user's preferred language using the Accept-Language header
|
||||
let language_tags: Vec<String> = req
|
||||
.headers()
|
||||
.get("Accept-Language")
|
||||
.map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default()))
|
||||
.iter()
|
||||
.flatten()
|
||||
// Remove the optional region code
|
||||
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
|
||||
.collect();
|
||||
|
||||
// Show nsfw content if param is true, or if content_warning exists
|
||||
let show_nsfw = data
|
||||
.show_nsfw
|
||||
.unwrap_or(site_view.site.content_warning.is_some());
|
||||
|
||||
let language_tags = get_language_tags(&req);
|
||||
|
||||
// Create the local user
|
||||
let local_user_form = LocalUserInsertForm {
|
||||
email: data.email.as_deref().map(str::to_lowercase),
|
||||
password_encrypted: data.password.to_string(),
|
||||
show_nsfw: Some(show_nsfw),
|
||||
accepted_application,
|
||||
default_listing_type: Some(local_site.default_post_listing_type),
|
||||
|
@ -151,21 +145,10 @@ pub async fn register(
|
|||
interface_language: language_tags.first().cloned(),
|
||||
// If its the initial site setup, they are an admin
|
||||
admin: Some(!local_site.site_setup),
|
||||
..LocalUserInsertForm::new(inserted_person.id, data.password.to_string())
|
||||
..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string()))
|
||||
};
|
||||
|
||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||
// use hashset to avoid duplicates
|
||||
let mut language_ids = HashSet::new();
|
||||
for l in language_tags {
|
||||
if let Some(found) = all_languages.iter().find(|all| all.code == l) {
|
||||
language_ids.insert(found.id);
|
||||
}
|
||||
}
|
||||
let language_ids = language_ids.into_iter().collect();
|
||||
|
||||
let inserted_local_user =
|
||||
LocalUser::create(&mut context.pool(), &local_user_form, language_ids).await?;
|
||||
let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?;
|
||||
|
||||
if local_site.site_setup && require_registration_application {
|
||||
// Create the registration application
|
||||
|
@ -198,29 +181,13 @@ pub async fn register(
|
|||
let jwt = Claims::generate(inserted_local_user.id, req, &context).await?;
|
||||
login_response.jwt = Some(jwt);
|
||||
} else {
|
||||
if local_site.require_email_verification {
|
||||
let local_user_view = LocalUserView {
|
||||
local_user: inserted_local_user,
|
||||
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
|
||||
person: inserted_person,
|
||||
counts: PersonAggregates::default(),
|
||||
};
|
||||
// we check at the beginning of this method that email is set
|
||||
let email = local_user_view
|
||||
.local_user
|
||||
.email
|
||||
.clone()
|
||||
.expect("email was provided");
|
||||
|
||||
send_verification_email(
|
||||
&local_user_view,
|
||||
&email,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
login_response.verify_email_sent = true;
|
||||
}
|
||||
login_response.verify_email_sent = send_verification_email_if_required(
|
||||
&context,
|
||||
&local_site,
|
||||
&inserted_local_user,
|
||||
&inserted_person,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if require_registration_application {
|
||||
login_response.registration_created = true;
|
||||
|
@ -229,3 +196,390 @@ pub async fn register(
|
|||
|
||||
Ok(Json(login_response))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn authenticate_with_oauth(
|
||||
data: Json<AuthenticateWithOauth>,
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let local_site = site_view.local_site.clone();
|
||||
|
||||
// validate inputs
|
||||
if data.oauth_provider_id == OAuthProviderId(0) || data.code.is_empty() || data.code.len() > 300 {
|
||||
return Err(LemmyErrorType::OauthAuthorizationInvalid)?;
|
||||
}
|
||||
|
||||
// validate the redirect_uri
|
||||
let redirect_uri = &data.redirect_uri;
|
||||
if redirect_uri.host_str().unwrap_or("").is_empty()
|
||||
|| !redirect_uri.path().eq(&String::from("/oauth/callback"))
|
||||
|| !redirect_uri.query().unwrap_or("").is_empty()
|
||||
{
|
||||
Err(LemmyErrorType::OauthAuthorizationInvalid)?
|
||||
}
|
||||
|
||||
// Fetch the OAUTH provider and make sure it's enabled
|
||||
let oauth_provider_id = data.oauth_provider_id;
|
||||
let oauth_provider = OAuthProvider::read(&mut context.pool(), oauth_provider_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(LemmyErrorType::OauthAuthorizationInvalid)?;
|
||||
|
||||
if !oauth_provider.enabled {
|
||||
return Err(LemmyErrorType::OauthAuthorizationInvalid)?;
|
||||
}
|
||||
|
||||
let token_response =
|
||||
oauth_request_access_token(&context, &oauth_provider, &data.code, redirect_uri.as_str())
|
||||
.await?;
|
||||
|
||||
let user_info = oidc_get_user_info(
|
||||
&context,
|
||||
&oauth_provider,
|
||||
token_response.access_token.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let oauth_user_id = read_user_info(&user_info, oauth_provider.id_claim.as_str())?;
|
||||
|
||||
let mut login_response = LoginResponse {
|
||||
jwt: None,
|
||||
registration_created: false,
|
||||
verify_email_sent: false,
|
||||
};
|
||||
|
||||
// Lookup user by oauth_user_id
|
||||
let mut local_user_view =
|
||||
LocalUserView::find_by_oauth_id(&mut context.pool(), oauth_provider.id, &oauth_user_id).await?;
|
||||
|
||||
let local_user: LocalUser;
|
||||
if let Some(user_view) = local_user_view {
|
||||
// user found by oauth_user_id => Login user
|
||||
local_user = user_view.clone().local_user;
|
||||
|
||||
check_user_valid(&user_view.person)?;
|
||||
check_email_verified(&user_view, &site_view)?;
|
||||
check_registration_application(&user_view, &site_view.local_site, &mut context.pool()).await?;
|
||||
} else {
|
||||
// user has never previously registered using oauth
|
||||
|
||||
// prevent registration if registration is closed
|
||||
if local_site.registration_mode == RegistrationMode::Closed {
|
||||
Err(LemmyErrorType::RegistrationClosed)?
|
||||
}
|
||||
|
||||
// prevent registration if registration is closed for OAUTH providers
|
||||
if !local_site.oauth_registration {
|
||||
return Err(LemmyErrorType::OauthRegistrationClosed)?;
|
||||
}
|
||||
|
||||
// Extract the OAUTH email claim from the returned user_info
|
||||
let email = read_user_info(&user_info, "email")?;
|
||||
|
||||
let require_registration_application =
|
||||
local_site.registration_mode == RegistrationMode::RequireApplication;
|
||||
|
||||
// Lookup user by OAUTH email and link accounts
|
||||
local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?;
|
||||
|
||||
let person;
|
||||
if let Some(user_view) = local_user_view {
|
||||
// user found by email => link and login if linking is allowed
|
||||
|
||||
// we only allow linking by email when email_verification is required otherwise emails cannot
|
||||
// be trusted
|
||||
if oauth_provider.account_linking_enabled && site_view.local_site.require_email_verification {
|
||||
// WARNING:
|
||||
// If an admin switches the require_email_verification config from false to true,
|
||||
// users who signed up before the switch could have accounts with unverified emails falsely
|
||||
// marked as verified.
|
||||
|
||||
check_user_valid(&user_view.person)?;
|
||||
check_email_verified(&user_view, &site_view)?;
|
||||
check_registration_application(&user_view, &site_view.local_site, &mut context.pool())
|
||||
.await?;
|
||||
|
||||
// Link with OAUTH => Login user
|
||||
let oauth_account_form =
|
||||
OAuthAccountInsertForm::new(user_view.local_user.id, oauth_provider.id, oauth_user_id);
|
||||
|
||||
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
|
||||
.await
|
||||
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
|
||||
|
||||
local_user = user_view.local_user.clone();
|
||||
} else {
|
||||
return Err(LemmyErrorType::EmailAlreadyExists)?;
|
||||
}
|
||||
} else {
|
||||
// No user was found by email => Register as new user
|
||||
|
||||
// make sure the registration answer is provided when the registration application is required
|
||||
validate_registration_answer(require_registration_application, &data.answer)?;
|
||||
|
||||
// make sure the username is provided
|
||||
let username = data
|
||||
.username
|
||||
.as_ref()
|
||||
.ok_or(LemmyErrorType::RegistrationUsernameRequired)?;
|
||||
|
||||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
check_slurs(username, &slur_regex)?;
|
||||
check_slurs_opt(&data.answer, &slur_regex)?;
|
||||
|
||||
if Person::is_username_taken(&mut context.pool(), username).await? {
|
||||
return Err(LemmyErrorType::UsernameAlreadyExists)?;
|
||||
}
|
||||
|
||||
// We have to create a person, a local_user, and an oauth_account
|
||||
person = create_person(
|
||||
username.clone(),
|
||||
&local_site,
|
||||
site_view.site.instance_id,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Show nsfw content if param is true, or if content_warning exists
|
||||
let show_nsfw = data
|
||||
.show_nsfw
|
||||
.unwrap_or(site_view.site.content_warning.is_some());
|
||||
|
||||
let language_tags = get_language_tags(&req);
|
||||
|
||||
// Create the local user
|
||||
let local_user_form = LocalUserInsertForm {
|
||||
email: Some(str::to_lowercase(&email)),
|
||||
show_nsfw: Some(show_nsfw),
|
||||
accepted_application: Some(!require_registration_application),
|
||||
email_verified: Some(oauth_provider.auto_verify_email),
|
||||
post_listing_mode: Some(local_site.default_post_listing_mode),
|
||||
interface_language: language_tags.first().cloned(),
|
||||
// If its the initial site setup, they are an admin
|
||||
admin: Some(!local_site.site_setup),
|
||||
..LocalUserInsertForm::new(person.id, None)
|
||||
};
|
||||
|
||||
local_user = create_local_user(&context, language_tags, &local_user_form).await?;
|
||||
|
||||
// Create the oauth account
|
||||
let oauth_account_form =
|
||||
OAuthAccountInsertForm::new(local_user.id, oauth_provider.id, oauth_user_id);
|
||||
|
||||
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
|
||||
.await
|
||||
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
// prevent sign in until application is accepted
|
||||
if local_site.site_setup
|
||||
&& require_registration_application
|
||||
&& !local_user.accepted_application
|
||||
&& !local_user.admin
|
||||
{
|
||||
// Create the registration application
|
||||
RegistrationApplication::create(
|
||||
&mut context.pool(),
|
||||
&RegistrationApplicationInsertForm {
|
||||
local_user_id: local_user.id,
|
||||
answer: data.answer.clone().expect("must have an answer"),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
login_response.registration_created = true;
|
||||
}
|
||||
|
||||
// Check email is verified when required
|
||||
login_response.verify_email_sent =
|
||||
send_verification_email_if_required(&context, &local_site, &local_user, &person).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !login_response.registration_created && !login_response.verify_email_sent {
|
||||
let jwt = Claims::generate(local_user.id, req, &context).await?;
|
||||
login_response.jwt = Some(jwt);
|
||||
}
|
||||
|
||||
return Ok(Json(login_response));
|
||||
}
|
||||
|
||||
async fn create_person(
|
||||
username: String,
|
||||
local_site: &LocalSite,
|
||||
instance_id: InstanceId,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> Result<Person, LemmyError> {
|
||||
let actor_keypair = generate_actor_keypair()?;
|
||||
is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?;
|
||||
let actor_id = generate_local_apub_endpoint(
|
||||
EndpointType::Person,
|
||||
&username,
|
||||
&context.settings().get_protocol_and_hostname(),
|
||||
)?;
|
||||
|
||||
// Register the new person
|
||||
let person_form = PersonInsertForm {
|
||||
actor_id: Some(actor_id.clone()),
|
||||
inbox_url: Some(generate_inbox_url(&actor_id)?),
|
||||
shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
|
||||
private_key: Some(actor_keypair.private_key),
|
||||
..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id)
|
||||
};
|
||||
|
||||
// insert the person
|
||||
let inserted_person = Person::create(&mut context.pool(), &person_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::UserAlreadyExists)?;
|
||||
|
||||
Ok(inserted_person)
|
||||
}
|
||||
|
||||
fn get_language_tags(req: &HttpRequest) -> Vec<String> {
|
||||
req
|
||||
.headers()
|
||||
.get("Accept-Language")
|
||||
.map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default()))
|
||||
.iter()
|
||||
.flatten()
|
||||
// Remove the optional region code
|
||||
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
async fn create_local_user(
|
||||
context: &Data<LemmyContext>,
|
||||
language_tags: Vec<String>,
|
||||
local_user_form: &LocalUserInsertForm,
|
||||
) -> Result<LocalUser, LemmyError> {
|
||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||
// use hashset to avoid duplicates
|
||||
let mut language_ids = HashSet::new();
|
||||
for l in language_tags {
|
||||
if let Some(found) = all_languages.iter().find(|all| all.code == l) {
|
||||
language_ids.insert(found.id);
|
||||
}
|
||||
}
|
||||
let language_ids = language_ids.into_iter().collect();
|
||||
|
||||
let inserted_local_user =
|
||||
LocalUser::create(&mut context.pool(), local_user_form, language_ids).await?;
|
||||
|
||||
Ok(inserted_local_user)
|
||||
}
|
||||
|
||||
async fn send_verification_email_if_required(
|
||||
context: &Data<LemmyContext>,
|
||||
local_site: &LocalSite,
|
||||
local_user: &LocalUser,
|
||||
person: &Person,
|
||||
) -> LemmyResult<bool> {
|
||||
let mut sent = false;
|
||||
if !local_user.admin && local_site.require_email_verification && !local_user.email_verified {
|
||||
let local_user_view = LocalUserView {
|
||||
local_user: local_user.clone(),
|
||||
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
|
||||
person: person.clone(),
|
||||
counts: PersonAggregates::default(),
|
||||
};
|
||||
|
||||
send_verification_email(
|
||||
&local_user_view,
|
||||
&local_user
|
||||
.email
|
||||
.clone()
|
||||
.expect("invalid verification email"),
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sent = true;
|
||||
}
|
||||
Ok(sent)
|
||||
}
|
||||
|
||||
fn validate_registration_answer(
|
||||
require_registration_application: bool,
|
||||
answer: &Option<String>,
|
||||
) -> LemmyResult<()> {
|
||||
if require_registration_application && answer.is_none() {
|
||||
Err(LemmyErrorType::RegistrationApplicationAnswerRequired)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn oauth_request_access_token(
|
||||
context: &Data<LemmyContext>,
|
||||
oauth_provider: &OAuthProvider,
|
||||
code: &str,
|
||||
redirect_uri: &str,
|
||||
) -> LemmyResult<TokenResponse> {
|
||||
// Request an Access Token from the OAUTH provider
|
||||
let response = context
|
||||
.client()
|
||||
.post(oauth_provider.token_endpoint.as_str())
|
||||
.header("Accept", "application/json")
|
||||
.form(&[
|
||||
("grant_type", "authorization_code"),
|
||||
("code", code),
|
||||
("redirect_uri", redirect_uri),
|
||||
("client_id", &oauth_provider.client_id),
|
||||
("client_secret", &oauth_provider.client_secret),
|
||||
])
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
|
||||
if !response.status().is_success() {
|
||||
Err(LemmyErrorType::OauthLoginFailed)?;
|
||||
}
|
||||
|
||||
// Extract the access token
|
||||
let token_response = response
|
||||
.json::<TokenResponse>()
|
||||
.await
|
||||
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
|
||||
|
||||
Ok(token_response)
|
||||
}
|
||||
|
||||
async fn oidc_get_user_info(
|
||||
context: &Data<LemmyContext>,
|
||||
oauth_provider: &OAuthProvider,
|
||||
access_token: &str,
|
||||
) -> LemmyResult<serde_json::Value> {
|
||||
// Request the user info from the OAUTH provider
|
||||
let response = context
|
||||
.client()
|
||||
.get(oauth_provider.userinfo_endpoint.as_str())
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
|
||||
if !response.status().is_success() {
|
||||
Err(LemmyErrorType::OauthLoginFailed)?;
|
||||
}
|
||||
|
||||
// Extract the OAUTH user_id claim from the returned user_info
|
||||
let user_info = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
|
||||
|
||||
Ok(user_info)
|
||||
}
|
||||
|
||||
fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult<String> {
|
||||
if let Some(value) = user_info.get(key) {
|
||||
let result = serde_json::from_value::<String>(value.clone())
|
||||
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
|
||||
return Ok(result);
|
||||
}
|
||||
Err(LemmyErrorType::OauthLoginFailed)?
|
||||
}
|
||||
|
|
|
@ -8,7 +8,11 @@ use lemmy_api_common::{
|
|||
utils::purge_user_account,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::source::{login_token::LoginToken, person::Person};
|
||||
use lemmy_db_schema::source::{
|
||||
login_token::LoginToken,
|
||||
oauth_account::OAuthAccount,
|
||||
person::Person,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
|
@ -19,11 +23,12 @@ pub async fn delete_account(
|
|||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Verify the password
|
||||
let valid: bool = verify(
|
||||
&data.password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
let valid: bool = local_user_view
|
||||
.local_user
|
||||
.password_encrypted
|
||||
.as_ref()
|
||||
.and_then(|password_encrypted| verify(&data.password, password_encrypted).ok())
|
||||
.unwrap_or(false);
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
|
@ -31,6 +36,7 @@ pub async fn delete_account(
|
|||
if data.delete_content {
|
||||
purge_user_account(local_user_view.person.id, &context).await?;
|
||||
} else {
|
||||
OAuthAccount::delete_user_accounts(&mut context.pool(), local_user_view.local_user.id).await?;
|
||||
Person::delete_account(&mut context.pool(), local_user_view.person.id).await?;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ use anyhow::anyhow;
|
|||
use chrono::{DateTime, Utc};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
utils::{remove_user_data, remove_user_data_in_community},
|
||||
utils::{remove_or_restore_user_data_in_community, remove_user_data},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -205,8 +205,13 @@ impl ActivityHandler for BlockUser {
|
|||
.ok();
|
||||
|
||||
if self.remove_data.unwrap_or(false) {
|
||||
remove_user_data_in_community(community.id, blocked_person.id, &mut context.pool())
|
||||
.await?;
|
||||
remove_or_restore_user_data_in_community(
|
||||
community.id,
|
||||
blocked_person.id,
|
||||
true,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// write to mod log
|
||||
|
|
|
@ -22,7 +22,6 @@ use lemmy_db_schema::{
|
|||
traits::Crud,
|
||||
utils::DbPool,
|
||||
};
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyResult},
|
||||
LemmyErrorType,
|
||||
|
@ -137,18 +136,12 @@ pub(crate) async fn send_ban_from_site(
|
|||
moderator: Person,
|
||||
banned_user: Person,
|
||||
reason: Option<String>,
|
||||
remove_data: Option<bool>,
|
||||
remove_or_restore_data: Option<bool>,
|
||||
ban: bool,
|
||||
expires: Option<i64>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
let site = SiteOrCommunity::Site(
|
||||
SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?
|
||||
.site
|
||||
.into(),
|
||||
);
|
||||
let site = SiteOrCommunity::Site(Site::read_local(&mut context.pool()).await?.into());
|
||||
let expires = check_expire_time(expires)?;
|
||||
|
||||
// if the action affects a local user, federate to other instances
|
||||
|
@ -158,7 +151,7 @@ pub(crate) async fn send_ban_from_site(
|
|||
&site,
|
||||
&banned_user.into(),
|
||||
&moderator.into(),
|
||||
remove_data.unwrap_or(false),
|
||||
remove_or_restore_data.unwrap_or(false),
|
||||
reason.clone(),
|
||||
expires,
|
||||
&context,
|
||||
|
@ -169,6 +162,7 @@ pub(crate) async fn send_ban_from_site(
|
|||
&site,
|
||||
&banned_user.into(),
|
||||
&moderator.into(),
|
||||
remove_or_restore_data.unwrap_or(false),
|
||||
reason.clone(),
|
||||
&context,
|
||||
)
|
||||
|
@ -197,7 +191,7 @@ pub(crate) async fn send_ban_from_community(
|
|||
&SiteOrCommunity::Community(community),
|
||||
&banned_person.into(),
|
||||
&mod_.into(),
|
||||
data.remove_data.unwrap_or(false),
|
||||
data.remove_or_restore_data.unwrap_or(false),
|
||||
data.reason.clone(),
|
||||
expires,
|
||||
&context,
|
||||
|
@ -208,6 +202,7 @@ pub(crate) async fn send_ban_from_community(
|
|||
&SiteOrCommunity::Community(community),
|
||||
&banned_person.into(),
|
||||
&mod_.into(),
|
||||
data.remove_or_restore_data.unwrap_or(false),
|
||||
data.reason.clone(),
|
||||
&context,
|
||||
)
|
||||
|
|
|
@ -17,7 +17,10 @@ use activitypub_federation::{
|
|||
protocol::verification::verify_domains_match,
|
||||
traits::{ActivityHandler, Actor},
|
||||
};
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
utils::{remove_or_restore_user_data_in_community, restore_user_data},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
activity::ActivitySendTargets,
|
||||
|
@ -36,6 +39,7 @@ impl UndoBlockUser {
|
|||
target: &SiteOrCommunity,
|
||||
user: &ApubPerson,
|
||||
mod_: &ApubPerson,
|
||||
restore_data: bool,
|
||||
reason: Option<String>,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
|
@ -58,6 +62,7 @@ impl UndoBlockUser {
|
|||
kind: UndoType::Undo,
|
||||
id: id.clone(),
|
||||
audience,
|
||||
restore_data: Some(restore_data),
|
||||
};
|
||||
|
||||
let mut inboxes = ActivitySendTargets::to_inbox(user.shared_inbox_or_inbox());
|
||||
|
@ -114,6 +119,10 @@ impl ActivityHandler for UndoBlockUser {
|
|||
)
|
||||
.await?;
|
||||
|
||||
if self.restore_data.unwrap_or(false) {
|
||||
restore_user_data(blocked_person.id, context).await?;
|
||||
}
|
||||
|
||||
// write mod log
|
||||
let form = ModBanForm {
|
||||
mod_person_id: mod_person.id,
|
||||
|
@ -132,6 +141,16 @@ impl ActivityHandler for UndoBlockUser {
|
|||
};
|
||||
CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form).await?;
|
||||
|
||||
if self.restore_data.unwrap_or(false) {
|
||||
remove_or_restore_user_data_in_community(
|
||||
community.id,
|
||||
blocked_person.id,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// write to mod log
|
||||
let form = ModBanFromCommunityForm {
|
||||
mod_person_id: mod_person.id,
|
||||
|
|
|
@ -340,7 +340,7 @@ pub async fn match_outgoing_activities(
|
|||
moderator,
|
||||
banned_user,
|
||||
reason,
|
||||
remove_data,
|
||||
remove_or_restore_data,
|
||||
ban,
|
||||
expires,
|
||||
} => {
|
||||
|
@ -348,7 +348,7 @@ pub async fn match_outgoing_activities(
|
|||
moderator,
|
||||
banned_user,
|
||||
reason,
|
||||
remove_data,
|
||||
remove_or_restore_data,
|
||||
ban,
|
||||
expires,
|
||||
context,
|
||||
|
|
|
@ -23,9 +23,7 @@ pub async fn list_posts(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
) -> LemmyResult<Json<GetPostsResponse>> {
|
||||
let local_site = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site.local_site)?;
|
||||
|
||||
|
|
|
@ -26,9 +26,7 @@ pub async fn read_person(
|
|||
Err(LemmyErrorType::NoIdGiven)?
|
||||
}
|
||||
|
||||
let local_site = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site.local_site)?;
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use lemmy_db_views::{
|
|||
structs::{LocalUserView, SiteView},
|
||||
};
|
||||
use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn search(
|
||||
|
@ -21,9 +21,7 @@ pub async fn search(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
) -> LemmyResult<Json<SearchResponse>> {
|
||||
let local_site = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site.local_site)?;
|
||||
|
||||
|
@ -56,133 +54,92 @@ pub async fn search(
|
|||
};
|
||||
let creator_id = data.creator_id;
|
||||
let local_user = local_user_view.as_ref().map(|l| &l.local_user);
|
||||
let post_title_only = data.post_title_only;
|
||||
|
||||
let posts_query = PostQuery {
|
||||
sort: (sort),
|
||||
listing_type: (listing_type),
|
||||
community_id: (community_id),
|
||||
creator_id: (creator_id),
|
||||
local_user,
|
||||
search_term: (Some(q.clone())),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
title_only: (post_title_only),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let comment_query = CommentQuery {
|
||||
sort: (sort.map(post_to_comment_sort_type)),
|
||||
listing_type: (listing_type),
|
||||
search_term: (Some(q.clone())),
|
||||
community_id: (community_id),
|
||||
creator_id: (creator_id),
|
||||
local_user,
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let community_query = CommunityQuery {
|
||||
sort: (sort),
|
||||
listing_type: (listing_type),
|
||||
search_term: (Some(q.clone())),
|
||||
local_user,
|
||||
is_mod_or_admin: (is_admin),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let person_query = PersonQuery {
|
||||
sort,
|
||||
search_term: (Some(q.clone())),
|
||||
listing_type: (listing_type),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
};
|
||||
|
||||
match search_type {
|
||||
SearchType::Posts => {
|
||||
posts = PostQuery {
|
||||
sort: (sort),
|
||||
listing_type: (listing_type),
|
||||
community_id: (community_id),
|
||||
creator_id: (creator_id),
|
||||
local_user,
|
||||
search_term: (Some(q)),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?;
|
||||
posts = posts_query
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?;
|
||||
}
|
||||
SearchType::Comments => {
|
||||
comments = CommentQuery {
|
||||
sort: (sort.map(post_to_comment_sort_type)),
|
||||
listing_type: (listing_type),
|
||||
search_term: (Some(q)),
|
||||
community_id: (community_id),
|
||||
creator_id: (creator_id),
|
||||
local_user,
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?;
|
||||
comments = comment_query.list(&mut context.pool()).await?;
|
||||
}
|
||||
SearchType::Communities => {
|
||||
communities = CommunityQuery {
|
||||
sort: (sort),
|
||||
listing_type: (listing_type),
|
||||
search_term: (Some(q)),
|
||||
local_user,
|
||||
is_mod_or_admin: (is_admin),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?;
|
||||
communities = community_query
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?;
|
||||
}
|
||||
SearchType::Users => {
|
||||
users = PersonQuery {
|
||||
sort,
|
||||
search_term: (Some(q)),
|
||||
listing_type: (listing_type),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?;
|
||||
users = person_query.list(&mut context.pool()).await?;
|
||||
}
|
||||
SearchType::All => {
|
||||
// If the community or creator is included, dont search communities or users
|
||||
let community_or_creator_included =
|
||||
data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
|
||||
|
||||
let q = data.q.clone();
|
||||
posts = posts_query
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?;
|
||||
|
||||
posts = PostQuery {
|
||||
sort: (sort),
|
||||
listing_type: (listing_type),
|
||||
community_id: (community_id),
|
||||
creator_id: (creator_id),
|
||||
local_user,
|
||||
search_term: (Some(q)),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?;
|
||||
|
||||
let q = data.q.clone();
|
||||
|
||||
comments = CommentQuery {
|
||||
sort: (sort.map(post_to_comment_sort_type)),
|
||||
listing_type: (listing_type),
|
||||
search_term: (Some(q)),
|
||||
community_id: (community_id),
|
||||
creator_id: (creator_id),
|
||||
local_user,
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?;
|
||||
|
||||
let q = data.q.clone();
|
||||
comments = comment_query.list(&mut context.pool()).await?;
|
||||
|
||||
communities = if community_or_creator_included {
|
||||
vec![]
|
||||
} else {
|
||||
CommunityQuery {
|
||||
sort: (sort),
|
||||
listing_type: (listing_type),
|
||||
search_term: (Some(q)),
|
||||
local_user,
|
||||
is_mod_or_admin: (is_admin),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?
|
||||
community_query
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
.await?
|
||||
};
|
||||
|
||||
let q = data.q.clone();
|
||||
|
||||
users = if community_or_creator_included {
|
||||
vec![]
|
||||
} else {
|
||||
PersonQuery {
|
||||
sort,
|
||||
search_term: (Some(q)),
|
||||
listing_type: (listing_type),
|
||||
page: (page),
|
||||
limit: (limit),
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?
|
||||
person_query.list(&mut context.pool()).await?
|
||||
};
|
||||
}
|
||||
SearchType::Url => {
|
||||
|
|
|
@ -18,12 +18,9 @@ use activitypub_federation::{
|
|||
};
|
||||
use futures::future::join_all;
|
||||
use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url};
|
||||
use lemmy_db_schema::{utils::FETCH_LIMIT_MAX, SortType};
|
||||
use lemmy_db_views::{post_view::PostQuery, structs::SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyResult},
|
||||
LemmyErrorType,
|
||||
};
|
||||
use lemmy_db_schema::{source::site::Site, utils::FETCH_LIMIT_MAX, SortType};
|
||||
use lemmy_db_views::post_view::PostQuery;
|
||||
use lemmy_utils::error::{LemmyError, LemmyResult};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -38,10 +35,7 @@ impl Collection for ApubCommunityOutbox {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn read_local(owner: &Self::Owner, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||
let site = SiteView::read_local(&mut data.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?
|
||||
.site;
|
||||
let site = Site::read_local(&mut data.pool()).await?;
|
||||
|
||||
let post_views = PostQuery {
|
||||
community_id: Some(owner.id),
|
||||
|
|
|
@ -6,16 +6,12 @@ use crate::{
|
|||
use activitypub_federation::{config::Data, traits::Object};
|
||||
use actix_web::HttpResponse;
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use lemmy_db_schema::source::site::Site;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) async fn get_apub_site_http(context: Data<LemmyContext>) -> LemmyResult<HttpResponse> {
|
||||
let site: ApubSite = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?
|
||||
.site
|
||||
.into();
|
||||
let site: ApubSite = Site::read_local(&mut context.pool()).await?.into();
|
||||
|
||||
let apub = site.into_json(&context).await?;
|
||||
create_apub_response(&apub)
|
||||
|
|
|
@ -267,9 +267,9 @@ impl Object for ApubPost {
|
|||
|
||||
// Generates a post thumbnail in background task, because some sites can be very slow to
|
||||
// respond.
|
||||
spawn_try_task(async move {
|
||||
generate_post_link_metadata(post_, None, |_| None, local_site, context_).await
|
||||
});
|
||||
spawn_try_task(
|
||||
async move { generate_post_link_metadata(post_, None, |_| None, context_).await },
|
||||
);
|
||||
|
||||
Ok(post.into())
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@ pub struct UndoBlockUser {
|
|||
pub(crate) kind: UndoType,
|
||||
pub(crate) id: Url,
|
||||
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
|
||||
|
||||
/// Quick and dirty solution.
|
||||
/// TODO: send a separate Delete activity instead
|
||||
pub(crate) restore_data: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
|
|
@ -35,9 +35,11 @@ impl LocalUser {
|
|||
) -> Result<LocalUser, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let mut form_with_encrypted_password = form.clone();
|
||||
let password_hash =
|
||||
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
|
||||
form_with_encrypted_password.password_encrypted = password_hash;
|
||||
|
||||
if let Some(password_encrypted) = &form.password_encrypted {
|
||||
let password_hash = hash(password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
|
||||
form_with_encrypted_password.password_encrypted = Some(password_hash);
|
||||
}
|
||||
|
||||
let local_user_ = insert_into(local_user::table)
|
||||
.values(form_with_encrypted_password)
|
||||
|
@ -348,7 +350,7 @@ impl LocalUserOptionHelper for Option<&LocalUser> {
|
|||
|
||||
impl LocalUserInsertForm {
|
||||
pub fn test_form(person_id: PersonId) -> Self {
|
||||
Self::new(person_id, String::new())
|
||||
Self::new(person_id, Some(String::new()))
|
||||
}
|
||||
|
||||
pub fn test_form_admin(person_id: PersonId) -> Self {
|
||||
|
|
|
@ -22,6 +22,8 @@ pub mod local_user;
|
|||
pub mod local_user_vote_display_mode;
|
||||
pub mod login_token;
|
||||
pub mod moderator;
|
||||
pub mod oauth_account;
|
||||
pub mod oauth_provider;
|
||||
pub mod password_reset_request;
|
||||
pub mod person;
|
||||
pub mod person_block;
|
||||
|
|
59
crates/db_schema/src/impls/oauth_account.rs
Normal file
59
crates/db_schema/src/impls/oauth_account.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use crate::{
|
||||
newtypes::{LocalUserId, OAuthProviderId},
|
||||
schema::{oauth_account, oauth_account::dsl::local_user_id},
|
||||
source::oauth_account::{OAuthAccount, OAuthAccountInsertForm},
|
||||
utils::{get_conn, DbPool},
|
||||
};
|
||||
use diesel::{
|
||||
dsl::{exists, insert_into},
|
||||
result::Error,
|
||||
select,
|
||||
ExpressionMethods,
|
||||
QueryDsl,
|
||||
};
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
impl OAuthAccount {
|
||||
pub async fn read(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_oauth_provider_id: OAuthProviderId,
|
||||
for_local_user_id: LocalUserId,
|
||||
) -> Result<bool, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
select(exists(
|
||||
oauth_account::table.find((for_oauth_provider_id, for_local_user_id)),
|
||||
))
|
||||
.get_result(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create(pool: &mut DbPool<'_>, form: &OAuthAccountInsertForm) -> Result<Self, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(oauth_account::table)
|
||||
.values(form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_oauth_provider_id: OAuthProviderId,
|
||||
for_local_user_id: LocalUserId,
|
||||
) -> Result<usize, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::delete(oauth_account::table.find((for_oauth_provider_id, for_local_user_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_user_accounts(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_local_user_id: LocalUserId,
|
||||
) -> Result<usize, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
diesel::delete(oauth_account::table.filter(local_user_id.eq(for_local_user_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
}
|
71
crates/db_schema/src/impls/oauth_provider.rs
Normal file
71
crates/db_schema/src/impls/oauth_provider.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use crate::{
|
||||
newtypes::OAuthProviderId,
|
||||
schema::oauth_provider,
|
||||
source::oauth_provider::{
|
||||
OAuthProvider,
|
||||
OAuthProviderInsertForm,
|
||||
OAuthProviderUpdateForm,
|
||||
PublicOAuthProvider,
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{get_conn, DbPool},
|
||||
};
|
||||
use diesel::{dsl::insert_into, result::Error, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
#[async_trait]
|
||||
impl Crud for OAuthProvider {
|
||||
type InsertForm = OAuthProviderInsertForm;
|
||||
type UpdateForm = OAuthProviderUpdateForm;
|
||||
type IdType = OAuthProviderId;
|
||||
|
||||
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(oauth_provider::table)
|
||||
.values(form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update(
|
||||
pool: &mut DbPool<'_>,
|
||||
oauth_provider_id: OAuthProviderId,
|
||||
form: &Self::UpdateForm,
|
||||
) -> Result<Self, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::update(oauth_provider::table.find(oauth_provider_id))
|
||||
.set(form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl OAuthProvider {
|
||||
pub async fn get_all(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let oauth_providers = oauth_provider::table
|
||||
.order(oauth_provider::id)
|
||||
.select(oauth_provider::all_columns)
|
||||
.load::<OAuthProvider>(conn)
|
||||
.await?;
|
||||
|
||||
Ok(oauth_providers)
|
||||
}
|
||||
|
||||
pub fn convert_providers_to_public(
|
||||
oauth_providers: Vec<OAuthProvider>,
|
||||
) -> Vec<PublicOAuthProvider> {
|
||||
let mut result = Vec::<PublicOAuthProvider>::new();
|
||||
for oauth_provider in &oauth_providers {
|
||||
if oauth_provider.enabled {
|
||||
result.push(PublicOAuthProvider(oauth_provider.clone()));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn get_all_public(pool: &mut DbPool<'_>) -> Result<Vec<PublicOAuthProvider>, Error> {
|
||||
let oauth_providers = OAuthProvider::get_all(pool).await?;
|
||||
Ok(Self::convert_providers_to_public(oauth_providers))
|
||||
}
|
||||
}
|
|
@ -121,6 +121,18 @@ impl Person {
|
|||
.load::<CommunityId>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn is_username_taken(pool: &mut DbPool<'_>, username: &str) -> Result<bool, Error> {
|
||||
use diesel::dsl::{exists, select};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
select(exists(
|
||||
person::table
|
||||
.filter(lower(person::name).eq(username.to_lowercase()))
|
||||
.filter(person::local.eq(true)),
|
||||
))
|
||||
.get_result(conn)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl PersonInsertForm {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
newtypes::{DbUrl, InstanceId, SiteId},
|
||||
schema::site,
|
||||
schema::{local_site, site},
|
||||
source::{
|
||||
actor_language::SiteLanguage,
|
||||
site::{Site, SiteInsertForm, SiteUpdateForm},
|
||||
|
@ -10,6 +10,7 @@ use crate::{
|
|||
};
|
||||
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use url::Url;
|
||||
|
||||
#[async_trait]
|
||||
|
@ -102,4 +103,18 @@ impl Site {
|
|||
url.set_query(None);
|
||||
url
|
||||
}
|
||||
|
||||
pub async fn read_local(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
Ok(
|
||||
site::table
|
||||
.inner_join(local_site::table)
|
||||
.select(site::all_columns)
|
||||
.first(conn)
|
||||
.await
|
||||
.optional()?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,6 +154,12 @@ pub struct CustomEmojiId(i32);
|
|||
/// The registration application id.
|
||||
pub struct RegistrationApplicationId(i32);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// The oauth provider id.
|
||||
pub struct OAuthProviderId(pub i32);
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "Ltree")]
|
||||
|
|
|
@ -370,7 +370,6 @@ diesel::table! {
|
|||
site_id -> Int4,
|
||||
site_setup -> Bool,
|
||||
enable_downvotes -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
community_creation_admin_only -> Bool,
|
||||
require_email_verification -> Bool,
|
||||
application_question -> Nullable<Text>,
|
||||
|
@ -393,6 +392,7 @@ diesel::table! {
|
|||
federation_signed_fetch -> Bool,
|
||||
default_post_listing_mode -> PostListingModeEnum,
|
||||
default_sort_type -> SortTypeEnum,
|
||||
oauth_registration -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,7 +436,7 @@ diesel::table! {
|
|||
local_user (id) {
|
||||
id -> Int4,
|
||||
person_id -> Int4,
|
||||
password_encrypted -> Text,
|
||||
password_encrypted -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
show_nsfw -> Bool,
|
||||
theme -> Text,
|
||||
|
@ -612,6 +612,36 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
oauth_account (oauth_provider_id, local_user_id) {
|
||||
local_user_id -> Int4,
|
||||
oauth_provider_id -> Int4,
|
||||
oauth_user_id -> Text,
|
||||
published -> Timestamptz,
|
||||
updated -> Nullable<Timestamptz>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
oauth_provider (id) {
|
||||
id -> Int4,
|
||||
display_name -> Text,
|
||||
issuer -> Text,
|
||||
authorization_endpoint -> Text,
|
||||
token_endpoint -> Text,
|
||||
userinfo_endpoint -> Text,
|
||||
id_claim -> Text,
|
||||
client_id -> Text,
|
||||
client_secret -> Text,
|
||||
scopes -> Text,
|
||||
auto_verify_email -> Bool,
|
||||
account_linking_enabled -> Bool,
|
||||
enabled -> Bool,
|
||||
published -> Timestamptz,
|
||||
updated -> Nullable<Timestamptz>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
password_reset_request (id) {
|
||||
id -> Int4,
|
||||
|
@ -1004,6 +1034,8 @@ diesel::joinable!(mod_remove_community -> person (mod_person_id));
|
|||
diesel::joinable!(mod_remove_post -> person (mod_person_id));
|
||||
diesel::joinable!(mod_remove_post -> post (post_id));
|
||||
diesel::joinable!(mod_transfer_community -> community (community_id));
|
||||
diesel::joinable!(oauth_account -> local_user (local_user_id));
|
||||
diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id));
|
||||
diesel::joinable!(password_reset_request -> local_user (local_user_id));
|
||||
diesel::joinable!(person -> instance (instance_id));
|
||||
diesel::joinable!(person_aggregates -> person (person_id));
|
||||
|
@ -1085,6 +1117,8 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||
mod_remove_community,
|
||||
mod_remove_post,
|
||||
mod_transfer_community,
|
||||
oauth_account,
|
||||
oauth_provider,
|
||||
password_reset_request,
|
||||
person,
|
||||
person_aggregates,
|
||||
|
|
|
@ -29,8 +29,6 @@ pub struct LocalSite {
|
|||
pub site_setup: bool,
|
||||
/// Whether downvotes are enabled.
|
||||
pub enable_downvotes: bool,
|
||||
/// Whether NSFW is enabled.
|
||||
pub enable_nsfw: bool,
|
||||
/// Whether only admins can create communities.
|
||||
pub community_creation_admin_only: bool,
|
||||
/// Whether emails are required.
|
||||
|
@ -70,6 +68,8 @@ pub struct LocalSite {
|
|||
pub default_post_listing_mode: PostListingMode,
|
||||
/// Default value for [LocalUser.post_listing_mode]
|
||||
pub default_sort_type: SortType,
|
||||
/// Whether or not external auth methods can auto-register users.
|
||||
pub oauth_registration: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, TypedBuilder)]
|
||||
|
@ -81,7 +81,6 @@ pub struct LocalSiteInsertForm {
|
|||
pub site_id: SiteId,
|
||||
pub site_setup: Option<bool>,
|
||||
pub enable_downvotes: Option<bool>,
|
||||
pub enable_nsfw: Option<bool>,
|
||||
pub community_creation_admin_only: Option<bool>,
|
||||
pub require_email_verification: Option<bool>,
|
||||
pub application_question: Option<String>,
|
||||
|
@ -97,6 +96,7 @@ pub struct LocalSiteInsertForm {
|
|||
pub captcha_enabled: Option<bool>,
|
||||
pub captcha_difficulty: Option<String>,
|
||||
pub registration_mode: Option<RegistrationMode>,
|
||||
pub oauth_registration: Option<bool>,
|
||||
pub reports_email_admins: Option<bool>,
|
||||
pub federation_signed_fetch: Option<bool>,
|
||||
pub default_post_listing_mode: Option<PostListingMode>,
|
||||
|
@ -109,7 +109,6 @@ pub struct LocalSiteInsertForm {
|
|||
pub struct LocalSiteUpdateForm {
|
||||
pub site_setup: Option<bool>,
|
||||
pub enable_downvotes: Option<bool>,
|
||||
pub enable_nsfw: Option<bool>,
|
||||
pub community_creation_admin_only: Option<bool>,
|
||||
pub require_email_verification: Option<bool>,
|
||||
pub application_question: Option<Option<String>>,
|
||||
|
@ -125,6 +124,7 @@ pub struct LocalSiteUpdateForm {
|
|||
pub captcha_enabled: Option<bool>,
|
||||
pub captcha_difficulty: Option<String>,
|
||||
pub registration_mode: Option<RegistrationMode>,
|
||||
pub oauth_registration: Option<bool>,
|
||||
pub reports_email_admins: Option<bool>,
|
||||
pub updated: Option<Option<DateTime<Utc>>>,
|
||||
pub federation_signed_fetch: Option<bool>,
|
||||
|
|
|
@ -24,7 +24,7 @@ pub struct LocalUser {
|
|||
/// The person_id for the local user.
|
||||
pub person_id: PersonId,
|
||||
#[serde(skip)]
|
||||
pub password_encrypted: SensitiveString,
|
||||
pub password_encrypted: Option<SensitiveString>,
|
||||
pub email: Option<SensitiveString>,
|
||||
/// Whether to show NSFW content.
|
||||
pub show_nsfw: bool,
|
||||
|
@ -70,7 +70,7 @@ pub struct LocalUser {
|
|||
#[cfg_attr(feature = "full", diesel(table_name = local_user))]
|
||||
pub struct LocalUserInsertForm {
|
||||
pub person_id: PersonId,
|
||||
pub password_encrypted: String,
|
||||
pub password_encrypted: Option<String>,
|
||||
#[new(default)]
|
||||
pub email: Option<String>,
|
||||
#[new(default)]
|
||||
|
|
|
@ -27,6 +27,8 @@ pub mod local_user;
|
|||
pub mod local_user_vote_display_mode;
|
||||
pub mod login_token;
|
||||
pub mod moderator;
|
||||
pub mod oauth_account;
|
||||
pub mod oauth_provider;
|
||||
pub mod password_reset_request;
|
||||
pub mod person;
|
||||
pub mod person_block;
|
||||
|
|
32
crates/db_schema/src/source/oauth_account.rs
Normal file
32
crates/db_schema/src/source/oauth_account.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::newtypes::{LocalUserId, OAuthProviderId};
|
||||
#[cfg(feature = "full")]
|
||||
use crate::schema::oauth_account;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "full", derive(Queryable, Selectable, TS))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = oauth_account))]
|
||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// An auth account method.
|
||||
pub struct OAuthAccount {
|
||||
pub local_user_id: LocalUserId,
|
||||
pub oauth_provider_id: OAuthProviderId,
|
||||
pub oauth_user_id: String,
|
||||
pub published: DateTime<Utc>,
|
||||
pub updated: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_new::new)]
|
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = oauth_account))]
|
||||
pub struct OAuthAccountInsertForm {
|
||||
pub local_user_id: LocalUserId,
|
||||
pub oauth_provider_id: OAuthProviderId,
|
||||
pub oauth_user_id: String,
|
||||
}
|
131
crates/db_schema/src/source/oauth_provider.rs
Normal file
131
crates/db_schema/src/source/oauth_provider.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
#[cfg(feature = "full")]
|
||||
use crate::schema::oauth_provider;
|
||||
use crate::{
|
||||
newtypes::{DbUrl, OAuthProviderId},
|
||||
sensitive::SensitiveString,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{
|
||||
ser::{SerializeStruct, Serializer},
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
use serde_with::skip_serializing_none;
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))]
|
||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// oauth provider with client_secret - should never be sent to the client
|
||||
pub struct OAuthProvider {
|
||||
pub id: OAuthProviderId,
|
||||
/// The OAuth 2.0 provider name displayed to the user on the Login page
|
||||
pub display_name: String,
|
||||
/// The issuer url of the OAUTH provider.
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub issuer: DbUrl,
|
||||
/// The authorization endpoint is used to interact with the resource owner and obtain an
|
||||
/// authorization grant. This is usually provided by the OAUTH provider.
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub authorization_endpoint: DbUrl,
|
||||
/// The token endpoint is used by the client to obtain an access token by presenting its
|
||||
/// authorization grant or refresh token. This is usually provided by the OAUTH provider.
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub token_endpoint: DbUrl,
|
||||
/// The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the
|
||||
/// authenticated End-User. This is defined in the OIDC specification.
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub userinfo_endpoint: DbUrl,
|
||||
/// The OAuth 2.0 claim containing the unique user ID returned by the provider. Usually this
|
||||
/// should be set to "sub".
|
||||
pub id_claim: String,
|
||||
/// The client_id is provided by the OAuth 2.0 provider and is a unique identifier to this
|
||||
/// service
|
||||
pub client_id: String,
|
||||
/// The client_secret is provided by the OAuth 2.0 provider and is used to authenticate this
|
||||
/// service with the provider
|
||||
#[serde(skip)]
|
||||
pub client_secret: SensitiveString,
|
||||
/// Lists the scopes requested from users. Users will have to grant access to the requested scope
|
||||
/// at sign up.
|
||||
pub scopes: String,
|
||||
/// Automatically sets email as verified on registration
|
||||
pub auto_verify_email: bool,
|
||||
/// Allows linking an OAUTH account to an existing user account by matching emails
|
||||
pub account_linking_enabled: bool,
|
||||
/// switch to enable or disable an oauth provider
|
||||
pub enabled: bool,
|
||||
pub published: DateTime<Utc>,
|
||||
pub updated: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
// A subset of OAuthProvider used for public requests, for example to display the OAUTH buttons on
|
||||
// the login page
|
||||
pub struct PublicOAuthProvider(pub OAuthProvider);
|
||||
|
||||
impl Serialize for PublicOAuthProvider {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("PublicOAuthProvider", 5)?;
|
||||
state.serialize_field("id", &self.0.id)?;
|
||||
state.serialize_field("display_name", &self.0.display_name)?;
|
||||
state.serialize_field("authorization_endpoint", &self.0.authorization_endpoint)?;
|
||||
state.serialize_field("client_id", &self.0.client_id)?;
|
||||
state.serialize_field("scopes", &self.0.scopes)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
pub struct OAuthProviderInsertForm {
|
||||
pub display_name: String,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub issuer: DbUrl,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub authorization_endpoint: DbUrl,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub token_endpoint: DbUrl,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub userinfo_endpoint: DbUrl,
|
||||
pub id_claim: String,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub scopes: String,
|
||||
pub auto_verify_email: bool,
|
||||
pub account_linking_enabled: bool,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
pub struct OAuthProviderUpdateForm {
|
||||
pub display_name: Option<String>,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub authorization_endpoint: Option<DbUrl>,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub token_endpoint: Option<DbUrl>,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub userinfo_endpoint: Option<DbUrl>,
|
||||
pub id_claim: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
pub scopes: Option<String>,
|
||||
pub auto_verify_email: Option<bool>,
|
||||
pub account_linking_enabled: Option<bool>,
|
||||
pub enabled: Option<bool>,
|
||||
pub updated: Option<Option<DateTime<Utc>>>,
|
||||
}
|
|
@ -30,7 +30,7 @@ use i_love_jesus::CursorKey;
|
|||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
settings::SETTINGS,
|
||||
utils::validation::clean_url_params,
|
||||
utils::validation::clean_url,
|
||||
};
|
||||
use regex::Regex;
|
||||
use rustls::{
|
||||
|
@ -288,7 +288,7 @@ pub fn is_email_regex(test: &str) -> bool {
|
|||
EMAIL_REGEX.is_match(test)
|
||||
}
|
||||
|
||||
/// Takes an API text input, and converts it to an optional diesel DB update.
|
||||
/// Takes an API optional text input, and converts it to an optional diesel DB update.
|
||||
pub fn diesel_string_update(opt: Option<&str>) -> Option<Option<String>> {
|
||||
match opt {
|
||||
// An empty string is an erase
|
||||
|
@ -298,6 +298,17 @@ pub fn diesel_string_update(opt: Option<&str>) -> Option<Option<String>> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Takes an API optional text input, and converts it to an optional diesel DB update (for non
|
||||
/// nullable properties).
|
||||
pub fn diesel_required_string_update(opt: Option<&str>) -> Option<String> {
|
||||
match opt {
|
||||
// An empty string is no change
|
||||
Some("") => None,
|
||||
Some(str) => Some(str.into()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes an optional API URL-type input, and converts it to an optional diesel DB update.
|
||||
/// Also cleans the url params.
|
||||
pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult<Option<Option<DbUrl>>> {
|
||||
|
@ -305,7 +316,20 @@ pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult<Option<Option<DbUrl>>
|
|||
// An empty string is an erase
|
||||
Some("") => Ok(Some(None)),
|
||||
Some(str_url) => Url::parse(str_url)
|
||||
.map(|u| Some(Some(clean_url_params(&u).into())))
|
||||
.map(|u| Some(Some(clean_url(&u).into())))
|
||||
.with_lemmy_type(LemmyErrorType::InvalidUrl),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes an optional API URL-type input, and converts it to an optional diesel DB update (for non
|
||||
/// nullable properties). Also cleans the url params.
|
||||
pub fn diesel_required_url_update(opt: Option<&str>) -> LemmyResult<Option<DbUrl>> {
|
||||
match opt {
|
||||
// An empty string is no change
|
||||
Some("") => Ok(None),
|
||||
Some(str_url) => Url::parse(str_url)
|
||||
.map(|u| Some(clean_url(&u).into()))
|
||||
.with_lemmy_type(LemmyErrorType::InvalidUrl),
|
||||
None => Ok(None),
|
||||
}
|
||||
|
@ -316,7 +340,7 @@ pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult<Option<Option<DbUrl>>
|
|||
pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult<Option<DbUrl>> {
|
||||
match opt {
|
||||
Some(str_url) => Url::parse(str_url)
|
||||
.map(|u| Some(clean_url_params(&u).into()))
|
||||
.map(|u| Some(clean_url(&u).into()))
|
||||
.with_lemmy_type(LemmyErrorType::InvalidUrl),
|
||||
None => Ok(None),
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
|
|||
use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{LocalUserId, PersonId},
|
||||
schema::{local_user, local_user_vote_display_mode, person, person_aggregates},
|
||||
newtypes::{LocalUserId, OAuthProviderId, PersonId},
|
||||
schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates},
|
||||
utils::{
|
||||
functions::{coalesce, lower},
|
||||
DbConn,
|
||||
|
@ -23,6 +23,7 @@ enum ReadBy<'a> {
|
|||
Name(&'a str),
|
||||
NameOrEmail(&'a str),
|
||||
Email(&'a str),
|
||||
OAuthID(OAuthProviderId, &'a str),
|
||||
}
|
||||
|
||||
enum ListMode {
|
||||
|
@ -58,12 +59,21 @@ fn queries<'a>(
|
|||
),
|
||||
_ => query,
|
||||
};
|
||||
query
|
||||
let query = query
|
||||
.inner_join(local_user_vote_display_mode::table)
|
||||
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
|
||||
.select(selection)
|
||||
.first(&mut conn)
|
||||
.await
|
||||
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)));
|
||||
|
||||
if let ReadBy::OAuthID(oauth_provider_id, oauth_user_id) = search {
|
||||
query
|
||||
.inner_join(oauth_account::table)
|
||||
.filter(oauth_account::oauth_provider_id.eq(oauth_provider_id))
|
||||
.filter(oauth_account::oauth_user_id.eq(oauth_user_id))
|
||||
.select(selection)
|
||||
.first(&mut conn)
|
||||
.await
|
||||
} else {
|
||||
query.select(selection).first(&mut conn).await
|
||||
}
|
||||
};
|
||||
|
||||
let list = move |mut conn: DbConn<'a>, mode: ListMode| async move {
|
||||
|
@ -120,6 +130,16 @@ impl LocalUserView {
|
|||
queries().read(pool, ReadBy::Email(from_email)).await
|
||||
}
|
||||
|
||||
pub async fn find_by_oauth_id(
|
||||
pool: &mut DbPool<'_>,
|
||||
oauth_provider_id: OAuthProviderId,
|
||||
oauth_user_id: &str,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
queries()
|
||||
.read(pool, ReadBy::OAuthID(oauth_provider_id, oauth_user_id))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
|
||||
queries().list(pool, ListMode::AdminsWithEmails).await
|
||||
}
|
||||
|
|
|
@ -387,13 +387,16 @@ fn queries<'a>() -> Queries<
|
|||
|
||||
if let Some(search_term) = &options.search_term {
|
||||
let searcher = fuzzy_search(search_term);
|
||||
query = query
|
||||
.filter(
|
||||
query = if options.title_only.unwrap_or_default() {
|
||||
query.filter(post::name.ilike(searcher))
|
||||
} else {
|
||||
query.filter(
|
||||
post::name
|
||||
.ilike(searcher.clone())
|
||||
.or(post::body.ilike(searcher)),
|
||||
)
|
||||
.filter(not(post::removed.or(post::deleted)));
|
||||
}
|
||||
.filter(not(post::removed.or(post::deleted)));
|
||||
}
|
||||
|
||||
if !options
|
||||
|
@ -617,6 +620,7 @@ pub struct PostQuery<'a> {
|
|||
pub saved_only: Option<bool>,
|
||||
pub liked_only: Option<bool>,
|
||||
pub disliked_only: Option<bool>,
|
||||
pub title_only: Option<bool>,
|
||||
pub page: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
pub page_after: Option<PaginationCursorData>,
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
use crate::structs::SiteView;
|
||||
use diesel::{result::Error, ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl};
|
||||
use diesel::{ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema::{
|
||||
schema::{local_site, local_site_rate_limit, site, site_aggregates},
|
||||
utils::{get_conn, DbPool},
|
||||
};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
impl SiteView {
|
||||
pub async fn read_local(pool: &mut DbPool<'_>) -> Result<Option<Self>, Error> {
|
||||
pub async fn read_local(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
site::table
|
||||
.inner_join(local_site::table)
|
||||
.inner_join(
|
||||
local_site_rate_limit::table.on(local_site::id.eq(local_site_rate_limit::local_site_id)),
|
||||
)
|
||||
.inner_join(site_aggregates::table)
|
||||
.select((
|
||||
site::all_columns,
|
||||
local_site::all_columns,
|
||||
local_site_rate_limit::all_columns,
|
||||
site_aggregates::all_columns,
|
||||
))
|
||||
.first(conn)
|
||||
.await
|
||||
.optional()
|
||||
Ok(
|
||||
site::table
|
||||
.inner_join(local_site::table)
|
||||
.inner_join(
|
||||
local_site_rate_limit::table.on(local_site::id.eq(local_site_rate_limit::local_site_id)),
|
||||
)
|
||||
.inner_join(site_aggregates::table)
|
||||
.select((
|
||||
site::all_columns,
|
||||
local_site::all_columns,
|
||||
local_site_rate_limit::all_columns,
|
||||
site_aggregates::all_columns,
|
||||
))
|
||||
.first(conn)
|
||||
.await
|
||||
.optional()?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,9 +151,7 @@ async fn get_feed_data(
|
|||
limit: i64,
|
||||
page: i64,
|
||||
) -> LemmyResult<HttpResponse> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&None, &site_view.local_site)?;
|
||||
|
||||
|
@ -258,9 +256,7 @@ async fn get_feed_user(
|
|||
page: &i64,
|
||||
user_name: &str,
|
||||
) -> LemmyResult<Channel> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let person = Person::read_from_name(&mut context.pool(), user_name, false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
@ -298,9 +294,7 @@ async fn get_feed_community(
|
|||
page: &i64,
|
||||
community_name: &str,
|
||||
) -> LemmyResult<Channel> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let community = Community::read_from_name(&mut context.pool(), community_name, false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
@ -345,9 +339,7 @@ async fn get_feed_front(
|
|||
page: &i64,
|
||||
jwt: &str,
|
||||
) -> LemmyResult<Channel> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let local_user = local_user_view_from_jwt(jwt, context).await?;
|
||||
|
||||
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
|
||||
|
@ -382,9 +374,7 @@ async fn get_feed_front(
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult<Channel> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let local_user = local_user_view_from_jwt(jwt, context).await?;
|
||||
let person_id = local_user.local_user.person_id;
|
||||
let show_bot_accounts = local_user.local_user.show_bot_accounts;
|
||||
|
|
|
@ -5,8 +5,7 @@ use actix_web::{
|
|||
Method,
|
||||
StatusCode,
|
||||
},
|
||||
web,
|
||||
web::Query,
|
||||
web::{self, Query},
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use actix_web::{error::ErrorBadRequest, web, Error, HttpResponse, Result};
|
||||
use anyhow::anyhow;
|
||||
use actix_web::{web, Error, HttpResponse, Result};
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_db_schema::RegistrationMode;
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_utils::{
|
||||
cache_header::{cache_1hour, cache_3days},
|
||||
error::{LemmyError, LemmyResult},
|
||||
error::LemmyResult,
|
||||
VERSION,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -44,10 +43,7 @@ async fn node_info_well_known(context: web::Data<LemmyContext>) -> LemmyResult<H
|
|||
}
|
||||
|
||||
async fn node_info(context: web::Data<LemmyContext>) -> Result<HttpResponse, Error> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await
|
||||
.map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?
|
||||
.ok_or(ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?;
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
// Since there are 3 registration options,
|
||||
// we need to set open_registrations as true if RegistrationMode is not Closed.
|
||||
|
|
|
@ -81,6 +81,7 @@ markdown-it = { version = "0.6.1", optional = true }
|
|||
ts-rs = { workspace = true, optional = true }
|
||||
enum-map = { workspace = true, optional = true }
|
||||
cfg-if = "1"
|
||||
clearurls = { version = "0.0.4", features = ["linkify"] }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { workspace = true }
|
||||
|
|
|
@ -54,6 +54,7 @@ pub enum LemmyErrorType {
|
|||
CouldntFindCommentReply,
|
||||
CouldntFindPrivateMessage,
|
||||
CouldntFindActivity,
|
||||
CouldntFindOauthProvider,
|
||||
PersonIsBlocked,
|
||||
CommunityIsBlocked,
|
||||
InstanceIsBlocked,
|
||||
|
@ -82,7 +83,9 @@ pub enum LemmyErrorType {
|
|||
InvalidDefaultPostListingType,
|
||||
RegistrationClosed,
|
||||
RegistrationApplicationAnswerRequired,
|
||||
RegistrationUsernameRequired,
|
||||
EmailAlreadyExists,
|
||||
UsernameAlreadyExists,
|
||||
FederationForbiddenByStrictAllowList,
|
||||
PersonIsBannedFromCommunity,
|
||||
ObjectIsNotPublic,
|
||||
|
@ -177,6 +180,10 @@ pub enum LemmyErrorType {
|
|||
CantBlockLocalInstance,
|
||||
UrlWithoutDomain,
|
||||
InboxTimeout,
|
||||
OauthAuthorizationInvalid,
|
||||
OauthLoginFailed,
|
||||
OauthRegistrationClosed,
|
||||
CouldntDeleteOauthProvider,
|
||||
Unknown(String),
|
||||
CantDeleteSite,
|
||||
UrlLengthOverflow,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use clearurls::UrlCleaner;
|
||||
use itertools::Itertools;
|
||||
use regex::{Regex, RegexBuilder, RegexSet};
|
||||
use std::sync::LazyLock;
|
||||
|
@ -10,12 +11,8 @@ static VALID_MATRIX_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
|||
.expect("compile regex")
|
||||
});
|
||||
// taken from https://en.wikipedia.org/wiki/UTM_parameters
|
||||
static CLEAN_URL_PARAMS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"^(utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid)=",
|
||||
)
|
||||
.expect("compile regex")
|
||||
});
|
||||
static URL_CLEANER: LazyLock<UrlCleaner> =
|
||||
LazyLock::new(|| UrlCleaner::from_embedded_rules().expect("compile clearurls"));
|
||||
const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"];
|
||||
|
||||
const BODY_MAX_LENGTH: usize = 10000;
|
||||
|
@ -257,16 +254,22 @@ pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult<Option
|
|||
)
|
||||
}
|
||||
|
||||
pub fn clean_url_params(url: &Url) -> Url {
|
||||
let mut url_out = url.clone();
|
||||
if let Some(query) = url.query() {
|
||||
let new_query = query
|
||||
.split_inclusive('&')
|
||||
.filter(|q| !CLEAN_URL_PARAMS_REGEX.is_match(q))
|
||||
.collect::<String>();
|
||||
url_out.set_query(Some(&new_query));
|
||||
/// Cleans a url of tracking parameters.
|
||||
pub fn clean_url(url: &Url) -> Url {
|
||||
match URL_CLEANER.clear_single_url(url) {
|
||||
Ok(res) => res.into_owned(),
|
||||
// If there are any errors, just return the original url
|
||||
Err(_) => url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans all the links in a string of tracking parameters.
|
||||
pub fn clean_urls_in_text(text: &str) -> String {
|
||||
match URL_CLEANER.clear_text(text) {
|
||||
Ok(res) => res.into_owned(),
|
||||
// If there are any errors, just return the original text
|
||||
Err(_) => text.to_owned(),
|
||||
}
|
||||
url_out
|
||||
}
|
||||
|
||||
pub fn check_site_visibility_valid(
|
||||
|
@ -357,7 +360,8 @@ mod tests {
|
|||
build_and_check_regex,
|
||||
check_site_visibility_valid,
|
||||
check_urls_are_valid,
|
||||
clean_url_params,
|
||||
clean_url,
|
||||
clean_urls_in_text,
|
||||
is_url_blocked,
|
||||
is_valid_actor_name,
|
||||
is_valid_bio_field,
|
||||
|
@ -378,18 +382,32 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_clean_url_params() -> LemmyResult<()> {
|
||||
let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user%20&id=123")?;
|
||||
let cleaned = clean_url_params(&url);
|
||||
let expected = Url::parse("https://example.com/path/123?user+name=random+user%20&id=123")?;
|
||||
let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123")?;
|
||||
let cleaned = clean_url(&url);
|
||||
let expected = Url::parse("https://example.com/path/123?user+name=random+user&id=123")?;
|
||||
assert_eq!(expected.to_string(), cleaned.to_string());
|
||||
|
||||
let url = Url::parse("https://example.com/path/123")?;
|
||||
let cleaned = clean_url_params(&url);
|
||||
let cleaned = clean_url(&url);
|
||||
assert_eq!(url.to_string(), cleaned.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_body() -> LemmyResult<()> {
|
||||
let text = "[a link](https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123)";
|
||||
let cleaned = clean_urls_in_text(text);
|
||||
let expected = "[a link](https://example.com/path/123?user+name=random+user&id=123)";
|
||||
assert_eq!(expected.to_string(), cleaned.to_string());
|
||||
|
||||
let text = "[a link](https://example.com/path/123)";
|
||||
let cleaned = clean_urls_in_text(text);
|
||||
assert_eq!(text.to_string(), cleaned);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_checks() {
|
||||
assert!(is_valid_post_title("hi").is_err());
|
||||
|
|
17
migrations/2024-09-12-130204_drop-enable-nsfw/down.sql
Normal file
17
migrations/2024-09-12-130204_drop-enable-nsfw/down.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
ALTER TABLE local_site
|
||||
ADD COLUMN enable_nsfw boolean NOT NULL DEFAULT FALSE;
|
||||
|
||||
UPDATE
|
||||
local_site
|
||||
SET
|
||||
enable_nsfw = CASE WHEN site.content_warning IS NULL THEN
|
||||
FALSE
|
||||
ELSE
|
||||
TRUE
|
||||
END
|
||||
FROM
|
||||
site
|
||||
WHERE
|
||||
-- only local site has private key
|
||||
site.private_key IS NOT NULL;
|
||||
|
20
migrations/2024-09-12-130204_drop-enable-nsfw/up.sql
Normal file
20
migrations/2024-09-12-130204_drop-enable-nsfw/up.sql
Normal file
|
@ -0,0 +1,20 @@
|
|||
-- if site has enable_nsfw, set a default content warning
|
||||
UPDATE
|
||||
site
|
||||
SET
|
||||
content_warning = CASE WHEN local_site.enable_nsfw THEN
|
||||
'NSFW'
|
||||
ELSE
|
||||
NULL
|
||||
END
|
||||
FROM
|
||||
local_site
|
||||
-- only local site has private key
|
||||
WHERE
|
||||
private_key IS NOT NULL
|
||||
-- dont overwrite existing content warning
|
||||
AND content_warning IS NOT NULL;
|
||||
|
||||
ALTER TABLE local_site
|
||||
DROP enable_nsfw;
|
||||
|
10
migrations/2024-09-16-174833_create_oauth_provider/down.sql
Normal file
10
migrations/2024-09-16-174833_create_oauth_provider/down.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
DROP TABLE oauth_account;
|
||||
|
||||
DROP TABLE oauth_provider;
|
||||
|
||||
ALTER TABLE local_site
|
||||
DROP COLUMN oauth_registration;
|
||||
|
||||
ALTER TABLE local_user
|
||||
ALTER COLUMN password_encrypted SET NOT NULL;
|
||||
|
34
migrations/2024-09-16-174833_create_oauth_provider/up.sql
Normal file
34
migrations/2024-09-16-174833_create_oauth_provider/up.sql
Normal file
|
@ -0,0 +1,34 @@
|
|||
ALTER TABLE local_user
|
||||
ALTER COLUMN password_encrypted DROP NOT NULL;
|
||||
|
||||
CREATE TABLE oauth_provider (
|
||||
id serial PRIMARY KEY,
|
||||
display_name text NOT NULL,
|
||||
issuer text NOT NULL,
|
||||
authorization_endpoint text NOT NULL,
|
||||
token_endpoint text NOT NULL,
|
||||
userinfo_endpoint text NOT NULL,
|
||||
id_claim text NOT NULL,
|
||||
client_id text NOT NULL UNIQUE,
|
||||
client_secret text NOT NULL,
|
||||
scopes text NOT NULL,
|
||||
auto_verify_email boolean DEFAULT TRUE NOT NULL,
|
||||
account_linking_enabled boolean DEFAULT FALSE NOT NULL,
|
||||
enabled boolean DEFAULT FALSE NOT NULL,
|
||||
published timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated timestamp with time zone
|
||||
);
|
||||
|
||||
ALTER TABLE local_site
|
||||
ADD COLUMN oauth_registration boolean DEFAULT FALSE NOT NULL;
|
||||
|
||||
CREATE TABLE oauth_account (
|
||||
local_user_id int REFERENCES local_user ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||
oauth_provider_id int REFERENCES oauth_provider ON UPDATE CASCADE ON DELETE RESTRICT NOT NULL,
|
||||
oauth_user_id text NOT NULL,
|
||||
published timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated timestamp with time zone,
|
||||
UNIQUE (oauth_provider_id, oauth_user_id),
|
||||
PRIMARY KEY (oauth_provider_id, local_user_id)
|
||||
);
|
||||
|
|
@ -109,6 +109,11 @@ use lemmy_api_crud::{
|
|||
delete::delete_custom_emoji,
|
||||
update::update_custom_emoji,
|
||||
},
|
||||
oauth_provider::{
|
||||
create::create_oauth_provider,
|
||||
delete::delete_oauth_provider,
|
||||
update::update_oauth_provider,
|
||||
},
|
||||
post::{
|
||||
create::create_post,
|
||||
delete::delete_post,
|
||||
|
@ -123,7 +128,10 @@ use lemmy_api_crud::{
|
|||
update::update_private_message,
|
||||
},
|
||||
site::{create::create_site, read::get_site, update::update_site},
|
||||
user::{create::register, delete::delete_account},
|
||||
user::{
|
||||
create::{authenticate_with_oauth, register},
|
||||
delete::delete_account,
|
||||
},
|
||||
};
|
||||
use lemmy_apub::api::{
|
||||
list_comments::list_comments,
|
||||
|
@ -381,6 +389,18 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
|||
.route("", web::post().to(create_custom_emoji))
|
||||
.route("", web::put().to(update_custom_emoji))
|
||||
.route("/delete", web::post().to(delete_custom_emoji)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/oauth_provider")
|
||||
.wrap(rate_limit.message())
|
||||
.route("", web::post().to(create_oauth_provider))
|
||||
.route("", web::put().to(update_oauth_provider))
|
||||
.route("/delete", web::post().to(delete_oauth_provider)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/oauth")
|
||||
.wrap(rate_limit.register())
|
||||
.route("/authenticate", web::post().to(authenticate_with_oauth)),
|
||||
),
|
||||
);
|
||||
cfg.service(
|
||||
|
|
|
@ -345,7 +345,7 @@ async fn instance_actor_2022_01_28(
|
|||
settings: &Settings,
|
||||
) -> LemmyResult<()> {
|
||||
info!("Running instance_actor_2021_09_29");
|
||||
if let Ok(Some(site_view)) = SiteView::read_local(pool).await {
|
||||
if let Ok(site_view) = SiteView::read_local(pool).await {
|
||||
let site = site_view.site;
|
||||
// if site already has public key, we dont need to do anything here
|
||||
if !site.public_key.is_empty() {
|
||||
|
@ -471,7 +471,7 @@ async fn initialize_local_site_2022_10_10(
|
|||
let local_user_form = LocalUserInsertForm {
|
||||
email: setup.admin_email.clone(),
|
||||
admin: Some(true),
|
||||
..LocalUserInsertForm::new(person_inserted.id, setup.admin_password.clone())
|
||||
..LocalUserInsertForm::new(person_inserted.id, Some(setup.admin_password.clone()))
|
||||
};
|
||||
LocalUser::create(pool, &local_user_form, vec![]).await?;
|
||||
};
|
||||
|
|
|
@ -127,9 +127,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> {
|
|||
.expect("Couldn't initialize secrets.");
|
||||
|
||||
// Make sure the local site is set up.
|
||||
let site_view = SiteView::read_local(&mut (&pool).into())
|
||||
.await?
|
||||
.expect("local site not set up");
|
||||
let site_view = SiteView::read_local(&mut (&pool).into()).await?;
|
||||
let local_site = site_view.local_site;
|
||||
let federation_enabled = local_site.federation_enabled;
|
||||
|
||||
|
|
29
src/main.rs
29
src/main.rs
|
@ -19,35 +19,6 @@ pub async fn main() -> LemmyResult<()> {
|
|||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
#[cfg(not(feature = "embed-pictrs"))]
|
||||
start_lemmy_server(args).await?;
|
||||
#[cfg(feature = "embed-pictrs")]
|
||||
{
|
||||
let pictrs_port = &SETTINGS
|
||||
.pictrs_config()
|
||||
.unwrap_or_default()
|
||||
.url
|
||||
.port()
|
||||
.unwrap_or(8080);
|
||||
let pictrs_address = ["127.0.0.1", &pictrs_port.to_string()].join(":");
|
||||
let pictrs_config = pict_rs::ConfigSource::memory(serde_json::json!({
|
||||
"server": {
|
||||
"address": pictrs_address
|
||||
},
|
||||
"repo": {
|
||||
"type": "sled",
|
||||
"path": "./pictrs/sled-repo"
|
||||
},
|
||||
"store": {
|
||||
"type": "filesystem",
|
||||
"path": "./pictrs/files"
|
||||
}
|
||||
}))
|
||||
.init::<&str>(None)
|
||||
.expect("initialize pictrs config");
|
||||
let (lemmy, pictrs) = tokio::join!(start_lemmy_server(args), pictrs_config.run_on_localset());
|
||||
lemmy?;
|
||||
pictrs.expect("run pictrs");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue