feat(controllers): add EmailConfirmationsController, create
This commit is contained in:
1 parent
29f97c2db4
commit
c66a866312
4 files changed
+188
No files matched your search
@@ -0,0 +1,42 @@
|
|||||||
|
class EmailConfirmationsController < ApplicationController
|
||||||
|
# Resend form - for whoever's original confirmation link expired out of
|
||||||
|
# their inbox, landed in spam, or was simply never sent because a typo in
|
||||||
|
# the signup form pointed User#generates_token_for at an address they
|
||||||
|
# don't control.
|
||||||
|
def new
|
||||||
|
render(:new)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Same enumeration-safe shape as PasswordResetsController#create: the
|
||||||
|
# response never reveals whether the address has an account here, or
|
||||||
|
# whether it is already confirmed - branching on either would turn this
|
||||||
|
# form into a one-guess-at-a-time oracle for both facts.
|
||||||
|
def create
|
||||||
|
email = params.require('email_confirmation').permit(:email).to_h['email']
|
||||||
|
user = User.find_by(email: email)
|
||||||
|
if user && !user.confirmed?
|
||||||
|
NotificationMailer.confirmation_instructions(user.id, user.generate_token_for(:email_confirmation)).deliver_later
|
||||||
|
end
|
||||||
|
flash[:notice] = t('email_confirmations.instructions_sent_notice')
|
||||||
|
redirect url_for(:new_session)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reached via a plain GET from the link in the confirmation email, like
|
||||||
|
# PasswordResetsController#edit - safe to expose over GET (and to visit
|
||||||
|
# more than once) because, unlike a password reset, confirming does not
|
||||||
|
# hand the visitor anything sensitive and does not itself change the
|
||||||
|
# fingerprint (email) the token was minted under, so a bookmarked or
|
||||||
|
# double-clicked link keeps working instead of dying after first use.
|
||||||
|
def show
|
||||||
|
user = User.find_by_token_for(:email_confirmation, params['token'])
|
||||||
|
unless user
|
||||||
|
flash[:notice] = t('email_confirmations.invalid_token_notice')
|
||||||
|
redirect url_for(:new_email_confirmation)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
user.update!(confirmed_at: Time.now.utc) unless user.confirmed?
|
||||||
|
flash[:notice] = t('email_confirmations.confirmed_notice')
|
||||||
|
redirect url_for(:root)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
class LikesController < ApplicationController
|
||||||
|
before_action :require_authentication
|
||||||
|
|
||||||
|
# Only Post/Comment may be liked - likeable_type otherwise rides in fully
|
||||||
|
# client-controlled request params, so without this whitelist a crafted
|
||||||
|
# request could aim a Like at an arbitrary model by name (e.g. User) and
|
||||||
|
# #likeable would happily resolve it.
|
||||||
|
LIKEABLE_TYPES = { 'Post' => Post, 'Comment' => Comment }.freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
likeable = find_likeable!
|
||||||
|
Like.create(user_id: current_user.id, likeable_id: likeable.id, likeable_type: likeable.class.name)
|
||||||
|
redirect url_for(:post, slug: post_slug_for(likeable))
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('likes.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
like = Like.find(params['id'])
|
||||||
|
authorize(like)
|
||||||
|
likeable = like.likeable
|
||||||
|
# really_destroy!, not the paranoid-aware plain #destroy - unliking is
|
||||||
|
# a permanent, unremarkable action with no "restore my unlike" concept
|
||||||
|
# in this app, unlike Post.acts_as_paranoid's actual trash/restore UI.
|
||||||
|
# Left as a soft delete, every unlike/relike cycle would pile up a
|
||||||
|
# dead Like row forever (uniqueness scope only sees live rows, so
|
||||||
|
# nothing would ever stop a visitor from accumulating one per cycle),
|
||||||
|
# and a post's dependent: :destroy cascade would then have no way to
|
||||||
|
# tell "trashed along with its post" apart from "explicitly unliked
|
||||||
|
# ages ago" when a restore brings the post's likes back.
|
||||||
|
like.really_destroy!
|
||||||
|
redirect url_for(:post, slug: post_slug_for(likeable))
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('likes.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_likeable!
|
||||||
|
klass = LIKEABLE_TYPES.fetch(params['likeable_type']) { raise Prism::Orm::RecordNotFound }
|
||||||
|
klass.find(params['likeable_id'])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
class LoginSessionsController < ApplicationController
|
||||||
|
before_action :require_authentication
|
||||||
|
|
||||||
|
# "Your devices": every remembered browser for the signed-in user, with
|
||||||
|
# whichever one answered THIS request called out separately so the view
|
||||||
|
# can label it "this device" instead of just another row in the list.
|
||||||
|
def index
|
||||||
|
render(:index, locals: {
|
||||||
|
current_session: current_login_session,
|
||||||
|
other_sessions: other_login_sessions.order(:created_at, :desc).to_a,
|
||||||
|
notice: flash[:notice]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Revokes one remembered browser. revoke_login_session! already checks
|
||||||
|
# ownership (a foreign id, or one belonging to someone else, is a no-op),
|
||||||
|
# so there is nothing else to authorize here.
|
||||||
|
def destroy
|
||||||
|
flash[:notice] = if revoke_login_session!(params['id'])
|
||||||
|
t('login_sessions.revoked_notice')
|
||||||
|
else
|
||||||
|
t('login_sessions.not_found')
|
||||||
|
end
|
||||||
|
redirect url_for(:login_sessions)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_out_others
|
||||||
|
sign_out_all_other_sessions
|
||||||
|
flash[:notice] = t('login_sessions.signed_out_others_notice')
|
||||||
|
redirect url_for(:login_sessions)
|
||||||
|
end
|
||||||
|
|
||||||
|
# "TestBrowser/1.0 from 203.0.113.7" - falls back to the generic
|
||||||
|
# "unknown browser"/"unknown address" strings for rows remembered before
|
||||||
|
# this metadata started being captured (a plain nil column, not a bug).
|
||||||
|
def device_detail_for(login_session)
|
||||||
|
t('login_sessions.device_detail',
|
||||||
|
user_agent: login_session.user_agent || t('login_sessions.unknown_user_agent'),
|
||||||
|
ip_address: login_session.ip_address || t('login_sessions.unknown_ip_address'))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
class PasswordResetsController < ApplicationController
|
||||||
|
def new
|
||||||
|
render(:new)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Always shows the SAME generic "check your email" notice whether or not
|
||||||
|
# the address belongs to an account - branching the response on
|
||||||
|
# User.find_by(email:) here would turn this form into an email-enumeration
|
||||||
|
# oracle one guess at a time, the same concern app.rb's signups/ip
|
||||||
|
# throttle exists for one step earlier in the account lifecycle (and this
|
||||||
|
# action gets its own password_resets/ip throttle below for the same
|
||||||
|
# reason: even a constant-response endpoint still leaks timing/rate
|
||||||
|
# information if it can be hit without limit).
|
||||||
|
def create
|
||||||
|
email = params.require('password_reset').permit(:email).to_h['email']
|
||||||
|
user = User.find_by(email: email)
|
||||||
|
if user
|
||||||
|
token = user.generate_token_for(:password_reset)
|
||||||
|
NotificationMailer.password_reset(user.id, token).deliver_later
|
||||||
|
end
|
||||||
|
flash[:notice] = t('password_resets.instructions_sent_notice')
|
||||||
|
redirect url_for(:new_session)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
user = User.find_by_token_for(:password_reset, params['token'])
|
||||||
|
if user
|
||||||
|
render(:edit, locals: { token: params['token'], errors: [] })
|
||||||
|
else
|
||||||
|
flash[:notice] = t('password_resets.invalid_token_notice')
|
||||||
|
redirect url_for(:new_password_reset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# find_by_token_for's fingerprint block (see User#generates_token_for)
|
||||||
|
# snapshots password_digest at #new/#create time - so the very save
|
||||||
|
# below that sets the new password also produces a new digest, which
|
||||||
|
# means the SAME token this action just accepted can never verify again
|
||||||
|
# afterwards. A stolen-but-unused link still dies once its owner resets
|
||||||
|
# through it; there is no separate "used" flag or token table to keep
|
||||||
|
# that guarantee true.
|
||||||
|
def update
|
||||||
|
user = User.find_by_token_for(:password_reset, params['token'])
|
||||||
|
unless user
|
||||||
|
flash[:notice] = t('password_resets.invalid_token_notice')
|
||||||
|
redirect url_for(:new_password_reset)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs = params.require('password_reset').permit(:password, :password_confirmation).to_h
|
||||||
|
user.password = attrs['password']
|
||||||
|
user.password_confirmation = attrs['password_confirmation']
|
||||||
|
|
||||||
|
if user.save
|
||||||
|
flash[:notice] = t('password_resets.reset_notice')
|
||||||
|
redirect url_for(:new_session)
|
||||||
|
else
|
||||||
|
status(422)
|
||||||
|
render(:edit, locals: { token: params['token'], errors: user.errors.full_messages })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user