Added new security question to sign up, Added notification for unconfirmed emails

• Added:
- new security question to sign up
- notification for unconfirmed emails
- modal for describing issue with Gab emails
This commit is contained in:
mgabdev 2020-10-16 16:25:37 -05:00
parent 9c0fc47777
commit 3c07e9d63b
16 changed files with 147 additions and 6 deletions

View File

@ -74,8 +74,9 @@ class Api::BaseController < ApplicationController
render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
# : todo : when figure out email/catpcha, put this back
# elsif !current_user.confirmed?
# render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
else

View File

@ -18,6 +18,27 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
render json: @account, serializer: REST::CredentialAccountSerializer
end
def resend_email_confirmation
@account = current_account
if !@account.user.confirmed?
redisResult = Redis.current.get("account:#{@account.id}:last_email_confirmation_resend") || 0
@lastSentDate = redisResult
if redisResult != 0
@lastSentDate = Time.at(redisResult.to_i).utc
end
if @lastSentDate == 0 || (@lastSentDate != 0 && Time.now.utc - @lastSentDate >= 1.hour)
@user = Account.find(@account.id).user || raise(ActiveRecord::RecordNotFound)
Redis.current.set("account:#{@account.id}:last_email_confirmation_resend", Time.now.utc.to_i)
@user.resend_confirmation_instructions
end
end
render json: { success: true, message: 'ok' }
end
private
def account_params
@ -35,4 +56,5 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
'setting_default_language' => source_params.fetch(:language, @account.user.setting_default_language),
}
end
end

View File

@ -4,6 +4,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
layout :determine_layout
before_action :set_invite, only: [:new, :create]
before_action :set_challenge, only: [:new]
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
@ -15,6 +16,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super(&:build_invite_request)
end
def create
if session[:challenge_answer].to_s == params[:user][:challenge].to_s.strip
# Reset after, may be errors to return and this ensures its still visible
set_challenge
super
else
return false
end
end
def destroy
not_found
end
@ -96,6 +107,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
@invite = invite&.valid_for_use? ? invite : nil
end
def set_challenge
@challenge_add_1 = rand(0...9)
@challenge_add_2 = rand(0...9)
session[:challenge_answer] = @challenge_add_1 + @challenge_add_2
end
def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth'
end

View File

@ -1,11 +1,15 @@
import isObject from 'lodash.isobject'
import api from '../api'
import { me } from '../initial_state'
import {
me,
emailConfirmed,
} from '../initial_state'
import { importFetchedAccount } from './importer'
export const SAVE_USER_PROFILE_INFORMATION_FETCH_REQUEST = 'SAVE_USER_PROFILE_INFORMATION_FETCH_REQUEST'
export const SAVE_USER_PROFILE_INFORMATION_FETCH_SUCCESS = 'SAVE_USER_PROFILE_INFORMATION_FETCH_SUCCESS'
export const SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL = 'SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL'
export const RESEND_USER_CONFIRMATION_EMAIL_SUCCESS = 'RESEND_USER_CONFIRMATION_EMAIL_SUCCESS'
export const saveUserProfileInformation = (data) => {
return function (dispatch, getState) {
@ -51,4 +55,13 @@ function saveUserProfileInformationFail(error) {
type: SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL,
error,
}
}
export const resendUserConfirmationEmail = () => (dispatch, getState) => {
if (!me || emailConfirmed) return
api(getState).post('/api/v1/accounts/resend_email_confirmation').then((response) => {
dispatch({ type: RESEND_USER_CONFIRMATION_EMAIL_SUCCESS })
})
}

View File

@ -0,0 +1,54 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
me,
meUsername,
} from '../../initial_state'
import Button from '../button'
import Text from '../text'
import ModalLayout from './modal_layout'
class EmailConfirmationReminderModal extends React.PureComponent {
render() {
const { onClose } = this.props
return (
<ModalLayout
title='Having Email Confirmation Issues?'
width={480}
onClose={onClose}
>
<Text size='medium' weight='medium' className={_s.mb10}>
Many email providers block Gabs emails.
</Text>
<Text size='medium' color='secondary'>
Please check your spam folder for the confirmation email. If you still do not see an email please reach out to us for help.
</Text>
<div className={[_s.d, _s.flexRow, _s.pt15, _s.pb10].join(' ')}>
<Button
isOutline
color='brand'
backgroundColor='none'
href={`mailto:support@gab.com?subject=Please%20confirm%20my%20email%20(${me})&body=My%20username%20is:%20${meUsername}%20and%20account%20id%20is:%20${me}`}
className={[_s.flexRow, _s.aiCenter, _s.jcCenter, _s.mr10].join(' ')}
>
<Text color='inherit' weight='medium' align='center'>
Email Gab Support
</Text>
</Button>
</div>
</ModalLayout>
)
}
}
EmailConfirmationReminderModal.propTypes = {
onClose: PropTypes.func.isRequired,
}
export default EmailConfirmationReminderModal

