Skip to content

feat: add RequireInvitation registration mode for invite-link-style signups#6450

Open
kryoseu wants to merge 19 commits intoLemmyNet:mainfrom
kryoseu:feat/registration-token
Open

feat: add RequireInvitation registration mode for invite-link-style signups#6450
kryoseu wants to merge 19 commits intoLemmyNet:mainfrom
kryoseu:feat/registration-token

Conversation

@kryoseu
Copy link
Copy Markdown
Contributor

@kryoseu kryoseu commented Apr 15, 2026

Summary

Adds an invite-link-based registration system to Lemmy, giving instance admins a new way to control who can register.

  • New RequireInvitation registration mode — admins can set the instance to require a valid invite link for registration (alongside the existing Open, RequireApplication, and Closed modes).

  • local_user_invite table — stores invite links with optional max_uses, expires_at, a uses_count, and a status (Active, Revoked, Exhausted, Expired). Each invite is tied to the user who created it.

  • invited_by_local_user_id column on local_user — tracks who invited each user at registration time.

  • Three new API endpoints:

    • POST /api/v4/account/invite — any authenticated user can create an invite link (with optional use cap and expiry).
    • GET /api/v4/account/invite/list — list your own invitations;
    • DELETE /api/v4/account/invite — revoke one of your own active invitations; returns an error if already revoked or exhausted.
  • Per-user invite cap — new max_invites_per_user_allowed config setting (optional u16) limits how many active invite links a non-admin user can hold at once. Admins bypass this limit.

  • Scheduled expiry task — the hourly scheduled task now also sets Active invites whose expires_at has passed to Expired.

Database migration

migrations/2026-04-16-000000-0000_add_invitation_table/up.sql:

  • Adds RequireInvitation value to the registration_mode_enum type.
  • Creates the local_user_invite_status_enum type and local_user_invite table.
  • Adds invited_by_local_user_id column to local_user.

New error types

Error When
MissingInviteToken RequireInvitation mode but no invite_link supplied at registration
InvalidInviteToken Link not found, or its status is not Active / it is Expired/Exhausted
TooManyInvites Non-admin user exceeds max_invites_per_user_allowed

Testing

  • Set registration_mode to RequireInvitation via the site settings.
  • Create an invite link as a logged-in user (POST /api/v3/invite). Verify the response includes a invite_link URL of the form {scheme}{hostname}/signup?token={token}.
  • Register a new account using the invite link. Confirm uses_count increments and invited_by_local_user_id is set on the new local_user row.
  • Create an invite with max_uses: 1, use it once, and verify the status becomes Exhausted and a second registration attempt returns InvalidInviteLink.
  • Create an invite with a past expires_at, wait for (or manually trigger) the hourly task, and verify status becomes Expired.
  • Revoke an active invite and confirm subsequent registration attempts return InvalidInviteToken.
  • Attempt to revoke an already-revoked or exhausted invite and confirm InviteAlreadyRevokedOrExhausted is returned.
  • Set max_invites_per_user_allowed: 2 in config, create 2 invites as a regular user, and confirm the third attempt returns TooManyInvites. Confirm an admin user is not rate-limited.
  • Confirm other registration modes are unaffected by this change.

Config

// How many active invite links a non-admin user may hold at once.
// Omit or set to null for no limit.

max_invites_per_user_allowed: 10

@kryoseu kryoseu force-pushed the feat/registration-token branch 4 times, most recently from dd73567 to f2e0575 Compare April 16, 2026 01:05
@Nutomic
Copy link
Copy Markdown
Member

Nutomic commented Apr 16, 2026

Very nice that you're taking this on!

So the way your code works right now is that there exists only a single, global registration token. This token can only be seen by admins, who then share it with others. It can also be used an unlimited amount of times, so if someone gets a registration token, he can register an unlimited amount of accounts.

Instead I would make it more similar to lobsters or Mastodon:
Screenshot_20260416_101920

Essentially:

  • Every user can create invite links (full link, not only token so its easier to use)
  • Invites are invalidated after first use, or explicitly specify the number of uses
  • New optional column in local_user table indicating who invited this user (to track down accounts who invite spammers)

Optional, can be added later:

  • Configurable amount of invites per user and day
  • Some way in the API and UI to find out who invited a certain user

@kryoseu
Copy link
Copy Markdown
Contributor Author

kryoseu commented Apr 16, 2026

Instead I would make it more similar to lobsters or Mastodon

Nice, thanks for the detailed info, that helps.

In the approach you suggested, my understanding is that anyone would be able to create invitation links, not just admins, right?

