Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User invites #1351

Closed
wants to merge 24 commits into from
Closed

User invites #1351

wants to merge 24 commits into from

Conversation

begedin
Copy link
Contributor

@begedin begedin commented Dec 28, 2017

What's in this PR?

This PR adds API support for UserInvite creation and claims.

The Issue this is referring to specifies that we should be adding an InvitedUser model, but UserInvite seems more consistent in naming, so I opted for that.

Creation of user invites

  • we have a new controller for this, as well as a new endpoint
  • we differentiate between a plain invite and an invite created to invite a user to a project
  • when creating a plain invite, the user is authorized to do so if they are the inviter, with no other conditions
  • when creating a project invite, the user needs to be an inviter, and additionally, they need to be of a high enough membership in the specified project, in order to invite a user with a specified role
    • pending users and contributors (?) can't do anything
    • admins can invite other pending (not useful) and contributor users
    • owners can do the same as admins and also invite other admin users
  • during creation of the invite, we require an email and also validate that a user account with that email doesn't exist already

Claiming of user invites

This is done through the user controller create action. The user creation was extracted into Accounts.create/1 and a new, special clause matches against a payload holding an "invite_id" key, to differentiate from normal account creation.

When claiming the invite we

  • try to load the invite by specified id - can fail and renders a 404 if it does
  • create the user (this can fail with a validation error if the user has been created regularly since the invite was created)
  • create a project user if a project and role are specified by the invite
  • associate invite with created user through invitee

The user controller is where we track any claimed invites.

Suggested issues to create

  • Add an extra step to normal account creation, which claims all invites associated with the created user's email
  • Add the same extra step to invite claim account creation, which claims all invites other than the one currently being claimed.
  • Add support for inviting existing users to project

References

Closes #1350


schema "user_invites" do
field :email, :string, null: false
field :role, :string, default: "contributor"
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't have a default because it's possible for an invite to not be for a project.

create table(:user_invites) do
add :email, :string, null: false
add :name, :string, null: true
add :role, :string, null: true, default: "contributor"
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here re: default.

-- Dumped from database version 10.1
-- Dumped by pg_dump version 10.1
-- Dumped from database version 10.0
-- Dumped by pg_dump version 10.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we see if there's a way to avoid the db version numbering while still getting the benefits of the structure.sql?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not seeing any documented way of doing so with mix ecto.dump

%UserInvite{}
|> Changeset.cast(params, [:email, :name, :role, :inviter_id, :project_id])
|> Changeset.validate_required([:email, :inviter_id])
|> Changeset.validate_inclusion(:role, ~w(contributor admin owner))
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally we would source the role types from the ProjectUser.


if count > 0 do
changeset |> Changeset.add_error(:email, "Already associated with a project member")
else
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the requirement wasn't quite clear here: we want to be sure that the email is not associated with a User.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Several questions then

  • will a project admin/owner ever be able to invite an existing codecorps user to their project?

    • would that not create a UserInvite and create something else? Maybe a ProjectUser record with a status of "invited"?
  • what happens in the following scenario:

    • invite is created for a "mike@mail.com"
    • later, independently, a user registers with that email
    • even later, that same user tries to claim the invite

end

@spec claim_invite(map) :: {:ok, User.t}
def claim_invite(%{} = params) do
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll need to accommodate invites for projects and general invites to Code Corps.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now, the code sort of does that. It will

  • create a user
  • join that user with the project, provided the project and role are specified
  • assign that user to the assignee

I'm guessing an email will also be involved, which should be pluggable into this process


@spec claim_new_user(map) :: {:ok, User.t}
defp claim_new_user(%{} = params) do
%User{} |> User.registration_changeset(params) |> Repo.insert
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if this will change, but in all the 1.6 auto-formatting I've seen everything uses (), so Repo.insert().

@joshsmith
Copy link
Contributor

Several questions then

  • will a project admin/owner ever be able to invite an existing codecorps user to their project?
    • would that not create a UserInvite and create something else? Maybe a ProjectUser record with a status of "invited"?

Yes, they would be able to invite an existing user. But this would be simply a ProjectUser with a different status, as you suggested. There wouldn't be a need for a UserInvite in this case. I'm not sure of a better name to represent the idea, but the invite is to someone who is not yet a user.

  • what happens in the following scenario:
    • invite is created for a "mike@mail.com"
    • later, independently, a user registers with that email
    • even later, that same user tries to claim the invite

Hadn't thought of this. Ideally upon registration we would look through the UserInvite table and create any ProjectUser records, thereby "claiming" the invite. And again ideally, we would then let a user know that an invite is claimed when they go to a URL with the invite id in the query params, and we would also have a status for such a claim that differentiates it from a sign up with the invite context.

end
def create(%{} = params) do
%User{} |> User.registration_changeset(params) |> Repo.insert()
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We needed to extract creation into a context. I didn't have a better idea other than this multiple clause approach to differentiate between plain account creation and an invite claim.

"""
@spec roles :: list(String.t())
def roles do
~w{pending contributor admin owner}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did an auto-formatting of the file as I was making the roles function public (to be used when validating a UserInvite

:stripe_platform_customer,
:user_categories,
:user_roles,
:user_skills
]

def preload(data) do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another auto-formatting of file as I was modifying the create function. Actual changes are in the function above.

[_role, _project_id] ->
changeset
end
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm hoping there's a shorter way to write this, but couldn't think of anything.

@begedin
Copy link
Contributor Author

begedin commented Jan 3, 2018

@joshsmith I think this is finally in a mergeable state. There are issues to be created to expand this further, pending feedback, but other than that, I think our initial bases are covered. I'll update the PR description with the current state of the PR

@joshsmith
Copy link
Contributor

@begedin remember to explicitly re-request a review from me so that it will show up in my reviews.

@begedin begedin mentioned this pull request Jan 22, 2018
@begedin
Copy link
Contributor Author

begedin commented Jan 22, 2018

Additional tracking to implement here:

  • When an invite gets created

    • current_user.id |> track("Created User Invite", user_invite)
    • user_invite.email |> track("User Invited", user_invite)
    • if invite is for project as well
      • current_user.id |> track("Invited to Project", user_invite)
      • user_invite.email |> track("Invited to Project (Invitee)", user_invite)
  • When an invite gets claimed

    • current_user.id |> track("Claimed User Invite", user_invite)
    • user_invite.email |> track("User Invited", user_invite)
    • if invite is for project as well
      • current_user.id |> track("Invited to Project", user_invite)
      • user_invite.email |> track("Invited to Project (Invitee)", user_invite)

I actually think the project specific ones could be ignored. For those, we would could have funnels with custom events instead, since the distinct_id is the same as the regular event and the difference is in the project_id property being present or not.

I'm not sure if we might also need a

  • user_invite.project_id |> track("Invited to Project", user_invite)

@begedin
Copy link
Contributor Author

begedin commented Jan 22, 2018

@joshsmith I got a question on tracking based on info from #1364

The rest of the code is good for another review.

@begedin begedin force-pushed the develop branch 18 times, most recently from e075407 to 6d6cc63 Compare February 12, 2018 16:17
@begedin begedin closed this Jul 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add InvitedUser model, policy, and controller
2 participants