View File

@ -16,6 +16,7 @@ import {
MODAL_DISPLAY_OPTIONS,
MODAL_EDIT_PROFILE,
MODAL_EDIT_SHORTCUTS,
MODAL_EMAIL_CONFIRMATION_REMINDER,
MODAL_EMBED,
MODAL_GROUP_CREATE,
MODAL_GROUP_DELETE,
@ -48,6 +49,7 @@ import {
DisplayOptionsModal,
EditProfileModal,
EditShortcutsModal,
EmailConfirmationReminderModal,
EmbedModal,
GroupCreateModal,
GroupDeleteModal,
@ -83,6 +85,7 @@ MODAL_COMPONENTS[MODAL_CONFIRM] = ConfirmationModal
MODAL_COMPONENTS[MODAL_DISPLAY_OPTIONS] = DisplayOptionsModal
MODAL_COMPONENTS[MODAL_EDIT_SHORTCUTS] = EditShortcutsModal
MODAL_COMPONENTS[MODAL_EDIT_PROFILE] = EditProfileModal
MODAL_COMPONENTS[MODAL_EMAIL_CONFIRMATION_REMINDER] = EmailConfirmationReminderModal
MODAL_COMPONENTS[MODAL_EMBED] = EmbedModal
MODAL_COMPONENTS[MODAL_GROUP_CREATE] = GroupCreateModal
MODAL_COMPONENTS[MODAL_GROUP_DELETE] = GroupDeleteModal

View File

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
CX,
BREAKPOINT_SMALL,
} from '../../constants'
import { emailConfirmed } from '../../initial_state'
import Button from '../button'
import { openModal } from '../../actions/modal'
import Responsive from '../../features/ui/util/responsive_component'
@ -26,10 +28,18 @@ class SidebarLayout extends React.PureComponent {
children,
} = this.props
const innerContainerClasses = CX({
d: 1,
posFixed: 1,
calcH53PX: emailConfirmed,
calcH106PX: !emailConfirmed,
bottom0: 1,
})
return (
<header role='banner' className={[_s.d, _s.flexGrow1, _s.z3, _s.aiEnd].join(' ')}>
<div className={[_s.d, _s.w240PX].join(' ')}>
<div className={[_s.d, _s.posFixed, _s.calcH53PX, _s.bottom0].join(' ')}>
<div className={innerContainerClasses}>
<div className={[_s.d, _s.h100PC, _s.aiStart, _s.w240PX, _s.pr15, _s.py10, _s.noScrollbar, _s.overflowYScroll].join(' ')}>
<div className={[_s.d, _s.w100PC].join(' ')}>
{

View File

@ -46,6 +46,7 @@ export const MODAL_CONFIRM = 'CONFIRM'
export const MODAL_DISPLAY_OPTIONS = 'DISPLAY_OPTIONS'
export const MODAL_EDIT_PROFILE = 'EDIT_PROFILE'
export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
export const MODAL_EMAIL_CONFIRMATION_REMINDER = 'EMAIL_CONFIRMATION_REMINDER'
export const MODAL_EMBED = 'EMBED'
export const MODAL_GROUP_CREATE = 'GROUP_CREATE'
export const MODAL_GROUP_DELETE = 'GROUP_DELETE'

View File

@ -26,5 +26,7 @@ export const lastReadNotificationId = getMeta('last_read_notification_id');
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
export const favouritesCount = getMeta('favourites_count');
export const isFirstSession = getMeta('is_first_session');
export const emailConfirmed = getMeta('email_confirmed');
export const meEmail = getMeta('email');
export default initialState;

View File

@ -2,12 +2,14 @@ import {
SAVE_USER_PROFILE_INFORMATION_FETCH_REQUEST,
SAVE_USER_PROFILE_INFORMATION_FETCH_SUCCESS,
SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL,
RESEND_USER_CONFIRMATION_EMAIL_SUCCESS,
} from '../actions/user'
import { Map as ImmutableMap } from 'immutable'
const initialState = ImmutableMap({
isLoading: false,
isError: false,
emailConfirmationResends: 0,
})
export default function (state = initialState, action) {
@ -18,6 +20,8 @@ export default function (state = initialState, action) {
return state
case SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL:
return state.set('isError', true)
case RESEND_USER_CONFIRMATION_EMAIL_SUCCESS:
return state.set('emailConfirmationResends', state.get('emailConfirmationResends') + 1)
default:
return state
}

View File

@ -443,6 +443,7 @@ pre {
.topNeg50PX { top: -50px; }
.top0 { top: 0; }
.top120PX { top: 120px; }
.top53PX { top: 53px; }
.top60PC { top: 60%; }
.top50PC { top: 50%; }
@ -478,12 +479,15 @@ pre {
.maxH340PX { max-height: 340px; }
.maxH200PX { max-height: 200px; }
.maxH56PX { max-height: 56px; }
.maxH56PX { max-height: 42px; }
.calcH106PX { height: calc(100vh - 106px); }
.calcH53PX { height: calc(100vh - 53px); }
.calcH80VH106PX { height: calc(80vh - 106px); }
.minH100VH { min-height: 100vh; }
.minH50VH { min-height: 50vh; }
.minH200PX { min-height: 200px; }
.minH106PX { min-height: 106px; }
.minH98PX { min-height: 98px; }
.minH80PX { min-height: 80px; }
.minH58PX { min-height: 58px; }

View File

@ -253,6 +253,10 @@ class User < ApplicationRecord
@invite_code = code
end
def challenge
#
end
def password_required?
return false if Devise.pam_authentication || Devise.ldap_authentication
super

View File

@ -39,6 +39,8 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0
store[:favourites_count] = object.current_account.favourites.count.to_s
store[:is_first_session] = is_first_session object.current_account
store[:email_confirmed] = object.current_account.user.confirmed?
store[:email] = object.current_account.user.confirmed? ? '[hidden]' : object.current_account.user.email
end
store

View File

@ -34,6 +34,9 @@
= f.input :invite_code, as: :hidden
.fields-group
= f.input :challenge, wrapper: :with_label, label: "Are you human? What is #{@challenge_add_1} + #{@challenge_add_2} = ", required: true, input_html: { 'aria-label' => "Are you human? What is #{@challenge_add_1} + #{@challenge_add_2}", :autocomplete => 'off' }
.fields-group-agreement
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', about_tos_path: about_tos_path)

View File

@ -190,7 +190,7 @@ Devise.setup do |config|
# able to access the website for two days without confirming their account,
# access will be blocked just in the third day. Default is 0.days, meaning
# the user cannot access the website without confirming their account.
# config.allow_unconfirmed_access_for = 2.days
config.allow_unconfirmed_access_for = 10.years
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 3.days, the user can confirm
@ -198,7 +198,7 @@ Devise.setup do |config|
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 2.days
config.confirm_within = 12.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email

View File

@ -405,6 +405,7 @@ Rails.application.routes.draw do
namespace :accounts do
get :verify_credentials, to: 'credentials#show'
patch :update_credentials, to: 'credentials#update'
post :resend_email_confirmation, to: 'credentials#resend_email_confirmation'
resource :search, only: :show, controller: :search
resources :relationships, only: :index
end