[deploy] Invite users to join and create password (#9294)
* Initial invitation work * Add more invitation code * Add proper registration page * Fix up tests * Fix failings * Fix spec * Add self-serve auth config * Add registered condition * Change profile image call and registered_at test * Change comment * Fix copy test * Stub emojipedia * Linting * Add registered at to factory * Ensure registered for user tag * Update app/views/internal/invitations/index.html.erb Co-authored-by: Michael Kohl <citizen428@dev.to> * Update app/controllers/internal/invitations_controller.rb Co-authored-by: Michael Kohl <citizen428@dev.to> * Slight changes * Update recover password flow * Update app/views/internal/invitations/index.html.erb Co-authored-by: Michael Kohl <citizen428@dev.to> * Update app/controllers/internal/invitations_controller.rb Co-authored-by: Michael Kohl <citizen428@dev.to> * Update db/schema.rb Co-authored-by: Michael Kohl <citizen428@dev.to>
This commit is contained in:
parent
66c70d38e8
commit
da6a6739e5
51 changed files with 547 additions and 83 deletions
1
Gemfile
1
Gemfile
|
|
@ -30,6 +30,7 @@ gem "cloudinary", "~> 1.16" # Client library for easily using the Cloudinary ser
|
|||
gem "counter_culture", "~> 2.5" # counter_culture provides turbo-charged counter caches that are kept up-to-date
|
||||
gem "ddtrace", "~> 0.37.0" # ddtrace is Datadog’s tracing client for Ruby.
|
||||
gem "devise", "~> 4.7" # Flexible authentication solution for Rails
|
||||
gem "devise_invitable", "~> 2.0.0" # Allows invitations to be sent for joining
|
||||
gem "dogstatsd-ruby", "~> 4.8" # A client for DogStatsD, an extension of the StatsD metric server for Datadog
|
||||
gem "doorkeeper", "~> 5.4" # Oauth 2 provider
|
||||
gem "dry-struct", "~> 1.2" # Typed structs and value objects
|
||||
|
|
|
|||
|
|
@ -251,6 +251,9 @@ GEM
|
|||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise_invitable (2.0.2)
|
||||
actionmailer (>= 5.0)
|
||||
devise (>= 4.6)
|
||||
diff-lcs (1.3)
|
||||
distribution (0.7.3)
|
||||
docile (1.3.2)
|
||||
|
|
@ -911,6 +914,7 @@ DEPENDENCIES
|
|||
ddtrace (~> 0.37.0)
|
||||
derailed_benchmarks (~> 1.7)
|
||||
devise (~> 4.7)
|
||||
devise_invitable (~> 2.0.0)
|
||||
dogstatsd-ruby (~> 4.8)
|
||||
doorkeeper (~> 5.4)
|
||||
dry-struct (~> 1.2)
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@
|
|||
}
|
||||
|
||||
.new-article-pitch {
|
||||
padding-top: calc(8% + 50px);
|
||||
padding-top: calc(3% + 25px);
|
||||
width: 900px;
|
||||
max-width: 94%;
|
||||
margin: auto;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Api
|
|||
|
||||
SHOW_ATTRIBUTES_FOR_SERIALIZATION = %i[
|
||||
id username name summary twitter_username github_username website_url
|
||||
location created_at profile_image
|
||||
location created_at profile_image registered
|
||||
].freeze
|
||||
private_constant :SHOW_ATTRIBUTES_FOR_SERIALIZATION
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ module Api
|
|||
else
|
||||
relation.find(params[:id])
|
||||
end
|
||||
not_found unless @user.registered
|
||||
end
|
||||
|
||||
def me
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ module Internal
|
|||
where("score > ? AND score < ?", -10, 8).
|
||||
limit(120)
|
||||
|
||||
@possible_spam_users = User.where(
|
||||
@possible_spam_users = User.registered.where(
|
||||
"github_created_at > ? OR twitter_created_at > ? OR length(name) > ?",
|
||||
50.hours.ago, 50.hours.ago, 30
|
||||
).
|
||||
|
|
|
|||
23
app/controllers/internal/invitations_controller.rb
Normal file
23
app/controllers/internal/invitations_controller.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
module Internal
|
||||
class InvitationsController < Internal::ApplicationController
|
||||
layout "internal"
|
||||
|
||||
def index
|
||||
@invitations = User.where(registered: false).page(params[:page]).per(50)
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
def create
|
||||
email = params.dig(:user, :email)
|
||||
name = params.dig(:user, :name)
|
||||
username = "#{name.downcase.tr(' ', '_').gsub(/[^0-9a-z ]/i, '')}_#{rand(1000)}"
|
||||
User.invite!(email: email,
|
||||
name: name,
|
||||
username: username,
|
||||
remote_profile_image_url: Users::ProfileImageGenerator.call,
|
||||
registered: false)
|
||||
redirect_to internal_invitations_path
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/controllers/invitations_controller.rb
Normal file
29
app/controllers/invitations_controller.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
class InvitationsController < Devise::InvitationsController
|
||||
def update
|
||||
# Copied from https://github.com/scambra/devise_invitable/blob/master/app/controllers/devise/invitations_controller.rb
|
||||
# And edited. This is a common devise pattern, similar to OmniauthCallbacksController.
|
||||
raw_invitation_token = update_resource_params[:invitation_token]
|
||||
self.resource = accept_resource
|
||||
invitation_accepted = resource.errors.empty?
|
||||
|
||||
yield resource if block_given?
|
||||
|
||||
if invitation_accepted
|
||||
resource.registered = true
|
||||
resource.registered_at = Time.current
|
||||
if resource.class.allow_insecure_sign_in_after_accept
|
||||
flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
|
||||
set_flash_message :notice, flash_message if is_flashing_format?
|
||||
resource.after_database_authentication
|
||||
sign_in(resource_name, resource)
|
||||
redirect_to onboarding_path
|
||||
else
|
||||
set_flash_message :notice, :updated_not_active if is_flashing_format?
|
||||
respond_with resource, location: new_session_path(resource_name)
|
||||
end
|
||||
else
|
||||
resource.invitation_token = raw_invitation_token
|
||||
respond_with_navigational(resource) { render :edit }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -218,6 +218,7 @@ class StoriesController < ApplicationController
|
|||
return
|
||||
end
|
||||
not_found if @user.username.include?("spam_") && @user.decorate.fully_banished?
|
||||
not_found unless @user.registered
|
||||
assign_user_comments
|
||||
assign_user_stories
|
||||
@list_of = "articles"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def any_selfserve_auth?
|
||||
authentication_enabled_providers.any?
|
||||
end
|
||||
|
||||
def beautified_url(url)
|
||||
url.sub(/\A((https?|ftp):\/)?\//, "").sub(/\?.*/, "").chomp("/")
|
||||
rescue StandardError
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module BadgeRewarder
|
|||
message = "Happy #{ApplicationConfig['COMMUNITY_NAME']} birthday! " \
|
||||
"Can you believe it's been #{i} #{'year'.pluralize(i)} already?!"
|
||||
badge = Badge.find_by!(slug: "#{YEARS[i]}-year-club")
|
||||
User.where("created_at < ? AND created_at > ?", i.year.ago, i.year.ago - 2.days).find_each do |user|
|
||||
User.registered.where("created_at < ? AND created_at > ?", i.year.ago, i.year.ago - 2.days).find_each do |user|
|
||||
achievement = BadgeAchievement.create(
|
||||
user_id: user.id,
|
||||
badge_id: badge.id,
|
||||
|
|
@ -114,7 +114,7 @@ module BadgeRewarder
|
|||
|
||||
def self.award_badges(usernames, slug, message_markdown)
|
||||
badge_id = Badge.find_by!(slug: slug).id
|
||||
User.where(username: usernames).find_each do |user|
|
||||
User.registered.where(username: usernames).find_each do |user|
|
||||
BadgeAchievement.create(
|
||||
user_id: user.id,
|
||||
badge_id: badge_id,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,6 @@ class EmailDigest
|
|||
private
|
||||
|
||||
def get_users
|
||||
User.where(email_digest_periodic: true).where.not(email: "")
|
||||
User.registered.where(email_digest_periodic: true).where.not(email: "")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class UserTag < LiquidTagBase
|
|||
private
|
||||
|
||||
def parse_username_to_user(user)
|
||||
User.find_by(username: user) || DELETED_USER
|
||||
User.find_by(username: user, registered: true) || DELETED_USER
|
||||
end
|
||||
|
||||
def path_to_profile(user)
|
||||
|
|
|
|||
7
app/models/invitation.rb
Normal file
7
app/models/invitation.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class Invitation < ApplicationRecord
|
||||
# This class exists to take advantage of Rolify for limiting authorization
|
||||
# on internal reports.
|
||||
# NOTE: It is not backed by a database table and should not be expected to
|
||||
# function like a traditional Rails model
|
||||
resourcify
|
||||
end
|
||||
|
|
@ -112,7 +112,7 @@ class User < ApplicationRecord
|
|||
|
||||
mount_uploader :profile_image, ProfileImageUploader
|
||||
|
||||
devise :omniauthable, :registerable, :database_authenticatable, :confirmable, :rememberable
|
||||
devise :invitable, :omniauthable, :registerable, :database_authenticatable, :confirmable, :rememberable, :recoverable
|
||||
|
||||
validates :behance_url, length: { maximum: 100 }, allow_blank: true, format: BEHANCE_URL_REGEXP
|
||||
validates :bg_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
|
||||
|
|
@ -164,6 +164,7 @@ class User < ApplicationRecord
|
|||
alias_attribute :subscribed_to_welcome_notifications?, :welcome_notifications
|
||||
|
||||
scope :eager_load_serialized_data, -> { includes(:roles) }
|
||||
scope :registered, -> { where(registered: true) }
|
||||
|
||||
after_save :bust_cache
|
||||
after_save :subscribe_to_mailchimp_newsletter
|
||||
|
|
@ -385,6 +386,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def subscribe_to_mailchimp_newsletter
|
||||
return unless registered
|
||||
return unless email.present? && email.include?("@")
|
||||
return if saved_changes["unconfirmed_email"] && saved_changes["confirmation_sent_at"]
|
||||
return unless saved_changes.key?(:email) || saved_changes.key?(:email_newsletter)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module Internal
|
||||
class UsersQuery
|
||||
def self.call(relation: User.all, options: {})
|
||||
def self.call(relation: User.registered, options: {})
|
||||
role, search = options.values_at(:role, :search)
|
||||
|
||||
relation = relation.with_role(role, :any) if role.presence
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ module Authentication
|
|||
{
|
||||
password: Devise.friendly_token(20),
|
||||
signup_cta_variant: cta_variant,
|
||||
registered: true,
|
||||
registered_at: Time.current,
|
||||
saw_onboarding: false,
|
||||
editor_version: :v2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module Mentions
|
|||
end
|
||||
|
||||
def collect_existing_users(usernames)
|
||||
User.where(username: usernames)
|
||||
User.registered.where(username: usernames)
|
||||
end
|
||||
|
||||
def create_mentions_for(users)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div id="sidebar-wrapper-right" class="sidebar-wrapper sidebar-wrapper-right crayons-layout__sidebar-right">
|
||||
<div class="sidebar-bg" id="sidebar-bg-right"></div>
|
||||
<aside class="side-bar sidebar-additional showing grid gap-4" id="sidebar-additional">
|
||||
<% unless user_signed_in? %>
|
||||
<% unless user_signed_in? || !any_selfserve_auth? %>
|
||||
<section class="crayons-card crayons-card--secondary signin-cta-widget">
|
||||
<header class="crayons-card__header">
|
||||
<h3 class="crayons-card__header__headline">Join <%= community_name %></h3>
|
||||
|
|
|
|||
31
app/views/devise/invitations/edit.html.erb
Normal file
31
app/views/devise/invitations/edit.html.erb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<% title "Set your Password to Join" %>
|
||||
<div class="crayons-layout crayons-layout--limited mb-10" style="max-width:640px">
|
||||
<div class="crayons-notice mt-4">
|
||||
<h1>Welcome to <%= community_name %></h1>
|
||||
<h3><%= SiteConfig.tagline %></h3>
|
||||
</div>
|
||||
<div class="mt-3 crayons-card p-7 align-left">
|
||||
<h2>Set a password to access your account</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
<%= f.hidden_field :invitation_token, readonly: true %>
|
||||
|
||||
<% if f.object.class.require_password_on_accepting %>
|
||||
<div class="crayons-field mt-5">
|
||||
<%= f.label :password, class: "crayons-field__label" %>
|
||||
<%= f.password_field :password, class: "crayons-textfield" %>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field">
|
||||
<%= f.label :password_confirmation, class: "crayons-field__label" %>
|
||||
<%= f.password_field :password_confirmation, class: "crayons-textfield" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= f.submit t("devise.invitations.edit.submit_button"), class: "crayons-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
11
app/views/devise/mailer/invitation_instructions.html.erb
Normal file
11
app/views/devise/mailer/invitation_instructions.html.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<p><%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %></p>
|
||||
|
||||
<p><%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %></p>
|
||||
|
||||
<p><%= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, invitation_token: @token) %></p>
|
||||
|
||||
<% if @resource.invitation_due_at %>
|
||||
<p><%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %></p>
|
||||
<% end %>
|
||||
|
||||
<p><%= t("devise.mailer.invitation_instructions.ignore") %></p>
|
||||
11
app/views/devise/mailer/invitation_instructions.text.erb
Normal file
11
app/views/devise/mailer/invitation_instructions.text.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>
|
||||
|
||||
<%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %>
|
||||
|
||||
<%= accept_invitation_url(@resource, invitation_token: @token) %>
|
||||
|
||||
<% if @resource.invitation_due_at %>
|
||||
<%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>
|
||||
<% end %>
|
||||
|
||||
<%= t("devise.mailer.invitation_instructions.ignore") %>
|
||||
|
|
@ -1,25 +1,27 @@
|
|||
<h2>Change your password</h2>
|
||||
<%= devise_error_messages! %>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
<div class="mt-8 mb-10 crayons-card p-7 pt-3 align-left mx-auto" style="max-width:490px;">
|
||||
<h2 class="pb-4">Change your password</h2>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password, "New password" %><br />
|
||||
<% if @minimum_password_length %>
|
||||
<em>(<%= @minimum_password_length %> characters minimum)</em><br />
|
||||
<% end %>
|
||||
<%= f.password_field :password, autofocus: true, autocomplete: "off" %>
|
||||
</div>
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation, "Confirm new password" %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off" %>
|
||||
</div>
|
||||
<div class="crayons-field">
|
||||
<%= f.label :password, "New password", class: "crayons-field__label" %>
|
||||
<% if @minimum_password_length %>
|
||||
<em>(<%= @minimum_password_length %> characters minimum)</em><br />
|
||||
<% end %>
|
||||
<%= f.password_field :password, autofocus: true, autocomplete: "off", class: "crayons-textfield" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Change my password" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="crayons-field">
|
||||
<%= f.label :password_confirmation, "Confirm new password", class: "crayons-field__label" %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: "crayons-textfield" %>
|
||||
</div>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
<div class="crayons-field pt-4">
|
||||
<%= f.submit "Change my password", class: "crayons-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
<h2>Forgot your password?</h2>
|
||||
<%= devise_error_messages! %>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
<div class="mt-8 mb-10 crayons-card p-7 pt-3 align-left mx-auto" style="max-width:490px;">
|
||||
<h2 class="pb-4">Forgot your password?</h2>
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true %>
|
||||
</div>
|
||||
<div class="crayons-field">
|
||||
<%= f.label :email, class: "crayons-field__label" %>
|
||||
<%= f.email_field :email, class: "crayons-textfield", placeholder: "you@email.com" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Send me reset password instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
<div class="crayons-field pt-6">
|
||||
<%= f.submit "Send me reset password instructions", class: "crayons-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -17,14 +17,42 @@
|
|||
<img class="secondary-logo" src="<%= cloudinary(safe_logo_url(SiteConfig.secondary_logo_url), 190) %>"
|
||||
alt="Secondary <%= community_name %> logo" />
|
||||
</div>
|
||||
<h1>Great to have you</h1>
|
||||
<% if any_selfserve_auth? %>
|
||||
<h1>Great to have you</h1>
|
||||
<% else %>
|
||||
<h1>Registration is by invitation only</h1>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="links">
|
||||
<div class="flex flex-col m:flex-row justify-center">
|
||||
<%= render partial: "shared/authentication/providers_registration_form" %>
|
||||
</div>
|
||||
<%= render partial: "shared/authentication/providers_registration_form" %>
|
||||
|
||||
<p class="pt-4"><em>We require social login to prevent abuse.</em></p>
|
||||
<div id="sign-in-password-form" class="mt-1 crayons-card p-7 align-left mx-auto" style="max-width:490px;">
|
||||
<p class="pb-4 fw-bold">If you have a password...</p>
|
||||
<%= form_for(User.new, as: :user, url: session_path(:user)) do |f| %>
|
||||
<div class="crayons-field">
|
||||
<%= f.label :email, class: "crayons-field__label" %>
|
||||
<%= f.email_field :email, autocomplete: "email", class: "crayons-textfield" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password, class: "crayons-field__label" %>
|
||||
<%= f.password_field :password, autocomplete: "current-password", class: "crayons-textfield" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.check_box :remember_me %>
|
||||
<%= f.label :remember_me %>
|
||||
</div>
|
||||
|
||||
<div class="actions pt-3">
|
||||
<%= f.submit "Log in", class: "crayons-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if any_selfserve_auth? %>
|
||||
<p class="pt-6 fs-s"><em>Sign in by social auth if you don't have a password set.</em></p>
|
||||
<% end %>
|
||||
<p class="pt-6 fs-s"><em><a href="<%= new_password_path(:user) %>">Forgot password?</a></em></p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Open Source 😇</p>
|
||||
<p>Free Forever ❤️</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<h3 class="m-4">Past <%= num_days %> day new users (<%= User.where("created_at > ?", num_days.day.ago).count %>)</h3>
|
||||
<h3 class="m-4">Past <%= num_days %> day new users (<%= User.where("registered_at > ?", num_days.day.ago).count %>)</h3>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
|
|
@ -7,13 +7,13 @@
|
|||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
Average comments: <%= User.where("created_at > ?", num_days.day.ago).average(:comments_count) %>
|
||||
Average comments: <%= User.where("registered_at > ?", num_days.day.ago).average(:comments_count) %>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Average articles: <%= User.where("created_at > ?", num_days.day.ago).average(:articles_count) %>
|
||||
Average articles: <%= User.where("registered_at > ?", num_days.day.ago).average(:articles_count) %>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Average reactions: <%= User.where("created_at > ?", num_days.day.ago).average(:reactions_count) %>
|
||||
Average reactions: <%= User.where("registered_at > ?", num_days.day.ago).average(:reactions_count) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
35
app/views/internal/invitations/index.html.erb
Normal file
35
app/views/internal/invitations/index.html.erb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<div class="row my-3">
|
||||
<div class="col">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<%= link_to "Registered Users", internal_users_path, class: "nav-link" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "New", new_internal_invitation_path, class: "nav-link" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">User</th>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @invitations.each do |user| %>
|
||||
<tr>
|
||||
<td><%= link_to user.username, internal_user_path(user) %></td>
|
||||
<td><%= user.id %></td>
|
||||
<td><%= user.name %></td>
|
||||
<td><%= user.email %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%= paginate @invitations, theme: "internal" %>
|
||||
13
app/views/internal/invitations/new.html.erb
Normal file
13
app/views/internal/invitations/new.html.erb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div class="row">
|
||||
<div class="col-12">
|
||||
<%= form_for(User.new, url: internal_invitations_path) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.label "Email:" %>
|
||||
<%= f.text_field :email, required: true, class: "form-control", placeholder: "Email of invitee" %>
|
||||
<%= f.label "Name:" %>
|
||||
<%= f.text_field :name, required: true, class: "form-control", placeholder: "Name of invitee" %>
|
||||
</div>
|
||||
<%= f.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="row my-3">
|
||||
<div class="col">
|
||||
<div class="col-8">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<%= link_to "All", internal_users_path, class: "nav-link #{'active' if params[:role].blank?}" %>
|
||||
|
|
@ -16,6 +16,9 @@
|
|||
<li class="nav-item">
|
||||
<%= link_to "Tag Mods", internal_users_path(role: :tag_moderator), class: "nav-link #{'active' if params[:role] == 'tag_moderator'}" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Invitations", internal_invitations_path, class: "nav-link" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,20 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<div class="border-0 border-b-1 border-solid border-base-20 p-2 grid gap-2">
|
||||
<%= render partial: "shared/authentication/providers_nav_menu" %>
|
||||
<% if any_selfserve_auth? %>
|
||||
<%= render partial: "shared/authentication/providers_nav_menu" %>
|
||||
<a
|
||||
href="/enter"
|
||||
class="crayons-btn crayons-btn--secondary">
|
||||
More Sign In Options
|
||||
</a>
|
||||
<% else %>
|
||||
<a
|
||||
href="/enter"
|
||||
class="crayons-btn crayons-btn--secondary">
|
||||
Sign In
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<a href="<%= information_path %>" class="crayons-link crayons-link--block" id="last-nav-link">All about <%= community_name %></a>
|
||||
|
|
|
|||
|
|
@ -9,19 +9,22 @@
|
|||
class: "mascot-image",
|
||||
alt: SiteConfig.mascot_image_description,
|
||||
loading: "lazy") %>
|
||||
<% if any_selfserve_auth? %>
|
||||
<h1>Join our <%= community_qualified_name %> :)</h1>
|
||||
|
||||
<h1>Join our <%= community_qualified_name %> :)</h1>
|
||||
<% if SiteConfig.tagline.present? %>
|
||||
<p><%= SiteConfig.tagline %></p>
|
||||
<% end %>
|
||||
|
||||
<% if SiteConfig.tagline.present? %>
|
||||
<p><%= SiteConfig.tagline %></p>
|
||||
<div class="flex flex-col l:flex-row justify-center">
|
||||
<%= render partial: "shared/authentication/providers_signup_modal" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<em>We strive for transparency and don't collect excess data.</em>
|
||||
</p>
|
||||
<% else %>
|
||||
<h1><a href="/enter" data-no-instant>Sign in</a> to <%= community_qualified_name %> :)</h1>
|
||||
<% end %>
|
||||
|
||||
<div class="flex flex-col l:flex-row justify-center">
|
||||
<%= render partial: "shared/authentication/providers_signup_modal" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<em>We strive for transparency and don't collect excess data.</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "navbar_basic") %>"
|
||||
class="crayons-btn crayons-btn--xl crayons-btn--brand-<%= provider.provider_name %> crayons-btn--icon-left m-1 whitespace-nowrap"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
Sign In with <%= provider.official_name %>
|
||||
</a>
|
||||
<% end %>
|
||||
<div class="flex flex-col m:flex-row justify-center">
|
||||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "navbar_basic") %>"
|
||||
class="crayons-btn crayons-btn--xl crayons-btn--brand-<%= provider.provider_name %> crayons-btn--icon-left m-1 whitespace-nowrap"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
Sign In with <%= provider.official_name %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if authentication_enabled_providers.any? %>
|
||||
<p class="pt-6 pb-8"><em>We require social registration to prevent abuse.</em></p>
|
||||
<% end %>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<h2>
|
||||
<a href="/"><%= community_name %></a> is a community of <br />
|
||||
<%= number_with_delimiter User.estimated_count %> amazing <%= community_members_label %>
|
||||
<%= number_with_delimiter User.registered.estimated_count %> amazing <%= community_members_label %>
|
||||
</h2>
|
||||
|
||||
<h3>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<%= render "users/additional_authentication" %>
|
||||
|
||||
<%= render "users/account_set_password" %>
|
||||
|
||||
<%= render "users/account_providers_emails" %>
|
||||
|
||||
<div class="crayons-card mb-6 p-6 grid gap-6">
|
||||
|
|
|
|||
15
app/views/users/_account_set_password.html.erb
Normal file
15
app/views/users/_account_set_password.html.erb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<div class="crayons-card mb-6 grid grid-flow-row gap-6 p-6">
|
||||
<header>
|
||||
<h2 class="mb-2">Set New Password</h2>
|
||||
<p>As an alternative to signing in via linked social accounts, you may create a password.</p>
|
||||
</header>
|
||||
<%= form_for @user do |f| %>
|
||||
<div class="crayons-field mb-5">
|
||||
<%= f.label :password, class: "crayons-field__label" %>
|
||||
<%= f.password_field :password, class: "crayons-textfield", autocomplete: "off" %>
|
||||
<%= f.label :password_confirmation, "Confirm new password", class: "crayons-field__label" %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: "crayons-textfield" %>
|
||||
</div>
|
||||
<button class="crayons-btn crayon-btn--secondary" id="new__api__secret__btn" type="submit">Set New Password</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -27,7 +27,7 @@ module Metrics
|
|||
DatadogStatsClient.count("articles.first_past_24h", first_articles_past_24h, tags: ["resource:articles"])
|
||||
|
||||
# Users who signed up in the past 24 hours who have made at least 1 comment so far
|
||||
new_users_min_1_comment_past_24h = User.where("comments_count >= ? AND created_at > ?", 1, 24.hours.ago).size
|
||||
new_users_min_1_comment_past_24h = User.where("comments_count >= ? AND registered_at > ?", 1, 24.hours.ago).size
|
||||
DatadogStatsClient.count(
|
||||
"users.new_min_1_comment_past_24h",
|
||||
new_users_min_1_comment_past_24h,
|
||||
|
|
@ -64,8 +64,8 @@ module Metrics
|
|||
ids_by_day << id
|
||||
end
|
||||
flat_id_list = ids_by_day.flatten.uniq
|
||||
non_new_user_ids = User.where("created_at < ?", 7.days.ago).where(id: flat_id_list).pluck(:id)
|
||||
new_user_ids = User.where("created_at > ? AND created_at < ?", 8.days.ago,
|
||||
non_new_user_ids = User.where("registered_at < ?", 7.days.ago).where(id: flat_id_list).pluck(:id)
|
||||
new_user_ids = User.where("registered_at > ? AND registered_at < ?", 8.days.ago,
|
||||
7.days.ago).where(id: flat_id_list).pluck(:id)
|
||||
record_active_days_of_group(ids_by_day, non_new_user_ids, "established")
|
||||
record_active_days_of_group(ids_by_day, new_user_ids, "new")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ import querystring;
|
|||
sub vcl_recv {
|
||||
# return this URL with only the parameters that match this regular expression
|
||||
if (req.url !~ "/internal/" && req.url !~ "/search/" && req.url !~ "/bulk_show") {
|
||||
set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|ut|verb)$");
|
||||
set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|invitation_token|reset_password_token|ut|verb)$");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,55 @@ Devise.setup do |config|
|
|||
# Setup a pepper to generate the encrypted password.
|
||||
# config.pepper = ''
|
||||
|
||||
# ==> Configuration for :invitable
|
||||
# The period the generated invitation token is valid.
|
||||
# After this period, the invited resource won't be able to accept the invitation.
|
||||
# When invite_for is 0 (the default), the invitation won't expire.
|
||||
# config.invite_for = 2.weeks
|
||||
|
||||
# Number of invitations users can send.
|
||||
# - If invitation_limit is nil, there is no limit for invitations, users can
|
||||
# send unlimited invitations, invitation_limit column is not used.
|
||||
# - If invitation_limit is 0, users can't send invitations by default.
|
||||
# - If invitation_limit n > 0, users can send n invitations.
|
||||
# You can change invitation_limit column for some users so they can send more
|
||||
# or less invitations, even with global invitation_limit = 0
|
||||
# Default: nil
|
||||
# config.invitation_limit = 5
|
||||
|
||||
# The key to be used to check existing users when sending an invitation
|
||||
# and the regexp used to test it when validate_on_invite is not set.
|
||||
# config.invite_key = { email: /\A[^@]+@[^@]+\z/ }
|
||||
# config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil }
|
||||
|
||||
# Ensure that invited record is valid.
|
||||
# The invitation won't be sent if this check fails.
|
||||
# Default: false
|
||||
# config.validate_on_invite = true
|
||||
|
||||
# Resend invitation if user with invited status is invited again
|
||||
# Default: true
|
||||
# config.resend_invitation = false
|
||||
|
||||
# The class name of the inviting model. If this is nil,
|
||||
# the #invited_by association is declared to be polymorphic.
|
||||
# Default: nil
|
||||
# config.invited_by_class_name = 'User'
|
||||
|
||||
# The foreign key to the inviting model (if invited_by_class_name is set)
|
||||
# Default: :invited_by_id
|
||||
# config.invited_by_foreign_key = :invited_by_id
|
||||
|
||||
# The column name used for counter_cache column. If this is nil,
|
||||
# the #invited_by association is declared without counter_cache.
|
||||
# Default: nil
|
||||
# config.invited_by_counter_cache = :invitations_count
|
||||
|
||||
# Auto-login after the user accepts the invite. If this is false,
|
||||
# the user will need to manually log in after accepting the invite.
|
||||
# Default: true
|
||||
# config.allow_insecure_sign_in_after_accept = false
|
||||
|
||||
# ==> Configuration for :confirmable
|
||||
# A period that the user is allowed to access the website even without
|
||||
# confirming their account. For instance, if set to 2.days, the user will be
|
||||
|
|
|
|||
31
config/locales/devise_invitable.en.yml
Normal file
31
config/locales/devise_invitable.en.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
en:
|
||||
devise:
|
||||
failure:
|
||||
invited: "You have a pending invitation, accept it to finish creating your account."
|
||||
invitations:
|
||||
send_instructions: "An invitation email has been sent to %{email}."
|
||||
invitation_token_invalid: "The invitation token provided is not valid!"
|
||||
updated: "Your password was set successfully. You are now signed in."
|
||||
updated_not_active: "Your password was set successfully."
|
||||
no_invitations_remaining: "No invitations remaining"
|
||||
invitation_removed: "Your invitation was removed."
|
||||
new:
|
||||
header: "Send invitation"
|
||||
submit_button: "Send an invitation"
|
||||
edit:
|
||||
header: "Set your password"
|
||||
submit_button: "Let's Get Started"
|
||||
mailer:
|
||||
invitation_instructions:
|
||||
subject: "Invitation instructions"
|
||||
hello: "Hello %{email}"
|
||||
someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below."
|
||||
accept: "Accept invitation"
|
||||
accept_until: "This invitation will be due in %{due_date}."
|
||||
ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password."
|
||||
time:
|
||||
formats:
|
||||
devise:
|
||||
mailer:
|
||||
invitation_instructions:
|
||||
accept_until_format: "%B %d, %Y %I:%M %p"
|
||||
|
|
@ -7,7 +7,8 @@ Rails.application.routes.draw do
|
|||
|
||||
devise_for :users, controllers: {
|
||||
omniauth_callbacks: "omniauth_callbacks",
|
||||
registrations: "registrations"
|
||||
registrations: "registrations",
|
||||
invitations: "invitations"
|
||||
}
|
||||
|
||||
devise_scope :user do
|
||||
|
|
@ -52,6 +53,7 @@ Rails.application.routes.draw do
|
|||
resources :comments, only: [:index]
|
||||
resources :events, only: %i[index create update]
|
||||
resources :feedback_messages, only: %i[index show]
|
||||
resources :invitations, only: %i[index new create]
|
||||
resources :pages, only: %i[index new create edit update destroy]
|
||||
resources :mods, only: %i[index update]
|
||||
resources :moderator_actions, only: %i[index]
|
||||
|
|
|
|||
32
db/migrate/20200712150048_devise_invitable_add_to_users.rb
Normal file
32
db/migrate/20200712150048_devise_invitable_add_to_users.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
class DeviseInvitableAddToUsers < ActiveRecord::Migration[6.0]
|
||||
def up
|
||||
safety_assured do
|
||||
change_table :users, bulk: true do |t|
|
||||
t.string :invitation_token
|
||||
t.boolean :registered, default: true
|
||||
t.datetime :registered_at
|
||||
t.datetime :invitation_created_at
|
||||
t.datetime :invitation_sent_at
|
||||
t.datetime :invitation_accepted_at
|
||||
t.integer :invitation_limit
|
||||
t.references :invited_by, polymorphic: true
|
||||
t.integer :invitations_count, default: 0
|
||||
t.index :invitations_count
|
||||
t.index :invitation_token, unique: true # for invitable
|
||||
t.index :invited_by_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
safety_assured do
|
||||
change_table :users do |t|
|
||||
t.remove_references :invited_by, polymorphic: true
|
||||
t.remove :invitations_count, :invitation_limit,
|
||||
:invitation_sent_at, :invitation_accepted_at,
|
||||
:invitation_token, :invitation_created_at,
|
||||
:registered, :registered_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
db/schema.rb
17
db/schema.rb
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2020_07_10_174257) do
|
||||
ActiveRecord::Schema.define(version: 2020_07_12_150048) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
|
@ -492,6 +492,7 @@ ActiveRecord::Schema.define(version: 2020_07_10_174257) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.bigint "user_id"
|
||||
t.datetime "verified_at"
|
||||
t.index ["user_id"], name: "index_email_authorizations_on_user_id"
|
||||
end
|
||||
|
||||
create_table "events", force: :cascade do |t|
|
||||
|
|
@ -1244,6 +1245,14 @@ ActiveRecord::Schema.define(version: 2020_07_10_174257) do
|
|||
t.string "inbox_guidelines"
|
||||
t.string "inbox_type", default: "private"
|
||||
t.string "instagram_url"
|
||||
t.datetime "invitation_accepted_at"
|
||||
t.datetime "invitation_created_at"
|
||||
t.integer "invitation_limit"
|
||||
t.datetime "invitation_sent_at"
|
||||
t.string "invitation_token"
|
||||
t.integer "invitations_count", default: 0
|
||||
t.bigint "invited_by_id"
|
||||
t.string "invited_by_type"
|
||||
t.jsonb "language_settings", default: {}, null: false
|
||||
t.datetime "last_article_at", default: "2017-01-01 05:00:00"
|
||||
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
|
||||
|
|
@ -1274,6 +1283,8 @@ ActiveRecord::Schema.define(version: 2020_07_10_174257) do
|
|||
t.datetime "profile_updated_at", default: "2017-01-01 05:00:00"
|
||||
t.integer "rating_votes_count", default: 0, null: false
|
||||
t.integer "reactions_count", default: 0, null: false
|
||||
t.boolean "registered", default: true
|
||||
t.datetime "registered_at"
|
||||
t.datetime "remember_created_at"
|
||||
t.string "remember_token"
|
||||
t.float "reputation_modifier", default: 1.0
|
||||
|
|
@ -1308,6 +1319,10 @@ ActiveRecord::Schema.define(version: 2020_07_10_174257) do
|
|||
t.index ["created_at"], name: "index_users_on_created_at"
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["github_username"], name: "index_users_on_github_username", unique: true
|
||||
t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true
|
||||
t.index ["invitations_count"], name: "index_users_on_invitations_count"
|
||||
t.index ["invited_by_id"], name: "index_users_on_invited_by_id"
|
||||
t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by_type_and_invited_by_id"
|
||||
t.index ["language_settings"], name: "index_users_on_language_settings", using: :gin
|
||||
t.index ["old_old_username"], name: "index_users_on_old_old_username"
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ FactoryBot.define do
|
|||
checked_code_of_conduct { true }
|
||||
checked_terms_and_conditions { true }
|
||||
display_announcements { true }
|
||||
registered_at { Time.current }
|
||||
signup_cta_variant { "navbar_basic" }
|
||||
email_digest_periodic { false }
|
||||
bg_color_hex { Faker::Color.hex_color }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,22 @@ RSpec.describe EmailDigest, type: :labor do
|
|||
expect(mailer).to have_received(:digest_email)
|
||||
expect(message_delivery).to have_received(:deliver_now)
|
||||
end
|
||||
|
||||
it "does not send email when user does not have email_digest_periodic" do
|
||||
articles = create_list(:article, 3, user_id: author.id, public_reactions_count: 20, score: 20)
|
||||
user.update_column(:email_digest_periodic, false)
|
||||
described_class.send_periodic_digest_email
|
||||
|
||||
expect(DigestMailer).not_to have_received(:with).with(user: user, articles: articles)
|
||||
end
|
||||
|
||||
it "does not send email when user is not registered" do
|
||||
articles = create_list(:article, 3, user_id: author.id, public_reactions_count: 20, score: 20)
|
||||
user.update_column(:registered, false)
|
||||
described_class.send_periodic_digest_email
|
||||
|
||||
expect(DigestMailer).not_to have_received(:with).with(user: user, articles: articles)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -724,6 +724,12 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
it "does not enqueue with a non-registered user" do
|
||||
sidekiq_assert_no_enqueued_jobs(only: Users::SubscribeToMailchimpNewsletterWorker) do
|
||||
user.update(registered: false)
|
||||
end
|
||||
end
|
||||
|
||||
it "does not enqueue when the email address or subscription status has not changed" do
|
||||
# The trait replaces the method with a dummy, but we need the actual method for this test.
|
||||
user = described_class.find(create(:user, :ignore_mailchimp_subscribe_callback).id)
|
||||
|
|
@ -901,6 +907,11 @@ RSpec.describe User, type: :model do
|
|||
expect(identity.auth_data_dump.provider).to eq(identity.provider)
|
||||
end
|
||||
|
||||
it "marks registered_at for newly registered user" do
|
||||
new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
|
||||
expect(new_user.registered_at).not_to be nil
|
||||
end
|
||||
|
||||
it "persists extracts relevant identity data from new twitter user" do
|
||||
new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
|
||||
expect(new_user.twitter_followers_count).to eq(100)
|
||||
|
|
|
|||
|
|
@ -138,6 +138,8 @@ RSpec.configure do |config|
|
|||
config.before do
|
||||
stub_request(:any, /res.cloudinary.com/).to_rack("dsdsdsds")
|
||||
|
||||
stub_request(:any, /emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/).to_rack("stubbed-emoji")
|
||||
|
||||
stub_request(:post, /api.fastly.com/).
|
||||
to_return(status: 200, body: "".to_json, headers: {})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ RSpec.describe "Api::V0::Users", type: :request do
|
|||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "returns 404 if the user is not registered" do
|
||||
user.update_column(:registered, false)
|
||||
get api_user_path(user.id)
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "returns 200 if the user username is found" do
|
||||
get api_user_path("by_username"), params: { url: user.username }
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
|
|
|||
33
spec/requests/internal/invitations_spec.rb
Normal file
33
spec/requests/internal/invitations_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "/internal/invitations", type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let(:admin) { create(:user, :super_admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
describe "GETS /internal/invitations" do
|
||||
it "renders to appropriate page" do
|
||||
user.update_column(:registered, false)
|
||||
get "/internal/invitations"
|
||||
expect(response.body).to include(user.username)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GETS /internal/invitations/new" do
|
||||
it "renders to appropriate page" do
|
||||
get "/internal/invitations/new"
|
||||
expect(response.body).to include("Email:")
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /internal/invitations" do
|
||||
it "creates new invitation" do
|
||||
post "/internal/invitations",
|
||||
params: { user: { email: "hey#{rand(1000)}@email.co", name: "Roger #{rand(1000)}" } }
|
||||
expect(User.last.registered).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,10 +5,22 @@ RSpec.describe "Registrations", type: :request do
|
|||
|
||||
describe "GET /enter" do
|
||||
context "when not logged in" do
|
||||
it "shows the sign in page" do
|
||||
it "shows the sign in page (with self-serve auth)" do
|
||||
get "/enter"
|
||||
expect(response.body).to include "Great to have you"
|
||||
end
|
||||
|
||||
it "shows the sign in text" do
|
||||
get "/enter"
|
||||
expect(response.body).to include "If you have a password"
|
||||
end
|
||||
|
||||
it "shows invite-only text if no self-serve" do
|
||||
SiteConfig.authentication_providers = []
|
||||
get "/enter"
|
||||
expect(response.body).to include "If you have a password"
|
||||
expect(response.body).not_to include "Sign in by social auth"
|
||||
end
|
||||
end
|
||||
|
||||
context "when logged in" do
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ RSpec.describe "UserProfiles", type: :request do
|
|||
expect { get "/#{banishable_user.reload.username}" }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "raises not found if user not registered" do
|
||||
user.update_column(:registered, false)
|
||||
expect { get "/#{user.username}" }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "renders noindex meta if banned" do
|
||||
user.add_role(:banned)
|
||||
get "/#{user.username}"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ RSpec.describe Users::Delete, type: :service do
|
|||
next
|
||||
end
|
||||
|
||||
next if possible_factory_name == "invited_by"
|
||||
|
||||
record = create(possible_factory_name, inverse_of => user)
|
||||
associations.push(record)
|
||||
end
|
||||
|
|
|
|||
BIN
vendor/cache/devise_invitable-2.0.2.gem
vendored
Normal file
BIN
vendor/cache/devise_invitable-2.0.2.gem
vendored
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue