mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Add OAuth2 authentication (#147)
- Added Site settings > Authentication section - Create/edit/delete your custom oauth2 configurations - Login or signup with oauth2
This commit is contained in:
committed by
GitHub
parent
3bda6dee08
commit
4c73b398e8
@@ -47,6 +47,12 @@ class ApplicationController < ActionController::Base
|
||||
I18n.locale = @tenant.locale
|
||||
end
|
||||
|
||||
def load_oauths
|
||||
@o_auths = Current.tenant_or_raise!.o_auths
|
||||
.where(is_enabled: true)
|
||||
.order(created_at: :asc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_not_authorized
|
||||
|
||||
136
app/controllers/o_auths_controller.rb
Normal file
136
app/controllers/o_auths_controller.rb
Normal file
@@ -0,0 +1,136 @@
|
||||
class OAuthsController < ApplicationController
|
||||
include HTTParty
|
||||
include OAuthsHelper
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :authenticate_admin, only: [:index, :create, :update, :destroy]
|
||||
|
||||
# [subdomain.]base_url/o_auths/:id/start?reason=user|test
|
||||
# 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?
|
||||
|
||||
# Generate random state + other query params
|
||||
token_state = "#{params[:reason]}|#{Devise.friendly_token(30)}"
|
||||
session[:token_state] = token_state
|
||||
@o_auth.state = token_state
|
||||
|
||||
redirect_to @o_auth.authorize_url_with_query_params
|
||||
end
|
||||
|
||||
# [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('|')
|
||||
|
||||
return unless session[:token_state] == params[:state]
|
||||
|
||||
@o_auth = OAuth.find(params[:id])
|
||||
|
||||
user_profile = OAuthExchangeAuthCodeForProfile.new(
|
||||
authorization_code: params[:code],
|
||||
o_auth: @o_auth
|
||||
).run
|
||||
|
||||
if reason == 'user'
|
||||
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
|
||||
else
|
||||
flash[:alert] = I18n.t('errors.o_auth_login_error', { name: @o_auth.name })
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
elsif reason == 'test'
|
||||
unless user_signed_in? and current_user.admin?
|
||||
flash[:alert] = I18n.t('errors.unauthorized')
|
||||
redirect_to root_url
|
||||
return
|
||||
end
|
||||
|
||||
@user_profile = user_profile
|
||||
@user_email = query_path_from_hash(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?
|
||||
|
||||
render 'o_auths/test', layout: false
|
||||
else
|
||||
flash[:alert] = I18n.t('errors.unknown')
|
||||
redirect_to root_url
|
||||
end
|
||||
end
|
||||
|
||||
### CRUD actions below ###
|
||||
|
||||
def index
|
||||
authorize OAuth
|
||||
|
||||
@o_auths = OAuth.order(created_at: :asc)
|
||||
|
||||
render json: to_json_custom(@o_auths)
|
||||
end
|
||||
|
||||
def create
|
||||
@o_auth = OAuth.new
|
||||
@o_auth.assign_attributes(o_auth_params)
|
||||
authorize @o_auth
|
||||
|
||||
if @o_auth.save
|
||||
render json: to_json_custom(@o_auth), status: :created
|
||||
else
|
||||
render json: {
|
||||
error: @o_auth.errors.full_messages
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@o_auth = OAuth.find(params[:id])
|
||||
authorize @o_auth
|
||||
|
||||
if @o_auth.update(o_auth_params)
|
||||
render json: to_json_custom(@o_auth)
|
||||
else
|
||||
render json: {
|
||||
error: @o_auth.errors.full_messages
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@o_auth = OAuth.find(params[:id])
|
||||
authorize @o_auth
|
||||
|
||||
if @o_auth.destroy
|
||||
render json: {
|
||||
id: params[:id]
|
||||
}, status: :accepted
|
||||
else
|
||||
render json: {
|
||||
error: @o_auth.errors.full_messages
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_json_custom(o_auth)
|
||||
o_auth.as_json(
|
||||
methods: :callback_url,
|
||||
except: [:client_secret]
|
||||
)
|
||||
end
|
||||
|
||||
def o_auth_params
|
||||
params
|
||||
.require(:o_auth)
|
||||
.permit(policy(@o_auth).permitted_attributes)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
# Needed to have Current.tenant available in Devise's controllers
|
||||
prepend_before_action :load_tenant_data
|
||||
before_action :load_oauths, only: [:new]
|
||||
|
||||
# Override destroy to soft delete
|
||||
def destroy
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class SessionsController < Devise::SessionsController
|
||||
# Needed to have Current.tenant available in Devise's controllers
|
||||
prepend_before_action :load_tenant_data
|
||||
before_action :load_oauths, only: [:new]
|
||||
end
|
||||
@@ -1,8 +1,11 @@
|
||||
class SiteSettingsController < ApplicationController
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :authenticate_admin, only: [:general, :boards, :post_statuses, :roadmap]
|
||||
before_action :authenticate_power_user, only: [:users]
|
||||
before_action :authenticate_admin,
|
||||
only: [:general, :boards, :post_statuses, :roadmap, :authentication]
|
||||
|
||||
before_action :authenticate_power_user,
|
||||
only: [:users]
|
||||
|
||||
def general
|
||||
end
|
||||
@@ -18,4 +21,7 @@ class SiteSettingsController < ApplicationController
|
||||
|
||||
def users
|
||||
end
|
||||
|
||||
def authentication
|
||||
end
|
||||
end
|
||||
@@ -30,6 +30,7 @@ module ApplicationHelper
|
||||
|
||||
def add_subdomain_to(url_helper, resource=nil, options={})
|
||||
options[:subdomain] = Current.tenant_or_raise!.subdomain if Rails.application.multi_tenancy?
|
||||
options[:host] = Rails.application.base_url
|
||||
|
||||
resource ? url_helper.call(resource, options) : url_helper.call(options)
|
||||
end
|
||||
|
||||
26
app/helpers/o_auths_helper.rb
Normal file
26
app/helpers/o_auths_helper.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
path_array.each do |selector|
|
||||
break if hash == nil
|
||||
|
||||
hash = hash[selector]
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
end
|
||||
69
app/javascript/actions/OAuth/deleteOAuth.ts
Normal file
69
app/javascript/actions/OAuth/deleteOAuth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Action } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import HttpStatus from "../../constants/http_status";
|
||||
|
||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||
import { State } from "../../reducers/rootReducer";
|
||||
|
||||
export const OAUTH_DELETE_START = 'OAUTH_DELETE_START';
|
||||
interface OAuthDeleteStartAction {
|
||||
type: typeof OAUTH_DELETE_START;
|
||||
}
|
||||
|
||||
export const OAUTH_DELETE_SUCCESS = 'OAUTH_DELETE_SUCCESS';
|
||||
interface OAuthDeleteSuccessAction {
|
||||
type: typeof OAUTH_DELETE_SUCCESS;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const OAUTH_DELETE_FAILURE = 'OAUTH_DELETE_FAILURE';
|
||||
interface OAuthDeleteFailureAction {
|
||||
type: typeof OAUTH_DELETE_FAILURE;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type OAuthDeleteActionTypes =
|
||||
OAuthDeleteStartAction |
|
||||
OAuthDeleteSuccessAction |
|
||||
OAuthDeleteFailureAction;
|
||||
|
||||
const oAuthDeleteStart = (): OAuthDeleteStartAction => ({
|
||||
type: OAUTH_DELETE_START,
|
||||
});
|
||||
|
||||
const oAuthDeleteSuccess = (
|
||||
id: number,
|
||||
): OAuthDeleteSuccessAction => ({
|
||||
type: OAUTH_DELETE_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
const oAuthDeleteFailure = (error: string): OAuthDeleteFailureAction => ({
|
||||
type: OAUTH_DELETE_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const deleteOAuth = (
|
||||
id: number,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => (
|
||||
async (dispatch) => {
|
||||
dispatch(oAuthDeleteStart());
|
||||
|
||||
try {
|
||||
const res = await fetch(`/o_auths/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: buildRequestHeaders(authenticityToken),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status === HttpStatus.Accepted) {
|
||||
dispatch(oAuthDeleteSuccess(id));
|
||||
} else {
|
||||
dispatch(oAuthDeleteFailure(json.error));
|
||||
}
|
||||
} catch (e) {
|
||||
dispatch(oAuthDeleteFailure(e));
|
||||
}
|
||||
}
|
||||
);
|
||||
60
app/javascript/actions/OAuth/requestOAuths.ts
Normal file
60
app/javascript/actions/OAuth/requestOAuths.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Action } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import { IOAuthJSON } from '../../interfaces/IOAuth';
|
||||
|
||||
import { State } from '../../reducers/rootReducer';
|
||||
|
||||
export const OAUTHS_REQUEST_START = 'OAUTHS_REQUEST_START';
|
||||
interface OAuthsRequestStartAction {
|
||||
type: typeof OAUTHS_REQUEST_START;
|
||||
}
|
||||
|
||||
export const OAUTHS_REQUEST_SUCCESS = 'OAUTHS_REQUEST_SUCCESS';
|
||||
interface OAuthsRequestSuccessAction {
|
||||
type: typeof OAUTHS_REQUEST_SUCCESS;
|
||||
oAuths: Array<IOAuthJSON>;
|
||||
}
|
||||
|
||||
export const OAUTHS_REQUEST_FAILURE = 'OAUTHS_REQUEST_FAILURE';
|
||||
interface OAuthsRequestFailureAction {
|
||||
type: typeof OAUTHS_REQUEST_FAILURE;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type OAuthsRequestActionTypes =
|
||||
OAuthsRequestStartAction |
|
||||
OAuthsRequestSuccessAction |
|
||||
OAuthsRequestFailureAction;
|
||||
|
||||
|
||||
const oAuthsRequestStart = (): OAuthsRequestActionTypes => ({
|
||||
type: OAUTHS_REQUEST_START,
|
||||
});
|
||||
|
||||
const oAuthsRequestSuccess = (
|
||||
oAuths: Array<IOAuthJSON>
|
||||
): OAuthsRequestActionTypes => ({
|
||||
type: OAUTHS_REQUEST_SUCCESS,
|
||||
oAuths,
|
||||
});
|
||||
|
||||
const oAuthsRequestFailure = (error: string): OAuthsRequestActionTypes => ({
|
||||
type: OAUTHS_REQUEST_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestOAuths = (): ThunkAction<void, State, null, Action<string>> => (
|
||||
async (dispatch) => {
|
||||
dispatch(oAuthsRequestStart());
|
||||
|
||||
try {
|
||||
const response = await fetch('/o_auths');
|
||||
const json = await response.json();
|
||||
|
||||
dispatch(oAuthsRequestSuccess(json));
|
||||
} catch (e) {
|
||||
dispatch(oAuthsRequestFailure(e));
|
||||
}
|
||||
}
|
||||
)
|
||||
78
app/javascript/actions/OAuth/submitOAuth.ts
Normal file
78
app/javascript/actions/OAuth/submitOAuth.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Action } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
|
||||
import HttpStatus from "../../constants/http_status";
|
||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||
import { IOAuth, IOAuthJSON, oAuthJS2JSON } from "../../interfaces/IOAuth";
|
||||
import { State } from "../../reducers/rootReducer";
|
||||
|
||||
export const OAUTH_SUBMIT_START = 'OAUTH_SUBMIT_START';
|
||||
interface OAuthSubmitStartAction {
|
||||
type: typeof OAUTH_SUBMIT_START;
|
||||
}
|
||||
|
||||
export const OAUTH_SUBMIT_SUCCESS = 'OAUTH_SUBMIT_SUCCESS';
|
||||
interface OAuthSubmitSuccessAction {
|
||||
type: typeof OAUTH_SUBMIT_SUCCESS;
|
||||
oAuth: IOAuthJSON;
|
||||
}
|
||||
|
||||
export const OAUTH_SUBMIT_FAILURE = 'OAUTH_SUBMIT_FAILURE';
|
||||
interface OAuthSubmitFailureAction {
|
||||
type: typeof OAUTH_SUBMIT_FAILURE;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type OAuthSubmitActionTypes =
|
||||
OAuthSubmitStartAction |
|
||||
OAuthSubmitSuccessAction |
|
||||
OAuthSubmitFailureAction;
|
||||
|
||||
const oAuthSubmitStart = (): OAuthSubmitStartAction => ({
|
||||
type: OAUTH_SUBMIT_START,
|
||||
});
|
||||
|
||||
const oAuthSubmitSuccess = (
|
||||
oAuthJSON: IOAuthJSON,
|
||||
): OAuthSubmitSuccessAction => ({
|
||||
type: OAUTH_SUBMIT_SUCCESS,
|
||||
oAuth: oAuthJSON,
|
||||
});
|
||||
|
||||
const oAuthSubmitFailure = (error: string): OAuthSubmitFailureAction => ({
|
||||
type: OAUTH_SUBMIT_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const submitOAuth = (
|
||||
oAuth: IOAuth,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(oAuthSubmitStart());
|
||||
|
||||
try {
|
||||
const res = await fetch(`/o_auths`, {
|
||||
method: 'POST',
|
||||
headers: buildRequestHeaders(authenticityToken),
|
||||
body: JSON.stringify({
|
||||
o_auth: {
|
||||
...oAuthJS2JSON(oAuth),
|
||||
is_enabled: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status === HttpStatus.Created) {
|
||||
dispatch(oAuthSubmitSuccess(json));
|
||||
} else {
|
||||
dispatch(oAuthSubmitFailure(json.error));
|
||||
}
|
||||
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
dispatch(oAuthSubmitFailure(e));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
98
app/javascript/actions/OAuth/updateOAuth.ts
Normal file
98
app/javascript/actions/OAuth/updateOAuth.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Action } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import { ISiteSettingsOAuthForm } from "../../components/SiteSettings/Authentication/OAuthForm";
|
||||
import HttpStatus from "../../constants/http_status";
|
||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||
import { IOAuthJSON } from "../../interfaces/IOAuth";
|
||||
import { State } from "../../reducers/rootReducer";
|
||||
|
||||
export const OAUTH_UPDATE_START = 'OAUTH_UPDATE_START';
|
||||
interface OAuthUpdateStartAction {
|
||||
type: typeof OAUTH_UPDATE_START;
|
||||
}
|
||||
|
||||
export const OAUTH_UPDATE_SUCCESS = 'OAUTH_UPDATE_SUCCESS';
|
||||
interface OAuthUpdateSuccessAction {
|
||||
type: typeof OAUTH_UPDATE_SUCCESS;
|
||||
oAuth: IOAuthJSON;
|
||||
}
|
||||
|
||||
export const OAUTH_UPDATE_FAILURE = 'OAUTH_UPDATE_FAILURE';
|
||||
interface OAuthUpdateFailureAction {
|
||||
type: typeof OAUTH_UPDATE_FAILURE;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type OAuthUpdateActionTypes =
|
||||
OAuthUpdateStartAction |
|
||||
OAuthUpdateSuccessAction |
|
||||
OAuthUpdateFailureAction;
|
||||
|
||||
const oAuthUpdateStart = (): OAuthUpdateStartAction => ({
|
||||
type: OAUTH_UPDATE_START,
|
||||
});
|
||||
|
||||
const oAuthUpdateSuccess = (
|
||||
oAuthJSON: IOAuthJSON,
|
||||
): OAuthUpdateSuccessAction => ({
|
||||
type: OAUTH_UPDATE_SUCCESS,
|
||||
oAuth: oAuthJSON,
|
||||
});
|
||||
|
||||
const oAuthUpdateFailure = (error: string): OAuthUpdateFailureAction => ({
|
||||
type: OAUTH_UPDATE_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
interface UpdateOAuthParams {
|
||||
id: number;
|
||||
form?: ISiteSettingsOAuthForm;
|
||||
isEnabled?: boolean;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
export const updateOAuth = ({
|
||||
id,
|
||||
form = null,
|
||||
isEnabled = null,
|
||||
authenticityToken,
|
||||
}: UpdateOAuthParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(oAuthUpdateStart());
|
||||
|
||||
const o_auth = Object.assign({},
|
||||
form !== null ? {
|
||||
name: form.name,
|
||||
logo: form.logo,
|
||||
client_id: form.clientId,
|
||||
client_secret: form.clientSecret,
|
||||
authorize_url: form.authorizeUrl,
|
||||
token_url: form.tokenUrl,
|
||||
profile_url: form.profileUrl,
|
||||
scope: form.scope,
|
||||
json_user_email_path: form.jsonUserEmailPath,
|
||||
json_user_name_path: form.jsonUserNamePath,
|
||||
} : null,
|
||||
isEnabled !== null ? {is_enabled: isEnabled} : null,
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/o_auths/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: buildRequestHeaders(authenticityToken),
|
||||
body: JSON.stringify({o_auth}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status === HttpStatus.OK) {
|
||||
dispatch(oAuthUpdateSuccess(json));
|
||||
} else {
|
||||
dispatch(oAuthUpdateFailure(json.error));
|
||||
}
|
||||
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
dispatch(oAuthUpdateFailure(e));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import Box from '../../common/Box';
|
||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||
import OAuthForm, { ISiteSettingsOAuthForm } from './OAuthForm';
|
||||
import Spinner from '../../common/Spinner';
|
||||
import { DangerText } from '../../common/CustomTexts';
|
||||
import { IOAuth } from '../../../interfaces/IOAuth';
|
||||
|
||||
interface Props {
|
||||
handleSubmitOAuth(oAuth: IOAuth): void;
|
||||
handleUpdateOAuth(id: number, form: ISiteSettingsOAuthForm): void;
|
||||
isSubmitting: boolean;
|
||||
submitError: string;
|
||||
selectedOAuth: IOAuth;
|
||||
page: AuthenticationPages;
|
||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||
}
|
||||
|
||||
const AuthenticationFormPage = ({
|
||||
handleSubmitOAuth,
|
||||
handleUpdateOAuth,
|
||||
isSubmitting,
|
||||
submitError,
|
||||
selectedOAuth,
|
||||
page,
|
||||
setPage,
|
||||
}: Props) => (
|
||||
<>
|
||||
<Box customClass="authenticationFormPage">
|
||||
<OAuthForm
|
||||
handleSubmitOAuth={handleSubmitOAuth}
|
||||
handleUpdateOAuth={handleUpdateOAuth}
|
||||
selectedOAuth={selectedOAuth}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
/>
|
||||
|
||||
{ isSubmitting && <Spinner /> }
|
||||
{ submitError && <DangerText>{submitError}</DangerText> }
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
export default AuthenticationFormPage;
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import Box from '../../common/Box';
|
||||
import OAuthProvidersList from './OAuthProvidersList';
|
||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||
import { OAuthsState } from '../../../reducers/oAuthsReducer';
|
||||
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
|
||||
|
||||
interface Props {
|
||||
oAuths: OAuthsState;
|
||||
isSubmitting: boolean;
|
||||
submitError: string;
|
||||
|
||||
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
||||
handleDeleteOAuth(id: number): void;
|
||||
|
||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const AuthenticationIndexPage = ({
|
||||
oAuths,
|
||||
isSubmitting,
|
||||
submitError,
|
||||
|
||||
handleToggleEnabledOAuth,
|
||||
handleDeleteOAuth,
|
||||
|
||||
setPage,
|
||||
setSelectedOAuth,
|
||||
}: Props) => (
|
||||
<>
|
||||
<Box customClass="authenticationIndexPage">
|
||||
<h2>{ I18n.t('site_settings.authentication.title') }</h2>
|
||||
|
||||
<OAuthProvidersList
|
||||
oAuths={oAuths.items}
|
||||
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
||||
handleDeleteOAuth={handleDeleteOAuth}
|
||||
setPage={setPage}
|
||||
setSelectedOAuth={setSelectedOAuth}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<SiteSettingsInfoBox
|
||||
areUpdating={oAuths.areLoading || isSubmitting}
|
||||
error={oAuths.error || submitError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export default AuthenticationIndexPage;
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import HttpStatus from '../../../constants/http_status';
|
||||
import { IOAuth } from '../../../interfaces/IOAuth';
|
||||
import { OAuthsState } from '../../../reducers/oAuthsReducer';
|
||||
|
||||
import AuthenticationFormPage from './AuthenticationFormPage';
|
||||
import AuthenticationIndexPage from './AuthenticationIndexPage';
|
||||
import { ISiteSettingsOAuthForm } from './OAuthForm';
|
||||
|
||||
interface Props {
|
||||
oAuths: OAuthsState;
|
||||
|
||||
requestOAuths(): void;
|
||||
onSubmitOAuth(oAuth: IOAuth, authenticityToken: string): Promise<any>;
|
||||
onUpdateOAuth(id: number, form: ISiteSettingsOAuthForm, authenticityToken: string): Promise<any>;
|
||||
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
|
||||
onDeleteOAuth(id: number, authenticityToken: string): void;
|
||||
|
||||
isSubmitting: boolean;
|
||||
submitError: string;
|
||||
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
export type AuthenticationPages = 'index' | 'new' | 'edit';
|
||||
|
||||
const AuthenticationSiteSettingsP = ({
|
||||
oAuths,
|
||||
requestOAuths,
|
||||
onSubmitOAuth,
|
||||
onUpdateOAuth,
|
||||
onToggleEnabledOAuth,
|
||||
onDeleteOAuth,
|
||||
isSubmitting,
|
||||
submitError,
|
||||
authenticityToken,
|
||||
}: Props) => {
|
||||
const [page, setPage] = useState<AuthenticationPages>('index');
|
||||
const [selectedOAuth, setSelectedOAuth] = useState<number>(null);
|
||||
|
||||
useEffect(requestOAuths, []);
|
||||
|
||||
const handleSubmitOAuth = (oAuth: IOAuth) => {
|
||||
onSubmitOAuth(oAuth, authenticityToken).then(res => {
|
||||
if (res?.status === HttpStatus.Created) setPage('index');
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateOAuth = (id: number, form: ISiteSettingsOAuthForm) => {
|
||||
onUpdateOAuth(id, form, authenticityToken).then(res => {
|
||||
if (res?.status === HttpStatus.OK) setPage('index');
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleEnabledOAuth = (id: number, enabled: boolean) => {
|
||||
onToggleEnabledOAuth(id, enabled, authenticityToken);
|
||||
};
|
||||
|
||||
const handleDeleteOAuth = (id: number) => {
|
||||
onDeleteOAuth(id, authenticityToken);
|
||||
};
|
||||
|
||||
return (
|
||||
page === 'index' ?
|
||||
<AuthenticationIndexPage
|
||||
oAuths={oAuths}
|
||||
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
||||
handleDeleteOAuth={handleDeleteOAuth}
|
||||
setPage={setPage}
|
||||
setSelectedOAuth={setSelectedOAuth}
|
||||
isSubmitting={isSubmitting}
|
||||
submitError={submitError}
|
||||
/>
|
||||
:
|
||||
<AuthenticationFormPage
|
||||
handleSubmitOAuth={handleSubmitOAuth}
|
||||
handleUpdateOAuth={handleUpdateOAuth}
|
||||
isSubmitting={isSubmitting}
|
||||
submitError={submitError}
|
||||
selectedOAuth={oAuths.items.find(oAuth => oAuth.id === selectedOAuth)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticationSiteSettingsP;
|
||||
@@ -0,0 +1,247 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { DangerText } from '../../common/CustomTexts';
|
||||
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
||||
import Button from '../../common/Button';
|
||||
import { URL_REGEX } from '../../../constants/regex';
|
||||
import { IOAuth } from '../../../interfaces/IOAuth';
|
||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||
import { useState } from 'react';
|
||||
import Separator from '../../common/Separator';
|
||||
|
||||
interface Props {
|
||||
selectedOAuth: IOAuth;
|
||||
page: AuthenticationPages;
|
||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||
|
||||
handleSubmitOAuth(oAuth: IOAuth): void;
|
||||
handleUpdateOAuth(id: number, form: ISiteSettingsOAuthForm): void;
|
||||
}
|
||||
|
||||
export interface ISiteSettingsOAuthForm {
|
||||
name: string;
|
||||
logo: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
authorizeUrl: string;
|
||||
tokenUrl: string;
|
||||
profileUrl: string;
|
||||
scope: string;
|
||||
jsonUserEmailPath: string;
|
||||
jsonUserNamePath: string;
|
||||
}
|
||||
|
||||
const OAuthForm = ({
|
||||
selectedOAuth,
|
||||
page,
|
||||
setPage,
|
||||
|
||||
handleSubmitOAuth,
|
||||
handleUpdateOAuth,
|
||||
}: Props) => {
|
||||
const [editClientSecret, setEditClientSecret] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty }
|
||||
} = useForm<ISiteSettingsOAuthForm>({
|
||||
defaultValues: page === 'new' ? {
|
||||
name: '',
|
||||
logo: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
authorizeUrl: '',
|
||||
tokenUrl: '',
|
||||
profileUrl: '',
|
||||
scope: '',
|
||||
jsonUserEmailPath: '',
|
||||
jsonUserNamePath: '',
|
||||
} : {
|
||||
name: selectedOAuth.name,
|
||||
logo: selectedOAuth.logo,
|
||||
clientId: selectedOAuth.clientId,
|
||||
clientSecret: selectedOAuth.clientSecret,
|
||||
authorizeUrl: selectedOAuth.authorizeUrl,
|
||||
tokenUrl: selectedOAuth.tokenUrl,
|
||||
profileUrl: selectedOAuth.profileUrl,
|
||||
scope: selectedOAuth.scope,
|
||||
jsonUserEmailPath: selectedOAuth.jsonUserEmailPath,
|
||||
jsonUserNamePath: selectedOAuth.jsonUserNamePath,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<ISiteSettingsOAuthForm> = data => {
|
||||
const oAuth = { ...data, isEnabled: false };
|
||||
|
||||
if (page === 'new') {
|
||||
handleSubmitOAuth(oAuth);
|
||||
} else if (page === 'edit') {
|
||||
if (editClientSecret === false) {
|
||||
delete oAuth.clientSecret;
|
||||
}
|
||||
|
||||
handleUpdateOAuth(selectedOAuth.id, oAuth as ISiteSettingsOAuthForm);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
onClick={() => {
|
||||
let confirmation = true;
|
||||
if (isDirty)
|
||||
confirmation = confirm(I18n.t('common.unsaved_changes') + ' ' + I18n.t('common.confirmation'));
|
||||
if (confirmation) setPage('index');
|
||||
}}
|
||||
className="backButton link">
|
||||
← { I18n.t('common.buttons.back') }
|
||||
</a>
|
||||
<h2>{ I18n.t(`site_settings.authentication.form.title_${page}`) }</h2>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="formRow">
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="name">{ getLabel('o_auth', 'name') }</label>
|
||||
<input
|
||||
{...register('name', { required: true })}
|
||||
id="name"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.name && getValidationMessage(errors.name.type, 'o_auth', 'name')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="logo">{ getLabel('o_auth', 'logo') }</label>
|
||||
<input
|
||||
{...register('logo')}
|
||||
id="logo"
|
||||
className="formControl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>{ I18n.t('site_settings.authentication.form.subtitle_oauth_config') }</h5>
|
||||
<div className="formRow">
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="clientId">{ getLabel('o_auth', 'client_id') }</label>
|
||||
<input
|
||||
{...register('clientId', { required: true })}
|
||||
id="clientId"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.clientId && getValidationMessage(errors.clientId.type, 'o_auth', 'client_id')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="clientSecret">{ getLabel('o_auth', 'client_secret') }</label>
|
||||
<input
|
||||
{...register('clientSecret', { required: page === 'new' || (page === 'edit' && editClientSecret) })}
|
||||
id="clientSecret"
|
||||
className="formControl"
|
||||
disabled={page === 'edit' && editClientSecret === false}
|
||||
/>
|
||||
{
|
||||
page === 'edit' &&
|
||||
<>
|
||||
<small>
|
||||
{I18n.t('site_settings.authentication.form.client_secret_help') + "\t"}
|
||||
</small>
|
||||
<Separator />
|
||||
{
|
||||
editClientSecret ?
|
||||
<a onClick={() => setEditClientSecret(false)} className="link">{I18n.t('common.buttons.cancel')}</a>
|
||||
:
|
||||
<a onClick={() => setEditClientSecret(true)} className="link">{I18n.t('common.buttons.edit')}</a>
|
||||
}
|
||||
<br />
|
||||
</>
|
||||
|
||||
}
|
||||
<DangerText>{errors.clientSecret && getValidationMessage(errors.clientSecret.type, 'o_auth', 'client_secret')}</DangerText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="authorizeUrl">{ getLabel('o_auth', 'authorize_url') }</label>
|
||||
<input
|
||||
{...register('authorizeUrl', { required: true, pattern: URL_REGEX })}
|
||||
id="authorizeUrl"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.authorizeUrl?.type === 'required' && getValidationMessage(errors.authorizeUrl.type, 'o_auth', 'authorize_url')}</DangerText>
|
||||
<DangerText>{errors.authorizeUrl?.type === 'pattern' && I18n.t('common.validations.url')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="tokenUrl">{ getLabel('o_auth', 'token_url') }</label>
|
||||
<input
|
||||
{...register('tokenUrl', { required: true, pattern: URL_REGEX })}
|
||||
id="tokenUrl"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.tokenUrl?.type === 'required' && getValidationMessage(errors.tokenUrl.type, 'o_auth', 'token_url')}</DangerText>
|
||||
<DangerText>{errors.tokenUrl?.type === 'pattern' && I18n.t('common.validations.url')}</DangerText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formGroup">
|
||||
<label htmlFor="scope">{ getLabel('o_auth', 'scope') }</label>
|
||||
<input
|
||||
{...register('scope', { required: true })}
|
||||
id="scope"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.scope && getValidationMessage(errors.scope.type, 'o_auth', 'scope')}</DangerText>
|
||||
</div>
|
||||
|
||||
<h5>{ I18n.t('site_settings.authentication.form.subtitle_user_profile_config') }</h5>
|
||||
<div className="formGroup">
|
||||
<label htmlFor="profileUrl">{ getLabel('o_auth', 'profile_url') }</label>
|
||||
<input
|
||||
{...register('profileUrl', { required: true, pattern: URL_REGEX })}
|
||||
id="profileUrl"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.profileUrl?.type === 'required' && getValidationMessage(errors.profileUrl.type, 'o_auth', 'profile_url')}</DangerText>
|
||||
<DangerText>{errors.profileUrl?.type === 'pattern' && I18n.t('common.validations.url')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="jsonUserEmailPath">{ getLabel('o_auth', 'json_user_email_path') }</label>
|
||||
<input
|
||||
{...register('jsonUserEmailPath', { required: true })}
|
||||
id="jsonUserEmailPath"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>
|
||||
{errors.jsonUserEmailPath && getValidationMessage(errors.jsonUserEmailPath.type, 'o_auth', 'json_user_email_path')}
|
||||
</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="jsonUserNamePath">{ getLabel('o_auth', 'json_user_name_path') }</label>
|
||||
<input
|
||||
{...register('jsonUserNamePath')}
|
||||
id="jsonUserNamePath"
|
||||
className="formControl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => null}>
|
||||
{
|
||||
page === 'new' ?
|
||||
I18n.t('common.buttons.create')
|
||||
:
|
||||
I18n.t('common.buttons.update')
|
||||
}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OAuthForm;
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
oAuth: IOAuth;
|
||||
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
||||
handleDeleteOAuth(id: number): void;
|
||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const OAuthProviderItem = ({
|
||||
oAuth,
|
||||
handleToggleEnabledOAuth,
|
||||
handleDeleteOAuth,
|
||||
setPage,
|
||||
setSelectedOAuth,
|
||||
}: Props) => (
|
||||
<li className="oAuthListItem">
|
||||
<div className="oAuthInfo">
|
||||
<img src={oAuth.logo} className="oAuthLogo" width={42} height={42} />
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="oAuthActions">
|
||||
<CopyToClipboardButton
|
||||
label={I18n.t('site_settings.authentication.copy_url')}
|
||||
textToCopy={oAuth.callbackUrl}
|
||||
/>
|
||||
<Separator />
|
||||
<a onClick={() =>
|
||||
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
|
||||
}>
|
||||
Test
|
||||
</a>
|
||||
<Separator />
|
||||
<a onClick={() => {
|
||||
setSelectedOAuth(oAuth.id);
|
||||
setPage('edit');
|
||||
}}>
|
||||
{ I18n.t('common.buttons.edit') }
|
||||
</a>
|
||||
<Separator />
|
||||
<a onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}>
|
||||
{ I18n.t('common.buttons.delete') }
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
export default OAuthProviderItem;
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||
import Button from '../../common/Button';
|
||||
import { IOAuth } from '../../../interfaces/IOAuth';
|
||||
import OAuthProviderItem from './OAuthProviderItem';
|
||||
|
||||
interface Props {
|
||||
oAuths: Array<IOAuth>;
|
||||
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
|
||||
handleDeleteOAuth(id: number): void;
|
||||
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
|
||||
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const OAuthProvidersList = ({
|
||||
oAuths,
|
||||
handleToggleEnabledOAuth,
|
||||
handleDeleteOAuth,
|
||||
setPage,
|
||||
setSelectedOAuth,
|
||||
}: Props) => (
|
||||
<>
|
||||
<div className="oauthProvidersTitle">
|
||||
<h3>{ I18n.t('site_settings.authentication.oauth_subtitle') }</h3>
|
||||
<Button onClick={() => setPage('new')}>
|
||||
{ I18n.t('common.buttons.new') }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul className="oAuthsList">
|
||||
{
|
||||
oAuths.map((oAuth, i) => (
|
||||
<OAuthProviderItem
|
||||
oAuth={oAuth}
|
||||
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
|
||||
handleDeleteOAuth={handleDeleteOAuth}
|
||||
setPage={setPage}
|
||||
setSelectedOAuth={setSelectedOAuth}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
|
||||
export default OAuthProvidersList;
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
|
||||
import AuthenticationSiteSettings from '../../../containers/AuthenticationSiteSettings';
|
||||
import createStoreHelper from '../../../helpers/createStore';
|
||||
import { State } from '../../../reducers/rootReducer';
|
||||
|
||||
interface Props {
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
class AuthenticationSiteSettingsRoot extends React.Component<Props> {
|
||||
store: Store<State, any>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.store = createStoreHelper();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<AuthenticationSiteSettings
|
||||
authenticityToken={this.props.authenticityToken}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticationSiteSettingsRoot;
|
||||
@@ -67,7 +67,7 @@ const BoardForm = ({
|
||||
<input
|
||||
{...register('name', { required: true })}
|
||||
placeholder={I18n.t('site_settings.boards.form.name')}
|
||||
autoFocus
|
||||
autoFocus={mode === 'update'}
|
||||
className="formControl"
|
||||
/>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TENANT_BRAND_NONE,
|
||||
} from '../../../interfaces/ITenant';
|
||||
import { DangerText } from '../../common/CustomTexts';
|
||||
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
||||
|
||||
export interface ISiteSettingsGeneralForm {
|
||||
siteName: string;
|
||||
@@ -79,17 +80,17 @@ const GeneralSiteSettingsP = ({
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="formRow">
|
||||
<div className="formGroup col-4">
|
||||
<label htmlFor="siteName">{ I18n.t('site_settings.general.site_name') }</label>
|
||||
<label htmlFor="siteName">{ getLabel('tenant', 'site_name') }</label>
|
||||
<input
|
||||
{...register('siteName', { required: true })}
|
||||
id="siteName"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.siteName && I18n.t('site_settings.general.validations.site_name')}</DangerText>
|
||||
<DangerText>{errors.siteName && getValidationMessage(errors.siteName.type, 'tenant', 'site_name')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-4">
|
||||
<label htmlFor="siteLogo">{ I18n.t('site_settings.general.site_logo') }</label>
|
||||
<label htmlFor="siteLogo">{ getLabel('tenant', 'site_logo') }</label>
|
||||
<input
|
||||
{...register('siteLogo')}
|
||||
id="siteLogo"
|
||||
@@ -98,7 +99,7 @@ const GeneralSiteSettingsP = ({
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-4">
|
||||
<label htmlFor="brandSetting">{ I18n.t('site_settings.general.brand_setting') }</label>
|
||||
<label htmlFor="brandSetting">{ getLabel('tenant', 'brand_setting') }</label>
|
||||
<select
|
||||
{...register('brandDisplaySetting')}
|
||||
id="brandSetting"
|
||||
@@ -121,7 +122,7 @@ const GeneralSiteSettingsP = ({
|
||||
</div>
|
||||
|
||||
<div className="formGroup">
|
||||
<label htmlFor="locale">{ I18n.t('site_settings.general.locale') }</label>
|
||||
<label htmlFor="locale">{ getLabel('tenant', 'locale') }</label>
|
||||
<select
|
||||
{...register('locale')}
|
||||
id="locale"
|
||||
|
||||
@@ -71,7 +71,7 @@ const PostStatusForm = ({
|
||||
<input
|
||||
{...register('name', { required: true })}
|
||||
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
||||
autoFocus
|
||||
autoFocus={mode === 'update'}
|
||||
className="formControl"
|
||||
/>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
|
||||
import { UsersState } from '../../../reducers/usersReducer';
|
||||
import { UserRoles, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED } from '../../../interfaces/IUser';
|
||||
import HttpStatus from '../../../constants/http_status';
|
||||
import Spinner from '../../common/Spinner';
|
||||
|
||||
interface Props {
|
||||
users: UsersState;
|
||||
@@ -79,17 +80,20 @@ class UsersSiteSettingsP extends React.Component<Props> {
|
||||
|
||||
<ul className="usersList">
|
||||
{
|
||||
users.items.map((user, i) => (
|
||||
<UserEditable
|
||||
user={user}
|
||||
updateUserRole={this._handleUpdateUserRole}
|
||||
updateUserStatus={this._handleUpdateUserStatus}
|
||||
users.areLoading === false ?
|
||||
users.items.map((user, i) => (
|
||||
<UserEditable
|
||||
user={user}
|
||||
updateUserRole={this._handleUpdateUserRole}
|
||||
updateUserStatus={this._handleUpdateUserStatus}
|
||||
|
||||
currentUserEmail={currentUserEmail}
|
||||
currentUserRole={currentUserRole}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
currentUserEmail={currentUserEmail}
|
||||
currentUserRole={currentUserRole}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
</ul>
|
||||
</Box>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Spinner from '../common/Spinner';
|
||||
import { DangerText } from '../common/CustomTexts';
|
||||
import { ITenantSignUpTenantForm } from './TenantSignUpP';
|
||||
import HttpStatus from '../../constants/http_status';
|
||||
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
||||
|
||||
interface Props {
|
||||
isSubmitting: boolean;
|
||||
@@ -34,11 +35,11 @@ const TenantSignUpForm = ({
|
||||
<input
|
||||
{...register('siteName', { required: true })}
|
||||
autoFocus
|
||||
placeholder={I18n.t('signup.step2.site_name')}
|
||||
placeholder={getLabel('tenant', 'site_name')}
|
||||
id="tenantSiteName"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{errors.siteName && I18n.t('signup.step2.validations.site_name')}</DangerText>
|
||||
<DangerText>{errors.siteName?.type === 'required' && getValidationMessage('required', 'tenant', 'site_name')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
@@ -51,7 +52,7 @@ const TenantSignUpForm = ({
|
||||
return res.status === HttpStatus.OK;
|
||||
},
|
||||
})}
|
||||
placeholder={I18n.t('signup.step2.subdomain')}
|
||||
placeholder={getLabel('tenant', 'subdomain')}
|
||||
id="tenantSubdomain"
|
||||
className="formControl"
|
||||
/>
|
||||
@@ -60,7 +61,7 @@ const TenantSignUpForm = ({
|
||||
</div>
|
||||
</div>
|
||||
<DangerText>
|
||||
{errors.subdomain?.type === 'required' && I18n.t('signup.step2.validations.subdomain')}
|
||||
{errors.subdomain?.type === 'required' && getValidationMessage('required', 'tenant', 'subdomain')}
|
||||
</DangerText>
|
||||
<DangerText>
|
||||
{errors.subdomain?.type === 'validate' && I18n.t('signup.step2.validations.subdomain_already_taken')}
|
||||
|
||||
@@ -6,6 +6,8 @@ import Box from '../common/Box';
|
||||
import Button from '../common/Button';
|
||||
import { ITenantSignUpUserForm } from './TenantSignUpP';
|
||||
import { DangerText } from '../common/CustomTexts';
|
||||
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
||||
import { EMAIL_REGEX } from '../../constants/regex';
|
||||
|
||||
interface Props {
|
||||
currentStep: number;
|
||||
@@ -24,7 +26,13 @@ const UserSignUpForm = ({
|
||||
userData,
|
||||
setUserData,
|
||||
}: Props) => {
|
||||
const { register, handleSubmit, setError, formState: { errors } } = useForm<ITenantSignUpUserForm>();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
getValues,
|
||||
formState: { errors }
|
||||
} = useForm<ITenantSignUpUserForm>();
|
||||
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
|
||||
if (data.password !== data.passwordConfirmation) {
|
||||
setError('passwordConfirmation', I18n.t('signup.step1.validations.password_mismatch'));
|
||||
@@ -53,22 +61,25 @@ const UserSignUpForm = ({
|
||||
<input
|
||||
{...register('fullName', { required: true, minLength: 2 })}
|
||||
autoFocus
|
||||
placeholder={I18n.t('common.forms.auth.full_name')}
|
||||
placeholder={getLabel('user', 'full_name')}
|
||||
id="userFullName"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{ errors.fullName && I18n.t('signup.step1.validations.full_name') }</DangerText>
|
||||
<DangerText>{errors.fullName && getValidationMessage('required', 'user', 'full_name')}</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<input
|
||||
{...register('email', { required: true, pattern: /(.+)@(.+){2,}\.(.+){2,}/ })}
|
||||
{...register('email', { required: true, pattern: EMAIL_REGEX })}
|
||||
type="email"
|
||||
placeholder={I18n.t('common.forms.auth.email')}
|
||||
placeholder={getLabel('user', 'email')}
|
||||
id="userEmail"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{ errors.email && I18n.t('signup.step1.validations.email') }</DangerText>
|
||||
<DangerText>{errors.email?.type === 'required' && getValidationMessage('required', 'user', 'email')}</DangerText>
|
||||
<DangerText>
|
||||
{errors.email?.type === 'pattern' && I18n.t('common.validations.email')}
|
||||
</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
@@ -76,22 +87,22 @@ const UserSignUpForm = ({
|
||||
<input
|
||||
{...register('password', { required: true, minLength: 6, maxLength: 128 })}
|
||||
type="password"
|
||||
placeholder={I18n.t('common.forms.auth.password')}
|
||||
placeholder={getLabel('user', 'password')}
|
||||
id="userPassword"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{ errors.password && I18n.t('signup.step1.validations.password', { n: 6 }) }</DangerText>
|
||||
<DangerText>{ errors.password && I18n.t('common.validations.password', { n: 6 }) }</DangerText>
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-6">
|
||||
<input
|
||||
{...register('passwordConfirmation')}
|
||||
type="password"
|
||||
placeholder={I18n.t('common.forms.auth.password_confirmation')}
|
||||
placeholder={getLabel('user', 'password_confirmation')}
|
||||
id="userPasswordConfirmation"
|
||||
className="formControl"
|
||||
/>
|
||||
<DangerText>{ errors.passwordConfirmation && I18n.t('signup.step1.validations.password_mismatch') }</DangerText>
|
||||
<DangerText>{ errors.passwordConfirmation && I18n.t('common.validations.password_mismatch') }</DangerText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
43
app/javascript/components/common/CopyToClipboardButton.tsx
Normal file
43
app/javascript/components/common/CopyToClipboardButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
textToCopy: string;
|
||||
copiedLabel?: string;
|
||||
}
|
||||
|
||||
const CopyToClipboardButton = ({
|
||||
label,
|
||||
textToCopy,
|
||||
copiedLabel = I18n.t('common.copied')
|
||||
}: Props) => {
|
||||
const [ready, setReady] = useState(true);
|
||||
|
||||
const alertError = () =>
|
||||
alert(`Error in automatically copying to clipboard. Please copy the callback url manually:\n\n${textToCopy}`);
|
||||
|
||||
return (
|
||||
ready ?
|
||||
<a
|
||||
onClick={() => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setReady(false);
|
||||
setTimeout(() => setReady(true), 2000);
|
||||
},
|
||||
alertError);
|
||||
} else {
|
||||
alertError();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
:
|
||||
<span>{copiedLabel}</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyToClipboardButton;
|
||||
2
app/javascript/constants/regex.ts
Normal file
2
app/javascript/constants/regex.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const EMAIL_REGEX = /(.+)@(.+){2,}\.(.+){2,}/;
|
||||
export const URL_REGEX = /^(ftp|http|https):\/\/[^ "]+$/;
|
||||
44
app/javascript/containers/AuthenticationSiteSettings.tsx
Normal file
44
app/javascript/containers/AuthenticationSiteSettings.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { connect } from "react-redux";
|
||||
import { deleteOAuth } from "../actions/OAuth/deleteOAuth";
|
||||
import { requestOAuths } from "../actions/OAuth/requestOAuths";
|
||||
import { submitOAuth } from "../actions/OAuth/submitOAuth";
|
||||
import { updateOAuth } from "../actions/OAuth/updateOAuth";
|
||||
|
||||
import AuthenticationSiteSettingsP from "../components/SiteSettings/Authentication/AuthenticationSiteSettingsP";
|
||||
import { ISiteSettingsOAuthForm } from "../components/SiteSettings/Authentication/OAuthForm";
|
||||
import { IOAuth } from "../interfaces/IOAuth";
|
||||
import { State } from "../reducers/rootReducer";
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
oAuths: state.oAuths,
|
||||
|
||||
isSubmitting: state.siteSettings.authentication.isSubmitting,
|
||||
submitError: state.siteSettings.authentication.error,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
requestOAuths() {
|
||||
dispatch(requestOAuths());
|
||||
},
|
||||
|
||||
onSubmitOAuth(oAuth: IOAuth, authenticityToken: string): Promise<any> {
|
||||
return dispatch(submitOAuth(oAuth, authenticityToken));
|
||||
},
|
||||
|
||||
onUpdateOAuth(id: number, form: ISiteSettingsOAuthForm, authenticityToken: string): Promise<any> {
|
||||
return dispatch(updateOAuth({id, form, authenticityToken}));
|
||||
},
|
||||
|
||||
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string) {
|
||||
dispatch(updateOAuth({id, isEnabled, authenticityToken}));
|
||||
},
|
||||
|
||||
onDeleteOAuth(id: number, authenticityToken: string) {
|
||||
dispatch(deleteOAuth(id, authenticityToken));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(AuthenticationSiteSettingsP);
|
||||
20
app/javascript/helpers/formUtils.ts
Normal file
20
app/javascript/helpers/formUtils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FieldError } from "react-hook-form";
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
export const getLabel = (
|
||||
entity: string,
|
||||
attribute: string,
|
||||
) => (
|
||||
I18n.t(`activerecord.attributes.${entity}.${attribute}`)
|
||||
);
|
||||
|
||||
export const getValidationMessage = (
|
||||
validationType: FieldError['type'],
|
||||
entity: string,
|
||||
attribute: string,
|
||||
) => (
|
||||
I18n.t(
|
||||
`common.validations.${validationType}`,
|
||||
{ attribute: getLabel(entity, attribute) }
|
||||
)
|
||||
);
|
||||
67
app/javascript/interfaces/IOAuth.ts
Normal file
67
app/javascript/interfaces/IOAuth.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export interface IOAuth {
|
||||
id?: number;
|
||||
name: string;
|
||||
logo?: string;
|
||||
isEnabled: boolean;
|
||||
clientId: string;
|
||||
clientSecret?: string;
|
||||
authorizeUrl: string;
|
||||
tokenUrl: string;
|
||||
profileUrl: string;
|
||||
scope: string;
|
||||
jsonUserEmailPath: string;
|
||||
jsonUserNamePath?: string;
|
||||
|
||||
callbackUrl?: string;
|
||||
}
|
||||
|
||||
export interface IOAuthJSON {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
is_enabled: boolean;
|
||||
client_id: string;
|
||||
client_secret?: string;
|
||||
authorize_url: string;
|
||||
token_url: string;
|
||||
profile_url: string;
|
||||
scope: string;
|
||||
json_user_email_path: string;
|
||||
json_user_name_path?: string;
|
||||
|
||||
callback_url?: string;
|
||||
}
|
||||
|
||||
export const oAuthJSON2JS = (oAuthJSON: IOAuthJSON) => ({
|
||||
id: parseInt(oAuthJSON.id),
|
||||
name: oAuthJSON.name,
|
||||
logo: oAuthJSON.logo,
|
||||
isEnabled: oAuthJSON.is_enabled,
|
||||
clientId: oAuthJSON.client_id,
|
||||
clientSecret: oAuthJSON.client_secret,
|
||||
authorizeUrl: oAuthJSON.authorize_url,
|
||||
tokenUrl: oAuthJSON.token_url,
|
||||
scope: oAuthJSON.scope,
|
||||
profileUrl: oAuthJSON.profile_url,
|
||||
jsonUserEmailPath: oAuthJSON.json_user_email_path,
|
||||
jsonUserNamePath: oAuthJSON.json_user_name_path,
|
||||
|
||||
callbackUrl: oAuthJSON.callback_url,
|
||||
});
|
||||
|
||||
export const oAuthJS2JSON = (oAuth: IOAuth) => ({
|
||||
id: oAuth.id?.toString(),
|
||||
name: oAuth.name,
|
||||
logo: oAuth.logo,
|
||||
is_enabled: oAuth.isEnabled,
|
||||
client_id: oAuth.clientId,
|
||||
client_secret: oAuth.clientSecret,
|
||||
authorize_url: oAuth.authorizeUrl,
|
||||
token_url: oAuth.tokenUrl,
|
||||
profile_url: oAuth.profileUrl,
|
||||
scope: oAuth.scope,
|
||||
json_user_email_path: oAuth.jsonUserEmailPath,
|
||||
json_user_name_path: oAuth.jsonUserNamePath,
|
||||
|
||||
callback_url: oAuth.callbackUrl,
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
OAuthSubmitActionTypes,
|
||||
OAUTH_SUBMIT_START,
|
||||
OAUTH_SUBMIT_SUCCESS,
|
||||
OAUTH_SUBMIT_FAILURE,
|
||||
} from '../../actions/OAuth/submitOAuth';
|
||||
|
||||
import {
|
||||
OAuthUpdateActionTypes,
|
||||
OAUTH_UPDATE_START,
|
||||
OAUTH_UPDATE_SUCCESS,
|
||||
OAUTH_UPDATE_FAILURE,
|
||||
} from '../../actions/OAuth/updateOAuth';
|
||||
|
||||
import {
|
||||
OAuthDeleteActionTypes,
|
||||
OAUTH_DELETE_START,
|
||||
OAUTH_DELETE_SUCCESS,
|
||||
OAUTH_DELETE_FAILURE,
|
||||
} from '../../actions/OAuth/deleteOAuth';
|
||||
|
||||
export interface SiteSettingsAuthenticationState {
|
||||
isSubmitting: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const initialState: SiteSettingsAuthenticationState = {
|
||||
isSubmitting: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
const siteSettingsAuthenticationReducer = (
|
||||
state = initialState,
|
||||
action:
|
||||
OAuthSubmitActionTypes |
|
||||
OAuthUpdateActionTypes |
|
||||
OAuthDeleteActionTypes,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case OAUTH_SUBMIT_START:
|
||||
case OAUTH_UPDATE_START:
|
||||
case OAUTH_DELETE_START:
|
||||
return {
|
||||
...state,
|
||||
isSubmitting: true,
|
||||
};
|
||||
|
||||
case OAUTH_SUBMIT_SUCCESS:
|
||||
case OAUTH_UPDATE_SUCCESS:
|
||||
case OAUTH_DELETE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
isSubmitting: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
case OAUTH_SUBMIT_FAILURE:
|
||||
case OAUTH_UPDATE_FAILURE:
|
||||
case OAUTH_DELETE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
isSubmitting: false,
|
||||
error: action.error,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default siteSettingsAuthenticationReducer;
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
BoardsRequestActionTypes,
|
||||
BOARDS_REQUEST_START,
|
||||
BOARDS_REQUEST_SUCCESS,
|
||||
BOARDS_REQUEST_FAILURE,
|
||||
} from '../../actions/Board/requestBoards';
|
||||
|
||||
import {
|
||||
BoardSubmitActionTypes,
|
||||
BOARD_SUBMIT_START,
|
||||
@@ -46,14 +39,12 @@ const initialState: SiteSettingsBoardsState = {
|
||||
const siteSettingsBoardsReducer = (
|
||||
state = initialState,
|
||||
action:
|
||||
BoardsRequestActionTypes |
|
||||
BoardSubmitActionTypes |
|
||||
BoardUpdateActionTypes |
|
||||
BoardOrderUpdateActionTypes |
|
||||
BoardDeleteActionTypes
|
||||
): SiteSettingsBoardsState => {
|
||||
switch (action.type) {
|
||||
case BOARDS_REQUEST_START:
|
||||
case BOARD_SUBMIT_START:
|
||||
case BOARD_UPDATE_START:
|
||||
case BOARD_ORDER_UPDATE_START:
|
||||
@@ -63,7 +54,6 @@ const siteSettingsBoardsReducer = (
|
||||
areUpdating: true,
|
||||
};
|
||||
|
||||
case BOARDS_REQUEST_SUCCESS:
|
||||
case BOARD_SUBMIT_SUCCESS:
|
||||
case BOARD_UPDATE_SUCCESS:
|
||||
case BOARD_ORDER_UPDATE_SUCCESS:
|
||||
@@ -74,7 +64,6 @@ const siteSettingsBoardsReducer = (
|
||||
error: '',
|
||||
};
|
||||
|
||||
case BOARDS_REQUEST_FAILURE:
|
||||
case BOARD_SUBMIT_FAILURE:
|
||||
case BOARD_UPDATE_FAILURE:
|
||||
case BOARD_ORDER_UPDATE_FAILURE:
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TENANT_UPDATE_FAILURE,
|
||||
} from '../../actions/Tenant/updateTenant';
|
||||
|
||||
|
||||
export interface SiteSettingsGeneralState {
|
||||
areUpdating: boolean;
|
||||
error: string;
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
PostStatusesRequestActionTypes,
|
||||
POST_STATUSES_REQUEST_START,
|
||||
POST_STATUSES_REQUEST_SUCCESS,
|
||||
POST_STATUSES_REQUEST_FAILURE,
|
||||
} from '../../actions/PostStatus/requestPostStatuses';
|
||||
|
||||
import {
|
||||
PostStatusOrderUpdateActionTypes,
|
||||
POSTSTATUS_ORDER_UPDATE_START,
|
||||
@@ -46,14 +39,12 @@ const initialState: SiteSettingsPostStatusesState = {
|
||||
const siteSettingsPostStatusesReducer = (
|
||||
state = initialState,
|
||||
action:
|
||||
PostStatusesRequestActionTypes |
|
||||
PostStatusOrderUpdateActionTypes |
|
||||
PostStatusDeleteActionTypes |
|
||||
PostStatusSubmitActionTypes |
|
||||
PostStatusUpdateActionTypes
|
||||
): SiteSettingsPostStatusesState => {
|
||||
switch (action.type) {
|
||||
case POST_STATUSES_REQUEST_START:
|
||||
case POSTSTATUS_SUBMIT_START:
|
||||
case POSTSTATUS_UPDATE_START:
|
||||
case POSTSTATUS_ORDER_UPDATE_START:
|
||||
@@ -63,7 +54,6 @@ const siteSettingsPostStatusesReducer = (
|
||||
areUpdating: true,
|
||||
};
|
||||
|
||||
case POST_STATUSES_REQUEST_SUCCESS:
|
||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||
case POSTSTATUS_UPDATE_SUCCESS:
|
||||
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
||||
@@ -74,7 +64,6 @@ const siteSettingsPostStatusesReducer = (
|
||||
error: '',
|
||||
};
|
||||
|
||||
case POST_STATUSES_REQUEST_FAILURE:
|
||||
case POSTSTATUS_SUBMIT_FAILURE:
|
||||
case POSTSTATUS_UPDATE_FAILURE:
|
||||
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
UsersRequestActionTypes,
|
||||
USERS_REQUEST_START,
|
||||
USERS_REQUEST_SUCCESS,
|
||||
USERS_REQUEST_FAILURE,
|
||||
} from '../../actions/User/requestUsers';
|
||||
|
||||
import {
|
||||
UserUpdateActionTypes,
|
||||
USER_UPDATE_START,
|
||||
@@ -24,17 +17,15 @@ const initialState: SiteSettingsUsersState = {
|
||||
|
||||
const siteSettingsUsersReducer = (
|
||||
state = initialState,
|
||||
action: UsersRequestActionTypes | UserUpdateActionTypes,
|
||||
action: UserUpdateActionTypes,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case USERS_REQUEST_START:
|
||||
case USER_UPDATE_START:
|
||||
return {
|
||||
...state,
|
||||
areUpdating: true,
|
||||
};
|
||||
|
||||
case USERS_REQUEST_SUCCESS:
|
||||
case USER_UPDATE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@@ -42,7 +33,6 @@ const siteSettingsUsersReducer = (
|
||||
error: '',
|
||||
};
|
||||
|
||||
case USERS_REQUEST_FAILURE:
|
||||
case USER_UPDATE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
93
app/javascript/reducers/oAuthsReducer.ts
Normal file
93
app/javascript/reducers/oAuthsReducer.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
OAuthsRequestActionTypes,
|
||||
OAUTHS_REQUEST_START,
|
||||
OAUTHS_REQUEST_SUCCESS,
|
||||
OAUTHS_REQUEST_FAILURE,
|
||||
} from '../actions/OAuth/requestOAuths';
|
||||
|
||||
import {
|
||||
OAuthSubmitActionTypes,
|
||||
OAUTH_SUBMIT_SUCCESS,
|
||||
} from '../actions/OAuth/submitOAuth';
|
||||
|
||||
import {
|
||||
OAuthUpdateActionTypes,
|
||||
OAUTH_UPDATE_SUCCESS,
|
||||
} from '../actions/OAuth/updateOAuth';
|
||||
|
||||
import {
|
||||
OAuthDeleteActionTypes,
|
||||
OAUTH_DELETE_SUCCESS,
|
||||
} from '../actions/OAuth/deleteOAuth';
|
||||
|
||||
import { IOAuth, oAuthJSON2JS } from '../interfaces/IOAuth';
|
||||
|
||||
export interface OAuthsState {
|
||||
items: Array<IOAuth>;
|
||||
areLoading: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const initialState: OAuthsState = {
|
||||
items: [],
|
||||
areLoading: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
const oAuthsReducer = (
|
||||
state = initialState,
|
||||
action:
|
||||
OAuthsRequestActionTypes |
|
||||
OAuthSubmitActionTypes |
|
||||
OAuthUpdateActionTypes |
|
||||
OAuthDeleteActionTypes,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case OAUTHS_REQUEST_START:
|
||||
return {
|
||||
...state,
|
||||
areLoading: true,
|
||||
};
|
||||
|
||||
case OAUTHS_REQUEST_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
areLoading: false,
|
||||
error: '',
|
||||
items: action.oAuths.map<IOAuth>(oAuthJson => oAuthJSON2JS(oAuthJson)),
|
||||
};
|
||||
|
||||
case OAUTHS_REQUEST_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
areLoading: false,
|
||||
error: action.error,
|
||||
};
|
||||
|
||||
case OAUTH_SUBMIT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
items: [...state.items, oAuthJSON2JS(action.oAuth)],
|
||||
};
|
||||
|
||||
case OAUTH_UPDATE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
items: state.items.map(oAuth => {
|
||||
if (oAuth.id !== parseInt(action.oAuth.id)) return oAuth;
|
||||
return oAuthJSON2JS(action.oAuth);
|
||||
})
|
||||
};
|
||||
|
||||
case OAUTH_DELETE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
items: state.items.filter(oAuth => oAuth.id !== action.id),
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default oAuthsReducer;
|
||||
@@ -8,6 +8,7 @@ import postStatusesReducer from './postStatusesReducer';
|
||||
import usersReducer from './usersReducer';
|
||||
import currentPostReducer from './currentPostReducer';
|
||||
import siteSettingsReducer from './siteSettingsReducer';
|
||||
import oAuthsReducer from './oAuthsReducer';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
tenantSignUp: tenantSignUpReducer,
|
||||
@@ -18,6 +19,7 @@ const rootReducer = combineReducers({
|
||||
users: usersReducer,
|
||||
currentPost: currentPostReducer,
|
||||
siteSettings: siteSettingsReducer,
|
||||
oAuths: oAuthsReducer,
|
||||
});
|
||||
|
||||
export type State = ReturnType<typeof rootReducer>
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
TenantRequestActionTypes,
|
||||
TENANT_REQUEST_START,
|
||||
TENANT_REQUEST_SUCCESS,
|
||||
TENANT_REQUEST_FAILURE,
|
||||
} from '../actions/Tenant/requestTenant';
|
||||
|
||||
import {
|
||||
TenantUpdateActionTypes,
|
||||
TENANT_UPDATE_START,
|
||||
@@ -12,13 +5,6 @@ import {
|
||||
TENANT_UPDATE_FAILURE,
|
||||
} from '../actions/Tenant/updateTenant';
|
||||
|
||||
import {
|
||||
BoardsRequestActionTypes,
|
||||
BOARDS_REQUEST_START,
|
||||
BOARDS_REQUEST_SUCCESS,
|
||||
BOARDS_REQUEST_FAILURE,
|
||||
} from '../actions/Board/requestBoards';
|
||||
|
||||
import {
|
||||
BoardSubmitActionTypes,
|
||||
BOARD_SUBMIT_START,
|
||||
@@ -47,13 +33,6 @@ import {
|
||||
BOARD_DELETE_FAILURE,
|
||||
} from '../actions/Board/deleteBoard';
|
||||
|
||||
import {
|
||||
PostStatusesRequestActionTypes,
|
||||
POST_STATUSES_REQUEST_START,
|
||||
POST_STATUSES_REQUEST_SUCCESS,
|
||||
POST_STATUSES_REQUEST_FAILURE,
|
||||
} from '../actions/PostStatus/requestPostStatuses';
|
||||
|
||||
import {
|
||||
PostStatusOrderUpdateActionTypes,
|
||||
POSTSTATUS_ORDER_UPDATE_START,
|
||||
@@ -82,13 +61,6 @@ import {
|
||||
POSTSTATUS_UPDATE_FAILURE,
|
||||
} from '../actions/PostStatus/updatePostStatus';
|
||||
|
||||
import {
|
||||
UsersRequestActionTypes,
|
||||
USERS_REQUEST_START,
|
||||
USERS_REQUEST_SUCCESS,
|
||||
USERS_REQUEST_FAILURE,
|
||||
} from '../actions/User/requestUsers';
|
||||
|
||||
import {
|
||||
UserUpdateActionTypes,
|
||||
USER_UPDATE_START,
|
||||
@@ -96,14 +68,37 @@ import {
|
||||
USER_UPDATE_FAILURE,
|
||||
} from '../actions/User/updateUser';
|
||||
|
||||
import {
|
||||
OAuthSubmitActionTypes,
|
||||
OAUTH_SUBMIT_START,
|
||||
OAUTH_SUBMIT_SUCCESS,
|
||||
OAUTH_SUBMIT_FAILURE,
|
||||
} from '../actions/OAuth/submitOAuth';
|
||||
|
||||
import {
|
||||
OAuthUpdateActionTypes,
|
||||
OAUTH_UPDATE_START,
|
||||
OAUTH_UPDATE_SUCCESS,
|
||||
OAUTH_UPDATE_FAILURE,
|
||||
} from '../actions/OAuth/updateOAuth';
|
||||
|
||||
import {
|
||||
OAuthDeleteActionTypes,
|
||||
OAUTH_DELETE_START,
|
||||
OAUTH_DELETE_SUCCESS,
|
||||
OAUTH_DELETE_FAILURE,
|
||||
} from '../actions/OAuth/deleteOAuth';
|
||||
|
||||
import siteSettingsGeneralReducer, { SiteSettingsGeneralState } from './SiteSettings/generalReducer';
|
||||
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
||||
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
||||
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
||||
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
|
||||
import siteSettingsAuthenticationReducer, { SiteSettingsAuthenticationState } from './SiteSettings/authenticationReducer';
|
||||
|
||||
interface SiteSettingsState {
|
||||
general: SiteSettingsGeneralState;
|
||||
authentication: SiteSettingsAuthenticationState;
|
||||
boards: SiteSettingsBoardsState;
|
||||
postStatuses: SiteSettingsPostStatusesState;
|
||||
roadmap: SiteSettingsRoadmapState;
|
||||
@@ -112,6 +107,7 @@ interface SiteSettingsState {
|
||||
|
||||
const initialState: SiteSettingsState = {
|
||||
general: siteSettingsGeneralReducer(undefined, {} as any),
|
||||
authentication: siteSettingsAuthenticationReducer(undefined, {} as any),
|
||||
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
||||
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
||||
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
||||
@@ -121,19 +117,18 @@ const initialState: SiteSettingsState = {
|
||||
const siteSettingsReducer = (
|
||||
state = initialState,
|
||||
action:
|
||||
TenantRequestActionTypes |
|
||||
TenantUpdateActionTypes |
|
||||
BoardsRequestActionTypes |
|
||||
OAuthSubmitActionTypes |
|
||||
OAuthUpdateActionTypes |
|
||||
OAuthDeleteActionTypes |
|
||||
BoardSubmitActionTypes |
|
||||
BoardUpdateActionTypes |
|
||||
BoardOrderUpdateActionTypes |
|
||||
BoardDeleteActionTypes |
|
||||
PostStatusesRequestActionTypes |
|
||||
PostStatusOrderUpdateActionTypes |
|
||||
PostStatusDeleteActionTypes |
|
||||
PostStatusSubmitActionTypes |
|
||||
PostStatusUpdateActionTypes |
|
||||
UsersRequestActionTypes |
|
||||
UserUpdateActionTypes
|
||||
): SiteSettingsState => {
|
||||
switch (action.type) {
|
||||
@@ -144,10 +139,21 @@ const siteSettingsReducer = (
|
||||
...state,
|
||||
general: siteSettingsGeneralReducer(state.general, action),
|
||||
};
|
||||
|
||||
case OAUTH_SUBMIT_START:
|
||||
case OAUTH_SUBMIT_SUCCESS:
|
||||
case OAUTH_SUBMIT_FAILURE:
|
||||
case OAUTH_UPDATE_START:
|
||||
case OAUTH_UPDATE_SUCCESS:
|
||||
case OAUTH_UPDATE_FAILURE:
|
||||
case OAUTH_DELETE_START:
|
||||
case OAUTH_DELETE_SUCCESS:
|
||||
case OAUTH_DELETE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
authentication: siteSettingsAuthenticationReducer(state.authentication, action),
|
||||
};
|
||||
|
||||
case BOARDS_REQUEST_START:
|
||||
case BOARDS_REQUEST_SUCCESS:
|
||||
case BOARDS_REQUEST_FAILURE:
|
||||
case BOARD_SUBMIT_START:
|
||||
case BOARD_SUBMIT_SUCCESS:
|
||||
case BOARD_SUBMIT_FAILURE:
|
||||
@@ -165,9 +171,6 @@ const siteSettingsReducer = (
|
||||
boards: siteSettingsBoardsReducer(state.boards, action),
|
||||
};
|
||||
|
||||
case POST_STATUSES_REQUEST_START:
|
||||
case POST_STATUSES_REQUEST_SUCCESS:
|
||||
case POST_STATUSES_REQUEST_FAILURE:
|
||||
case POSTSTATUS_SUBMIT_START:
|
||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||
case POSTSTATUS_SUBMIT_FAILURE:
|
||||
@@ -191,9 +194,6 @@ const siteSettingsReducer = (
|
||||
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
|
||||
};
|
||||
|
||||
case USERS_REQUEST_START:
|
||||
case USERS_REQUEST_SUCCESS:
|
||||
case USERS_REQUEST_FAILURE:
|
||||
case USER_UPDATE_START:
|
||||
case USER_UPDATE_SUCCESS:
|
||||
case USER_UPDATE_FAILURE:
|
||||
|
||||
@@ -182,4 +182,9 @@
|
||||
.selectPicker {
|
||||
@extend
|
||||
.custom-select;
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.authenticationIndexPage {
|
||||
h2 { @extend .mb-3; }
|
||||
|
||||
.oauthProvidersTitle {
|
||||
@extend .d-flex;
|
||||
|
||||
button {
|
||||
@extend .ml-2;
|
||||
|
||||
height: min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.oAuthsList {
|
||||
@extend .pl-1;
|
||||
|
||||
list-style: none;
|
||||
|
||||
.oAuthListItem {
|
||||
@extend
|
||||
.d-flex,
|
||||
.justify-content-between,
|
||||
.my-2,
|
||||
.p-3;
|
||||
|
||||
.oAuthInfo {
|
||||
@extend .d-flex;
|
||||
|
||||
column-gap: 32px;
|
||||
|
||||
.oAuthLogo { border-radius: 100%; align-self: center; }
|
||||
|
||||
.oAuthName { font-size: 18px; }
|
||||
}
|
||||
|
||||
.oAuthActions {
|
||||
align-self: center;
|
||||
|
||||
a {
|
||||
@extend .link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.authenticationFormPage {
|
||||
a.backButton {
|
||||
@extend .mb-2;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
a.link {
|
||||
@extend .link;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
@import 'components/SiteSettings/PostStatuses';
|
||||
@import 'components/SiteSettings/Roadmap';
|
||||
@import 'components/SiteSettings/Users';
|
||||
@import 'components/SiteSettings/Authentication';
|
||||
|
||||
/* Icons */
|
||||
@import 'icons/drag_icon';
|
||||
30
app/models/o_auth.rb
Normal file
30
app/models/o_auth.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
class OAuth < ApplicationRecord
|
||||
include TenantOwnable
|
||||
include ApplicationHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attr_accessor :state
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
||||
validates :is_enabled, inclusion: { in: [true, false] }
|
||||
validates :client_id, presence: true
|
||||
validates :client_secret, presence: true
|
||||
validates :authorize_url, presence: true
|
||||
validates :token_url, presence: true
|
||||
validates :profile_url, presence: true
|
||||
validates :scope, presence: true
|
||||
validates :json_user_email_path, presence: true
|
||||
|
||||
def callback_url
|
||||
add_subdomain_to(method(:o_auth_callback_url), id)
|
||||
end
|
||||
|
||||
def authorize_url_with_query_params
|
||||
"#{authorize_url}?"\
|
||||
"response_type=code&"\
|
||||
"client_id=#{client_id}&"\
|
||||
"redirect_uri=#{callback_url()}&"\
|
||||
"scope=#{scope}&"\
|
||||
"state=#{state}"
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
class Tenant < ApplicationRecord
|
||||
has_many :boards
|
||||
has_many :o_auths
|
||||
has_many :post_statuses
|
||||
has_many :posts
|
||||
has_many :users
|
||||
|
||||
37
app/policies/o_auth_policy.rb
Normal file
37
app/policies/o_auth_policy.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class OAuthPolicy < ApplicationPolicy
|
||||
def permitted_attributes
|
||||
if user.admin?
|
||||
[
|
||||
:name,
|
||||
:logo,
|
||||
:is_enabled,
|
||||
:client_id,
|
||||
:client_secret,
|
||||
:authorize_url,
|
||||
:token_url,
|
||||
:profile_url,
|
||||
:scope,
|
||||
:json_user_name_path,
|
||||
:json_user_email_path
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def index?
|
||||
user.admin?
|
||||
end
|
||||
|
||||
def create?
|
||||
user.admin?
|
||||
end
|
||||
|
||||
def update?
|
||||
user.admin? and user.tenant_id == record.tenant_id
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user.admin? and user.tenant_id == record.tenant_id
|
||||
end
|
||||
end
|
||||
@@ -40,6 +40,17 @@
|
||||
<div class="actions">
|
||||
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<% if not @o_auths.empty? %>
|
||||
<% @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 %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
|
||||
@@ -29,6 +29,17 @@
|
||||
<div class="actions">
|
||||
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<% if not @o_auths.empty? %>
|
||||
<% @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/links" %>
|
||||
47
app/views/o_auths/test.html.erb
Normal file
47
app/views/o_auths/test.html.erb
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('site_settings.authentication.test_page.title', { name: @o_auth.name }) %></title>
|
||||
</head>
|
||||
<body>
|
||||
<h1><%= t('site_settings.authentication.test_page.title', { name: @o_auth.name }) %></h1>
|
||||
|
||||
<div>
|
||||
<h2><%= t('site_settings.authentication.test_page.fetched_user_data') %></h2>
|
||||
|
||||
<pre><%= JSON.pretty_generate(@user_profile, { indent: " ", object_nl: "\n" }) %></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2><%= t('activerecord.attributes.user.email') %> <%= @email_valid ? "✅" : "❌" %></h2>
|
||||
<ul>
|
||||
<li><b><%= t('site_settings.authentication.form.jsonUserEmailPath') %>:</b> <%= @o_auth.json_user_email_path %></li>
|
||||
<li><b><%= t('site_settings.authentication.test_page.found') %>:</b> <%= @user_email %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2><%= t('activerecord.attributes.user.full_name') %> <%= @name_valid ? "✅" : "⚠️" %></h2>
|
||||
<ul>
|
||||
<li><b><%= t('site_settings.authentication.form.jsonUserNamePath') %>:</b> <%= @o_auth.json_user_name_path %></li>
|
||||
<li><b><%= t('site_settings.authentication.test_page.found') %>:</b> <%= @user_name %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2><%= t('site_settings.authentication.test_page.summary') %> <%= !@email_valid ? "❌" : !@name_valid ? "⚠️" : "✅" %></h2>
|
||||
|
||||
<% if @email_valid and @name_valid %>
|
||||
<p><%= t('site_settings.authentication.test_page.valid_configuration') %></p>
|
||||
<% end %>
|
||||
|
||||
<% if @email_valid and not @name_valid %>
|
||||
<p><%= t('site_settings.authentication.test_page.warning_configuration', { name: t('defaults.user_full_name') }) %></p>
|
||||
<% end %>
|
||||
|
||||
<% if not @email_valid %>
|
||||
<p><%= t('site_settings.authentication.test_page.invalid_configuration') %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,6 +5,7 @@
|
||||
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
|
||||
<% if current_user.admin? %>
|
||||
<%= render 'menu_link', label: t('site_settings.menu.general'), path: site_settings_general_path %>
|
||||
<%= render 'menu_link', label: t('site_settings.menu.authentication'), path: site_settings_authentication_path %>
|
||||
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
|
||||
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
|
||||
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
|
||||
|
||||
13
app/views/site_settings/authentication.html.erb
Normal file
13
app/views/site_settings/authentication.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="twoColumnsContainer">
|
||||
<%= render 'menu' %>
|
||||
<div>
|
||||
<%=
|
||||
react_component(
|
||||
'SiteSettings/Authentication',
|
||||
{
|
||||
authenticityToken: form_authenticity_token
|
||||
}
|
||||
)
|
||||
%>
|
||||
</div>
|
||||
</div>
|
||||
56
app/workflows/OAuthExchangeAuthCodeForProfile.rb
Normal file
56
app/workflows/OAuthExchangeAuthCodeForProfile.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
class OAuthExchangeAuthCodeForProfile
|
||||
include HTTParty
|
||||
|
||||
attr_accessor :authorization_code, :o_auth
|
||||
|
||||
# Given:
|
||||
# authorization_code: code returned by OAuth provider on user confirmation
|
||||
# o_auth: ActiveRecord model with information about the OAuth provider
|
||||
#
|
||||
# The workfow first exchanges the authorization code for an access token
|
||||
# Then it uses the access token to fetch user profile information
|
||||
#
|
||||
# Returns:
|
||||
# The fetched user profile as a Hash, if successful
|
||||
# nil, if unsuccessful
|
||||
|
||||
def initialize(authorization_code: "", o_auth: "")
|
||||
@authorization_code = authorization_code
|
||||
@o_auth = o_auth
|
||||
end
|
||||
|
||||
def run
|
||||
return nil unless @o_auth and @o_auth.class == OAuth
|
||||
return nil unless @authorization_code and @authorization_code.class == String
|
||||
|
||||
begin
|
||||
# Exchange authorization code for access token
|
||||
token_request_params = {
|
||||
code: @authorization_code,
|
||||
client_id: @o_auth.client_id,
|
||||
client_secret: @o_auth.client_secret,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: @o_auth.callback_url
|
||||
}
|
||||
|
||||
token_response = HTTParty.post(
|
||||
@o_auth.token_url,
|
||||
headers: { "Accept": "application/json" },
|
||||
body: token_request_params
|
||||
)
|
||||
access_token = token_response['access_token']
|
||||
|
||||
# Exchange access token for profile info
|
||||
profile_response = HTTParty.get(
|
||||
@o_auth.profile_url,
|
||||
headers: { "Authorization": "Bearer #{access_token}" },
|
||||
format: :json
|
||||
).parsed_response
|
||||
|
||||
return profile_response
|
||||
rescue => error
|
||||
print(error)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
59
app/workflows/OAuthSignInUser.rb
Normal file
59
app/workflows/OAuthSignInUser.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
class OAuthSignInUser
|
||||
include OAuthsHelper
|
||||
|
||||
attr_accessor :user_profile, :o_auth
|
||||
|
||||
# Given:
|
||||
# user_profile: ruby Hash containing information about the user
|
||||
# Could've been returned from OAuthExchangeAuthCodeForProfile
|
||||
# o_auth: ActiveRecord model with information about the OAuth provider
|
||||
#
|
||||
# The workfow creates a new user if it doesn't exist, or select the existing one
|
||||
# NOTE: it does NOT actually sign in the user, but rather returns it to the controller
|
||||
# where it'll be signed in
|
||||
#
|
||||
# Returns:
|
||||
# the user, if successful
|
||||
# nil, if unsuccessful
|
||||
|
||||
def initialize(user_profile: "", o_auth: "")
|
||||
@user_profile = user_profile
|
||||
@o_auth = o_auth
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
begin
|
||||
# Attempts to get email from user_profile Hash
|
||||
email = query_path_from_hash(@user_profile, @o_auth.json_user_email_path)
|
||||
|
||||
return nil if email.nil? or not URI::MailTo::EMAIL_REGEXP.match?(email)
|
||||
|
||||
# Select existing / create new user
|
||||
user = User.find_by(email: email)
|
||||
|
||||
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)
|
||||
end
|
||||
full_name ||= I18n.t('defaults.user_full_name')
|
||||
|
||||
user = User.new(
|
||||
email: email,
|
||||
full_name: full_name,
|
||||
password: Devise.friendly_token,
|
||||
status: 'active'
|
||||
)
|
||||
user.skip_confirmation!
|
||||
user.save
|
||||
end
|
||||
|
||||
return user
|
||||
rescue => error
|
||||
print(error)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user