Also, should invitation links work regardless of the registration mode (Open, Close, Require application) selected by the admin or should we add a new registration mode like Registration link, and only in which case are invitation links accepted?

@kryoseu kryoseu marked this pull request as draft April 16, 2026 16:08
@kryoseu kryoseu force-pushed the feat/registration-token branch from dc5d1b6 to 61266f6 Compare April 16, 2026 16:14
@Nutomic
Copy link
Copy Markdown
Member

Nutomic commented Apr 17, 2026

In the approach you suggested, my understanding is that anyone would be able to create invitation links, not just admins, right?

Yes exactly.

Also, should invitation links work regardless of the registration mode (Open, Close, Require application) selected by the admin or should we add a new registration mode like Registration link, and only in which case are invitation links accepted?

Good question, I asked about that in the issue.

@dessalines
Copy link
Copy Markdown
Member

Also, should invitation links work regardless of the registration mode (Open, Close, Require application) selected by the admin or should we add a new registration mode like Registration link, and only in which case are invitation links accepted?

The latter IMO. This should be a 4th type (exclusive from the others), called something like Invitation Only or Require Invitation.

The # of uses stuff is tricky. Personally I think it'd be easier to only have an expiration link (and maybe even just a default of a week), and make sure that the inviter_id: Option<LocalUserId> is tracked in the local_user table.

Every user can create invite links (full link, not only token so its easier to use)

Agree with this also, every user should be able to create invite links. The local_user_invite table could store the expiration time, and a daily job in scheduled_tasks.rs could clear out the expired ones.

@kryoseu
Copy link
Copy Markdown
Contributor Author

kryoseu commented Apr 21, 2026

This is the schema I have so far.

New table: local_user_invite:

 id |                       invite_link                        | creator_id | max_uses | uses_count |       expires_at       | status |         published_at          
----+----------------------------------------------------------+------------+----------+------------+------------------------+--------+-------------------------------
  1 | https://lemmy-dev.com:8536/invite/Q5b0vO1GSuKzPWPwYTx6zQ |          1 |        2 |          0 |                        | Active | 2026-04-21 10:20:29.296532-07
  2 | https://lemmy-dev.com:8536/invite/GnUmI1sVRdqOQxbUOEYxCA |          1 |          |          0 | 2026-05-20 17:00:00-07 | Active | 2026-04-21 10:20:56.21102-07
  3 | https://lemmy-dev.com:8536/invite/vv8WYx2CSni-MWaiu38cJA |          1 |          |          0 |                        | Active | 2026-04-21 10:21:09.455183-07

creator_id = LocalUserId.

Update local_site registration mode

$ curl -v -X PUT http://lemmy-dev.com:8536/api/v4/site -H "Authorization: Bearer jwt" -d '{"registration_mode": "require_invitation"}' -H "Content-Type: application/json"

< HTTP/1.1 200 OK

Register users

Using invite Q5b0vO1GSuKzPWPwYTx6zQ (which has max uses set):

$ curl -X POST -v "http://lemmy-dev.com:8536/api/v4/account/auth/register" \                                            INT ✘ 
  -H "Content-Type: application/json" \
  -d '{"username": "user1", "password": "passwordpassword1", "password_verify": "passwordpassword1", "invite_link": "https://lemmy-dev.com:8536/invite/Q5b0vO1GSuKzPWPwYTx6zQ"}'
< HTTP/1.1 200 OK

$ curl -X POST -v "http://lemmy-dev.com:8536/api/v4/account/auth/register" \                                            INT ✘ 
  -H "Content-Type: application/json" \
  -d '{"username": "user1", "password": "passwordpassword1", "password_verify": "passwordpassword1", "invite_link": "https://lemmy-dev.com:8536/invite/Q5b0vO1GSuKzPWPwYTx6zQ"}'
  < HTTP/1.1 200 OK

Invite switches to Exhausted at usage time:

lemmy=# SELECT * from local_user_invite;
 id |                       invite_link                        | creator_id | max_uses | uses_count |       expires_at       |  status   |         published_at          
----+----------------------------------------------------------+------------+----------+------------+------------------------+-----------+-------------------------------
  1 | https://lemmy-dev.com:8536/invite/Q5b0vO1GSuKzPWPwYTx6zQ |          1 |        2 |          2 |                        | Exhausted | 2026-04-21 10:20:29.296532-07

