Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ members = [
"crates/db_views/private_message",
"crates/db_views/local_user",
"crates/db_views/local_image",
"crates/db_views/local_user_invite",
"crates/db_views/person",
"crates/db_views/post",
"crates/db_views/vote",
Expand Down Expand Up @@ -146,6 +147,7 @@ lemmy_db_views_report_combined = { version = "=1.0.0-alpha.18", path = "./crates
lemmy_db_views_report_combined_sql = { version = "=1.0.0-alpha.18", path = "./crates/db_views/report_combined_sql" }
lemmy_db_views_site = { version = "=1.0.0-alpha.18", path = "./crates/db_views/site" }
lemmy_db_views_vote = { version = "=1.0.0-alpha.18", path = "./crates/db_views/vote" }
lemmy_db_views_local_user_invite = { version = "=1.0.0-alpha.18", path = "./crates/db_views/local_user_invite" }
activitypub_federation = { version = "0.7.0-beta.11", default-features = false, features = [
"actix-web",
] }
Expand Down Expand Up @@ -188,6 +190,7 @@ chrono = { version = "0.4.44", features = [
], default-features = false }
serde_json = { version = "1.0.149", features = ["preserve_order"] }
base64 = "0.22.1"
rand = "0.10.0"
uuid = { version = "1.22.0", features = ["serde"] }
anyhow = { version = "1.0.102", features = ["backtrace"] }
diesel_ltree = "0.4.0"
Expand Down
2 changes: 2 additions & 0 deletions crates/api/api_crud/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ lemmy_db_views_post = { workspace = true, features = ["full"] }
lemmy_db_views_local_user = { workspace = true, features = ["full"] }
lemmy_db_views_person = { workspace = true, features = ["full"] }
lemmy_db_views_custom_emoji = { workspace = true, features = ["full"] }
lemmy_db_views_local_user_invite = { workspace = true, features = ["full"] }
lemmy_db_views_private_message = { workspace = true, features = ["full"] }
lemmy_db_views_registration_applications = { workspace = true, features = [
"full",
Expand All @@ -45,6 +46,7 @@ futures = { workspace = true }
futures-util = { workspace = true }
anyhow.workspace = true
chrono.workspace = true
rand = { workspace = true }
accept-language = "3.1.0"
regex = { workspace = true }
serde_json = { workspace = true }
Expand Down
57 changes: 57 additions & 0 deletions crates/api/api_crud/src/invite/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::{context::LemmyContext, utils::is_admin};
use lemmy_db_schema::source::local_user_invite::{LocalUserInvite, LocalUserInviteInsertForm};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_local_user_invite::{
api::{CreateInvitation, CreateInvitationResponse},
impls::LocalUserInviteQuery,
};
use lemmy_db_views_site::SiteView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use rand::{RngExt, distr::Alphanumeric};

pub async fn create_invitation(
Json(data): Json<CreateInvitation>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CreateInvitationResponse>> {
let pool = &mut context.pool();

let local_user_id = local_user_view.local_user.id;
let local_site = SiteView::read_local(pool).await?.local_site;

let active_invite_count = LocalUserInviteQuery {
local_user_id,
..Default::default()
}
.count(pool)
.await?;

// admins bypass the max invite per user limit
if is_admin(&local_user_view).is_err()
Comment thread
dessalines marked this conversation as resolved.
&& active_invite_count >= i64::from(local_site.max_invites_per_user_allowed)
{
return Err(LemmyErrorType::TooManyInvites.into());
}

let token = generate_invite_token();

let insert = LocalUserInviteInsertForm {
token,
local_user_id,
max_uses: data.max_uses,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be validate a max_uses? Seems like having a limit here might be a good idea.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add this in a subsequent PR along with a setting to control max expires_at, if that's okay?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thats fine.

expires_at: data.expires_at,
};

let invite = LocalUserInvite::create(pool, &insert).await?;

Ok(Json(CreateInvitationResponse { invite }))
}

fn generate_invite_token() -> String {
rand::rng()
.sample_iter(Alphanumeric)
.take(12)
.map(char::from)
.collect()
}
25 changes: 25 additions & 0 deletions crates/api/api_crud/src/invite/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::local_user_invite::LocalUserInvite;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_local_user_invite::{api::ListInvitations, impls::LocalUserInviteQuery};
use lemmy_diesel_utils::pagination::PagedResponse;
use lemmy_utils::error::LemmyResult;

pub async fn list_invitations(
data: Query<ListInvitations>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PagedResponse<LocalUserInvite>>> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm returning PagedResponse<LocalUserInvite>, which works and looks clean IMO. But need input from reviewers as it was mentioned about returning LocalUserInviteView instead, which I removed entirely in 465f05a. My understanding is that views are used when we need to join tables, which I don't think it's required for invites.

let pool = &mut context.pool();

let paged = LocalUserInviteQuery {
local_user_id: local_user_view.local_user.id,
page_cursor: data.page_cursor.clone(),
limit: data.limit,
}
.list(pool)
.await?;

Ok(Json(paged))
}
3 changes: 3 additions & 0 deletions crates/api/api_crud/src/invite/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod create;
pub mod list;
pub mod revoke;
33 changes: 33 additions & 0 deletions crates/api/api_crud/src/invite/revoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::local_user_invite::LocalUserInvite;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_local_user_invite::api::RevokeInvitation;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};

pub async fn revoke_invitation(
Json(data): Json<RevokeInvitation>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let pool = &mut context.pool();

let invite = LocalUserInvite::read_by_token(pool, &data.token).await?;

check_valid_invite(&invite, &local_user_view)?;

LocalUserInvite::delete_by_token(pool, &data.token).await?;

Ok(Json(SuccessResponse::default()))
}

fn check_valid_invite(
invite: &LocalUserInvite,
local_user_view: &LocalUserView,
) -> LemmyResult<()> {
if local_user_view.local_user.id != invite.local_user_id {
return Err(LemmyErrorType::InvalidInviteToken.into());
}
Ok(())
}
1 change: 1 addition & 0 deletions crates/api/api_crud/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use lemmy_db_schema::source::community::{Community, CommunityActions};
pub mod comment;
pub mod community;
pub mod custom_emoji;
pub mod invite;
pub mod multi_community;
pub mod oauth_provider;
pub mod post;
Expand Down
1 change: 1 addition & 0 deletions crates/api/api_crud/src/site/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ pub async fn create_site(
image_max_upload_size: data.image_max_upload_size,
image_allow_video_uploads: data.image_allow_video_uploads,
image_upload_disabled: data.image_upload_disabled,
max_invites_per_user_allowed: data.max_invites_per_user_allowed,
};

LocalSite::update(&mut context.pool(), &local_site_form).await?;
Expand Down
1 change: 1 addition & 0 deletions crates/api/api_crud/src/site/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub async fn edit_site(
image_max_upload_size: data.image_max_upload_size,
image_allow_video_uploads: data.image_allow_video_uploads,
image_upload_disabled: data.image_upload_disabled,
max_invites_per_user_allowed: data.max_invites_per_user_allowed,
};

let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
Expand Down
32 changes: 32 additions & 0 deletions crates/api/api_crud/src/user/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use lemmy_db_schema::{
language::Language,
local_site::LocalSite,
local_user::{LocalUser, LocalUserInsertForm},
local_user_invite::{LocalUserInvite, LocalUserInviteUpdateForm},
oauth_account::{OAuthAccount, OAuthAccountInsertForm},
oauth_provider::AdminOAuthProvider,
person::{Person, PersonInsertForm},
Expand Down Expand Up @@ -89,6 +90,20 @@ pub async fn register(
let local_site = site_view.local_site.clone();
let require_registration_application =
local_site.registration_mode == RegistrationMode::RequireApplication;
let token = data.token.as_deref();

let local_user_invite = if local_site.registration_mode == RegistrationMode::RequireInvitation {
let token = token.ok_or(LemmyErrorType::MissingInviteToken)?;
let inv = LocalUserInvite::read_by_token(pool, token)
.await
.map_err(|_e| LemmyError::from(LemmyErrorType::InvalidInviteToken))?;
if !inv.is_expired() {
return Err(LemmyErrorType::InvalidInviteToken.into());
}
Some(inv)
} else {
None
};

if local_site.registration_mode == RegistrationMode::Closed {
return Err(LemmyErrorType::RegistrationClosed.into());
Expand Down Expand Up @@ -154,6 +169,7 @@ pub async fn register(
email: tx_data.email.as_deref().map(str::to_lowercase),
show_nsfw: Some(show_nsfw),
accepted_application,
invited_by_local_user_id: local_user_invite.as_ref().map(|inv| inv.local_user_id),
..LocalUserInsertForm::new(person.id, Some(tx_data.password.to_string()))
};

Expand All @@ -179,6 +195,22 @@ pub async fn register(
RegistrationApplication::create(&mut conn.into(), &form).await?;
}

if let Some(inv) = local_user_invite {
let new_uses_count = inv.uses_count + 1;
if inv.max_uses.map(|m| new_uses_count >= m).unwrap_or(false) {
LocalUserInvite::delete_by_token(&mut conn.into(), &inv.token).await?;
} else {
LocalUserInvite::update(
&mut conn.into(),
inv.id,
&LocalUserInviteUpdateForm {
uses_count: Some(new_uses_count),
},
)
.await?;
}
}

Ok(LocalUserView {
person,
local_user,
Expand Down
7 changes: 7 additions & 0 deletions crates/api/routes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ use lemmy_api_crud::{
list::list_custom_emojis,
update::edit_custom_emoji,
},
invite::{create::create_invitation, list::list_invitations, revoke::revoke_invitation},
multi_community::{
create::create_multi_community,
create_entry::create_multi_community_entry,
Expand Down Expand Up @@ -408,6 +409,12 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
resource("/data/export")
.wrap(rate_limit.import_user_settings())
.route(get().to(export_user_data)),
)
.service(
scope("/invite")
.route("", post().to(create_invitation))
.route("", delete().to(revoke_invitation))
.route("/list", get().to(list_invitations)),
),
)
// Person / User actions
Expand Down
1 change: 1 addition & 0 deletions crates/api/routes_v3/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ pub(crate) fn convert_local_site(local_site: LocalSite) -> LocalSiteV3 {
RegistrationMode::Closed => RegistrationModeV3::Closed,
RegistrationMode::RequireApplication => RegistrationModeV3::RequireApplication,
RegistrationMode::Open => RegistrationModeV3::Open,
RegistrationMode::RequireInvitation => RegistrationModeV3::Closed,
};
LocalSiteV3 {
id: Default::default(),
Expand Down
Loading