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

Add registering and login with OTP through email #1068

Merged
merged 15 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,6 @@ Rails/ApplicationController:
- 'app/controllers/images_controller.rb'
- 'app/controllers/labels_controller.rb'

# Offense count: 1
# Cop supports --auto-correct.
Rails/ApplicationMailer:
Exclude:
- 'app/mailers/usergroup_mailer.rb'

# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: strict, flexible
Expand Down
36 changes: 36 additions & 0 deletions app/controllers/concerns/with_email_auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

# Based on https://github.com/weg-li/weg-li/blob/master/app/controllers/sessions_controller.rb
# Original author: https://github.com/phoet

module WithEmailAuth
def email; end

def email_login
email = normalize_email(params[:email])
if email.present? && valid_looking_email?(email)
token = EmailAuthToken.generate(email)
from = Whitelabel[:email]
label_name = t("label.#{Whitelabel[:label_id]}.name")
label_link = Whitelabel[:canonical_url]
UserMailer.login_link(email, token, from, I18n.locale,
label_name, label_link).deliver_later

redirect_to root_path, notice: t('email_auth.email_sent', email:)
else
flash.now[:alert] = t('email_auth.invalid_email')

render :email, status: :unprocessable_entity
end
end

protected

def normalize_email(email)
email.to_s.strip.downcase
end