Local user has a new column showing who it was invited by:

 id | person_id |                      password_encrypted                      |      email       | show_nsfw |  theme  | default_post_sort_type | default_listing_type | interface_language | show_avatars | send_notifications_to_email | show_bot_accounts | show_read_posts | email_verified | accepted_application | totp_2fa_secret | open_links_in_new_tab | blur_nsfw | infinite_scroll_enabled | admin | post_listing_mode | totp_2fa_enabled | animated_images_enabled | collapse_bot_comments | last_donation_notification_at | private_messages_enabled | default_comment_sort_type | auto_mark_fetched_posts_as_read | hide_media | default_post_time_range_seconds | show_score | show_upvotes | show_downvotes | show_upvote_percentage | show_person_votes | default_items_per_page | invited_by 
----+-----------+--------------------------------------------------------------+------------------+-----------+---------+------------------------+----------------------+--------------------+--------------+-----------------------------+-------------------+-----------------+----------------+----------------------+-----------------+-----------------------+-----------+-------------------------+-------+-------------------+------------------+-------------------------+-----------------------+-------------------------------+--------------------------+---------------------------+---------------------------------+------------+---------------------------------+------------+--------------+----------------+------------------------+-------------------+------------------------+------------
  1 |         2 | $2b$12$hMnsOL.4w0/D6mdi5aZekO7XE.vyOu8dhOaGhdmA3VvL3WK7w1edy | user@example.com | f         | browser | Active                 | Local                | browser            | t            | f                           | t                 | t               | f              | f                    |                 | f                     | t         | f                       | t     | List              | f                | t                       | f                     | 2026-03-20 10:18:32.430561-07 | t                        | Hot                       | f                               | f          |                                 | f          | t            | Show           | f                      | t                 |                     20 |           
  2 |         4 | $2b$12$Zetmue4nKtYVZJ1y1FurMu9w4wXZG8QSSzOeHpGIwdrYoVxsWZhbS |                  | f         | browser | Active                 | Local                | browser            | t            | f                           | t                 | t               | f              | t                    |                 | f                     | t         | f                       | f     | List              | f                | t                       | f                     | 2025-09-27 18:06:32.914105-07 | t                        | Hot                       | f                               | f          |                                 | f          | t            | Show           | f                      | t                 |                     20 |          1
  3 |         5 | $2b$12$8bcE1FjFx5m19gejqtO.1OsAkLyjmS4lVgyWokLJN5k.xx8jy/4Im |                  | f         | browser | Active                 | Local                | browser            | t            | f                           | t                 | t               | f              | t                    |                 | f                     | t         | f                       | f     | List              | f                | t                       | f                     | 2025-05-03 00:08:46.176033-07 | t                        | Hot                       | f                               | f          |                                 | f          | t            | Show           | f                      | t                 |                     20 |          1
(3 rows)

At the moment I have an hourly job update_invitations_when_expired() to clear out expired invitations (i.e update their statuses to Expired that the user can see on the UI).

@Nutomic
Copy link
Copy Markdown
Member

Nutomic commented Apr 22, 2026

New table: local_user_invite:

You dont need to store the full invite url in the db, its enough to store the token and concatenate when returning or validating. Either way its not much difference.

creator_id = LocalUserId.

Call it local_user_id to avoid any confusion. And instead of invited_by, better call it invited_by_local_user_id even if its a bit long.

At the moment I have an hourly job update_invitations_when_expired() to clear out expired invitations (i.e update their statuses to Expired that the user can see on the UI).

Storing the status seems unnecessary. You can calculate it on demand based on the expiration date and usage count (eg when registering a new user or when returning the list of invites).

@kryoseu
Copy link
Copy Markdown
Contributor Author

kryoseu commented Apr 22, 2026

You dont need to store the full invite url in the db, its enough to store the token and concatenate when returning or validating. Either way its not much difference.

I had it that way, but thought storing them as links directly would help avoid having to iterate over all user invites to return in a list call or the front end having to form the URL.

Call it local_user_id to avoid any confusion. And instead of invited_by, better call it invited_by_local_user_id even if its a bit long.

Ack.

Storing the status seems unnecessary. You can calculate it on demand based on the expiration date and usage count (eg when registering a new user or when returning the list of invites).

Imo it's the more natural thing to do, rather than computing their statuses for all invites from a user in every list call.

@Nutomic
Copy link
Copy Markdown
Member

Nutomic commented Apr 22, 2026

Imo it's the more natural thing to do, rather than computing their statuses for all invites from a user in every list call.

It requires an extra scheduled task which is not really necessary. And the status will be wrong when the scheduled task did not run yet. Anyway Exhausted invite links can be excluded from queries, or deleted from the db.

Regardless you should push the code that you have so far, its better to review that way.

@kryoseu kryoseu changed the title feat: add registration token mode for invite-link-style signups feat: add RequireInvitation registration mode for invite-link-style signups Apr 22, 2026
@kryoseu kryoseu force-pushed the feat/registration-token branch from 035feb4 to 0f257da Compare April 22, 2026 23:06
Comment thread crates/db_schema/src/lib.rs Outdated
#[default]
Own,
/// all invitations, admin only
All,
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.

