Skip to content

Commit

Permalink
Merge pull request #327 from debtcollective/od/email-confirmation
Browse files Browse the repository at this point in the history
Send email confirmation if there's a user with the same email on Discourse
  • Loading branch information
orlando authored Sep 26, 2020
2 parents 853f9fb + de92c2c commit d8db8c4
Show file tree
Hide file tree
Showing 20 changed files with 352 additions and 50 deletions.
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ DISCOURSE_API_KEY=
DISCOURSE_USERNAME=system
DISCOURSE_ADMIN_ROLE=dispute-pro

MEMBER_HUB_URL=http://lvh.me:8000/hub

MAIL_FROM=The Debt Collective <admin@debtcollective.org>
ASSET_HOST=
SMTP_ADDRESS=
Expand Down
57 changes: 57 additions & 0 deletions app/controllers/user_confirmations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

class UserConfirmationsController < ApplicationController
layout "minimal"

# GET /user_confirmations?confirmation_token=abcdef
def index
@confirmation_token = params[:confirmation_token]
@user = User.find_by_confirmation_token(@confirmation_token)

respond_to do |format|
if @user
format.html { render :index, status: :ok }
else
format.html { render :index, status: :not_found }
end
end
end

# POST /user_confirmations
def create
user = User.send_confirmation_instructions(email: current_user&.email || params[:email])

respond_to do |format|
if user.errors.empty?
format.json { render json: {status: "success", message: "Confirmation email sent"}, status: :ok }
else
format.json { render json: {status: "failed", message: "Invalid confirmation token"}, status: :not_found }
end
end
end

# POST /user_confirmations/confirm
def confirm
@user = User.confirm_by_token(user_confirmation_params[:confirmation_token])
@user_confirmed = @user.errors.empty?

respond_to do |format|
if @user_confirmed
# run the Discourse link account job to fetch and link with a Discourse account
LinkDiscourseAccountJob.perform_later(@user)

format.html { render :confirm, notice: "Email confirmed", status: :ok }
format.json { render json: {status: "success", message: "Email confirmed"}, status: :ok }
else
format.html { render :confirm, notice: "Invalid confirmation token", status: :not_found }
format.json { render json: {status: "failed", message: "Invalid confirmation token"}, status: :not_found }
end
end
end

private

def user_confirmation_params
params.require(:user_confirmation).permit(:confirmation_token)
end
end
13 changes: 11 additions & 2 deletions app/jobs/link_discourse_account_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ class LinkDiscourseAccountJob < ApplicationJob
def perform(user)
discourse = DiscourseService.new(user)

# If user has external_id, skip
return if user.external_id

# Find Discourse User
discourse_user = discourse.find_user_by_email

# If there's a Discourse user, send verification email
if discourse_user
# TODO: implement verification email to link a membership with a Discourse account
user.update(external_id: discourse_user["id"])
# and the user confirmed the email, set external_id
if user.confirmed?
user.update(external_id: discourse_user["id"])
else
# send verification email
User.send_confirmation_instructions(email: user.email)
end

return
end

Expand Down
8 changes: 8 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ def welcome_email(user:)

mail to: email, from: ENV["MAIL_FROM"]
end

def confirmation_email(user:)
@user = user
@confirmation_token = @user.confirmation_token
email = @user.email

mail to: email, from: ENV["MAIL_FROM"]
end
end
2 changes: 1 addition & 1 deletion app/models/current_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def active?
end

def as_json
json = super(only: [:id, :name, :email, :external_id], methods: [:active_subscription])
json = super(only: [:id, :name, :email, :external_id], methods: [:active_subscription, :confirmed?])
json["subscription"] = json.delete("active_subscription")

json
Expand Down
59 changes: 47 additions & 12 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
#
# Table name: users
#
# id :bigint not null, primary key
# admin :boolean default(FALSE)
# avatar_url :string
# banned :boolean default(FALSE)
# custom_fields :jsonb
# email :string
# name :string
# username :string
# created_at :datetime not null
# updated_at :datetime not null
# external_id :bigint
# stripe_id :string
# id :bigint not null, primary key
# admin :boolean default(FALSE)
# avatar_url :string
# banned :boolean default(FALSE)
# confirmation_sent_at :datetime
# confirmation_token :string
# confirmed_at :datetime
# custom_fields :jsonb
# email :string
# name :string
# username :string
# created_at :datetime not null
# updated_at :datetime not null
# external_id :bigint
# stripe_id :string
#
class User < ApplicationRecord
include ActionView::Helpers::DateHelper
Expand Down Expand Up @@ -50,10 +53,42 @@ def self.find_or_create_from_sso(payload)
[user, new_record]
end

