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