mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 03:07:52 +01:00
Add default OAuths (#259)
This commit is contained in:
committed by
GitHub
parent
0828c9c879
commit
653e139a9e
@@ -208,4 +208,19 @@
|
||||
text-decoration: none !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.oauthProviderBtn {
|
||||
@extend
|
||||
.btn,
|
||||
.btn-block,
|
||||
.btn-outline-dark,
|
||||
.mt-2,
|
||||
.mb-2;
|
||||
|
||||
.oauthProviderText {
|
||||
@extend .ml-2;
|
||||
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def load_oauths
|
||||
@o_auths = Current.tenant_or_raise!.o_auths
|
||||
@o_auths = OAuth
|
||||
.include_defaults
|
||||
.where(is_enabled: true)
|
||||
.order(created_at: :asc)
|
||||
end
|
||||
|
||||
@@ -7,15 +7,17 @@ class OAuthsController < ApplicationController
|
||||
|
||||
TOKEN_STATE_SEPARATOR = '-'
|
||||
|
||||
# [subdomain.]base_url/o_auths/:id/start?reason=user|test
|
||||
# [subdomain.]base_url/o_auths/:id/start?reason=login|test|tenantsignup
|
||||
# Generates authorize url with required parameters and redirects to provider
|
||||
def start
|
||||
@o_auth = OAuth.find(params[:id])
|
||||
return if params[:reason] == 'user' and not @o_auth.is_enabled?
|
||||
@o_auth = OAuth.unscoped.include_defaults.find(params[:id])
|
||||
|
||||
return if params[:reason] != 'test' and not @o_auth.is_enabled?
|
||||
|
||||
# Generate random state + other query params
|
||||
token_state = "#{params[:reason]}#{TOKEN_STATE_SEPARATOR}#{Devise.friendly_token(30)}"
|
||||
session[:token_state] = token_state
|
||||
tenant_domain = Current.tenant ? Current.tenant_or_raise!.subdomain : "null"
|
||||
token_state = "#{params[:reason]}#{TOKEN_STATE_SEPARATOR}#{tenant_domain}#{TOKEN_STATE_SEPARATOR}#{Devise.friendly_token(30)}"
|
||||
cookies[:token_state] = { value: token_state, domain: ".#{request.domain}", httponly: true }
|
||||
@o_auth.state = token_state
|
||||
|
||||
redirect_to @o_auth.authorize_url_with_query_params
|
||||
@@ -24,32 +26,42 @@ class OAuthsController < ApplicationController
|
||||
# [subdomain.]base_url/o_auths/:id/callback
|
||||
# Exchange authorization code for access token, fetch user info and sign in/up
|
||||
def callback
|
||||
reason, token_state = params[:state].split(TOKEN_STATE_SEPARATOR, 2)
|
||||
reason, tenant_domain, token_state = params[:state].split(TOKEN_STATE_SEPARATOR, 3)
|
||||
|
||||
return unless session[:token_state] == params[:state]
|
||||
return unless cookies[:token_state] == params[:state]
|
||||
cookies.delete(:token_state, domain: ".#{request.domain}")
|
||||
|
||||
@o_auth = OAuth.find(params[:id])
|
||||
@o_auth = OAuth.unscoped.include_defaults.find(params[:id])
|
||||
|
||||
return if reason != 'test' and not @o_auth.is_enabled?
|
||||
|
||||
# If it is a default OAuth we need to set the tenant
|
||||
if @o_auth.is_default?
|
||||
Current.tenant = Tenant.find_by(subdomain: tenant_domain)
|
||||
end
|
||||
|
||||
user_profile = OAuthExchangeAuthCodeForProfile.new(
|
||||
authorization_code: params[:code],
|
||||
o_auth: @o_auth
|
||||
).run
|
||||
|
||||
if reason == 'user'
|
||||
if reason == 'login'
|
||||
|
||||
user = OAuthSignInUser.new(
|
||||
user_profile: user_profile,
|
||||
o_auth: @o_auth
|
||||
).run
|
||||
|
||||
if user
|
||||
sign_in user
|
||||
flash[:notice] = I18n.t('devise.sessions.signed_in')
|
||||
redirect_to root_path
|
||||
oauth_token = user.generate_oauth_token
|
||||
redirect_to add_subdomain_to(method(:o_auth_sign_in_from_oauth_token_url), nil, {user_id: user.id, token: oauth_token})
|
||||
else
|
||||
flash[:alert] = I18n.t('errors.o_auth_login_error', name: @o_auth.name)
|
||||
redirect_to new_user_session_path
|
||||
redirect_to add_subdomain_to(method(:new_user_session_url))
|
||||
end
|
||||
|
||||
elsif reason == 'test'
|
||||
|
||||
unless user_signed_in? and current_user.admin?
|
||||
flash[:alert] = I18n.t('errors.unauthorized')
|
||||
redirect_to root_url
|
||||
@@ -57,15 +69,53 @@ class OAuthsController < ApplicationController
|
||||
end
|
||||
|
||||
@user_profile = user_profile
|
||||
@user_email = query_path_from_hash(user_profile, @o_auth.json_user_email_path)
|
||||
@user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path)
|
||||
@email_valid = URI::MailTo::EMAIL_REGEXP.match?(@user_email)
|
||||
@user_name = query_path_from_hash(user_profile, @o_auth.json_user_name_path)
|
||||
@name_valid = !@user_name.nil?
|
||||
if not @o_auth.json_user_name_path.blank?
|
||||
@user_name = query_path_from_object(user_profile, @o_auth.json_user_name_path)
|
||||
@name_valid = !@user_name.nil?
|
||||
end
|
||||
|
||||
render 'o_auths/test', layout: false
|
||||
|
||||
elsif reason == 'tenantsignup'
|
||||
|
||||
@o_auths = []
|
||||
@user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path)
|
||||
if not @o_auth.json_user_name_path.blank?
|
||||
@user_name = query_path_from_object(user_profile, @o_auth.json_user_name_path)
|
||||
end
|
||||
@o_auth_login_completed = true
|
||||
|
||||
session[:o_auth_sign_up] = "#{@user_email},#{@user_name}"
|
||||
|
||||
render 'tenants/new'
|
||||
|
||||
else
|
||||
|
||||
flash[:alert] = I18n.t('errors.unknown')
|
||||
redirect_to root_url
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# [subdomain.]base_url/o_auths/sign_in_from_oauth_token?user_id=<id>&token=<token>
|
||||
# Used for OAuth with reason 'login'
|
||||
# It has been introduced because of default OAuth providers,
|
||||
# since they must redirect to a common domain for all tenants.
|
||||
def sign_in_from_oauth_token
|
||||
return unless params[:user_id] and params[:token]
|
||||
|
||||
user = User.find(params[:user_id])
|
||||
|
||||
if user.oauth_token == params[:token]
|
||||
sign_in user
|
||||
user.invalidate_oauth_token
|
||||
flash[:notice] = I18n.t('devise.sessions.signed_in')
|
||||
redirect_to root_path
|
||||
else
|
||||
flash[:alert] = I18n.t('errors.o_auth_login_error', name: @o_auth.name)
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
|
||||
@@ -74,7 +124,7 @@ class OAuthsController < ApplicationController
|
||||
def index
|
||||
authorize OAuth
|
||||
|
||||
@o_auths = OAuth.order(created_at: :asc)
|
||||
@o_auths = OAuth.include_defaults.order(created_at: :asc)
|
||||
|
||||
render json: to_json_custom(@o_auths)
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ class TenantsController < ApplicationController
|
||||
|
||||
def new
|
||||
@page_title = t('signup.page_title')
|
||||
@o_auths = OAuth.unscoped.where(tenant_id: nil)
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -16,17 +17,34 @@ class TenantsController < ApplicationController
|
||||
@tenant.assign_attributes(tenant_create_params)
|
||||
authorize @tenant
|
||||
|
||||
is_o_auth_login = params[:settings][:is_o_auth_login]
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
if is_o_auth_login
|
||||
# Check if OAuth email and username coincide with submitted ones
|
||||
# (session[:o_auth_sign_up] set in oauth#callback)
|
||||
email, username = session[:o_auth_sign_up].split(",", 2)
|
||||
raise "Mismatching email in OAuth login" unless email == params[:user][:email]
|
||||
|
||||
@tenant.status = "active" # no need to verify email address if logged in with oauth
|
||||
end
|
||||
|
||||
@tenant.save!
|
||||
Current.tenant = @tenant
|
||||
|
||||
@user = User.create!(
|
||||
full_name: params[:user][:full_name],
|
||||
|
||||
@user = User.new(
|
||||
full_name: params[:user][:full_name] || I18n.t('defaults.user_full_name'),
|
||||
email: params[:user][:email],
|
||||
password: params[:user][:password],
|
||||
password: is_o_auth_login ? Devise.friendly_token : params[:user][:password],
|
||||
role: "owner"
|
||||
)
|
||||
|
||||
|
||||
if is_o_auth_login
|
||||
@user.skip_confirmation
|
||||
end
|
||||
|
||||
@user.save!
|
||||
|
||||
render json: @tenant, status: :created
|
||||
|
||||
rescue ActiveRecord::RecordInvalid => exception
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
module OAuthsHelper
|
||||
def query_path_from_hash(hash, path)
|
||||
return nil unless hash.class == Hash and path.class == String
|
||||
|
||||
path_array = path
|
||||
.split(Regexp.union([ '.', '[', ']' ])) # split by . and []
|
||||
.filter { |v| not v.blank? } # remove possible blank values
|
||||
.map do |v| # convert indexes to integer
|
||||
if v == "0"
|
||||
0
|
||||
elsif v.to_i == 0
|
||||
v
|
||||
else
|
||||
v.to_i
|
||||
# Retrieves a value from a hash/array using a given path.
|
||||
# obj [Hash, Array] The object or hash to query.
|
||||
# path [String] The path to the desired value, using dot notation for nested keys and square brackets for array indexes.
|
||||
# returns [Object, nil] The value at the specified path, or nil if the path is invalid or the value is not found.
|
||||
def query_path_from_object(obj, path)
|
||||
return nil unless obj.class == Hash or obj.class == Array
|
||||
return nil unless path.class == String
|
||||
begin
|
||||
path_array = path
|
||||
.split(Regexp.union([ '.', '[', ']' ])) # split by . and []
|
||||
.filter { |v| not v.blank? } # remove possible blank values
|
||||
.map do |v| # convert indexes to integer
|
||||
if v == "0"
|
||||
0
|
||||
elsif v.to_i == 0
|
||||
v
|
||||
else
|
||||
v.to_i
|
||||
end
|
||||
end
|
||||
|
||||
path_array.each do |selector|
|
||||
break if obj == nil
|
||||
|
||||
obj = obj[selector]
|
||||
end
|
||||
|
||||
path_array.each do |selector|
|
||||
break if hash == nil
|
||||
|
||||
hash = hash[selector]
|
||||
obj
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
end
|
||||
@@ -49,6 +49,7 @@ export const submitTenant = (
|
||||
userPassword: string,
|
||||
siteName: string,
|
||||
subdomain: string,
|
||||
isOAuthLogin: boolean,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(tenantSubmitStart());
|
||||
@@ -67,6 +68,9 @@ export const submitTenant = (
|
||||
site_name: siteName,
|
||||
subdomain: subdomain,
|
||||
},
|
||||
settings: {
|
||||
is_o_auth_login: isOAuthLogin,
|
||||
}
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
@@ -3,11 +3,11 @@ import I18n from 'i18n-js';
|
||||
|
||||
import { IOAuth } from '../../../interfaces/IOAuth';
|
||||
import Switch from '../../common/Switch';
|
||||
import Separator from '../../common/Separator';
|
||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||
import CopyToClipboardButton from '../../common/CopyToClipboardButton';
|
||||
import ActionLink from '../../common/ActionLink';
|
||||
import { DeleteIcon, EditIcon, TestIcon } from '../../common/Icons';
|
||||
import { MutedText } from '../../common/CustomTexts';
|
||||
|
||||
interface Props {
|
||||
oAuth: IOAuth;
|
||||
@@ -30,52 +30,60 @@ const OAuthProviderItem = ({
|
||||
|
||||
<div className="oAuthNameAndEnabled">
|
||||
<span className="oAuthName">{oAuth.name}</span>
|
||||
<div className="oAuthIsEnabled">
|
||||
<Switch
|
||||
label={I18n.t(`common.${oAuth.isEnabled ? 'enabled' : 'disabled'}`)}
|
||||
onClick={() => handleToggleEnabledOAuth(oAuth.id, !oAuth.isEnabled)}
|
||||
checked={oAuth.isEnabled}
|
||||
htmlId={`oAuth${oAuth.name}EnabledSwitch`}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
oAuth.tenantId ?
|
||||
<div className="oAuthIsEnabled">
|
||||
<Switch
|
||||
label={I18n.t(`common.${oAuth.isEnabled ? 'enabled' : 'disabled'}`)}
|
||||
onClick={() => handleToggleEnabledOAuth(oAuth.id, !oAuth.isEnabled)}
|
||||
checked={oAuth.isEnabled}
|
||||
htmlId={`oAuth${oAuth.name}EnabledSwitch`}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<div><MutedText>{I18n.t('site_settings.authentication.default_oauth')}</MutedText></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="oAuthActions">
|
||||
<CopyToClipboardButton
|
||||
label={I18n.t('site_settings.authentication.copy_url')}
|
||||
textToCopy={oAuth.callbackUrl}
|
||||
/>
|
||||
|
||||
<ActionLink
|
||||
onClick={() =>
|
||||
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
|
||||
}
|
||||
icon={<TestIcon />}
|
||||
customClass='testAction'
|
||||
>
|
||||
{I18n.t('common.buttons.test')}
|
||||
</ActionLink>
|
||||
|
||||
<ActionLink
|
||||
onClick={() => {
|
||||
setSelectedOAuth(oAuth.id);
|
||||
setPage('edit');
|
||||
}}
|
||||
icon={<EditIcon />}
|
||||
customClass='editAction'
|
||||
>
|
||||
{I18n.t('common.buttons.edit')}
|
||||
</ActionLink>
|
||||
|
||||
<ActionLink
|
||||
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}
|
||||
icon={<DeleteIcon />}
|
||||
customClass='deleteAction'
|
||||
>
|
||||
{I18n.t('common.buttons.delete')}
|
||||
</ActionLink>
|
||||
</div>
|
||||
{
|
||||
oAuth.tenantId &&
|
||||
<div className="oAuthActions">
|
||||
<CopyToClipboardButton
|
||||
label={I18n.t('site_settings.authentication.copy_url')}
|
||||
textToCopy={oAuth.callbackUrl}
|
||||
/>
|
||||
|
||||
<ActionLink
|
||||
onClick={() =>
|
||||
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
|
||||
}
|
||||
icon={<TestIcon />}
|
||||
customClass='testAction'
|
||||
>
|
||||
{I18n.t('common.buttons.test')}
|
||||
</ActionLink>
|
||||
|
||||
<ActionLink
|
||||
onClick={() => {
|
||||
setSelectedOAuth(oAuth.id);
|
||||
setPage('edit');
|
||||
}}
|
||||
icon={<EditIcon />}
|
||||
customClass='editAction'
|
||||
>
|
||||
{I18n.t('common.buttons.edit')}
|
||||
</ActionLink>
|
||||
|
||||
<ActionLink
|
||||
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}
|
||||
icon={<DeleteIcon />}
|
||||
customClass='deleteAction'
|
||||
>
|
||||
{I18n.t('common.buttons.delete')}
|
||||
</ActionLink>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
);
|
||||
|
||||
|
||||
@@ -47,9 +47,13 @@ const TenantSignUpForm = ({
|
||||
<input
|
||||
{...register('subdomain', {
|
||||
required: true,
|
||||
validate: async (newSubdomain) => {
|
||||
const res = await fetch(`/is_available?new_subdomain=${newSubdomain}`);
|
||||
return res.status === HttpStatus.OK;
|
||||
pattern: /^[a-zA-Z0-9-]+$/,
|
||||
validate: {
|
||||
noSpaces: (value) => !/\s/.test(value),
|
||||
notAlreadyTaken: async (newSubdomain) => {
|
||||
const res = await fetch(`/is_available?new_subdomain=${newSubdomain}`);
|
||||
return res.status === HttpStatus.OK;
|
||||
},
|
||||
},
|
||||
})}
|
||||
placeholder={getLabel('tenant', 'subdomain')}
|
||||
@@ -64,7 +68,10 @@ const TenantSignUpForm = ({
|
||||
{errors.subdomain?.type === 'required' && getValidationMessage('required', 'tenant', 'subdomain')}
|
||||
</DangerText>
|
||||
<DangerText>
|
||||
{errors.subdomain?.type === 'validate' && I18n.t('signup.step2.validations.subdomain_already_taken')}
|
||||
{errors.subdomain?.type === 'pattern' && I18n.t('signup.step2.validations.subdomain_only_letters_and_numbers')}
|
||||
</DangerText>
|
||||
<DangerText>
|
||||
{errors.subdomain?.type === 'notAlreadyTaken' && I18n.t('signup.step2.validations.subdomain_already_taken')}
|
||||
</DangerText>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,8 +5,14 @@ import ConfirmSignUpPage from './ConfirmSignUpPage';
|
||||
|
||||
import TenantSignUpForm from './TenantSignUpForm';
|
||||
import UserSignUpForm from './UserSignUpForm';
|
||||
import { IOAuth } from '../../interfaces/IOAuth';
|
||||
|
||||
interface Props {
|
||||
oAuthLoginCompleted: boolean;
|
||||
oauthUserEmail?: string;
|
||||
oauthUserName?: string;
|
||||
oAuths: Array<IOAuth>;
|
||||
|
||||
isSubmitting: boolean;
|
||||
error: string;
|
||||
|
||||
@@ -16,9 +22,11 @@ interface Props {
|
||||
userPassword: string,
|
||||
siteName: string,
|
||||
subdomain: string,
|
||||
isOAuthLogin: boolean,
|
||||
authenticityToken: string,
|
||||
): Promise<any>;
|
||||
|
||||
baseUrl: string;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
@@ -35,9 +43,14 @@ export interface ITenantSignUpTenantForm {
|
||||
}
|
||||
|
||||
const TenantSignUpP = ({
|
||||
oAuths,
|
||||
oAuthLoginCompleted,
|
||||
oauthUserEmail,
|
||||
oauthUserName,
|
||||
isSubmitting,
|
||||
error,
|
||||
handleSubmit,
|
||||
baseUrl,
|
||||
authenticityToken
|
||||
}: Props) => {
|
||||
const [userData, setUserData] = useState({
|
||||
@@ -52,19 +65,27 @@ const TenantSignUpP = ({
|
||||
subdomain: '',
|
||||
});
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [currentStep, setCurrentStep] = useState(oAuthLoginCompleted ? 2 : 1);
|
||||
|
||||
const [emailAuth, setEmailAuth] = useState(false);
|
||||
|
||||
const handleSignUpSubmit = (siteName: string, subdomain: string) => {
|
||||
handleSubmit(
|
||||
userData.fullName,
|
||||
userData.email,
|
||||
oAuthLoginCompleted ? oauthUserName : userData.fullName,
|
||||
oAuthLoginCompleted ? oauthUserEmail : userData.email,
|
||||
userData.password,
|
||||
siteName,
|
||||
subdomain,
|
||||
oAuthLoginCompleted,
|
||||
authenticityToken,
|
||||
).then(res => {
|
||||
if (res?.status !== HttpStatus.Created) return;
|
||||
if (oAuthLoginCompleted) {
|
||||
let redirectUrl = new URL(baseUrl);
|
||||
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
|
||||
window.location.href = `${redirectUrl.toString()}users/sign_in`;
|
||||
return;
|
||||
}
|
||||
|
||||
setTenantData({ siteName, subdomain });
|
||||
setCurrentStep(currentStep + 1);
|
||||
@@ -80,6 +101,10 @@ const TenantSignUpP = ({
|
||||
setCurrentStep={setCurrentStep}
|
||||
emailAuth={emailAuth}
|
||||
setEmailAuth={setEmailAuth}
|
||||
oAuths={oAuths}
|
||||
oAuthLoginCompleted={oAuthLoginCompleted}
|
||||
oauthUserEmail={oauthUserEmail}
|
||||
oauthUserName={oauthUserName}
|
||||
userData={userData}
|
||||
setUserData={setUserData}
|
||||
/>
|
||||
|
||||
@@ -4,16 +4,24 @@ import I18n from 'i18n-js';
|
||||
|
||||
import Box from '../common/Box';
|
||||
import Button from '../common/Button';
|
||||
import OAuthProviderLink from '../common/OAuthProviderLink';
|
||||
import { ITenantSignUpUserForm } from './TenantSignUpP';
|
||||
import { DangerText } from '../common/CustomTexts';
|
||||
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
||||
import { EMAIL_REGEX } from '../../constants/regex';
|
||||
import { IOAuth } from '../../interfaces/IOAuth';
|
||||
import ActionLink from '../common/ActionLink';
|
||||
import { BackIcon } from '../common/Icons';
|
||||
|
||||
interface Props {
|
||||
currentStep: number;
|
||||
setCurrentStep(step: number): void;
|
||||
emailAuth: boolean;
|
||||
setEmailAuth(enabled: boolean): void;
|
||||
oAuths: Array<IOAuth>;
|
||||
oAuthLoginCompleted: boolean;
|
||||
oauthUserEmail?: string;
|
||||
oauthUserName?: string;
|
||||
userData: ITenantSignUpUserForm;
|
||||
setUserData({}: ITenantSignUpUserForm): void;
|
||||
}
|
||||
@@ -23,6 +31,10 @@ const UserSignUpForm = ({
|
||||
setCurrentStep,
|
||||
emailAuth,
|
||||
setEmailAuth,
|
||||
oAuths,
|
||||
oAuthLoginCompleted,
|
||||
oauthUserEmail,
|
||||
oauthUserName,
|
||||
userData,
|
||||
setUserData,
|
||||
}: Props) => {
|
||||
@@ -49,14 +61,37 @@ const UserSignUpForm = ({
|
||||
|
||||
{
|
||||
currentStep === 1 && !emailAuth &&
|
||||
<>
|
||||
<Button className="emailAuth" onClick={() => setEmailAuth(true)}>
|
||||
{ I18n.t('signup.step1.email_auth') }
|
||||
</Button>
|
||||
|
||||
{
|
||||
oAuths.filter(oAuth => oAuth.isEnabled).map((oAuth, i) =>
|
||||
<OAuthProviderLink
|
||||
oAuthId={oAuth.id}
|
||||
oAuthName={oAuth.name}
|
||||
oAuthLogo={oAuth.logo}
|
||||
oAuthReason='tenantsignup'
|
||||
isSignUp
|
||||
key={i}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
currentStep === 1 && emailAuth &&
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ActionLink
|
||||
onClick={() => setEmailAuth(false)}
|
||||
icon={<BackIcon />}
|
||||
customClass="backButton"
|
||||
>
|
||||
{I18n.t('common.buttons.back')}
|
||||
</ActionLink>
|
||||
|
||||
<div className="formRow">
|
||||
<input
|
||||
{...register('fullName', { required: true, minLength: 2 })}
|
||||
@@ -116,9 +151,14 @@ const UserSignUpForm = ({
|
||||
}
|
||||
|
||||
{
|
||||
currentStep === 2 &&
|
||||
currentStep === 2 && !oAuthLoginCompleted &&
|
||||
<p><b>{userData.fullName}</b> ({userData.email})</p>
|
||||
}
|
||||
|
||||
{
|
||||
currentStep === 2 && oAuthLoginCompleted &&
|
||||
<p><b>{oauthUserName}</b> ({oauthUserEmail})</p>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import createStoreHelper from '../../helpers/createStore';
|
||||
|
||||
import TenantSignUp from '../../containers/TenantSignUp';
|
||||
|
||||
import { Store } from 'redux';
|
||||
import { State } from '../../reducers/rootReducer';
|
||||
import { IOAuthJSON, oAuthJSON2JS } from '../../interfaces/IOAuth';
|
||||
|
||||
interface Props {
|
||||
oAuths: Array<IOAuthJSON>;
|
||||
oAuthLoginCompleted: boolean;
|
||||
oauthUserEmail?: string;
|
||||
oauthUserName?: string;
|
||||
baseUrl: string;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
@@ -22,11 +26,23 @@ class TenantSignUpRoot extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { authenticityToken } = this.props;
|
||||
const {
|
||||
oAuths,
|
||||
oAuthLoginCompleted,
|
||||
oauthUserEmail,
|
||||
oauthUserName,
|
||||
baseUrl,
|
||||
authenticityToken,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<TenantSignUp
|
||||
oAuthLoginCompleted={oAuthLoginCompleted}
|
||||
oauthUserEmail={oauthUserEmail}
|
||||
oauthUserName={oauthUserName}
|
||||
oAuths={oAuths.map(oAuth => oAuthJSON2JS(oAuth))}
|
||||
baseUrl={baseUrl}
|
||||
authenticityToken={authenticityToken}
|
||||
/>
|
||||
</Provider>
|
||||
|
||||
@@ -21,6 +21,10 @@ export const MutedText = ({ children }: Props) => (
|
||||
<span className="mutedText">{children}</span>
|
||||
);
|
||||
|
||||
export const CenteredText = ({ children }: Props) => (
|
||||
<p className="centeredText">{children}</p>
|
||||
);
|
||||
|
||||
export const CenteredMutedText = ({ children }: Props) => (
|
||||
<p className="centeredText"><span className="mutedText">{children}</span></p>
|
||||
);
|
||||
|
||||
29
app/javascript/components/common/OAuthProviderLink.tsx
Normal file
29
app/javascript/components/common/OAuthProviderLink.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
interface Props {
|
||||
oAuthId: number;
|
||||
oAuthName: string;
|
||||
oAuthLogo?: string;
|
||||
oAuthReason: string;
|
||||
isSignUp?: boolean;
|
||||
}
|
||||
|
||||
const OAuthProviderLink = ({ oAuthId, oAuthName, oAuthLogo, oAuthReason, isSignUp = false }: Props) => (
|
||||
<button
|
||||
onClick={() => window.location.href = `/o_auths/${oAuthId}/start?reason=${oAuthReason}`}
|
||||
className={`oauthProviderBtn oauthProvider${oAuthName.replace(' ', '')}`}
|
||||
>
|
||||
<img src={oAuthLogo} alt={oAuthName} width={28} height={28} />
|
||||
<span className='oauthProviderText'>
|
||||
{
|
||||
isSignUp ?
|
||||
I18n.t('common.forms.auth.sign_up_with', { o_auth: oAuthName })
|
||||
:
|
||||
I18n.t('common.forms.auth.log_in_with', { o_auth: oAuthName })
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default OAuthProviderLink;
|
||||
@@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
userPassword: string,
|
||||
siteName: string,
|
||||
subdomain: string,
|
||||
isOAuthLogin: boolean,
|
||||
authenticityToken: string,
|
||||
): Promise<any> {
|
||||
return dispatch(submitTenant(
|
||||
@@ -25,6 +26,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
userPassword,
|
||||
siteName,
|
||||
subdomain,
|
||||
isOAuthLogin,
|
||||
authenticityToken,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ export interface IOAuth {
|
||||
scope: string;
|
||||
jsonUserEmailPath: string;
|
||||
jsonUserNamePath?: string;
|
||||
|
||||
|
||||
callbackUrl?: string;
|
||||
tenantId?: number;
|
||||
}
|
||||
|
||||
export interface IOAuthJSON {
|
||||
@@ -30,9 +31,10 @@ export interface IOAuthJSON {
|
||||
json_user_name_path?: string;
|
||||
|
||||
callback_url?: string;
|
||||
tenant_id?: string;
|
||||
}
|
||||
|
||||
export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON) => ({
|
||||
export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON): IOAuth => ({
|
||||
id: parseInt(oAuthJSON.id),
|
||||
name: oAuthJSON.name,
|
||||
logo: oAuthJSON.logo,
|
||||
@@ -47,6 +49,7 @@ export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON) => ({
|
||||
jsonUserNamePath: oAuthJSON.json_user_name_path,
|
||||
|
||||
callbackUrl: oAuthJSON.callback_url,
|
||||
tenantId: oAuthJSON.tenant_id ? parseInt(oAuthJSON.tenant_id) : null,
|
||||
});
|
||||
|
||||
export const oAuthJS2JSON = (oAuth: IOAuth) => ({
|
||||
@@ -64,4 +67,5 @@ export const oAuthJS2JSON = (oAuth: IOAuth) => ({
|
||||
json_user_name_path: oAuth.jsonUserNamePath,
|
||||
|
||||
callback_url: oAuth.callbackUrl,
|
||||
tenant_id: oAuth.tenantId,
|
||||
});
|
||||
@@ -3,6 +3,8 @@ class OAuth < ApplicationRecord
|
||||
include ApplicationHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
scope :include_defaults, -> { unscope(where: :tenant_id).where(tenant_id: Current.tenant).or(unscope(where: :tenant_id).where(tenant_id: nil, is_enabled: true)) }
|
||||
|
||||
attr_accessor :state
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
||||
@@ -15,8 +17,20 @@ class OAuth < ApplicationRecord
|
||||
validates :scope, presence: true
|
||||
validates :json_user_email_path, presence: true
|
||||
|
||||
def is_default?
|
||||
tenant_id == nil
|
||||
end
|
||||
|
||||
def callback_url
|
||||
add_subdomain_to(method(:o_auth_callback_url), id)
|
||||
# Default OAuths are available to all tenants
|
||||
# but must have a single callback url:
|
||||
# for this reason, we don't preprend tenant subdomain
|
||||
# but rather use the "login" subdomain
|
||||
if self.is_default?
|
||||
o_auth_callback_url(id, host: Rails.application.base_url, subdomain: "login")
|
||||
else
|
||||
add_subdomain_to(method(:o_auth_callback_url), id)
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_url_with_query_params
|
||||
|
||||
@@ -9,6 +9,7 @@ class Tenant < ApplicationRecord
|
||||
enum status: [:active, :pending, :blocked]
|
||||
|
||||
after_initialize :set_default_status, if: :new_record?
|
||||
before_save :downcase_subdomain
|
||||
|
||||
validates :site_name, presence: true
|
||||
validates :subdomain, presence: true, uniqueness: true
|
||||
@@ -18,4 +19,8 @@ class Tenant < ApplicationRecord
|
||||
def set_default_status
|
||||
self.status ||= :pending
|
||||
end
|
||||
|
||||
def downcase_subdomain
|
||||
self.subdomain = self.subdomain.downcase
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,8 +16,6 @@ class User < ApplicationRecord
|
||||
after_initialize :set_default_role, if: :new_record?
|
||||
after_initialize :set_default_status, if: :new_record?
|
||||
|
||||
before_save :skip_confirmation
|
||||
|
||||
validates :full_name, presence: true, length: { in: 2..32 }
|
||||
validates :email,
|
||||
presence: true,
|
||||
@@ -54,7 +52,6 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def skip_confirmation
|
||||
return if Rails.application.email_confirmation?
|
||||
skip_confirmation!
|
||||
skip_confirmation_notification!
|
||||
skip_reconfirmation!
|
||||
@@ -84,4 +81,15 @@ class User < ApplicationRecord
|
||||
def blocked?
|
||||
status == 'blocked'
|
||||
end
|
||||
|
||||
def generate_oauth_token
|
||||
self.oauth_token = SecureRandom.urlsafe_base64
|
||||
self.save!
|
||||
oauth_token
|
||||
end
|
||||
|
||||
def invalidate_oauth_token
|
||||
self.oauth_token = nil
|
||||
self.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,55 +1,49 @@
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
|
||||
<h2><%= t('common.forms.auth.sign_up') %></h2>
|
||||
<div class="new_user">
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "new_user_form" }) do |f| %>
|
||||
<h2><%= t('common.forms.auth.sign_up') %></h2>
|
||||
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :full_name, class: "sr-only" %>
|
||||
<%= f.text_field :full_name,
|
||||
autofocus: true,
|
||||
placeholder: t('common.forms.auth.full_name'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= f.label :full_name, class: "sr-only" %>
|
||||
<%= f.text_field :full_name,
|
||||
autofocus: true,
|
||||
placeholder: t('common.forms.auth.full_name'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :email, class: "sr-only" %>
|
||||
<%= f.email_field :email,
|
||||
autocomplete: "email",
|
||||
placeholder: t('common.forms.auth.email'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= f.label :email, class: "sr-only" %>
|
||||
<%= f.email_field :email,
|
||||
autocomplete: "email",
|
||||
placeholder: t('common.forms.auth.email'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password, class: "sr-only" %>
|
||||
<%= f.password_field :password,
|
||||
placeholder: t('common.forms.auth.password'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= f.label :password, class: "sr-only" %>
|
||||
<%= f.password_field :password,
|
||||
placeholder: t('common.forms.auth.password'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password_confirmation, class: "sr-only" %>
|
||||
<%= f.password_field :password_confirmation,
|
||||
placeholder: t('common.forms.auth.password_confirmation'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= f.label :password_confirmation, class: "sr-only" %>
|
||||
<%= f.password_field :password_confirmation,
|
||||
placeholder: t('common.forms.auth.password_confirmation'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
||||
</div>
|
||||
|
||||
<% if not @o_auths.blank? %>
|
||||
<hr />
|
||||
<% @o_auths.each do |o_auth| %>
|
||||
<p>
|
||||
<%= link_to t('common.forms.auth.sign_up_with', o_auth: o_auth.name),
|
||||
o_auth_start_path(o_auth, reason: 'user') %>
|
||||
</p>
|
||||
<% end %>
|
||||
<div class="actions">
|
||||
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/o_auths", is_sign_up: true %>
|
||||
</div>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
|
||||
<h2><%= t('common.forms.auth.log_in') %></h2>
|
||||
<div class="new_user">
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "new_user_form" }) do |f| %>
|
||||
<h2><%= t('common.forms.auth.log_in') %></h2>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :email, class: "sr-only" %>
|
||||
<%= f.email_field :email,
|
||||
autocomplete: "email",
|
||||
placeholder: t('common.forms.auth.email'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= f.label :email, class: "sr-only" %>
|
||||
<%= f.email_field :email,
|
||||
autocomplete: "email",
|
||||
placeholder: t('common.forms.auth.email'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password, class: "sr-only" %>
|
||||
<%= f.password_field :password,
|
||||
autocomplete: "current-password",
|
||||
placeholder: t('common.forms.auth.password'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= f.label :password, class: "sr-only" %>
|
||||
<%= f.password_field :password,
|
||||
autocomplete: "current-password",
|
||||
placeholder: t('common.forms.auth.password'),
|
||||
required: true,
|
||||
class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.rememberable? %>
|
||||
<div class="form-group form-check">
|
||||
<%= f.check_box :remember_me, class: "form-check-input" %>
|
||||
<%= f.label :remember_me, t('common.forms.auth.remember_me'), class: "form-check-label" %>
|
||||
<% if devise_mapping.rememberable? %>
|
||||
<div class="form-group form-check">
|
||||
<%= f.check_box :remember_me, class: "form-check-input" %>
|
||||
<%= f.label :remember_me, t('common.forms.auth.remember_me'), class: "form-check-label" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
||||
</div>
|
||||
|
||||
<% if not @o_auths.empty? %>
|
||||
<hr />
|
||||
<% @o_auths.each do |o_auth| %>
|
||||
<p>
|
||||
<%= link_to t('common.forms.auth.log_in_with', o_auth: o_auth.name),
|
||||
o_auth_start_path(o_auth, reason: 'user') %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render "devise/shared/o_auths" %>
|
||||
</div>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
17
app/views/devise/shared/_o_auths.html.erb
Normal file
17
app/views/devise/shared/_o_auths.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<% if not @o_auths.blank? %>
|
||||
<hr />
|
||||
<% @o_auths.each do |o_auth| %>
|
||||
<%=
|
||||
react_component(
|
||||
'common/OAuthProviderLink',
|
||||
{
|
||||
oAuthId: o_auth.id,
|
||||
oAuthName: o_auth.name,
|
||||
oAuthLogo: o_auth.logo,
|
||||
oAuthReason: "login",
|
||||
isSignUp: defined?(is_sign_up) ? is_sign_up : false,
|
||||
}
|
||||
)
|
||||
%>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -2,6 +2,11 @@
|
||||
react_component(
|
||||
'TenantSignUp',
|
||||
{
|
||||
oAuths: @o_auths || [],
|
||||
oAuthLoginCompleted: @o_auth_login_completed || false,
|
||||
oauthUserEmail: @user_email,
|
||||
oauthUserName: @user_name,
|
||||
baseUrl: Rails.application.base_url,
|
||||
authenticityToken: form_authenticity_token
|
||||
}
|
||||
)
|
||||
|
||||
@@ -23,11 +23,11 @@ class OAuthSignInUser
|
||||
|
||||
def run
|
||||
return nil unless @o_auth and @o_auth.class == OAuth and @o_auth.is_enabled?
|
||||
return nil unless @user_profile and @user_profile.class == Hash
|
||||
return nil unless @user_profile and (@user_profile.class == Hash or @user_profile.class == Array)
|
||||
|
||||
begin
|
||||
# Attempts to get email from user_profile Hash
|
||||
email = query_path_from_hash(@user_profile, @o_auth.json_user_email_path)
|
||||
email = query_path_from_object(@user_profile, @o_auth.json_user_email_path)
|
||||
|
||||
return nil if email.nil? or not URI::MailTo::EMAIL_REGEXP.match?(email)
|
||||
|
||||
@@ -36,7 +36,7 @@ class OAuthSignInUser
|
||||
|
||||
if user.nil?
|
||||
if not @o_auth.json_user_name_path.blank?
|
||||
full_name = query_path_from_hash(@user_profile, @o_auth.json_user_name_path)
|
||||
full_name = query_path_from_object(@user_profile, @o_auth.json_user_name_path)
|
||||
end
|
||||
full_name ||= I18n.t('defaults.user_full_name')
|
||||
|
||||
@@ -46,7 +46,7 @@ class OAuthSignInUser
|
||||
password: Devise.friendly_token,
|
||||
status: 'active'
|
||||
)
|
||||
user.skip_confirmation!
|
||||
user.skip_confirmation
|
||||
user.save
|
||||
end
|
||||
|
||||
|
||||
@@ -24,10 +24,6 @@ module App
|
||||
ENV["MULTI_TENANCY"] == "true"
|
||||
end
|
||||
|
||||
def email_confirmation?
|
||||
ENV["EMAIL_CONFIRMATION"] == "true"
|
||||
end
|
||||
|
||||
def posts_per_page
|
||||
15
|
||||
end
|
||||
|
||||
@@ -81,12 +81,10 @@ Rails.application.configure do
|
||||
}
|
||||
end
|
||||
|
||||
if ENV['EMAIL_CONFIRMATION']
|
||||
config.action_mailer.default_options = {
|
||||
from: ENV.fetch("EMAIL_MAIL_FROM", "Astuto <notifications@astuto.io>"),
|
||||
config.action_mailer.default_options = {
|
||||
from: ENV.fetch("EMAIL_MAIL_FROM", "Astuto <notifications@astuto.io>"),
|
||||
reply_to: ENV.fetch("EMAIL_MAIL_REPLY_TO", ENV.fetch("EMAIL_MAIL_FROM", "Astuto <notifications@astuto.io>"))
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation cannot be found).
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
# your test database is "scratch space" for the test suite and is wiped
|
||||
# and recreated between test runs. Don't rely on the data there!
|
||||
|
||||
# Set up default environment variables
|
||||
ENV["EMAIL_CONFIRMATION"] = "no"
|
||||
|
||||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
# For Devise
|
||||
|
||||
@@ -69,12 +69,13 @@ en:
|
||||
page_title: 'Create your feedback space'
|
||||
step1:
|
||||
title: '1. Create user account'
|
||||
email_auth: 'Register with email'
|
||||
email_auth: 'Sign up with email'
|
||||
step2:
|
||||
title: '2. Create feedback space'
|
||||
create_button: 'Create feedback space'
|
||||
validations:
|
||||
subdomain_already_taken: 'Sorry, this subdomain is not available'
|
||||
subdomain_only_letters_and_numbers: 'Subdomain can only contain alphanumeric characters and hyphen'
|
||||
step3:
|
||||
title: "You're almost done!"
|
||||
message: "Check your email %{email} to activate your new feedback space %{subdomain}!"
|
||||
@@ -195,6 +196,7 @@ en:
|
||||
authentication:
|
||||
title: 'Authentication'
|
||||
oauth_subtitle: 'OAuth providers'
|
||||
default_oauth: 'Default OAuth provider'
|
||||
copy_url: 'Copy URL'
|
||||
test_page:
|
||||
title: '%{name} OAuth test results'
|
||||
|
||||
@@ -29,6 +29,7 @@ Rails.application.routes.draw do
|
||||
resources :o_auths, only: [:index, :create, :update, :destroy]
|
||||
get '/o_auths/:id/start', to: 'o_auths#start', as: :o_auth_start
|
||||
get '/o_auths/:id/callback', to: 'o_auths#callback', as: :o_auth_callback
|
||||
get '/o_auths/sign_in_from_oauth_token', to: 'o_auths#sign_in_from_oauth_token', as: :o_auth_sign_in_from_oauth_token
|
||||
|
||||
resources :posts, only: [:index, :create, :show, :update, :destroy] do
|
||||
resource :follows, only: [:create, :destroy]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemoveNotNullConstraintToOAuthsTenant < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
change_column_null :o_auths, :tenant_id, true
|
||||
end
|
||||
end
|
||||
5
db/migrate/20240117112502_add_oauth_token_to_users.rb
Normal file
5
db/migrate/20240117112502_add_oauth_token_to_users.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddOauthTokenToUsers < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :users, :oauth_token, :string, null: true
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2023_02_11_095500) do
|
||||
ActiveRecord::Schema.define(version: 2024_01_17_112502) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@@ -77,7 +77,7 @@ ActiveRecord::Schema.define(version: 2023_02_11_095500) do
|
||||
t.string "scope", null: false
|
||||
t.string "json_user_name_path"
|
||||
t.string "json_user_email_path", null: false
|
||||
t.bigint "tenant_id", null: false
|
||||
t.bigint "tenant_id"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["name", "tenant_id"], name: "index_o_auths_on_name_and_tenant_id", unique: true
|
||||
@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 2023_02_11_095500) do
|
||||
t.boolean "notifications_enabled", default: true, null: false
|
||||
t.integer "status"
|
||||
t.bigint "tenant_id", null: false
|
||||
t.string "oauth_token"
|
||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||
t.index ["email", "tenant_id"], name: "index_users_on_email_and_tenant_id", unique: true
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe OAuthsHelper, type: :helper do
|
||||
context 'query_path_from_hash method' do
|
||||
context 'query_path_from_object method' do
|
||||
it 'queries a path from hash' do
|
||||
email = "admin@example.com"
|
||||
name = "Admin"
|
||||
@@ -23,14 +23,40 @@ RSpec.describe OAuthsHelper, type: :helper do
|
||||
name_path = "info.name"
|
||||
surname_path = "info.additional_info.surnames[2][0].surname"
|
||||
|
||||
expect(helper.query_path_from_hash(hash, name_path)).to eq(name)
|
||||
expect(helper.query_path_from_hash(hash, email_path)).to eq(email)
|
||||
expect(helper.query_path_from_hash(hash, surname_path)).to eq(surname)
|
||||
expect(helper.query_path_from_object(hash, name_path)).to eq(name)
|
||||
expect(helper.query_path_from_object(hash, email_path)).to eq(email)
|
||||
expect(helper.query_path_from_object(hash, surname_path)).to eq(surname)
|
||||
end
|
||||
|
||||
it 'queries a path from array' do
|
||||
email1 = "admin1@example.com"
|
||||
email2 = "admin2@example.com"
|
||||
address = "Address1"
|
||||
|
||||
array = [
|
||||
{
|
||||
"email" => email1,
|
||||
},
|
||||
{
|
||||
"email" => email2,
|
||||
"additional_info" => {
|
||||
"addresses" => [{ "name" => address }]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
email1_path = "[0].email"
|
||||
email2_path = "[1].email"
|
||||
address_path = "[1].additional_info.addresses[0].name"
|
||||
|
||||
expect(helper.query_path_from_object(array, email1_path)).to eq(email1)
|
||||
expect(helper.query_path_from_object(array, email2_path)).to eq(email2)
|
||||
expect(helper.query_path_from_object(array, address_path)).to eq(address)
|
||||
end
|
||||
|
||||
it 'returns nil if inputs are not of type Hash and String respectively' do
|
||||
expect(helper.query_path_from_hash({"valid" => true}, ["invalid"])).to eq(nil)
|
||||
expect(helper.query_path_from_hash("invalid", "valid")).to eq(nil)
|
||||
expect(helper.query_path_from_object({"valid" => true}, ["invalid"])).to eq(nil)
|
||||
expect(helper.query_path_from_object("invalid", "valid")).to eq(nil)
|
||||
end
|
||||
|
||||
it 'returns nil if path not found' do
|
||||
@@ -44,10 +70,10 @@ RSpec.describe OAuthsHelper, type: :helper do
|
||||
}
|
||||
|
||||
name_path = "name"
|
||||
expect(helper.query_path_from_hash(hash, name_path)).to eq(nil)
|
||||
expect(helper.query_path_from_object(hash, name_path)).to eq(nil)
|
||||
|
||||
name_path = "info.names[0]"
|
||||
expect(helper.query_path_from_hash(hash, name_path)).to eq(nil)
|
||||
expect(helper.query_path_from_object(hash, name_path)).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user