def self.send_confirmation_instructions(email:)
user = User.find_by_email(email)

if user
confirmation_token = user.confirmation_token || SecureRandom.hex(20)
user.update(confirmation_token: confirmation_token, confirmation_sent_at: DateTime.now)
UserMailer.confirmation_email(user: user).deliver_later
else
user = User.new
user.errors.add(:base, "User not found")
end

user
end

def self.confirm_by_token(confirmation_token)
user = User.find_by_confirmation_token(confirmation_token)

if user
user.update(confirmation_token: nil, confirmed_at: DateTime.now)
else
user = User.new
user.errors.add(:base, "Invalid confirmation token")
end

user
end

def admin?
!!admin
end

def confirmed?
external_id.present?|| confirmed_at.present?
end

def current_streak
subscription = active_subscription

Expand Down
29 changes: 29 additions & 0 deletions app/views/layouts/minimal.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Membership</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= content_for?(:head) ? yield(:head) : '' %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<link href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;400;500;700&display=swap" rel="stylesheet" />

<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />

<%= render 'layouts/sentry' %>
<%= render 'shared/analytics_scripts'%>
</head>

<body>
<div class="content">
<%= yield %>
</div>
</body>
</html>
7 changes: 7 additions & 0 deletions app/views/user_confirmations/confirm.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% if @user_confirmed %>
<p>Your account is now confirmed. Please visit your personalized hub by clicking the link below.</p>

<a href="<%= ENV["MEMBER_HUB_URL"]%>" class="button primary">Go to Member Hub</a>
<% else %>
<p>Your email is not confirmed.</p>
<% end %>
19 changes: 19 additions & 0 deletions app/views/user_confirmations/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<% if @user %>
<% if @user.confirmed?%>
<p>This email has been already confirmed</p>
<% else %>
<p>Please click the button below to confirm your email</p>

<%= form_with(url: confirm_user_confirmations_path, method: 'post', local: true) do %>
<%= fields_for(:user_confirmation) do |f|%>
<%= f.hidden_field(:confirmation_token, value: @confirmation_token) %>

<div class="form-row">
<button id="submit-payment" type="submit" class="button primary">Confirm my email</button>
</div>
<% end %>
<% end %>
<% end %>
<% else %>
<p>Invalid confirmation token</p>
<% end %>
34 changes: 34 additions & 0 deletions app/views/user_mailer/confirmation_email.html.inky
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<container>
<row>
<columns large="12">
<spacer size="24"></spacer>
<h5>Hello <strong><%= @user.name %></strong></h5>

<p>
We need to confirm your email address. Please click the button below.
</p>

<spacer size="12"></spacer>

<button
target="_blank"
href="<%= confirm_user_confirmations_url(confirmation_token: @confirmation_token) %>"
class="expanded"
>
Confirm your email
</button>

<spacer size="12"></spacer>

<p>
If you have any questions, please feel free to reach out to us at
admin@debtcollective.org.
</p>

<p>
In solidarity,<br />
Debt Collective
</p>
</columns>
</row>
</container>
3 changes: 2 additions & 1 deletion config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker

# Mailcatcher
# ActionMailer
config.action_mailer.default_url_options = {host: "http://#{ENV["DEV_HOST"]}:#{ENV["DEV_PORT"]}"}
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {address: "127.0.0.1", port: 1025}
config.action_mailer.raise_delivery_errors = false
Expand Down
1 change: 1 addition & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
config.action_mailer.default_url_options = {host: "http://localhost"}

# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
Expand Down
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
resources :plan_changes, only: %i[index create]
end

get "/users/current" => "users#current", :constraints => {format: "json" }
get "/users/current" => "users#current", :constraints => {format: "json"}

resources :user_confirmations, only: %i[index create] do
post "/confirm" => "user_confirmations#confirm", :on => :collection
end

get "/login" => "sessions#login"
get "/signup" => "sessions#signup"
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20200925234657_add_user_confirmation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddUserConfirmation < ActiveRecord::Migration[6.0]
def change
add_column :users, :confirmation_token, :string, index: true
add_column :users, :confirmed_at, :datetime
add_column :users, :confirmation_sent_at, :datetime
end
end
5 changes: 4 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2020_09_24_215740) do
ActiveRecord::Schema.define(version: 2020_09_25_234657) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -129,6 +129,9 @@
t.bigint "external_id"
t.string "name"
t.string "username"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
end

add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
Expand Down
Loading

0 comments on commit d8db8c4

Please sign in to comment.