Not sure on this tbh. Not sure it makes sense to allow admins to list all invites, unless we add an option for admins to be able to revoke any invite.

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 this doesnt make so much sense, even on a small instance you would see hundreds of invites. Makes more sense to list invites for a specific user.

In any case I would leave all out the advanced admin functionality from this PR (except basic setting to enable invite mode). Lets finish the basic backend first, then implement the ui and after that think which extra admin features make sense.

Comment thread crates/utils/src/settings/structs.rs Outdated
/// Data for loading Lemmy plugins
pub plugins: Vec<PluginSettings>,
/// How many active invite links a user can have
pub max_invites_per_user_allowed: Option<u16>,
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.

This kind of setting should go into the local_site table, so it can be changed through the admin ui without restart.

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.

Yeah, that makes more sense.

Comment thread crates/routes/src/utils/scheduled_tasks.rs
'Revoked',
'Exhausted',
'Expired'
);
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.

Again the status is unnecessary, because any invalid tokens can be directly deleted from the db. And registration needs to check expires_at against current time, because the scheduled task to delete expired items might not have run yet.

#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct CreateInvitationResponse {
pub invite_link: Url,
}
Copy link
Copy Markdown
Member

@Nutomic Nutomic Apr 23, 2026

Choose a reason for hiding this comment

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

This response isnt useful, clients would have to make another api call to update the list of invites. So better to return PagedResponse<LocalUserInviteView> from the create_invitation api call.

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.

So you're saying the create_invitation should return a list of invites? So it would function similarly to the list_invites endpoint, plus the newly created invite.

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.

Actually not, other create calls return a view for the object that was created. For example create_post returns PostResponse which contains PostView so it should be similar here.

@kryoseu
Copy link
Copy Markdown
Contributor Author

kryoseu commented Apr 23, 2026

Nice, thanks guys, lots of stuff to address, but I appreciate the comments.

kryoseu added 4 commits April 24, 2026 08:24
Instead of tracking Active/Revoked/Exhausted/Expired via a status column
invites are simply deleted when revoked, consumed, or expired. This removes
`LocalUserInviteStatus`, `LocalUserInviteInsertForm`.status, and
`InvitationListingType` from the schema and simplifies the query and API layers.
Return the full `LocalUserInvite` object from create/list endpoints
instead of a pre-computed `invite_link` URL, letting clients construct
the link themselves. Simplify list_invitations to pass `PagedResponse`
through directly without an intermediate view wrapper.
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.

Replaces the settings.hjson config field with a proper `local_site`
column so admins can configure the invite limit via the site API
(CreateSite/EditSite) without restarting the server.

- Add `max_invites_per_user_allowed` column to `local_site` (default 10)
- Expose field in `CreateSite` and `EditSite` API structs
- Read limit from DB in `create_invitation` instead of app settings
- Remove `max_invites_per_user_allowed` from `Settings` struct
- Update migration up/down SQL accordingly
@kryoseu
Copy link
Copy Markdown
Contributor Author

kryoseu commented Apr 24, 2026

Hey comrades, still going through the feedback, but I have made some changes and implemented some of the things you suggested. Please take a look and review when you have time.
One think it's still unclear to me is that need to a LocalUserInviteView instead of a LocalUserInviteQuery.

Let me know, thanks.

