mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 19:57:52 +01:00
Add the possibility to enable/disable default OAuths (#303)
This commit is contained in:
committed by
GitHub
parent
719f1ad4e9
commit
32d19cbe7c
@@ -269,7 +269,15 @@ body {
|
|||||||
.btn-block,
|
.btn-block,
|
||||||
.btn-outline-dark,
|
.btn-outline-dark,
|
||||||
.mt-2,
|
.mt-2,
|
||||||
.mb-2;
|
.mb-2,
|
||||||
|
.p-0;
|
||||||
|
|
||||||
|
height: 38px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: white;
|
||||||
|
color: var(--astuto-black);
|
||||||
|
}
|
||||||
|
|
||||||
.oauthProviderText {
|
.oauthProviderText {
|
||||||
@extend .ml-2;
|
@extend .ml-2;
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.defaultOAuthDiv {
|
||||||
|
@extend .d-flex;
|
||||||
|
|
||||||
|
.defaultOAuthLabel { @extend .align-self-center; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emailAuth {
|
||||||
|
@extend .mt-2, .mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
.userConfirm, .tenantConfirm {
|
.userConfirm, .tenantConfirm {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ class OAuthsController < ApplicationController
|
|||||||
|
|
||||||
before_action :authenticate_admin, only: [:index, :create, :update, :destroy]
|
before_action :authenticate_admin, only: [:index, :create, :update, :destroy]
|
||||||
|
|
||||||
TOKEN_STATE_SEPARATOR = '-'
|
TOKEN_STATE_SEPARATOR = ','
|
||||||
|
|
||||||
# [subdomain.]base_url/o_auths/:id/start?reason=login|test|tenantsignup
|
# [subdomain.]base_url/o_auths/:id/start?reason=login|test|tenantsignup
|
||||||
# Generates authorize url with required parameters and redirects to provider
|
# Generates authorize url with required parameters and redirects to provider
|
||||||
def start
|
def start
|
||||||
@o_auth = OAuth.unscoped.include_defaults.find(params[:id])
|
if params[:reason] == 'tenantsignup'
|
||||||
|
@o_auth = OAuth.include_only_defaults.find(params[:id])
|
||||||
|
else
|
||||||
|
@o_auth = OAuth.include_defaults.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
return if params[:reason] != 'test' and not @o_auth.is_enabled?
|
return if params[:reason] != 'test' and not @o_auth.is_enabled?
|
||||||
|
|
||||||
@@ -31,15 +35,17 @@ class OAuthsController < ApplicationController
|
|||||||
return unless cookies[:token_state] == params[:state]
|
return unless cookies[:token_state] == params[:state]
|
||||||
cookies.delete(:token_state, domain: ".#{request.domain}")
|
cookies.delete(:token_state, domain: ".#{request.domain}")
|
||||||
|
|
||||||
@o_auth = OAuth.unscoped.include_defaults.find(params[:id])
|
# if it is a default oauth, tenant is not yet set
|
||||||
|
Current.tenant ||= Tenant.find_by(subdomain: tenant_domain)
|
||||||
|
|
||||||
|
if reason == 'tenantsignup'
|
||||||
|
@o_auth = OAuth.include_only_defaults.find(params[:id])
|
||||||
|
else
|
||||||
|
@o_auth = OAuth.include_defaults.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
return if reason != 'test' and not @o_auth.is_enabled?
|
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 = OAuthExchangeAuthCodeForProfileWorkflow.new(
|
user_profile = OAuthExchangeAuthCodeForProfileWorkflow.new(
|
||||||
authorization_code: params[:code],
|
authorization_code: params[:code],
|
||||||
o_auth: @o_auth
|
o_auth: @o_auth
|
||||||
@@ -80,12 +86,20 @@ class OAuthsController < ApplicationController
|
|||||||
|
|
||||||
elsif reason == 'tenantsignup'
|
elsif reason == 'tenantsignup'
|
||||||
|
|
||||||
@o_auths = []
|
@o_auths = @o_auths = OAuth.unscoped.where(tenant_id: nil, is_enabled: true)
|
||||||
|
|
||||||
@user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path)
|
@user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path)
|
||||||
if not @o_auth.json_user_name_path.blank?
|
if not @o_auth.json_user_name_path.blank?
|
||||||
@user_name = query_path_from_object(user_profile, @o_auth.json_user_name_path)
|
@user_name = query_path_from_object(user_profile, @o_auth.json_user_name_path)
|
||||||
end
|
end
|
||||||
@o_auth_login_completed = true
|
|
||||||
|
@o_auth_login_completed = (not @user_email.blank?)
|
||||||
|
|
||||||
|
if not @o_auth_login_completed
|
||||||
|
flash[:alert] = I18n.t('errors.o_auth_login_error', name: @o_auth.name)
|
||||||
|
redirect_to signup_url
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
session[:o_auth_sign_up] = "#{@user_email},#{@user_name}"
|
session[:o_auth_sign_up] = "#{@user_email},#{@user_name}"
|
||||||
|
|
||||||
@@ -124,7 +138,9 @@ class OAuthsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
authorize OAuth
|
authorize OAuth
|
||||||
|
|
||||||
@o_auths = OAuth.include_defaults.order(created_at: :asc)
|
@o_auths = OAuth
|
||||||
|
.include_all_defaults
|
||||||
|
.order(tenant_id: :asc, created_at: :asc)
|
||||||
|
|
||||||
render json: to_json_custom(@o_auths)
|
render json: to_json_custom(@o_auths)
|
||||||
end
|
end
|
||||||
@@ -175,7 +191,7 @@ class OAuthsController < ApplicationController
|
|||||||
|
|
||||||
def to_json_custom(o_auth)
|
def to_json_custom(o_auth)
|
||||||
o_auth.as_json(
|
o_auth.as_json(
|
||||||
methods: :callback_url,
|
methods: [:callback_url, :default_o_auth_is_enabled],
|
||||||
except: [:client_secret]
|
except: [:client_secret]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
35
app/controllers/tenant_default_o_auths_controller.rb
Normal file
35
app/controllers/tenant_default_o_auths_controller.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
class TenantDefaultOAuthsController < ApplicationController
|
||||||
|
include ApplicationHelper
|
||||||
|
|
||||||
|
before_action :authenticate_admin, only: [:create, :destroy]
|
||||||
|
|
||||||
|
def create
|
||||||
|
enabled_default_oauth = TenantDefaultOAuth.new(o_auth_id: params[:o_auth_id])
|
||||||
|
|
||||||
|
if enabled_default_oauth.save
|
||||||
|
render json: {
|
||||||
|
id: params[:o_auth_id]
|
||||||
|
}, status: :created
|
||||||
|
else
|
||||||
|
render json: {
|
||||||
|
error: enabled_default_oauth.errors.full_messages
|
||||||
|
}, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
enabled_default_oauth = TenantDefaultOAuth.find_by(o_auth_id: params[:o_auth_id])
|
||||||
|
|
||||||
|
return if enabled_default_oauth.nil?
|
||||||
|
|
||||||
|
if enabled_default_oauth.destroy
|
||||||
|
render json: {
|
||||||
|
id: params[:o_auth_id],
|
||||||
|
}, status: :accepted
|
||||||
|
else
|
||||||
|
render json: {
|
||||||
|
error: enabled_default_oauth.errors.full_messages
|
||||||
|
}, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,7 +5,7 @@ class TenantsController < ApplicationController
|
|||||||
|
|
||||||
def new
|
def new
|
||||||
@page_title = "Create your feedback space"
|
@page_title = "Create your feedback space"
|
||||||
@o_auths = OAuth.unscoped.where(tenant_id: nil)
|
@o_auths = OAuth.unscoped.where(tenant_id: nil, is_enabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -46,6 +46,9 @@ class TenantsController < ApplicationController
|
|||||||
@user.save!
|
@user.save!
|
||||||
|
|
||||||
CreateWelcomeEntitiesWorkflow.new().run
|
CreateWelcomeEntitiesWorkflow.new().run
|
||||||
|
OAuth.include_only_defaults.each do |o_auth|
|
||||||
|
TenantDefaultOAuth.create(o_auth_id: o_auth.id)
|
||||||
|
end
|
||||||
|
|
||||||
logger.info { "New tenant registration: #{Current.tenant.inspect}" }
|
logger.info { "New tenant registration: #{Current.tenant.inspect}" }
|
||||||
|
|
||||||
|
|||||||
75
app/javascript/actions/OAuth/updateDefaultOAuth.ts
Normal file
75
app/javascript/actions/OAuth/updateDefaultOAuth.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
import { ThunkAction } from "redux-thunk";
|
||||||
|
|
||||||
|
import { IOAuthJSON } from "../../interfaces/IOAuth";
|
||||||
|
import { State } from "../../reducers/rootReducer";
|
||||||
|
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||||
|
import HttpStatus from "../../constants/http_status";
|
||||||
|
|
||||||
|
export const DEFAULT_OAUTH_UPDATE_START = 'DEFAULT_OAUTH_UPDATE_START';
|
||||||
|
interface DefaultOAuthUpdateStartAction {
|
||||||
|
type: typeof DEFAULT_OAUTH_UPDATE_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_OAUTH_UPDATE_SUCCESS = 'DEFAULT_OAUTH_UPDATE_SUCCESS';
|
||||||
|
interface DefaultOAuthUpdateSuccessAction {
|
||||||
|
type: typeof DEFAULT_OAUTH_UPDATE_SUCCESS;
|
||||||
|
id: number;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_OAUTH_UPDATE_FAILURE = 'DEFAULT_OAUTH_UPDATE_FAILURE';
|
||||||
|
interface DefaultOAuthUpdateFailureAction {
|
||||||
|
type: typeof DEFAULT_OAUTH_UPDATE_FAILURE;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultOAuthUpdateActionTypes =
|
||||||
|
DefaultOAuthUpdateStartAction |
|
||||||
|
DefaultOAuthUpdateSuccessAction |
|
||||||
|
DefaultOAuthUpdateFailureAction;
|
||||||
|
|
||||||
|
const defaultOAuthUpdateStart = (): DefaultOAuthUpdateStartAction => ({
|
||||||
|
type: DEFAULT_OAUTH_UPDATE_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultOAuthUpdateSuccess = (
|
||||||
|
id: number,
|
||||||
|
isEnabled: boolean,
|
||||||
|
): DefaultOAuthUpdateSuccessAction => ({
|
||||||
|
type: DEFAULT_OAUTH_UPDATE_SUCCESS,
|
||||||
|
id,
|
||||||
|
isEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultOAuthUpdateFailure = (error: string): DefaultOAuthUpdateFailureAction => ({
|
||||||
|
type: DEFAULT_OAUTH_UPDATE_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UpdateDefaultOAuthParams {
|
||||||
|
id: number;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDefaultOAuth = ({
|
||||||
|
id,
|
||||||
|
isEnabled = null,
|
||||||
|
authenticityToken,
|
||||||
|
}: UpdateDefaultOAuthParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(defaultOAuthUpdateStart());
|
||||||
|
|
||||||
|
const res = await fetch(`/o_auths/${id}/tenant_default_o_auths`, {
|
||||||
|
method: isEnabled ? 'POST' : 'DELETE',
|
||||||
|
headers: buildRequestHeaders(authenticityToken),
|
||||||
|
});
|
||||||
|
await res.json();
|
||||||
|
|
||||||
|
if (res.status === HttpStatus.Created || res.status === HttpStatus.Accepted)
|
||||||
|
dispatch(defaultOAuthUpdateSuccess(id, isEnabled));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('An error occurred while enabling/disabling default OAuth');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ interface Props {
|
|||||||
submitError: string;
|
submitError: string;
|
||||||
|
|
||||||
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
||||||
|
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
|
||||||
handleDeleteOAuth(id: number): void;
|
handleDeleteOAuth(id: number): void;
|
||||||
|
|
||||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||||
@@ -27,6 +28,7 @@ const AuthenticationIndexPage = ({
|
|||||||
submitError,
|
submitError,
|
||||||
|
|
||||||
handleToggleEnabledOAuth,
|
handleToggleEnabledOAuth,
|
||||||
|
handleToggleEnabledDefaultOAuth,
|
||||||
handleDeleteOAuth,
|
handleDeleteOAuth,
|
||||||
|
|
||||||
setPage,
|
setPage,
|
||||||
@@ -48,6 +50,7 @@ const AuthenticationIndexPage = ({
|
|||||||
<OAuthProvidersList
|
<OAuthProvidersList
|
||||||
oAuths={oAuths.items}
|
oAuths={oAuths.items}
|
||||||
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
||||||
|
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
|
||||||
handleDeleteOAuth={handleDeleteOAuth}
|
handleDeleteOAuth={handleDeleteOAuth}
|
||||||
setPage={setPage}
|
setPage={setPage}
|
||||||
setSelectedOAuth={setSelectedOAuth}
|
setSelectedOAuth={setSelectedOAuth}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface Props {
|
|||||||
onSubmitOAuth(oAuth: IOAuth, authenticityToken: string): Promise<any>;
|
onSubmitOAuth(oAuth: IOAuth, authenticityToken: string): Promise<any>;
|
||||||
onUpdateOAuth(id: number, form: ISiteSettingsOAuthForm, authenticityToken: string): Promise<any>;
|
onUpdateOAuth(id: number, form: ISiteSettingsOAuthForm, authenticityToken: string): Promise<any>;
|
||||||
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
|
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
|
||||||
|
onToggleEnabledDefaultOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
|
||||||
onDeleteOAuth(id: number, authenticityToken: string): void;
|
onDeleteOAuth(id: number, authenticityToken: string): void;
|
||||||
|
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
@@ -32,6 +33,7 @@ const AuthenticationSiteSettingsP = ({
|
|||||||
onSubmitOAuth,
|
onSubmitOAuth,
|
||||||
onUpdateOAuth,
|
onUpdateOAuth,
|
||||||
onToggleEnabledOAuth,
|
onToggleEnabledOAuth,
|
||||||
|
onToggleEnabledDefaultOAuth,
|
||||||
onDeleteOAuth,
|
onDeleteOAuth,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
submitError,
|
submitError,
|
||||||
@@ -58,6 +60,10 @@ const AuthenticationSiteSettingsP = ({
|
|||||||
onToggleEnabledOAuth(id, enabled, authenticityToken);
|
onToggleEnabledOAuth(id, enabled, authenticityToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleEnabledDefaultOAuth = (id: number, enabled: boolean) => {
|
||||||
|
onToggleEnabledDefaultOAuth(id, enabled, authenticityToken);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteOAuth = (id: number) => {
|
const handleDeleteOAuth = (id: number) => {
|
||||||
onDeleteOAuth(id, authenticityToken);
|
onDeleteOAuth(id, authenticityToken);
|
||||||
};
|
};
|
||||||
@@ -67,6 +73,7 @@ const AuthenticationSiteSettingsP = ({
|
|||||||
<AuthenticationIndexPage
|
<AuthenticationIndexPage
|
||||||
oAuths={oAuths}
|
oAuths={oAuths}
|
||||||
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
||||||
|
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
|
||||||
handleDeleteOAuth={handleDeleteOAuth}
|
handleDeleteOAuth={handleDeleteOAuth}
|
||||||
setPage={setPage}
|
setPage={setPage}
|
||||||
setSelectedOAuth={setSelectedOAuth}
|
setSelectedOAuth={setSelectedOAuth}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { MutedText } from '../../common/CustomTexts';
|
|||||||
interface Props {
|
interface Props {
|
||||||
oAuth: IOAuth;
|
oAuth: IOAuth;
|
||||||
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
||||||
|
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
|
||||||
handleDeleteOAuth(id: number): void;
|
handleDeleteOAuth(id: number): void;
|
||||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||||
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
||||||
@@ -20,6 +21,7 @@ interface Props {
|
|||||||
const OAuthProviderItem = ({
|
const OAuthProviderItem = ({
|
||||||
oAuth,
|
oAuth,
|
||||||
handleToggleEnabledOAuth,
|
handleToggleEnabledOAuth,
|
||||||
|
handleToggleEnabledDefaultOAuth,
|
||||||
handleDeleteOAuth,
|
handleDeleteOAuth,
|
||||||
setPage,
|
setPage,
|
||||||
setSelectedOAuth,
|
setSelectedOAuth,
|
||||||
@@ -41,13 +43,20 @@ const OAuthProviderItem = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div><MutedText>{I18n.t('site_settings.authentication.default_oauth')}</MutedText></div>
|
<div className="oAuthIsEnabled">
|
||||||
|
<Switch
|
||||||
|
label={I18n.t(`common.${oAuth.isEnabled ? 'enabled' : 'disabled'}`)}
|
||||||
|
onClick={() => handleToggleEnabledDefaultOAuth(oAuth.id, !oAuth.defaultOAuthIsEnabled)}
|
||||||
|
checked={oAuth.defaultOAuthIsEnabled}
|
||||||
|
htmlId={`oAuth${oAuth.name}EnabledSwitch`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
oAuth.tenantId &&
|
oAuth.tenantId ?
|
||||||
<div className="oAuthActions">
|
<div className="oAuthActions">
|
||||||
<CopyToClipboardButton
|
<CopyToClipboardButton
|
||||||
label={I18n.t('site_settings.authentication.copy_url')}
|
label={I18n.t('site_settings.authentication.copy_url')}
|
||||||
@@ -83,6 +92,10 @@ const OAuthProviderItem = ({
|
|||||||
{I18n.t('common.buttons.delete')}
|
{I18n.t('common.buttons.delete')}
|
||||||
</ActionLink>
|
</ActionLink>
|
||||||
</div>
|
</div>
|
||||||
|
:
|
||||||
|
<div className="defaultOAuthDiv">
|
||||||
|
<span className="defaultOAuthLabel"><MutedText>{I18n.t('site_settings.authentication.default_oauth')}</MutedText></span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import OAuthProviderItem from './OAuthProviderItem';
|
|||||||
interface Props {
|
interface Props {
|
||||||
oAuths: Array<IOAuth>;
|
oAuths: Array<IOAuth>;
|
||||||
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
||||||
|
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
|
||||||
handleDeleteOAuth(id: number): void;
|
handleDeleteOAuth(id: number): void;
|
||||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||||
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
||||||
@@ -17,6 +18,7 @@ interface Props {
|
|||||||
const OAuthProvidersList = ({
|
const OAuthProvidersList = ({
|
||||||
oAuths,
|
oAuths,
|
||||||
handleToggleEnabledOAuth,
|
handleToggleEnabledOAuth,
|
||||||
|
handleToggleEnabledDefaultOAuth,
|
||||||
handleDeleteOAuth,
|
handleDeleteOAuth,
|
||||||
setPage,
|
setPage,
|
||||||
setSelectedOAuth,
|
setSelectedOAuth,
|
||||||
@@ -35,6 +37,7 @@ const OAuthProvidersList = ({
|
|||||||
<OAuthProviderItem
|
<OAuthProviderItem
|
||||||
oAuth={oAuth}
|
oAuth={oAuth}
|
||||||
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
||||||
|
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
|
||||||
handleDeleteOAuth={handleDeleteOAuth}
|
handleDeleteOAuth={handleDeleteOAuth}
|
||||||
setPage={setPage}
|
setPage={setPage}
|
||||||
setSelectedOAuth={setSelectedOAuth}
|
setSelectedOAuth={setSelectedOAuth}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export interface ITenantSignUpTenantForm {
|
|||||||
subdomain: string;
|
subdomain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthMethod = 'none' | 'email' | 'oauth';
|
||||||
|
|
||||||
const TenantSignUpP = ({
|
const TenantSignUpP = ({
|
||||||
oAuths,
|
oAuths,
|
||||||
oAuthLoginCompleted,
|
oAuthLoginCompleted,
|
||||||
@@ -58,9 +60,12 @@ const TenantSignUpP = ({
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
authenticityToken
|
authenticityToken
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
// authMethod is either 'none', 'email' or 'oauth'
|
||||||
|
const [authMethod, setAuthMethod] = useState<AuthMethod>(oAuthLoginCompleted ? 'oauth' : 'none');
|
||||||
|
|
||||||
const [userData, setUserData] = useState({
|
const [userData, setUserData] = useState({
|
||||||
fullName: '',
|
fullName: oAuthLoginCompleted ? oauthUserName : '',
|
||||||
email: '',
|
email: oAuthLoginCompleted ? oauthUserEmail : '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirmation: '',
|
passwordConfirmation: '',
|
||||||
});
|
});
|
||||||
@@ -72,20 +77,18 @@ const TenantSignUpP = ({
|
|||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(oAuthLoginCompleted ? 2 : 1);
|
const [currentStep, setCurrentStep] = useState(oAuthLoginCompleted ? 2 : 1);
|
||||||
|
|
||||||
const [emailAuth, setEmailAuth] = useState(false);
|
|
||||||
|
|
||||||
const handleSignUpSubmit = (siteName: string, subdomain: string) => {
|
const handleSignUpSubmit = (siteName: string, subdomain: string) => {
|
||||||
handleSubmit(
|
handleSubmit(
|
||||||
oAuthLoginCompleted ? oauthUserName : userData.fullName,
|
userData.fullName,
|
||||||
oAuthLoginCompleted ? oauthUserEmail : userData.email,
|
userData.email,
|
||||||
userData.password,
|
userData.password,
|
||||||
siteName,
|
siteName,
|
||||||
subdomain,
|
subdomain,
|
||||||
oAuthLoginCompleted,
|
authMethod == 'oauth',
|
||||||
authenticityToken,
|
authenticityToken,
|
||||||
).then(res => {
|
).then(res => {
|
||||||
if (res?.status !== HttpStatus.Created) return;
|
if (res?.status !== HttpStatus.Created) return;
|
||||||
if (oAuthLoginCompleted) {
|
if (authMethod == 'oauth') {
|
||||||
let redirectUrl = new URL(baseUrl);
|
let redirectUrl = new URL(baseUrl);
|
||||||
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
|
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
|
||||||
window.location.href = `${redirectUrl.toString()}users/sign_in`;
|
window.location.href = `${redirectUrl.toString()}users/sign_in`;
|
||||||
@@ -107,12 +110,9 @@ const TenantSignUpP = ({
|
|||||||
<UserSignUpForm
|
<UserSignUpForm
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
setCurrentStep={setCurrentStep}
|
setCurrentStep={setCurrentStep}
|
||||||
emailAuth={emailAuth}
|
authMethod={authMethod}
|
||||||
setEmailAuth={setEmailAuth}
|
setAuthMethod={setAuthMethod}
|
||||||
oAuths={oAuths}
|
oAuths={oAuths}
|
||||||
oAuthLoginCompleted={oAuthLoginCompleted}
|
|
||||||
oauthUserEmail={oauthUserEmail}
|
|
||||||
oauthUserName={oauthUserName}
|
|
||||||
userData={userData}
|
userData={userData}
|
||||||
setUserData={setUserData}
|
setUserData={setUserData}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import I18n from 'i18n-js';
|
|||||||
import Box from '../common/Box';
|
import Box from '../common/Box';
|
||||||
import Button from '../common/Button';
|
import Button from '../common/Button';
|
||||||
import OAuthProviderLink from '../common/OAuthProviderLink';
|
import OAuthProviderLink from '../common/OAuthProviderLink';
|
||||||
import { ITenantSignUpUserForm } from './TenantSignUpP';
|
import { AuthMethod, ITenantSignUpUserForm } from './TenantSignUpP';
|
||||||
import { DangerText } from '../common/CustomTexts';
|
import { DangerText } from '../common/CustomTexts';
|
||||||
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
||||||
import { EMAIL_REGEX } from '../../constants/regex';
|
import { EMAIL_REGEX } from '../../constants/regex';
|
||||||
@@ -16,12 +16,9 @@ import { BackIcon, EditIcon } from '../common/Icons';
|
|||||||
interface Props {
|
interface Props {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
setCurrentStep(step: number): void;
|
setCurrentStep(step: number): void;
|
||||||
emailAuth: boolean;
|
authMethod: AuthMethod;
|
||||||
setEmailAuth(enabled: boolean): void;
|
setAuthMethod(method: AuthMethod): void;
|
||||||
oAuths: Array<IOAuth>;
|
oAuths: Array<IOAuth>;
|
||||||
oAuthLoginCompleted: boolean;
|
|
||||||
oauthUserEmail?: string;
|
|
||||||
oauthUserName?: string;
|
|
||||||
userData: ITenantSignUpUserForm;
|
userData: ITenantSignUpUserForm;
|
||||||
setUserData({}: ITenantSignUpUserForm): void;
|
setUserData({}: ITenantSignUpUserForm): void;
|
||||||
}
|
}
|
||||||
@@ -29,12 +26,9 @@ interface Props {
|
|||||||
const UserSignUpForm = ({
|
const UserSignUpForm = ({
|
||||||
currentStep,
|
currentStep,
|
||||||
setCurrentStep,
|
setCurrentStep,
|
||||||
emailAuth,
|
authMethod,
|
||||||
setEmailAuth,
|
setAuthMethod,
|
||||||
oAuths,
|
oAuths,
|
||||||
oAuthLoginCompleted,
|
|
||||||
oauthUserEmail,
|
|
||||||
oauthUserName,
|
|
||||||
userData,
|
userData,
|
||||||
setUserData,
|
setUserData,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -44,7 +38,15 @@ const UserSignUpForm = ({
|
|||||||
setError,
|
setError,
|
||||||
getValues,
|
getValues,
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useForm<ITenantSignUpUserForm>();
|
} = useForm<ITenantSignUpUserForm>({
|
||||||
|
defaultValues: {
|
||||||
|
fullName: userData.fullName,
|
||||||
|
email: userData.email,
|
||||||
|
password: userData.password,
|
||||||
|
passwordConfirmation: userData.passwordConfirmation,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
|
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
|
||||||
if (data.password !== data.passwordConfirmation) {
|
if (data.password !== data.passwordConfirmation) {
|
||||||
setError('passwordConfirmation', I18n.t('common.validations.password_mismatch'));
|
setError('passwordConfirmation', I18n.t('common.validations.password_mismatch'));
|
||||||
@@ -60,12 +62,15 @@ const UserSignUpForm = ({
|
|||||||
<h3>Create user account</h3>
|
<h3>Create user account</h3>
|
||||||
|
|
||||||
{
|
{
|
||||||
currentStep === 1 && !emailAuth &&
|
currentStep === 1 && authMethod == 'none' &&
|
||||||
<>
|
<>
|
||||||
<Button className="emailAuth" onClick={() => setEmailAuth(true)}>
|
<Button className="emailAuth" onClick={() => setAuthMethod('email')}>
|
||||||
Sign up with email
|
Sign up with email
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{ oAuths.length > 0 && <hr /> }
|
||||||
|
|
||||||
|
<div className="oauthProviderList">
|
||||||
{
|
{
|
||||||
oAuths.filter(oAuth => oAuth.isEnabled).map((oAuth, i) =>
|
oAuths.filter(oAuth => oAuth.isEnabled).map((oAuth, i) =>
|
||||||
<OAuthProviderLink
|
<OAuthProviderLink
|
||||||
@@ -78,18 +83,19 @@ const UserSignUpForm = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
currentStep === 1 && emailAuth &&
|
currentStep === 1 && (authMethod == 'email' || authMethod == 'oauth') &&
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<ActionLink
|
<ActionLink
|
||||||
onClick={() => setEmailAuth(false)}
|
onClick={() => setAuthMethod('none')}
|
||||||
icon={<BackIcon />}
|
icon={<BackIcon />}
|
||||||
customClass="backButton"
|
customClass="backButton"
|
||||||
>
|
>
|
||||||
{I18n.t('common.buttons.back')}
|
Use another method
|
||||||
</ActionLink>
|
</ActionLink>
|
||||||
|
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
@@ -106,6 +112,7 @@ const UserSignUpForm = ({
|
|||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
<input
|
<input
|
||||||
{...register('email', { required: true, pattern: EMAIL_REGEX })}
|
{...register('email', { required: true, pattern: EMAIL_REGEX })}
|
||||||
|
disabled={authMethod == 'oauth'}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={getLabel('user', 'email')}
|
placeholder={getLabel('user', 'email')}
|
||||||
id="userEmail"
|
id="userEmail"
|
||||||
@@ -117,6 +124,8 @@ const UserSignUpForm = ({
|
|||||||
</DangerText>
|
</DangerText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
authMethod == 'email' &&
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
<div className="userPasswordDiv">
|
<div className="userPasswordDiv">
|
||||||
<input
|
<input
|
||||||
@@ -140,6 +149,7 @@ const UserSignUpForm = ({
|
|||||||
<DangerText>{ errors.passwordConfirmation && I18n.t('common.validations.password_mismatch') }</DangerText>
|
<DangerText>{ errors.passwordConfirmation && I18n.t('common.validations.password_mismatch') }</DangerText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => null}
|
onClick={() => null}
|
||||||
@@ -151,9 +161,9 @@ const UserSignUpForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
currentStep === 2 && !oAuthLoginCompleted &&
|
currentStep === 2 &&
|
||||||
<p className="userRecap">
|
<p className="userRecap">
|
||||||
<b>{oAuthLoginCompleted ? oauthUserName : userData.fullName}</b> ({oAuthLoginCompleted ? oauthUserEmail : userData.email})
|
<b>{userData.fullName}</b> ({userData.email})
|
||||||
<ActionLink onClick={() => setCurrentStep(currentStep-1)} icon={<EditIcon />} customClass="editUser">Edit</ActionLink>
|
<ActionLink onClick={() => setCurrentStep(currentStep-1)} icon={<EditIcon />} customClass="editUser">Edit</ActionLink>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import I18n from 'i18n-js';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ActionLink from './ActionLink';
|
import ActionLink from './ActionLink';
|
||||||
import { CopyIcon, DoneIcon } from './Icons';
|
import { CopyIcon, DoneIcon } from './Icons';
|
||||||
|
import { SuccessText } from './CustomTexts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -40,7 +41,7 @@ const CopyToClipboardButton = ({
|
|||||||
</ActionLink>
|
</ActionLink>
|
||||||
:
|
:
|
||||||
<span style={{display: 'flex', marginRight: 12}}>
|
<span style={{display: 'flex', marginRight: 12}}>
|
||||||
{copiedLabel}
|
<SuccessText>{copiedLabel}</SuccessText>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import AuthenticationSiteSettingsP from "../components/SiteSettings/Authenticati
|
|||||||
import { ISiteSettingsOAuthForm } from "../components/SiteSettings/Authentication/OAuthForm";
|
import { ISiteSettingsOAuthForm } from "../components/SiteSettings/Authentication/OAuthForm";
|
||||||
import { IOAuth } from "../interfaces/IOAuth";
|
import { IOAuth } from "../interfaces/IOAuth";
|
||||||
import { State } from "../reducers/rootReducer";
|
import { State } from "../reducers/rootReducer";
|
||||||
|
import { updateDefaultOAuth } from "../actions/OAuth/updateDefaultOAuth";
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => ({
|
const mapStateToProps = (state: State) => ({
|
||||||
oAuths: state.oAuths,
|
oAuths: state.oAuths,
|
||||||
@@ -33,6 +34,10 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||||||
dispatch(updateOAuth({id, isEnabled, authenticityToken}));
|
dispatch(updateOAuth({id, isEnabled, authenticityToken}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onToggleEnabledDefaultOAuth(id: number, isEnabled: boolean, authenticityToken: string) {
|
||||||
|
dispatch(updateDefaultOAuth({id, isEnabled, authenticityToken}));
|
||||||
|
},
|
||||||
|
|
||||||
onDeleteOAuth(id: number, authenticityToken: string) {
|
onDeleteOAuth(id: number, authenticityToken: string) {
|
||||||
dispatch(deleteOAuth(id, authenticityToken));
|
dispatch(deleteOAuth(id, authenticityToken));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface IOAuth {
|
|||||||
|
|
||||||
callbackUrl?: string;
|
callbackUrl?: string;
|
||||||
tenantId?: number;
|
tenantId?: number;
|
||||||
|
defaultOAuthIsEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOAuthJSON {
|
export interface IOAuthJSON {
|
||||||
@@ -32,6 +33,7 @@ export interface IOAuthJSON {
|
|||||||
|
|
||||||
callback_url?: string;
|
callback_url?: string;
|
||||||
tenant_id?: string;
|
tenant_id?: string;
|
||||||
|
default_o_auth_is_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON): IOAuth => ({
|
export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON): IOAuth => ({
|
||||||
@@ -50,6 +52,7 @@ export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON): IOAuth => ({
|
|||||||
|
|
||||||
callbackUrl: oAuthJSON.callback_url,
|
callbackUrl: oAuthJSON.callback_url,
|
||||||
tenantId: oAuthJSON.tenant_id ? parseInt(oAuthJSON.tenant_id) : null,
|
tenantId: oAuthJSON.tenant_id ? parseInt(oAuthJSON.tenant_id) : null,
|
||||||
|
defaultOAuthIsEnabled: oAuthJSON.default_o_auth_is_enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const oAuthJS2JSON = (oAuth: IOAuth) => ({
|
export const oAuthJS2JSON = (oAuth: IOAuth) => ({
|
||||||
@@ -68,4 +71,5 @@ export const oAuthJS2JSON = (oAuth: IOAuth) => ({
|
|||||||
|
|
||||||
callback_url: oAuth.callbackUrl,
|
callback_url: oAuth.callbackUrl,
|
||||||
tenant_id: oAuth.tenantId,
|
tenant_id: oAuth.tenantId,
|
||||||
|
default_o_auth_is_enabled: oAuth.defaultOAuthIsEnabled,
|
||||||
});
|
});
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from '../actions/OAuth/deleteOAuth';
|
} from '../actions/OAuth/deleteOAuth';
|
||||||
|
|
||||||
import { IOAuth, oAuthJSON2JS } from '../interfaces/IOAuth';
|
import { IOAuth, oAuthJSON2JS } from '../interfaces/IOAuth';
|
||||||
|
import { DEFAULT_OAUTH_UPDATE_FAILURE, DEFAULT_OAUTH_UPDATE_START, DEFAULT_OAUTH_UPDATE_SUCCESS, DefaultOAuthUpdateActionTypes } from '../actions/OAuth/updateDefaultOAuth';
|
||||||
|
|
||||||
export interface OAuthsState {
|
export interface OAuthsState {
|
||||||
items: Array<IOAuth>;
|
items: Array<IOAuth>;
|
||||||
@@ -40,10 +41,12 @@ const oAuthsReducer = (
|
|||||||
OAuthsRequestActionTypes |
|
OAuthsRequestActionTypes |
|
||||||
OAuthSubmitActionTypes |
|
OAuthSubmitActionTypes |
|
||||||
OAuthUpdateActionTypes |
|
OAuthUpdateActionTypes |
|
||||||
OAuthDeleteActionTypes,
|
OAuthDeleteActionTypes |
|
||||||
|
DefaultOAuthUpdateActionTypes,
|
||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case OAUTHS_REQUEST_START:
|
case OAUTHS_REQUEST_START:
|
||||||
|
case DEFAULT_OAUTH_UPDATE_START:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
areLoading: true,
|
areLoading: true,
|
||||||
@@ -58,6 +61,7 @@ const oAuthsReducer = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
case OAUTHS_REQUEST_FAILURE:
|
case OAUTHS_REQUEST_FAILURE:
|
||||||
|
case DEFAULT_OAUTH_UPDATE_FAILURE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
areLoading: false,
|
areLoading: false,
|
||||||
@@ -79,6 +83,19 @@ const oAuthsReducer = (
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case DEFAULT_OAUTH_UPDATE_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: false,
|
||||||
|
items: state.items.map(oAuth => {
|
||||||
|
if (oAuth.id !== action.id) return oAuth;
|
||||||
|
return {
|
||||||
|
...oAuth,
|
||||||
|
defaultOAuthIsEnabled: action.isEnabled,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
case OAUTH_DELETE_SUCCESS:
|
case OAUTH_DELETE_SUCCESS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class OAuth < ApplicationRecord
|
|||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
include Rails.application.routes.url_helpers
|
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)) }
|
has_many :tenant_default_o_auths, dependent: :destroy
|
||||||
|
|
||||||
attr_accessor :state
|
attr_accessor :state
|
||||||
|
|
||||||
@@ -41,4 +41,25 @@ class OAuth < ApplicationRecord
|
|||||||
"scope=#{scope}&"\
|
"scope=#{scope}&"\
|
||||||
"state=#{state}"
|
"state=#{state}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def default_o_auth_is_enabled
|
||||||
|
is_default? and tenant_default_o_auths.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# returns all tenant-specific o_auths plus all default o_auths that are enabled site-wide
|
||||||
|
def include_all_defaults
|
||||||
|
unscoped.where(tenant_id: nil, is_enabled: true).or(where(tenant_id: Current.tenant))
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns all tenant-specific o_auths plus all default o_auths that are enabled both site-wide and for the current tenant
|
||||||
|
def include_defaults
|
||||||
|
unscoped.left_outer_joins(:tenant_default_o_auths).where(tenant_default_o_auths: { tenant_id: Current.tenant }, is_enabled: true).or(where(tenant_id: Current.tenant))
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns all default o_auths that are enabled site-wide
|
||||||
|
def include_only_defaults
|
||||||
|
unscoped.where(tenant_id: nil, is_enabled: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
class Tenant < ApplicationRecord
|
class Tenant < ApplicationRecord
|
||||||
has_one :tenant_setting, dependent: :destroy
|
has_one :tenant_setting, dependent: :destroy
|
||||||
has_many :boards, dependent: :destroy
|
has_many :boards, dependent: :destroy
|
||||||
has_many :o_auths, dependent: :destroy
|
|
||||||
has_many :post_statuses, dependent: :destroy
|
has_many :post_statuses, dependent: :destroy
|
||||||
has_many :posts, dependent: :destroy
|
has_many :posts, dependent: :destroy
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
|
|
||||||
|
has_many :o_auths, dependent: :destroy
|
||||||
|
# used to enable/disable a default oauth for a specific tenant
|
||||||
|
has_many :tenant_default_o_auths, dependent: :destroy
|
||||||
|
# used to query all globally enabled default oauths that are also enabled by the specific tenant
|
||||||
|
has_many :default_o_auths, -> { where tenant_id: nil, is_enabled: true }, through: :tenant_default_o_auths, source: :o_auth
|
||||||
|
|
||||||
enum status: [:active, :pending, :blocked]
|
enum status: [:active, :pending, :blocked]
|
||||||
|
|
||||||
after_initialize :set_default_status, if: :new_record?
|
after_initialize :set_default_status, if: :new_record?
|
||||||
|
|||||||
11
app/models/tenant_default_o_auth.rb
Normal file
11
app/models/tenant_default_o_auth.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# This is just a table to record whether tenant has
|
||||||
|
# enabled o_auth or not (and is used only for default
|
||||||
|
# o_auths, i.e. o_auths with tenant_id = nil, because
|
||||||
|
# they are available to multiple tenants and so their
|
||||||
|
# is_enabled column cannot be used)
|
||||||
|
|
||||||
|
class TenantDefaultOAuth < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
|
belongs_to :o_auth, -> { unscope(where: :tenant_id) }
|
||||||
|
end
|
||||||
@@ -19,6 +19,14 @@ class OAuthExchangeAuthCodeForProfileWorkflow
|
|||||||
@o_auth = o_auth
|
@o_auth = o_auth
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def request_profile(profile_url, access_token)
|
||||||
|
HTTParty.get(
|
||||||
|
profile_url,
|
||||||
|
headers: { "Authorization": "Bearer #{access_token}" },
|
||||||
|
format: :json
|
||||||
|
).parsed_response
|
||||||
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
return nil unless @o_auth and @o_auth.class == OAuth
|
return nil unless @o_auth and @o_auth.class == OAuth
|
||||||
return nil unless @authorization_code and @authorization_code.class == String
|
return nil unless @authorization_code and @authorization_code.class == String
|
||||||
@@ -41,16 +49,18 @@ class OAuthExchangeAuthCodeForProfileWorkflow
|
|||||||
access_token = token_response['access_token']
|
access_token = token_response['access_token']
|
||||||
|
|
||||||
# Exchange access token for profile info
|
# Exchange access token for profile info
|
||||||
profile_response = HTTParty.get(
|
profile_urls = @o_auth.profile_url.split(',')
|
||||||
@o_auth.profile_url,
|
if profile_urls.length == 1
|
||||||
headers: { "Authorization": "Bearer #{access_token}" },
|
profile_response = request_profile(profile_urls[0], access_token)
|
||||||
format: :json
|
else
|
||||||
).parsed_response
|
profile_response = {}
|
||||||
|
profile_urls.each_with_index do |profile_url, n|
|
||||||
|
profile_response["profile#{n}"] = request_profile(profile_url, access_token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return profile_response
|
return profile_response
|
||||||
rescue => error
|
rescue => error
|
||||||
logger.error { "Error in OAuthExchangeAuthCodeForProfileWorkflow: #{error}, o_auth: #{@o_auth.inspect}" }
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class OAuthSignInUserWorkflow
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
rescue => error
|
rescue => error
|
||||||
logger.error { "Error in OAuthSignInUserWorkflow: #{error}, o_auth: #{@o_auth.inspect}" }
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :tenants, only: [:show, :update]
|
resources :tenants, only: [:show, :update]
|
||||||
resources :users, only: [:index, :update]
|
resources :users, only: [:index, :update]
|
||||||
resources :o_auths, only: [:index, :create, :update, :destroy]
|
resources :o_auths, only: [:index, :create, :update, :destroy] do
|
||||||
|
resource :tenant_default_o_auths, only: [:create, :destroy]
|
||||||
|
end
|
||||||
get '/o_auths/:id/start', to: 'o_auths#start', as: :o_auth_start
|
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/: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
|
get '/o_auths/sign_in_from_oauth_token', to: 'o_auths#sign_in_from_oauth_token', as: :o_auth_sign_in_from_oauth_token
|
||||||
|
|||||||
10
db/migrate/20240303103945_create_tenant_default_o_auths.rb
Normal file
10
db/migrate/20240303103945_create_tenant_default_o_auths.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class CreateTenantDefaultOAuths < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :tenant_default_o_auths do |t|
|
||||||
|
t.references :tenant, null: false, foreign_key: true
|
||||||
|
t.references :o_auth, null: false, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
13
db/schema.rb
13
db/schema.rb
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2024_02_27_110058) do
|
ActiveRecord::Schema.define(version: 2024_03_03_103945) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -124,6 +124,15 @@ ActiveRecord::Schema.define(version: 2024_02_27_110058) do
|
|||||||
t.index ["user_id"], name: "index_posts_on_user_id"
|
t.index ["user_id"], name: "index_posts_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "tenant_default_o_auths", force: :cascade do |t|
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
|
t.bigint "o_auth_id", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["o_auth_id"], name: "index_tenant_default_o_auths_on_o_auth_id"
|
||||||
|
t.index ["tenant_id"], name: "index_tenant_default_o_auths_on_tenant_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "tenant_settings", force: :cascade do |t|
|
create_table "tenant_settings", force: :cascade do |t|
|
||||||
t.integer "brand_display", default: 0, null: false
|
t.integer "brand_display", default: 0, null: false
|
||||||
t.bigint "tenant_id", null: false
|
t.bigint "tenant_id", null: false
|
||||||
@@ -195,6 +204,8 @@ ActiveRecord::Schema.define(version: 2024_02_27_110058) do
|
|||||||
add_foreign_key "posts", "post_statuses"
|
add_foreign_key "posts", "post_statuses"
|
||||||
add_foreign_key "posts", "tenants"
|
add_foreign_key "posts", "tenants"
|
||||||
add_foreign_key "posts", "users"
|
add_foreign_key "posts", "users"
|
||||||
|
add_foreign_key "tenant_default_o_auths", "o_auths"
|
||||||
|
add_foreign_key "tenant_default_o_auths", "tenants"
|
||||||
add_foreign_key "tenant_settings", "tenants"
|
add_foreign_key "tenant_settings", "tenants"
|
||||||
add_foreign_key "users", "tenants"
|
add_foreign_key "users", "tenants"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,4 +12,19 @@ FactoryBot.define do
|
|||||||
json_user_name_path { "user.name" }
|
json_user_name_path { "user.name" }
|
||||||
json_user_email_path { "user.email" }
|
json_user_email_path { "user.email" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :default_o_auth, class: OAuth do
|
||||||
|
tenant { nil }
|
||||||
|
sequence(:name) { |n| "DefaultOAuth#{n}" }
|
||||||
|
logo { "https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg" }
|
||||||
|
is_enabled { false }
|
||||||
|
client_id { "123456" }
|
||||||
|
client_secret { "123456" }
|
||||||
|
authorize_url { "https://example.com/authorize" }
|
||||||
|
token_url { "https://example.com/token" }
|
||||||
|
profile_url { "https://example.com/profile" }
|
||||||
|
scope { "read" }
|
||||||
|
json_user_name_path { "user.name" }
|
||||||
|
json_user_email_path { "user.email" }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
6
spec/factories/tenant_default_o_auths.rb
Normal file
6
spec/factories/tenant_default_o_auths.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :tenant_default_o_auth do
|
||||||
|
tenant
|
||||||
|
o_auth
|
||||||
|
end
|
||||||
|
end
|
||||||
14
spec/models/tenant_default_o_auth_spec.rb
Normal file
14
spec/models/tenant_default_o_auth_spec.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe TenantDefaultOAuth, type: :model do
|
||||||
|
let(:tenant_default_o_auth) { FactoryBot.build(:tenant_default_o_auth) }
|
||||||
|
|
||||||
|
it 'is valid' do
|
||||||
|
expect(tenant_default_o_auth).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must have a o_auth_id' do
|
||||||
|
tenant_default_o_auth.o_auth = nil
|
||||||
|
expect(tenant_default_o_auth).to be_invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,8 @@ feature 'site settings: authentication', type: :system, js: true do
|
|||||||
let(:admin) { FactoryBot.create(:admin) }
|
let(:admin) { FactoryBot.create(:admin) }
|
||||||
|
|
||||||
let(:o_auth) { FactoryBot.create(:o_auth) }
|
let(:o_auth) { FactoryBot.create(:o_auth) }
|
||||||
|
let(:disabled_default_o_auth) { FactoryBot.create(:default_o_auth, is_enabled: false) }
|
||||||
|
let(:enabled_default_o_auth) { FactoryBot.create(:default_o_auth, is_enabled: true) }
|
||||||
|
|
||||||
let(:o_auths_list_selector) { '.oAuthsList' }
|
let(:o_auths_list_selector) { '.oAuthsList' }
|
||||||
let(:o_auth_list_item_selector) { '.oAuthListItem' }
|
let(:o_auth_list_item_selector) { '.oAuthListItem' }
|
||||||
@@ -26,6 +28,18 @@ feature 'site settings: authentication', type: :system, js: true do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'lets view existing default oauths, if enabled' do
|
||||||
|
disabled_default_o_auth # should not be visible
|
||||||
|
enabled_default_o_auth # should be visible
|
||||||
|
|
||||||
|
visit site_settings_authentication_path
|
||||||
|
|
||||||
|
within o_auths_list_selector do
|
||||||
|
expect(page).to have_content(/#{enabled_default_o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{disabled_default_o_auth.name}/i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'lets create new oauths' do
|
it 'lets create new oauths' do
|
||||||
n_of_o_auths = OAuth.count
|
n_of_o_auths = OAuth.count
|
||||||
new_o_auth_name = 'My new oauth'
|
new_o_auth_name = 'My new oauth'
|
||||||
|
|||||||
49
spec/system/user_o_auth_sign_up_and_log_in.rb
Normal file
49
spec/system/user_o_auth_sign_up_and_log_in.rb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
feature 'oauth sign up / log in', type: :system, js: true do
|
||||||
|
let(:o_auth) { FactoryBot.create(:o_auth, is_enabled: true) }
|
||||||
|
let(:disabled_o_auth) { FactoryBot.create(:o_auth, is_enabled: false) }
|
||||||
|
let(:default_o_auth) { FactoryBot.create(:default_o_auth, is_enabled: true) }
|
||||||
|
let(:disabled_default_o_auth) { FactoryBot.create(:default_o_auth, is_enabled: false) }
|
||||||
|
|
||||||
|
let(:o_auth_button_selector) { '.oauthProviderBtn' }
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
o_auth
|
||||||
|
disabled_o_auth
|
||||||
|
default_o_auth
|
||||||
|
disabled_default_o_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows sign up links for enabled oauths' do
|
||||||
|
visit new_user_registration_path
|
||||||
|
|
||||||
|
expect(page).to have_css(o_auth_button_selector, count: 1)
|
||||||
|
expect(page).to have_content(/#{o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{default_o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{disabled_o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{disabled_default_o_auth.name}/i)
|
||||||
|
|
||||||
|
OAuth.tenant_default_o_auths.create
|
||||||
|
|
||||||
|
visit new_user_registration_path
|
||||||
|
expect(page).to have_css(o_auth_button_selector, count: 2)
|
||||||
|
expect(page).to have_content(/#{default_o_auth.name}/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows log in links for enabled oauths' do
|
||||||
|
visit new_user_session_path
|
||||||
|
|
||||||
|
expect(page).to have_css(o_auth_button_selector, count: 1)
|
||||||
|
expect(page).to have_content(/#{o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{default_o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{disabled_o_auth.name}/i)
|
||||||
|
expect(page).not_to have_content(/#{disabled_default_o_auth.name}/i)
|
||||||
|
|
||||||
|
OAuth.tenant_default_o_auths.create
|
||||||
|
|
||||||
|
visit new_user_session_path
|
||||||
|
expect(page).to have_css(o_auth_button_selector, count: 2)
|
||||||
|
expect(page).to have_content(/#{default_o_auth.name}/i)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user