{"id":13601968,"url":"https://github.com/scambra/devise_invitable","last_synced_at":"2025-05-13T00:13:28.791Z","repository":{"id":38629660,"uuid":"412126","full_name":"scambra/devise_invitable","owner":"scambra","description":"An invitation strategy for devise","archived":false,"fork":false,"pushed_at":"2025-05-09T14:21:29.000Z","size":1501,"stargazers_count":2666,"open_issues_count":75,"forks_count":551,"subscribers_count":39,"default_branch":"master","last_synced_at":"2025-05-13T00:13:21.225Z","etag":null,"topics":["devise","invitation","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/scambra.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2009-12-09T23:11:54.000Z","updated_at":"2025-05-09T14:21:33.000Z","dependencies_parsed_at":"2023-02-08T12:31:20.968Z","dependency_job_id":"e7b22cf3-3a33-4215-9930-effef38a2fd5","html_url":"https://github.com/scambra/devise_invitable","commit_stats":{"total_commits":921,"total_committers":188,"mean_commits":4.898936170212766,"dds":0.6883821932681867,"last_synced_commit":"de5c41f1aa2bc1397c91d59119b02b740203e77f"},"previous_names":[],"tags_count":90,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scambra%2Fdevise_invitable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scambra%2Fdevise_invitable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scambra%2Fdevise_invitable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scambra%2Fdevise_invitable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/scambra","download_url":"https://codeload.github.com/scambra/devise_invitable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253843225,"owners_count":21972874,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["devise","invitation","rails","ruby"],"created_at":"2024-08-01T18:01:10.860Z","updated_at":"2025-05-13T00:13:28.770Z","avatar_url":"https://github.com/scambra.png","language":"Ruby","readme":"# DeviseInvitable\n[\u003cimg src=\"https://badge.fury.io/rb/devise_invitable.svg\"/\u003e](http://badge.fury.io/rb/devise_invitable) [\u003cimg\nsrc=\"https://github.com/scambra/devise_invitable/actions/workflows/ci.yml/badg\ne.svg\"/\u003e](https://github.com/scambra/devise_invitable/actions/workflows/ci.yml\n) [\u003cimg\nsrc=\"https://codeclimate.com/github/scambra/devise_invitable/badges/gpa.svg\"/\u003e\n](https://codeclimate.com/github/scambra/devise_invitable)\n\nIt adds support to [Devise](https://github.com/plataformatec/devise) for\nsending invitations by email (it requires to be authenticated) and accept the\ninvitation setting the password.\n\n## Requirements\n\nThe latest version of DeviseInvitable works with Devise \u003e= 4.6.\n\nIf you want to use devise_invitable with earlier Devise releases (4.0 \u003c= x \u003c\n4.6), use version 1.7.5.\n\n## Installation\n\nInstall DeviseInvitable gem:\n\n```shell\ngem install devise_invitable\n```\n\nAdd DeviseInvitable to your Gemfile:\n\n```ruby\ngem 'devise_invitable', '~\u003e 2.0.0'\n```\n\n### Automatic installation\n\nRun the following generator to add DeviseInvitable’s configuration option in\nthe Devise configuration file (`config/initializers/devise.rb`):\n\n```shell\nrails generate devise_invitable:install\n```\n\nWhen you are done, you are ready to add DeviseInvitable to any of your Devise\nmodels using the following generator:\n\n```shell\nrails generate devise_invitable MODEL\n```\n\nReplace MODEL by the class name you want to add DeviseInvitable, like `User`,\n`Admin`, etc. This will add the `:invitable` flag to your model's Devise\nmodules. The generator will also create a migration file (if your ORM supports\nthem).\n\n### Manual installation\n\nFollow the walkthrough for Devise and after it's done, follow this\nwalkthrough.\n\n#### Devise Configuration\nAdd `:invitable` to the `devise` call in your model (we’re assuming here you\nalready have a User model with some Devise modules):\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  devise :database_authenticatable, :confirmable, :invitable\nend\n```\n\n#### ActiveRecord Migration\nAdd `t.invitable` to your Devise model migration:\n\n```ruby\ncreate_table :users do\n  ...\n    ## Invitable\n    t.string   :invitation_token\n    t.datetime :invitation_created_at\n    t.datetime :invitation_sent_at\n    t.datetime :invitation_accepted_at\n    t.integer  :invitation_limit\n    t.integer  :invited_by_id\n    t.string   :invited_by_type\n  ...\nend\nadd_index :users, :invitation_token, unique: true\n```\n\nor for a model that already exists, define a migration to add DeviseInvitable\nto your model:\n\n```ruby\ndef change\n    add_column :users, :invitation_token, :string\n    add_column :users, :invitation_created_at, :datetime\n    add_column :users, :invitation_sent_at, :datetime\n    add_column :users, :invitation_accepted_at, :datetime\n    add_column :users, :invitation_limit, :integer\n    add_column :users, :invited_by_id, :integer\n    add_column :users, :invited_by_type, :string\n    add_index :users, :invitation_token, unique: true\nend\n```\n\nIf you previously used devise_invitable with a `:limit` on\n`:invitation_token`, remove it:\n\n```ruby\ndef up\n  change_column :users, :invitation_token, :string, limit: nil\nend\n\ndef down\n  change_column :users, :invitation_token, :string, limit: 60\nend\n```\n\n## Mongoid Field Definitions\nIf you are using Mongoid, define the following fields and indexes within your\ninvitable model:\n\n```ruby\nfield :invitation_token, type: String\nfield :invitation_created_at, type: Time\nfield :invitation_sent_at, type: Time\nfield :invitation_accepted_at, type: Time\nfield :invitation_limit, type: Integer\n\nindex( { invitation_token: 1 }, { background: true} )\nindex( { invitation_by_id: 1 }, { background: true} )\n```\n\nYou do not need to define a `belongs_to` relationship, as DeviseInvitable does\nthis on your behalf:\n```ruby\nbelongs_to :invited_by, polymorphic: true\n```\n\nRemember to create indexes within the MongoDB database after deploying your\nchanges.\n```shell\nrake db:mongoid:create_indexes\n```\n\n## Model configuration\n\nDeviseInvitable adds some new configuration options:\n\n*   `invite_for`: The period the generated invitation token is valid. After\nthis period, the invited resource won't be able to accept the invitation.\nWhen `invite_for` is `0` (the default), the invitation won't expire.\n\n\nYou can set this configuration option in the Devise initializer as follow:\n\n```ruby\n# ==\u003e Configuration for :invitable\n# The period the generated invitation token is valid.\n# After this period, the invited resource won't be able to accept the invitation.\n# When invite_for is 0 (the default), the invitation won't expire.\n# config.invite_for = 2.weeks\n```\n\nor directly as parameters to the `devise` method:\n\n```ruby\ndevise :database_authenticatable, :confirmable, :invitable, invite_for: 2.weeks\n```\n\n*   `invitation_limit`: The number of invitations users can send. The default\nvalue of `nil` means users can send as many invites as they want, there is\nno limit for any user, `invitation_limit` column is not used.  A setting\nof `0` means they can't send invitations. A setting `n \u003e 0` means they can\nsend `n` invitations. You can change `invitation_limit` column for some\nusers so they can send more or less invitations, even with global\n`invitation_limit = 0`.\n\n*   `invite_key`: The key to be used to check existing users when sending an\ninvitation. You can use multiple keys. This value must be a hash with the\ninvite key as hash keys, and values that respond to the `===` operator\n(including procs and regexes). The default value is looking for users by\nemail and validating with `Devise.email_regexp`.\n\n*   `validate_on_invite`: force a record to be valid before being actually\ninvited.\n\n*   `resend_invitation`: resend invitation if user with invited status is\ninvited again. Enabled by default.\n\n*   `invited_by_class_name`: the class name of the inviting model. If this is\n`nil`, polymorphic association is used.\n\n*   `invited_by_foreign_key`: the foreign key to the inviting model (only used\nif `invited_by_class_name` is set, otherwise `:invited_by_id`)\n\n*   `invited_by_counter_cache`: the column name used for counter_cache column.\nIf this is `nil` (default value), the `invited_by` association is declared\nwithout `counter_cache`.\n\n*   `allow_insecure_sign_in_after_accept`: automatically sign in the user\nafter they set a password. Enabled by default.\n\n*   `require_password_on_accepting`: require password when user accepts the\ninvitation. Enabled by default. Disable if you don't want to ask or\nenforce to set password while accepting, because is set when user is\ninvited or it will be set later.\n\n\nFor more details, see `config/initializers/devise.rb` (after you invoked the\n`devise_invitable:install` generator described above).\n\n## Configuring views\n\nAll the views are packaged inside the gem. If you'd like to customize the\nviews, invoke the following generator and it will copy all the views to your\napplication:\n\n```shell\nrails generate devise_invitable:views\n```\n\nYou can also use the generator to generate scoped views:\n\n```shell\nrails generate devise_invitable:views users\n```\n\nThen turn scoped views on in `config/initializers/devise.rb`:\n\n```ruby\nconfig.scoped_views = true\n```\n\nPlease refer to [Devise's README](https://github.com/plataformatec/devise) for\nmore information about views.\n\n## Configuring controllers\n\nTo change the controller's behavior, create a controller that inherits from\n`Devise::InvitationsController`. The available methods are: `new`, `create`,\n`edit`, and `update`. Refer to the [original controllers source](https://github.com/scambra/devise_invitable/blob/master/app/controllers/devise/invitations_controller.rb)\nbefore editing any of these actions. Your\ncontroller might now look something like this:\n\n```ruby\nclass Users::InvitationsController \u003c Devise::InvitationsController\n  def update\n    if some_condition\n      redirect_to root_path\n    else\n      super\n    end\n  end\nend\n```\n\nNow just tell Devise that you want to use your controller, the controller\nabove is `'users/invitations'`, so our routes.rb would have this line:\n\n```ruby\ndevise_for :users, controllers: { invitations: 'users/invitations' }\n```\n\nbe sure that you generate the views and put them into the controller that you\ngenerated, so for this example it would be:\n\n```shell\nrails generate devise_invitable:views users\n```\n\nTo change behaviour of inviting or accepting users, you can simply override\ntwo methods:\n\n```ruby\nclass Users::InvitationsController \u003c Devise::InvitationsController\n  private\n\n    # This is called when creating invitation.\n    # It should return an instance of resource class.\n    def invite_resource\n      # skip sending emails on invite\n      super { |user| user.skip_invitation = true }\n    end\n\n    # This is called when accepting invitation.\n    # It should return an instance of resource class.\n    def accept_resource\n      resource = resource_class.accept_invitation!(update_resource_params)\n      # Report accepting invitation to analytics\n      Analytics.report('invite.accept', resource.id)\n      resource\n    end\nend\n```\n\n## Strong Parameters\n\nWhen you customize your own views, you may end up adding new attributes to\nforms. Rails 4 moved the parameter sanitization from the model to the\ncontroller, causing DeviseInvitable to handle this concern at the controller\nas well. Read about it in [Devise\nREADME](https://github.com/plataformatec/devise#strong-parameters)\n\nThere are just two actions in DeviseInvitable that allows any set of\nparameters to be passed down to the model, therefore requiring sanitization.\nTheir names and the permited parameters by default are:\n\n*   `invite` (Devise::InvitationsController#create) - Permits only the\nauthentication keys (like `email`)\n*   `accept_invitation` (Devise::InvitationsController#update) - Permits\n`invitation_token` plus `password` and `password_confirmation`.\n\nHere is an example of the steps needed to add a first_name, last_name and role to invited Users.\n\nCaution: Adding roles requires additional security measures, such as preventing a standard user from inviting an administrator. Implement appropriate access controls to ensure system security.\n\n### Configuring your application controller to accept :first_name, :last_name, and :role for a User\n\nNote: These modifications can be applied directly in the InvitationsController if not needed for other Devise actions.\n\n```ruby\n  before_action :configure_permitted_parameters, if: :devise_controller?\n\n  protected\n\n  # Permit the new params here.\n  def configure_permitted_parameters\n    devise_parameter_sanitizer.permit(:invite, keys: [:first_name, :last_name, :role])\n  end\n```\n\n### Define your roles in the User model\n\n```ruby\nclass User \u003c ApplicationRecord\n  has_many :models\n\n  enum role: {Role 1 Name: 0, Role 2 Name: 1, Role 3 Name: 2, etc...}\nend\n```\n\n### In the Invitation view\n\n```ruby\n\u003ch2\u003e\u003c%= t \"devise.invitations.new.header\" %\u003e\u003c/h2\u003e\n\n\u003c%= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :post }) do |f| %\u003e\n  \u003c%= render \"devise/shared/error_messages\", resource: resource %\u003e\n  \u003c% resource.class.invite_key_fields.each do |field| -%\u003e\n    \u003cdiv class=\"field\"\u003e\n      \u003c%= f.label field %\u003e\u003cbr /\u003e\n      \u003c%= f.text_field field %\u003e\n    \u003c/div\u003e\n  \u003c% end %\u003e\n\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :first_name %\u003e\n    \u003c%= f.text_field :first_name %\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :last_name %\u003e\n    \u003c%= f.text_field :last_name %\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :role %\u003e\n    \u003c%= f.select :role, options_for_select(User.roles.map { |key, value| [key.humanize, key] }), {prompt: \"Select Role\"} %\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"actions\"\u003e\n    \u003c%= f.submit t(\"devise.invitations.new.submit_button\") %\u003e\n  \u003c/div\u003e\n\u003c% end %\u003e\n```\n\n## Usage\n\n### Send an invitation\n\nTo send an invitation to a user, use the `invite!` class method. **Note: This\nwill create a user, and send an email for the invite.** `:email` must be\npresent in the parameters hash. You can also include other attributes in the\nhash. The record will not be validated.\n\n```ruby\nUser.invite!(email: 'new_user@example.com', name: 'John Doe')\n# =\u003e an invitation email will be sent to new_user@example.com\n```\n\nIf you want to create the invitation but not send it, you can set\n`skip_invitation` to `true`.\n\n```ruby\nuser = User.invite!(email: 'new_user@example.com', name: 'John Doe') do |u|\n  u.skip_invitation = true\nend\n# =\u003e the record will be created, but the invitation email will not be sent\n```\n\nWhen generating the `accept_user_invitation_url` yourself, you must use the\n`raw_invitation_token`. This value is temporarily available when you invite a\nuser and will be decrypted when received.\n\n```ruby\naccept_user_invitation_url(invitation_token: user.raw_invitation_token)\n```\n\nWhen `skip_invitation` is used, you must also then set the\n`invitation_sent_at` field when the user is sent their token. Failure to do so\nwill yield \"Invalid invitation token\" error when the user attempts to accept\nthe invite. You can set the column, or call `deliver_invitation` to send the\ninvitation and set the column:\n\n```ruby\nuser.deliver_invitation\n```\n\nYou can add `:skip_invitation` to attributes hash if `skip_invitation` is\nadded to `attr_accessible`.\n\n```ruby\nUser.invite!(email: 'new_user@example.com', name: 'John Doe', skip_invitation: true)\n# =\u003e the record will be created, but the invitation email will not be sent\n```\n\n`skip_invitation` skips sending the email, but sets `invitation_token`, so\n`invited_to_sign_up?` on the resulting user returns `true`.\n\nTo check if a particular user is created by invitation, irrespective to state\nof invitation one can use `created_by_invite?`\n\n**Warning**\n\nWhen using `skip_invitation` you must send the email with the user object\ninstance that generated the tokens, as `user.raw_invitation_token` is\navailable only to the instance and is not persisted in the database.\n\nYou can also set `invited_by` when using the `invite!` class method:\n\n```ruby\nUser.invite!({ email: 'new_user@example.com' }, current_user) # current_user will be set as invited_by\n```\n\n### Sending an invitation after user creation\n\nYou can send an invitation to an existing user if your workflow creates them\nseparately:\n\n```ruby\nuser = User.find(42)\nuser.invite!(current_user)  # current user is optional to set the invited_by attribute\n```\n\n### Find by invitation token\n\nTo find by invitation token use the `find_by_invitation_token` class method.\n\n```ruby\nuser = User.find_by_invitation_token(params[:invitation_token], true)\n```\n\n### Accept an invitation\n\nTo accept an invitation with a token use the `accept_invitation!` class\nmethod. `:invitation_token` must be present in the parameters hash. You can\nalso include other attributes in the hash.\n\n```ruby\nUser.accept_invitation!(invitation_token: params[:invitation_token], password: 'ad97nwj3o2', name: 'John Doe')\n```\n\n### Callbacks\n\nA callback event is fired before and after an invitation is created\n(User#invite!) or accepted (User#accept_invitation!). For example, in your\nresource model you can add:\n\n```ruby\n# Note: callbacks should be placed after devise: :invitable is specified.\nbefore_invitation_created :email_admins\nafter_invitation_accepted :email_invited_by\n\ndef email_admins\n  # ...\nend\n\ndef email_invited_by\n  # ...\nend\n```\n\nThe callbacks support all options and arguments available to the standard\ncallbacks provided by ActiveRecord.\n\n### Scopes\n\nA pair of scopes to find those users that have accepted, and those that have\nnot accepted, invitations are defined:\n\n```ruby\nUser.invitation_accepted     # =\u003e returns all Users for whom the invitation_accepted_at attribute is not nil\nUser.invitation_not_accepted # =\u003e returns all Users for whom the invitation_accepted_at attribute is nil\nUser.created_by_invite       # =\u003e returns all Users who are created by invitations, irrespective to invitation status\n```\n\n## Integration in a Rails application\n\nSince the invitations controller takes care of all the creation/acceptation of\nan invitation, in most cases you wouldn't call the `invite!` and\n`accept_invitation!` methods directly. Instead, in your views, put a link to\n`new_user_invitation_path` or `new_invitation_path(:user)` or even\n`/users/invitation/new` to prepare and send an invitation (to a user in this\nexample).\n\nAfter an invitation is created and sent, the inviter will be redirected to\n`after_invite_path_for(inviter, invitee)`, which is the same path as\n`signed_in_root_path` by default.\n\nAfter an invitation is accepted, the invitee will be redirected to\n`after_accept_path_for(resource)`, which is the same path as\n`signed_in_root_path` by default. If you want to override the path, override\ninvitations controller and define `after_accept_path_for` method. This is\nuseful in the common case that a user is invited to a specific location in\nyour application. More on [Devise's\nREADME](https://github.com/plataformatec/devise), \"Controller filters and\nhelpers\" section.\n\nThe invitation email includes a link to accept the invitation that looks like\nthis: `/users/invitation/accept?invitation_token=abcd123`. When clicked, the\ninvited must set a password in order to accept its invitation. Note that if\nthe `invitation_token` is not present or not valid, the invited is redirected\nto `invalid_token_path_for(resource_name)`, which by default is\n`after_sign_out_path_for(resource_name)`.\n\nThe controller sets the `invited_by_id` attribute for the new user to the\ncurrent user.  This will let you easily keep track of who invited whom.\n\n## Controller filter\n\nInvitationsController uses `authenticate_inviter!` filter to restrict who can\nsend invitations. You can override this method in your\n`ApplicationController`.\n\nDefault behavior requires authentication of the same resource as the invited\none. For example, if your model `User` is invitable, it will allow all\nauthenticated users to send invitations to other users.\n\nYou would have a `User` model which is configured as invitable and an `Admin`\nmodel which is not. If you want to allow only admins to send invitations,\nsimply overwrite the `authenticate_inviter!` method as follow:\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  protected\n\n    def authenticate_inviter!\n      authenticate_admin!(force: true)\n    end\nend\n```\n\nAnd include `DeviseInvitable::Inviter` module into `Admin` model:\n\n```ruby\nclass Admin \u003c ActiveRecord::Base\n  devise :database_authenticatable, :validatable\n  include DeviseInvitable::Inviter\nend\n```\n\n## Has many invitations\n\nIf you want to get all records invited by a resource, you should define\n`has_many` association in the model allowed to send invitations.\n\nFor the default behavior, define it like this:\n\n```ruby\nhas_many :invitations, class_name: self.to_s, as: :invited_by\n```\n\nFor the previous example, where admins send invitations to users, define it\nlike this:\n\n```ruby\nhas_many :invitations, class_name: 'User', as: :invited_by\n```\n\n## I18n\n\nDeviseInvitable uses flash messages with I18n with the flash keys\n`:send_instructions`, `:invitation_token_invalid` and `:updated`. To customize\nyour app, you can modify the generated locale file:\n\n```yaml\nen:\n  devise:\n    invitations:\n      send_instructions: 'An invitation email has been sent to %{email}.'\n      invitation_token_invalid: 'The invitation token provided is not valid!'\n      updated: 'Your password was set successfully. You are now signed in.'\n      updated_not_active: 'Your password was set successfully.'\n```\n\nYou can also create distinct messages based on the resource you've configured\nusing the singular name given in routes:\n\n```yaml\nen:\n  devise:\n    invitations:\n      user:\n        send_instructions: 'A new user invitation has been sent to %{email}.'\n        invitation_token_invalid: 'Your invitation token is not valid!'\n        updated: 'Welcome on board! You are now signed in.'\n        updated_not_active: 'Welcome on board! Sign in to continue.'\n```\n\nThe DeviseInvitable mailer uses the same pattern as Devise to create mail\nsubject messages:\n\n```yaml\nen:\n  devise:\n    mailer:\n      invitation_instructions:\n        subject: 'You got an invitation!'\n        user_subject: 'You got a user invitation!'\n```\n\nTake a look at the [generated locale file](https://github.com/scambra/devise_invitable/blob/master/config/locales/en.yml) to check all available messages.\n\nCheck out [wiki](https://github.com/scambra/devise_invitable/wiki/I18n) for\ntranslations.\n\n### Use with sub schema\nIf you are using sub schema in you application, you need to make sure that you\nare prioritizing your sub schema scheme over Warden in Rack. For instance, if\nyou are using the Apartment gem go inside your `config/application.rb` file,\nadd the following lines:\n\n```ruby\nmodule YourSite\n  class Application \u003c Rails::Application\n    ...\n    Rails.application.config.middleware.insert_before Warden::Manager, Apartment::Elevators::Subdomain\n  end\nend\n```\n\n\n## Other ORMs\n\nDeviseInvitable supports ActiveRecord and Mongoid, like Devise.\n\n## Wiki\n\nIt's possible to find additional information about DeviseInvitable on the\nWiki:\n\nhttps://github.com/scambra/devise_invitable/wiki\n\n## Testing\n\nTo run tests:\n\n```shell\nbundle install\nbundle exec rake test\n```\n\n## Contributors\n\nCheck them all at:\n\nhttps://github.com/scambra/devise_invitable/contributors\n\nSpecial thanks to [rymai](https://github.com/rymai) for the Rails 3 support,\nhis fork was a great help.\n\n## Note on Patches/Pull Requests\n\n*   Fork the project.\n*   Make your feature addition or bug fix.\n*   Add tests for it. This is important so I don't break it in a future\nversion unintentionally.\n*   Commit, do not mess with rakefile, version, or history. (if you want to\nhave your own version, that is fine but bump version in a commit by itself\nI can ignore when I pull)\n*   Send me a pull request. Bonus points for topic branches.\n\n\n## Copyright\n\nCopyright (c) 2019 Sergio Cambra. See LICENSE for details.\n","funding_links":[],"categories":["Web 后端","Ruby","Authentication"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscambra%2Fdevise_invitable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fscambra%2Fdevise_invitable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscambra%2Fdevise_invitable/lists"}