def valid_looking_email?(email)
email.match(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
end
end
11 changes: 10 additions & 1 deletion app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class SessionsController < ApplicationController
include WithEmailAuth

def offline_login
user = User.find_by(nickname: params[:nickname])
sign_in(user)
Expand All @@ -19,7 +21,14 @@ def create
options = { alert: t('flash.duplicate_nick', name: e.nickname) }
end

redirect_to request.env['omniauth.origin'].presence || root_path, options
redirect_path = request.env['omniauth.origin'].presence || root_path

if current_user&.missing_name?
josepegea marked this conversation as resolved.
Show resolved Hide resolved
options = { alert: t('flash.update_profile_details') }
redirect_path = edit_user_path(current_user)
end

redirect_to redirect_path, options
end

def destroy
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ def index; end

def show; end

def edit; end
def edit
return unless current_user.missing_name?

user.name = nil
user.errors.add(:name, :required)
end

def calendar
respond_to do |format|
Expand Down
12 changes: 11 additions & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ def cache_image_path(model)
end

def login_providers
%w[twitter github google_oauth2]
%w[twitter github google_oauth2 email]
end

def icon_for_provider(provider)
return 'envelope' if provider == 'email'

provider
end

def whitelabel_stylesheet_link_tag
Expand Down Expand Up @@ -81,6 +87,10 @@ def hint(close = true)
end
end

def user_name(user)
user.missing_name? ? '-' : user.name
end

private

def markdown_parser
Expand Down
6 changes: 3 additions & 3 deletions app/helpers/link_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

module LinkHelper
def link_to_user(user, image: false, image_class: nil)
link_to(user, title: user.name) do
image ? user_image(user, image_class:) + user.name : user.name
link_to(user, title: user_name(user)) do
image ? user_image(user, image_class:) + user_name(user) : user_name(user)
end
end

def user_image(user, image_class: nil)
image_class ||= 'small-user-image'
image_tag(cache_image_path(user), title: user.name, class: image_class)
image_tag(cache_image_path(user), title: user_name(user), class: image_class)
end

def link_to_job(job)
Expand Down
22 changes: 22 additions & 0 deletions app/lib/email_auth_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Based on https://github.com/weg-li/weg-li/blob/master/app/lib/token.rb
# Original author: https://github.com/phoet

class EmailAuthToken
def self.generate(
email,
expiration: 15.minutes,
secret: Rails.application.secrets.secret_key_base
)
now_seconds = Time.now.to_i
payload = { iss: email, iat: now_seconds, exp: now_seconds + expiration }
token = ::JWT.encode(payload, secret, 'HS256')
Base64.encode64(token)
end

def self.decode(string, secret: Rails.application.secrets.secret_key_base)
token = Base64.decode64(string)
JWT.decode(token, secret, true, algorithm: 'HS256').first
end
end
4 changes: 4 additions & 0 deletions app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class ApplicationMailer < ActionMailer::Base
end
17 changes: 17 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# Based on https://github.com/weg-li/weg-li/blob/master/app/mailers/user_mailer.rb
# Original author: https://github.com/phoet

class UserMailer < ApplicationMailer
def login_link(email, token, from, locale, label_name, label_link) # rubocop:disable Metrics/ParameterLists
@token = token
@from = from
@label_name = label_name
@label_link = label_link

I18n.with_locale(locale) do
mail from: @from, to: email, subject: t('email_auth.subject', label: label_name)
end
end
end
2 changes: 1 addition & 1 deletion app/mailers/usergroup_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class UsergroupMailer < ActionMailer::Base
class UsergroupMailer < ApplicationMailer
def invitation_mail(event)
@event = event
options = {
Expand Down
37 changes: 36 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class User < ApplicationRecord
class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
include Slug
extend ApiHandling
slugged_by(:nickname)
Expand Down Expand Up @@ -84,10 +84,45 @@ def handle_google_oauth2_attributes(hash)
self.image = hash['info']['image']
end

def handle_email_attributes(hash)
received_email = hash['info']['email']

self.nickname = nickname_from_email(received_email) unless nickname
self.name = name_from_email(received_email) unless name
self.image = image_from_email(received_email) unless image
self.email = received_email
end

def hash_for_email(email)
Digest::SHA256.new.hexdigest(email)
end

def nickname_from_email(email)
hash_for_email(email)
end

def hide_nickname?
nickname == nickname_from_email(email)
end

EMPTY_NAME = '********'

def name_from_email(_email)
EMPTY_NAME
end

def image_from_email(email)
"https://www.gravatar.com/avatar/#{hash_for_email(email)}"
end

def with_provider?(provider)
authorizations.map(&:provider).include?(provider)
end

def missing_name?
name == EMPTY_NAME
end

class DuplicateNickname < StandardError
attr_reader :nickname

Expand Down
2 changes: 1 addition & 1 deletion app/views/application/_nav.slim
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ nav.navbar.sticky-top.navbar-expand-lg#nav
.dropdown-menu.dropdown-menu-right(aria-labelledby="loginDropdown")
- login_providers.each do |provider|
= button_to(label_auth_url(provider), class: 'dropdown-item') do
= fa_icon(provider, class: 'fa-fw', text: t("login.#{provider}_login"))
= fa_icon(icon_for_provider(provider), class: 'fa-fw', text: t("login.#{provider}_login"))


li.nav-item.dropdown.pr-4
Expand Down
9 changes: 9 additions & 0 deletions app/views/sessions/email.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
section
h3= t("email_auth.header")

.d-flex.justify-content-center
= form_tag email_login_path do |form|
legend= t('email_auth.enter_email')
- email = signed_in? ? current_user.email : params[:email]
= email_field_tag :email, email, placeholder: '[email protected]', required: true
= submit_tag t('email_auth.submit'), class: 'btn-primary'
2 changes: 1 addition & 1 deletion app/views/sessions/index.slim
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ section
- login_providers.each do |provider|
li.list-group-item
= button_to(label_auth_url(provider)) do
= fa_icon(provider, class: 'fa-fw dropdown-list-icon', text: t("login.#{provider}_login"))
= fa_icon(icon_for_provider(provider), class: 'fa-fw dropdown-list-icon', text: t("login.#{provider}_login"))
11 changes: 11 additions & 0 deletions app/views/user_mailer/login_link.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= t("email_auth.body.salute") %>,

<%= t("email_auth.body.intro", label: @label_name) %>,

<%= provider_callback_url(provider: :email, token: @token, host: @label_link) %>

<%= t("email_auth.body.final_details") %>

--
<%= @label_name %>
<%= @label_link %>
2 changes: 1 addition & 1 deletion app/views/users/edit.slim
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ section
- login_providers.each do |provider|
li
= button_to label_auth_url(provider), title: t("login.#{provider}_login"), class: "btn btn-#{existing_providers.include?(provider) ? 'disabled' : 'secondary'}", disabled: existing_providers.include?(provider) do
= fa_icon provider, class: 'fa-fw dropdown-list-icon'
= fa_icon icon_for_provider(provider), class: 'fa-fw dropdown-list-icon'
=> t("login.#{provider}_login")
- if existing_providers.include? provider
= fa_icon 'check', class: 'fa-fw dropdown-list-icon'
3 changes: 2 additions & 1 deletion app/views/users/show.slim
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
.card-body
h2.card-title
= user_image(user)
= "#{user.name} (#{user.nickname})"
= "#{user_name(user)}"
= " (#{user.nickname})" unless user.hide_nickname?
small.text-muted
span>= I18n.tw("profile.freelancer") if user.freelancer?
span>= "(#{t("profile.available")})" if user.available?
Expand Down
3 changes: 3 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@

# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true

# For testing async jobs
config.active_job.queue_adapter = :test
end
3 changes: 3 additions & 0 deletions config/initializers/omniauth.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require_relative File.join(Rails.root, 'lib/omni_auth/strategies/email')
josepegea marked this conversation as resolved.
Show resolved Hide resolved

OmniAuth.config.logger = Rails.logger

Rails.application.config.middleware.use OmniAuth::Builder do
Expand All @@ -14,4 +16,5 @@
env['omniauth.strategy'].options[:client_secret] = ENV["#{name}_SECRET"]
end,
}
provider :email
end
14 changes: 14 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,17 @@ de:
previous: "<"
next: ">"
truncate: "..."
email_auth:
header: "Mit E-Mail anmelden"
enter_email: "Bitte geben Sie Ihre E-Mail-Adresse ein"
submit: "Autorisieren"
email_sent: "Eine E-Mail wurde an %{email} mit Details zum Einloggen gesendet"
invalid_email: "Bitte geben Sie eine gültige E-Mail-Adresse ein"
Copy link
Member

Choose a reason for hiding this comment

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

we do not use "Sie" but "Du" in the rest of our communication

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I speak no German (nor Polish) I'm afraid. I asked ChatGPT to do the translations hoping they would be a good starting point that could be improved by native speakers. Still, I asked again to rewrite the German texts in the colloquial form. Hoping they're not too bad!!

subject: "Anmeldung zu %{label}"
josepegea marked this conversation as resolved.
Show resolved Hide resolved
body:
salute: "Hallo"
intro: "Verwenden Sie diesen Link, um sich bei %{label} anzumelden"
good_bye: "Mit freundlichen Grüßen"
final_details: >-
Dieser Link ist nur 15 Minuten gültig.
Wenn Sie nicht auf den Link klicken können, kopieren Sie ihn einfach und fügen Sie ihn in die Adresszeile Ihres Browsers ein.
15 changes: 15 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ en:
topic_added: "Added new Topic."
topic_updated: "Updated the Topic."
add_email: "Please add an email address, so that we can get in contact!"
update_profile_details: "Please, fill in your name so that we can refer to you!"
mobile:
settings: "Settings"
back: "Back"
Expand All @@ -172,3 +173,17 @@ en:
previous: "<"
next: ">"
truncate: "..."
email_auth:
header: "Login with email"
enter_email: "Please, enter your email"
submit: "Authorize"
email_sent: "An email has been sent to %{email} with details for logging in"
invalid_email: "Please enter a valid email address"
subject: "Login to %{label}"
body:
salute: "Hi"
intro: "Use this link to log into %{label}"
good_bye: "Best"
final_details: >-
This link will be valid just for 15 minutes.
If you cannot click on the link, just copy and paste it into your browser address bar.
16 changes: 15 additions & 1 deletion config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ es:
route: "Mapa"
material: "Material"
description: "Infos"
no_location: "¡Estamos bustando sitio!"
no_location: "¡Estamos buscando sitio!"
home:
the_usergroup: "<strong>%{usergroup}</strong> es un grupo de usuarios, grupo de interés o simplemente de personas interesadas en Ruby. Contacta con nosotros en la siguiente reunión! Todo el mundo es bienvenido, incluso si no tienes mucha experiencia con Ruby."
like_to_talk: "Quieres dar una charla en el grupo, o quieres proponer un tema para una?"
Expand Down Expand Up @@ -173,3 +173,17 @@ es:
previous: "<"
next: ">"
truncate: "..."
email_auth:
header: "Acceder por Email"
enter_email: "Por favor, introduce tu email"
submit: "Autorizar"
email_sent: "Te hemos enviado un email a %{email} con los detalles para acceder"
invalid_email: "Por favor, asegúrate de que el email es correcto"
subject: "Accede a %{label}"
body:
salute: "Hola"
intro: "Usa este enlace para acceder a %{label}"
good_bye: "Saludos"
final_details: >-
Este enlace es válido sólo durante 15 minutos.
Si no puedes pulsar sobre el enlace, puedes copiarlo y pegarlo en la barra de tu navegador.
Loading
Loading