- Change revoke_invitation from POST /invite/revoke to DELETE invite
- Move list_invitations from /invites to /invite/list
- Add lemmy_db_views_local_user_invite and uuid/base64 dependencies to Cargo.lock
async fn delete_invitations_when_expired(pool: &mut DbPool<'_>) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
diesel::delete(
local_user_invite::table.filter(local_user_invite::expires_at.lt(now().nullable())),
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.

Here you can also delete rows where max_uses >= usage_count.

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.

We're already deleting the tokens at registration time when invite.uses_count + 1 >= invite.max_uses. Adding it here would be redundant as such cases won't exist.

ALTER TYPE registration_mode_enum
ADD VALUE 'RequireInvitation';

CREATE TABLE local_user_invite (
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.

Calling this registration_invite would be clearer in my opinion, what do you think?

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.

IMO I prefer local_user_invite. It follows the same convention as local_user and makes the ownership clear (an invite for a local user).

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.

The thing is that at a glance, I can misread local_user_invite as local_user_view. With registration_invite there is much less potential for confusion.

.first(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
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.

This method is unnecessary as tokens are unique.

.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
}
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.

Also unnecessary.

Copy link
Copy Markdown
Contributor Author

@kryoseu kryoseu Apr 27, 2026

Choose a reason for hiding this comment

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

This method is used when revoking a user invite. If removed and delete_by_token used instead, that means any user can delete any invite, provided that they know the token, right?

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.

Ah youre right. What you could do is read the token first and compare the user id before deleting it with delete_by_token.

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.

Doable, but that means two queries instead of one. I can do that if you think it's better.

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.

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

if local_user_id != invite.local_user_id {
  return Err(LemmyErrorType::InvalidInviteToken.into());
}

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

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.

The problem is that async and generic code significantly increases compilation time. These two methods probably dont make much difference, but still its probably better to have less. Instead you can make a simple helper method like:

fn check_valid_invite(invite: &Invite, local_user_view: &LocalUserView) -> LemmyResult<()> {
  if local_user_id != invite.local_user_id {
    return Err(LemmyErrorType::InvalidInviteToken.into());
  }
  Ok(())
}

@dessalines
Copy link
Copy Markdown
Member

Check CI, you might need to put some of the deps you added behind a feature gate: https://woodpecker.join-lemmy.org/repos/129/pipeline/20186/18

Also ping or unmark as draft when you want us to take another look.

@kryoseu kryoseu marked this pull request as ready for review May 2, 2026 04:03
query = query.filter(local_user_invite::local_user_id.eq(self.local_user_id));

let paginated_query =
LocalUserInvite::paginate(query, &self.page_cursor, SortDirection::Asc, pool)
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.

Suggested change
LocalUserInvite::paginate(query, &self.page_cursor, SortDirection::Asc, pool)
LocalUserInvite::paginate(query, &self.page_cursor, SortDirection::Desc, pool)

.first(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
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.

This methid is only used for pagination. You can remove it and use read_by_token for pagination instead.

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.

Good call out, thanks.

Copy link
Copy Markdown
Member

@dessalines dessalines left a comment

Choose a reason for hiding this comment

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

Also check merge conflicts.

Comment thread crates/api/api_crud/src/invite/create.rs
Comment thread crates/api/api_crud/src/user/create.rs Outdated
Comment thread crates/db_schema/src/impls/local_user_invite.rs
Comment thread crates/db_schema/src/impls/local_user_invite.rs Outdated
Comment thread crates/db_schema/src/impls/local_user_invite.rs Outdated
Comment thread crates/db_schema/src/source/local_user_invite.rs
Comment thread crates/db_views/local_user_invite/src/impls.rs Outdated
Comment thread crates/routes/src/utils/scheduled_tasks.rs
Comment thread migrations/2026-04-16-000000-0000_add_invitation_table/up.sql
Comment thread crates/db_views/local_user_invite/src/impls.rs
@dessalines
Copy link
Copy Markdown
Member

Looked over again, and I think the admin list invites endpoint is still missing.

@kryoseu
Copy link
Copy Markdown
Contributor Author

kryoseu commented May 5, 2026

Looked over again, and I think the admin list invites endpoint is still missing.

I was under the impression this wouldn't add much value at this point, unless we implement APIs to allow admins to also revoke any invite, etc. If we decide to do so, I could do this in a subsequent PR, if that's okay.

But imo I think more importantly would be for admins to be able to see list of users and who they were invited by.

kryoseu added 2 commits May 4, 2026 20:34
- Replace `is_active()` check with `is_expired()` in registration: exhausted
  invites are deleted on use, so only expiry needs to be checked at read time
- Remove `is_active()` helper as it's no longer needed
- Remove unused `read()` by ID; switch pagination cursor from integer ID to
  invite token via `CursorData::new_plain
- Change invite list sort to descending (newest first)
- Add unit and integration tests for `is_expired`, `get_invite_url`,
  CRUD operations, and `uses_count` updates
@Nutomic
Copy link
Copy Markdown
Member

Nutomic commented May 5, 2026

The admin functionality can be added later, otherwise this PR will get too complex. In principle this is ready to merge. However I want to hold off a bit longer, because we will publish the first beta version of 1.0.0 in the next few days.

Anyway you can already start to work on the corresponding frontend changes. For that clone the lemmy-js-client repo, run ./scripts/copy_generated_types_from_lemmy.sh and make a PR with the generated changes. Then we will publish a test version of the js client which you can use in lemmy-ui.

Copy link
Copy Markdown
Member

@dessalines dessalines left a comment

Choose a reason for hiding this comment

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

This is good for now, we might have to make tweaks as we work on the front end anyway.

As @Nutomic said, we might wait for a few days to merge this, when we plan to get a lemmy beta out (without this included).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants