mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 03:07: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
@@ -3,6 +3,7 @@
|
|||||||
# For more information check out this page:
|
# For more information check out this page:
|
||||||
# https://github.com/riggraz/astuto/wiki/Required-environment-variables
|
# https://github.com/riggraz/astuto/wiki/Required-environment-variables
|
||||||
|
|
||||||
|
BASE_URL=http://feedback.yoursitename.com
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
SECRET_KEY_BASE=secretkeybasehere
|
SECRET_KEY_BASE=secretkeybasehere
|
||||||
|
|
||||||
|
|||||||
3
Gemfile
3
Gemfile
@@ -19,6 +19,9 @@ gem 'jbuilder', '~> 2.7'
|
|||||||
|
|
||||||
gem 'bootsnap', '>= 1.4.2', require: false
|
gem 'bootsnap', '>= 1.4.2', require: false
|
||||||
|
|
||||||
|
# HTTP requests
|
||||||
|
gem 'httparty', '0.18.0'
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
gem 'devise', '4.7.3'
|
gem 'devise', '4.7.3'
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ GEM
|
|||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
globalid (1.0.0)
|
globalid (1.0.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
|
httparty (0.18.0)
|
||||||
|
mime-types (~> 3.0)
|
||||||
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.10.0)
|
i18n (1.10.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-js (3.9.2)
|
i18n-js (3.9.2)
|
||||||
@@ -129,10 +132,14 @@ GEM
|
|||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
|
mime-types (3.4.1)
|
||||||
|
mime-types-data (~> 3.2015)
|
||||||
|
mime-types-data (3.2022.0105)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.0)
|
mini_portile2 (2.8.0)
|
||||||
minitest (5.15.0)
|
minitest (5.15.0)
|
||||||
msgpack (1.5.2)
|
msgpack (1.5.2)
|
||||||
|
multi_xml (0.6.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.6)
|
nokogiri (1.13.6)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
@@ -275,6 +282,7 @@ DEPENDENCIES
|
|||||||
capybara (>= 2.15)
|
capybara (>= 2.15)
|
||||||
devise (= 4.7.3)
|
devise (= 4.7.3)
|
||||||
factory_bot_rails (~> 5.0.2)
|
factory_bot_rails (~> 5.0.2)
|
||||||
|
httparty (= 0.18.0)
|
||||||
i18n-js
|
i18n-js
|
||||||
jbuilder (~> 2.7)
|
jbuilder (~> 2.7)
|
||||||
kaminari (~> 1.2.1)
|
kaminari (~> 1.2.1)
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ class ApplicationController < ActionController::Base
|
|||||||
I18n.locale = @tenant.locale
|
I18n.locale = @tenant.locale
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_oauths
|
||||||
|
@o_auths = Current.tenant_or_raise!.o_auths
|
||||||
|
.where(is_enabled: true)
|
||||||
|
.order(created_at: :asc)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_not_authorized
|
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
|
class RegistrationsController < Devise::RegistrationsController
|
||||||
# Needed to have Current.tenant available in Devise's controllers
|
# Needed to have Current.tenant available in Devise's controllers
|
||||||
prepend_before_action :load_tenant_data
|
prepend_before_action :load_tenant_data
|
||||||
|
before_action :load_oauths, only: [:new]
|
||||||
|
|
||||||
# Override destroy to soft delete
|
# Override destroy to soft delete
|
||||||
def destroy
|
def destroy
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class SessionsController < Devise::SessionsController
|
class SessionsController < Devise::SessionsController
|
||||||
# Needed to have Current.tenant available in Devise's controllers
|
# Needed to have Current.tenant available in Devise's controllers
|
||||||
prepend_before_action :load_tenant_data
|
prepend_before_action :load_tenant_data
|
||||||
|
before_action :load_oauths, only: [:new]
|
||||||
end
|
end
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
class SiteSettingsController < ApplicationController
|
class SiteSettingsController < ApplicationController
|
||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
|
|
||||||
before_action :authenticate_admin, only: [:general, :boards, :post_statuses, :roadmap]
|
before_action :authenticate_admin,
|
||||||
before_action :authenticate_power_user, only: [:users]
|
only: [:general, :boards, :post_statuses, :roadmap, :authentication]
|
||||||
|
|
||||||
|
before_action :authenticate_power_user,
|
||||||
|
only: [:users]
|
||||||
|
|
||||||
def general
|
def general
|
||||||
end
|
end
|
||||||
@@ -18,4 +21,7 @@ class SiteSettingsController < ApplicationController
|
|||||||
|
|
||||||
def users
|
def users
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def authentication
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -30,6 +30,7 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def add_subdomain_to(url_helper, resource=nil, options={})
|
def add_subdomain_to(url_helper, resource=nil, options={})
|
||||||
options[:subdomain] = Current.tenant_or_raise!.subdomain if Rails.application.multi_tenancy?
|
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)
|
resource ? url_helper.call(resource, options) : url_helper.call(options)
|
||||||
end
|
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
|
<input
|
||||||
{...register('name', { required: true })}
|
{...register('name', { required: true })}
|
||||||
placeholder={I18n.t('site_settings.boards.form.name')}
|
placeholder={I18n.t('site_settings.boards.form.name')}
|
||||||
autoFocus
|
autoFocus={mode === 'update'}
|
||||||
className="formControl"
|
className="formControl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
TENANT_BRAND_NONE,
|
TENANT_BRAND_NONE,
|
||||||
} from '../../../interfaces/ITenant';
|
} from '../../../interfaces/ITenant';
|
||||||
import { DangerText } from '../../common/CustomTexts';
|
import { DangerText } from '../../common/CustomTexts';
|
||||||
|
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
||||||
|
|
||||||
export interface ISiteSettingsGeneralForm {
|
export interface ISiteSettingsGeneralForm {
|
||||||
siteName: string;
|
siteName: string;
|
||||||
@@ -79,17 +80,17 @@ const GeneralSiteSettingsP = ({
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
<div className="formGroup col-4">
|
<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
|
<input
|
||||||
{...register('siteName', { required: true })}
|
{...register('siteName', { required: true })}
|
||||||
id="siteName"
|
id="siteName"
|
||||||
className="formControl"
|
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>
|
||||||
|
|
||||||
<div className="formGroup col-4">
|
<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
|
<input
|
||||||
{...register('siteLogo')}
|
{...register('siteLogo')}
|
||||||
id="siteLogo"
|
id="siteLogo"
|
||||||
@@ -98,7 +99,7 @@ const GeneralSiteSettingsP = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formGroup col-4">
|
<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
|
<select
|
||||||
{...register('brandDisplaySetting')}
|
{...register('brandDisplaySetting')}
|
||||||
id="brandSetting"
|
id="brandSetting"
|
||||||
@@ -121,7 +122,7 @@ const GeneralSiteSettingsP = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formGroup">
|
<div className="formGroup">
|
||||||
<label htmlFor="locale">{ I18n.t('site_settings.general.locale') }</label>
|
<label htmlFor="locale">{ getLabel('tenant', 'locale') }</label>
|
||||||
<select
|
<select
|
||||||
{...register('locale')}
|
{...register('locale')}
|
||||||
id="locale"
|
id="locale"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const PostStatusForm = ({
|
|||||||
<input
|
<input
|
||||||
{...register('name', { required: true })}
|
{...register('name', { required: true })}
|
||||||
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
||||||
autoFocus
|
autoFocus={mode === 'update'}
|
||||||
className="formControl"
|
className="formControl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
|
|||||||
import { UsersState } from '../../../reducers/usersReducer';
|
import { UsersState } from '../../../reducers/usersReducer';
|
||||||
import { UserRoles, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED } from '../../../interfaces/IUser';
|
import { UserRoles, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED } from '../../../interfaces/IUser';
|
||||||
import HttpStatus from '../../../constants/http_status';
|
import HttpStatus from '../../../constants/http_status';
|
||||||
|
import Spinner from '../../common/Spinner';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
users: UsersState;
|
users: UsersState;
|
||||||
@@ -79,17 +80,20 @@ class UsersSiteSettingsP extends React.Component<Props> {
|
|||||||
|
|
||||||
<ul className="usersList">
|
<ul className="usersList">
|
||||||
{
|
{
|
||||||
users.items.map((user, i) => (
|
users.areLoading === false ?
|
||||||
<UserEditable
|
users.items.map((user, i) => (
|
||||||
user={user}
|
<UserEditable
|
||||||
updateUserRole={this._handleUpdateUserRole}
|
user={user}
|
||||||
updateUserStatus={this._handleUpdateUserStatus}
|
updateUserRole={this._handleUpdateUserRole}
|
||||||
|
updateUserStatus={this._handleUpdateUserStatus}
|
||||||
|
|
||||||
currentUserEmail={currentUserEmail}
|
currentUserEmail={currentUserEmail}
|
||||||
currentUserRole={currentUserRole}
|
currentUserRole={currentUserRole}
|
||||||
key={i}
|
key={i}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
:
|
||||||
|
<Spinner />
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Spinner from '../common/Spinner';
|
|||||||
import { DangerText } from '../common/CustomTexts';
|
import { DangerText } from '../common/CustomTexts';
|
||||||
import { ITenantSignUpTenantForm } from './TenantSignUpP';
|
import { ITenantSignUpTenantForm } from './TenantSignUpP';
|
||||||
import HttpStatus from '../../constants/http_status';
|
import HttpStatus from '../../constants/http_status';
|
||||||
|
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
@@ -34,11 +35,11 @@ const TenantSignUpForm = ({
|
|||||||
<input
|
<input
|
||||||
{...register('siteName', { required: true })}
|
{...register('siteName', { required: true })}
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={I18n.t('signup.step2.site_name')}
|
placeholder={getLabel('tenant', 'site_name')}
|
||||||
id="tenantSiteName"
|
id="tenantSiteName"
|
||||||
className="formControl"
|
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>
|
||||||
|
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
@@ -51,7 +52,7 @@ const TenantSignUpForm = ({
|
|||||||
return res.status === HttpStatus.OK;
|
return res.status === HttpStatus.OK;
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
placeholder={I18n.t('signup.step2.subdomain')}
|
placeholder={getLabel('tenant', 'subdomain')}
|
||||||
id="tenantSubdomain"
|
id="tenantSubdomain"
|
||||||
className="formControl"
|
className="formControl"
|
||||||
/>
|
/>
|
||||||
@@ -60,7 +61,7 @@ const TenantSignUpForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DangerText>
|
<DangerText>
|
||||||
{errors.subdomain?.type === 'required' && I18n.t('signup.step2.validations.subdomain')}
|
{errors.subdomain?.type === 'required' && getValidationMessage('required', 'tenant', 'subdomain')}
|
||||||
</DangerText>
|
</DangerText>
|
||||||
<DangerText>
|
<DangerText>
|
||||||
{errors.subdomain?.type === 'validate' && I18n.t('signup.step2.validations.subdomain_already_taken')}
|
{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 Button from '../common/Button';
|
||||||
import { ITenantSignUpUserForm } from './TenantSignUpP';
|
import { ITenantSignUpUserForm } from './TenantSignUpP';
|
||||||
import { DangerText } from '../common/CustomTexts';
|
import { DangerText } from '../common/CustomTexts';
|
||||||
|
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
|
||||||
|
import { EMAIL_REGEX } from '../../constants/regex';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
@@ -24,7 +26,13 @@ const UserSignUpForm = ({
|
|||||||
userData,
|
userData,
|
||||||
setUserData,
|
setUserData,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { register, handleSubmit, setError, formState: { errors } } = useForm<ITenantSignUpUserForm>();
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setError,
|
||||||
|
getValues,
|
||||||
|
formState: { errors }
|
||||||
|
} = useForm<ITenantSignUpUserForm>();
|
||||||
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
|
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
|
||||||
if (data.password !== data.passwordConfirmation) {
|
if (data.password !== data.passwordConfirmation) {
|
||||||
setError('passwordConfirmation', I18n.t('signup.step1.validations.password_mismatch'));
|
setError('passwordConfirmation', I18n.t('signup.step1.validations.password_mismatch'));
|
||||||
@@ -53,22 +61,25 @@ const UserSignUpForm = ({
|
|||||||
<input
|
<input
|
||||||
{...register('fullName', { required: true, minLength: 2 })}
|
{...register('fullName', { required: true, minLength: 2 })}
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={I18n.t('common.forms.auth.full_name')}
|
placeholder={getLabel('user', 'full_name')}
|
||||||
id="userFullName"
|
id="userFullName"
|
||||||
className="formControl"
|
className="formControl"
|
||||||
/>
|
/>
|
||||||
<DangerText>{ errors.fullName && I18n.t('signup.step1.validations.full_name') }</DangerText>
|
<DangerText>{errors.fullName && getValidationMessage('required', 'user', 'full_name')}</DangerText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
<input
|
<input
|
||||||
{...register('email', { required: true, pattern: /(.+)@(.+){2,}\.(.+){2,}/ })}
|
{...register('email', { required: true, pattern: EMAIL_REGEX })}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={I18n.t('common.forms.auth.email')}
|
placeholder={getLabel('user', 'email')}
|
||||||
id="userEmail"
|
id="userEmail"
|
||||||
className="formControl"
|
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>
|
||||||
|
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
@@ -76,22 +87,22 @@ const UserSignUpForm = ({
|
|||||||
<input
|
<input
|
||||||
{...register('password', { required: true, minLength: 6, maxLength: 128 })}
|
{...register('password', { required: true, minLength: 6, maxLength: 128 })}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={I18n.t('common.forms.auth.password')}
|
placeholder={getLabel('user', 'password')}
|
||||||
id="userPassword"
|
id="userPassword"
|
||||||
className="formControl"
|
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>
|
||||||
|
|
||||||
<div className="formGroup col-6">
|
<div className="formGroup col-6">
|
||||||
<input
|
<input
|
||||||
{...register('passwordConfirmation')}
|
{...register('passwordConfirmation')}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={I18n.t('common.forms.auth.password_confirmation')}
|
placeholder={getLabel('user', 'password_confirmation')}
|
||||||
id="userPasswordConfirmation"
|
id="userPasswordConfirmation"
|
||||||
className="formControl"
|
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>
|
||||||
</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 {
|
import {
|
||||||
BoardSubmitActionTypes,
|
BoardSubmitActionTypes,
|
||||||
BOARD_SUBMIT_START,
|
BOARD_SUBMIT_START,
|
||||||
@@ -46,14 +39,12 @@ const initialState: SiteSettingsBoardsState = {
|
|||||||
const siteSettingsBoardsReducer = (
|
const siteSettingsBoardsReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action:
|
action:
|
||||||
BoardsRequestActionTypes |
|
|
||||||
BoardSubmitActionTypes |
|
BoardSubmitActionTypes |
|
||||||
BoardUpdateActionTypes |
|
BoardUpdateActionTypes |
|
||||||
BoardOrderUpdateActionTypes |
|
BoardOrderUpdateActionTypes |
|
||||||
BoardDeleteActionTypes
|
BoardDeleteActionTypes
|
||||||
): SiteSettingsBoardsState => {
|
): SiteSettingsBoardsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case BOARDS_REQUEST_START:
|
|
||||||
case BOARD_SUBMIT_START:
|
case BOARD_SUBMIT_START:
|
||||||
case BOARD_UPDATE_START:
|
case BOARD_UPDATE_START:
|
||||||
case BOARD_ORDER_UPDATE_START:
|
case BOARD_ORDER_UPDATE_START:
|
||||||
@@ -63,7 +54,6 @@ const siteSettingsBoardsReducer = (
|
|||||||
areUpdating: true,
|
areUpdating: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
case BOARDS_REQUEST_SUCCESS:
|
|
||||||
case BOARD_SUBMIT_SUCCESS:
|
case BOARD_SUBMIT_SUCCESS:
|
||||||
case BOARD_UPDATE_SUCCESS:
|
case BOARD_UPDATE_SUCCESS:
|
||||||
case BOARD_ORDER_UPDATE_SUCCESS:
|
case BOARD_ORDER_UPDATE_SUCCESS:
|
||||||
@@ -74,7 +64,6 @@ const siteSettingsBoardsReducer = (
|
|||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
case BOARDS_REQUEST_FAILURE:
|
|
||||||
case BOARD_SUBMIT_FAILURE:
|
case BOARD_SUBMIT_FAILURE:
|
||||||
case BOARD_UPDATE_FAILURE:
|
case BOARD_UPDATE_FAILURE:
|
||||||
case BOARD_ORDER_UPDATE_FAILURE:
|
case BOARD_ORDER_UPDATE_FAILURE:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TENANT_UPDATE_FAILURE,
|
TENANT_UPDATE_FAILURE,
|
||||||
} from '../../actions/Tenant/updateTenant';
|
} from '../../actions/Tenant/updateTenant';
|
||||||
|
|
||||||
|
|
||||||
export interface SiteSettingsGeneralState {
|
export interface SiteSettingsGeneralState {
|
||||||
areUpdating: boolean;
|
areUpdating: boolean;
|
||||||
error: string;
|
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 {
|
import {
|
||||||
PostStatusOrderUpdateActionTypes,
|
PostStatusOrderUpdateActionTypes,
|
||||||
POSTSTATUS_ORDER_UPDATE_START,
|
POSTSTATUS_ORDER_UPDATE_START,
|
||||||
@@ -46,14 +39,12 @@ const initialState: SiteSettingsPostStatusesState = {
|
|||||||
const siteSettingsPostStatusesReducer = (
|
const siteSettingsPostStatusesReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action:
|
action:
|
||||||
PostStatusesRequestActionTypes |
|
|
||||||
PostStatusOrderUpdateActionTypes |
|
PostStatusOrderUpdateActionTypes |
|
||||||
PostStatusDeleteActionTypes |
|
PostStatusDeleteActionTypes |
|
||||||
PostStatusSubmitActionTypes |
|
PostStatusSubmitActionTypes |
|
||||||
PostStatusUpdateActionTypes
|
PostStatusUpdateActionTypes
|
||||||
): SiteSettingsPostStatusesState => {
|
): SiteSettingsPostStatusesState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case POST_STATUSES_REQUEST_START:
|
|
||||||
case POSTSTATUS_SUBMIT_START:
|
case POSTSTATUS_SUBMIT_START:
|
||||||
case POSTSTATUS_UPDATE_START:
|
case POSTSTATUS_UPDATE_START:
|
||||||
case POSTSTATUS_ORDER_UPDATE_START:
|
case POSTSTATUS_ORDER_UPDATE_START:
|
||||||
@@ -63,7 +54,6 @@ const siteSettingsPostStatusesReducer = (
|
|||||||
areUpdating: true,
|
areUpdating: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
case POST_STATUSES_REQUEST_SUCCESS:
|
|
||||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||||
case POSTSTATUS_UPDATE_SUCCESS:
|
case POSTSTATUS_UPDATE_SUCCESS:
|
||||||
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
||||||
@@ -74,7 +64,6 @@ const siteSettingsPostStatusesReducer = (
|
|||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
case POST_STATUSES_REQUEST_FAILURE:
|
|
||||||
case POSTSTATUS_SUBMIT_FAILURE:
|
case POSTSTATUS_SUBMIT_FAILURE:
|
||||||
case POSTSTATUS_UPDATE_FAILURE:
|
case POSTSTATUS_UPDATE_FAILURE:
|
||||||
case POSTSTATUS_ORDER_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 {
|
import {
|
||||||
UserUpdateActionTypes,
|
UserUpdateActionTypes,
|
||||||
USER_UPDATE_START,
|
USER_UPDATE_START,
|
||||||
@@ -24,17 +17,15 @@ const initialState: SiteSettingsUsersState = {
|
|||||||
|
|
||||||
const siteSettingsUsersReducer = (
|
const siteSettingsUsersReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action: UsersRequestActionTypes | UserUpdateActionTypes,
|
action: UserUpdateActionTypes,
|
||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case USERS_REQUEST_START:
|
|
||||||
case USER_UPDATE_START:
|
case USER_UPDATE_START:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
areUpdating: true,
|
areUpdating: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
case USERS_REQUEST_SUCCESS:
|
|
||||||
case USER_UPDATE_SUCCESS:
|
case USER_UPDATE_SUCCESS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -42,7 +33,6 @@ const siteSettingsUsersReducer = (
|
|||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
case USERS_REQUEST_FAILURE:
|
|
||||||
case USER_UPDATE_FAILURE:
|
case USER_UPDATE_FAILURE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...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 usersReducer from './usersReducer';
|
||||||
import currentPostReducer from './currentPostReducer';
|
import currentPostReducer from './currentPostReducer';
|
||||||
import siteSettingsReducer from './siteSettingsReducer';
|
import siteSettingsReducer from './siteSettingsReducer';
|
||||||
|
import oAuthsReducer from './oAuthsReducer';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
tenantSignUp: tenantSignUpReducer,
|
tenantSignUp: tenantSignUpReducer,
|
||||||
@@ -18,6 +19,7 @@ const rootReducer = combineReducers({
|
|||||||
users: usersReducer,
|
users: usersReducer,
|
||||||
currentPost: currentPostReducer,
|
currentPost: currentPostReducer,
|
||||||
siteSettings: siteSettingsReducer,
|
siteSettings: siteSettingsReducer,
|
||||||
|
oAuths: oAuthsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type State = ReturnType<typeof rootReducer>
|
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 {
|
import {
|
||||||
TenantUpdateActionTypes,
|
TenantUpdateActionTypes,
|
||||||
TENANT_UPDATE_START,
|
TENANT_UPDATE_START,
|
||||||
@@ -12,13 +5,6 @@ import {
|
|||||||
TENANT_UPDATE_FAILURE,
|
TENANT_UPDATE_FAILURE,
|
||||||
} from '../actions/Tenant/updateTenant';
|
} from '../actions/Tenant/updateTenant';
|
||||||
|
|
||||||
import {
|
|
||||||
BoardsRequestActionTypes,
|
|
||||||
BOARDS_REQUEST_START,
|
|
||||||
BOARDS_REQUEST_SUCCESS,
|
|
||||||
BOARDS_REQUEST_FAILURE,
|
|
||||||
} from '../actions/Board/requestBoards';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BoardSubmitActionTypes,
|
BoardSubmitActionTypes,
|
||||||
BOARD_SUBMIT_START,
|
BOARD_SUBMIT_START,
|
||||||
@@ -47,13 +33,6 @@ import {
|
|||||||
BOARD_DELETE_FAILURE,
|
BOARD_DELETE_FAILURE,
|
||||||
} from '../actions/Board/deleteBoard';
|
} from '../actions/Board/deleteBoard';
|
||||||
|
|
||||||
import {
|
|
||||||
PostStatusesRequestActionTypes,
|
|
||||||
POST_STATUSES_REQUEST_START,
|
|
||||||
POST_STATUSES_REQUEST_SUCCESS,
|
|
||||||
POST_STATUSES_REQUEST_FAILURE,
|
|
||||||
} from '../actions/PostStatus/requestPostStatuses';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PostStatusOrderUpdateActionTypes,
|
PostStatusOrderUpdateActionTypes,
|
||||||
POSTSTATUS_ORDER_UPDATE_START,
|
POSTSTATUS_ORDER_UPDATE_START,
|
||||||
@@ -82,13 +61,6 @@ import {
|
|||||||
POSTSTATUS_UPDATE_FAILURE,
|
POSTSTATUS_UPDATE_FAILURE,
|
||||||
} from '../actions/PostStatus/updatePostStatus';
|
} from '../actions/PostStatus/updatePostStatus';
|
||||||
|
|
||||||
import {
|
|
||||||
UsersRequestActionTypes,
|
|
||||||
USERS_REQUEST_START,
|
|
||||||
USERS_REQUEST_SUCCESS,
|
|
||||||
USERS_REQUEST_FAILURE,
|
|
||||||
} from '../actions/User/requestUsers';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserUpdateActionTypes,
|
UserUpdateActionTypes,
|
||||||
USER_UPDATE_START,
|
USER_UPDATE_START,
|
||||||
@@ -96,14 +68,37 @@ import {
|
|||||||
USER_UPDATE_FAILURE,
|
USER_UPDATE_FAILURE,
|
||||||
} from '../actions/User/updateUser';
|
} 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 siteSettingsGeneralReducer, { SiteSettingsGeneralState } from './SiteSettings/generalReducer';
|
||||||
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
||||||
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
||||||
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
||||||
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
|
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
|
||||||
|
import siteSettingsAuthenticationReducer, { SiteSettingsAuthenticationState } from './SiteSettings/authenticationReducer';
|
||||||
|
|
||||||
interface SiteSettingsState {
|
interface SiteSettingsState {
|
||||||
general: SiteSettingsGeneralState;
|
general: SiteSettingsGeneralState;
|
||||||
|
authentication: SiteSettingsAuthenticationState;
|
||||||
boards: SiteSettingsBoardsState;
|
boards: SiteSettingsBoardsState;
|
||||||
postStatuses: SiteSettingsPostStatusesState;
|
postStatuses: SiteSettingsPostStatusesState;
|
||||||
roadmap: SiteSettingsRoadmapState;
|
roadmap: SiteSettingsRoadmapState;
|
||||||
@@ -112,6 +107,7 @@ interface SiteSettingsState {
|
|||||||
|
|
||||||
const initialState: SiteSettingsState = {
|
const initialState: SiteSettingsState = {
|
||||||
general: siteSettingsGeneralReducer(undefined, {} as any),
|
general: siteSettingsGeneralReducer(undefined, {} as any),
|
||||||
|
authentication: siteSettingsAuthenticationReducer(undefined, {} as any),
|
||||||
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
||||||
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
||||||
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
||||||
@@ -121,19 +117,18 @@ const initialState: SiteSettingsState = {
|
|||||||
const siteSettingsReducer = (
|
const siteSettingsReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action:
|
action:
|
||||||
TenantRequestActionTypes |
|
|
||||||
TenantUpdateActionTypes |
|
TenantUpdateActionTypes |
|
||||||
BoardsRequestActionTypes |
|
OAuthSubmitActionTypes |
|
||||||
|
OAuthUpdateActionTypes |
|
||||||
|
OAuthDeleteActionTypes |
|
||||||
BoardSubmitActionTypes |
|
BoardSubmitActionTypes |
|
||||||
BoardUpdateActionTypes |
|
BoardUpdateActionTypes |
|
||||||
BoardOrderUpdateActionTypes |
|
BoardOrderUpdateActionTypes |
|
||||||
BoardDeleteActionTypes |
|
BoardDeleteActionTypes |
|
||||||
PostStatusesRequestActionTypes |
|
|
||||||
PostStatusOrderUpdateActionTypes |
|
PostStatusOrderUpdateActionTypes |
|
||||||
PostStatusDeleteActionTypes |
|
PostStatusDeleteActionTypes |
|
||||||
PostStatusSubmitActionTypes |
|
PostStatusSubmitActionTypes |
|
||||||
PostStatusUpdateActionTypes |
|
PostStatusUpdateActionTypes |
|
||||||
UsersRequestActionTypes |
|
|
||||||
UserUpdateActionTypes
|
UserUpdateActionTypes
|
||||||
): SiteSettingsState => {
|
): SiteSettingsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -144,10 +139,21 @@ const siteSettingsReducer = (
|
|||||||
...state,
|
...state,
|
||||||
general: siteSettingsGeneralReducer(state.general, action),
|
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_START:
|
||||||
case BOARD_SUBMIT_SUCCESS:
|
case BOARD_SUBMIT_SUCCESS:
|
||||||
case BOARD_SUBMIT_FAILURE:
|
case BOARD_SUBMIT_FAILURE:
|
||||||
@@ -165,9 +171,6 @@ const siteSettingsReducer = (
|
|||||||
boards: siteSettingsBoardsReducer(state.boards, action),
|
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_START:
|
||||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||||
case POSTSTATUS_SUBMIT_FAILURE:
|
case POSTSTATUS_SUBMIT_FAILURE:
|
||||||
@@ -191,9 +194,6 @@ const siteSettingsReducer = (
|
|||||||
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
|
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
|
||||||
};
|
};
|
||||||
|
|
||||||
case USERS_REQUEST_START:
|
|
||||||
case USERS_REQUEST_SUCCESS:
|
|
||||||
case USERS_REQUEST_FAILURE:
|
|
||||||
case USER_UPDATE_START:
|
case USER_UPDATE_START:
|
||||||
case USER_UPDATE_SUCCESS:
|
case USER_UPDATE_SUCCESS:
|
||||||
case USER_UPDATE_FAILURE:
|
case USER_UPDATE_FAILURE:
|
||||||
|
|||||||
@@ -182,4 +182,9 @@
|
|||||||
.selectPicker {
|
.selectPicker {
|
||||||
@extend
|
@extend
|
||||||
.custom-select;
|
.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/PostStatuses';
|
||||||
@import 'components/SiteSettings/Roadmap';
|
@import 'components/SiteSettings/Roadmap';
|
||||||
@import 'components/SiteSettings/Users';
|
@import 'components/SiteSettings/Users';
|
||||||
|
@import 'components/SiteSettings/Authentication';
|
||||||
|
|
||||||
/* Icons */
|
/* Icons */
|
||||||
@import 'icons/drag_icon';
|
@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
|
class Tenant < ApplicationRecord
|
||||||
has_many :boards
|
has_many :boards
|
||||||
|
has_many :o_auths
|
||||||
has_many :post_statuses
|
has_many :post_statuses
|
||||||
has_many :posts
|
has_many :posts
|
||||||
has_many :users
|
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">
|
<div class="actions">
|
||||||
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
||||||
</div>
|
</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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= render "devise/shared/links" %>
|
<%= render "devise/shared/links" %>
|
||||||
|
|||||||
@@ -29,6 +29,17 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
||||||
</div>
|
</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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= render "devise/shared/links" %>
|
<%= 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">
|
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
|
||||||
<% if current_user.admin? %>
|
<% 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.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.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.post_statuses'), path: site_settings_post_statuses_path %>
|
||||||
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_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
|
||||||
@@ -16,6 +16,10 @@ module App
|
|||||||
# -- all .rb files in that directory are automatically loaded after loading
|
# -- all .rb files in that directory are automatically loaded after loading
|
||||||
# the framework and any gems in your application.
|
# the framework and any gems in your application.
|
||||||
|
|
||||||
|
def base_url
|
||||||
|
ENV["BASE_URL"]
|
||||||
|
end
|
||||||
|
|
||||||
def multi_tenancy?
|
def multi_tenancy?
|
||||||
ENV["MULTI_TENANCY"] == "true"
|
ENV["MULTI_TENANCY"] == "true"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
# For subdomains in localhost
|
config.hosts << ".localhost:3000"
|
||||||
|
config.hosts << ".lvh.me:3000" # used to test oauth strategies in development
|
||||||
|
|
||||||
|
# 0 if using localhost, 1 if using lvh.me
|
||||||
config.action_dispatch.tld_length = 0
|
config.action_dispatch.tld_length = 0
|
||||||
|
|
||||||
# For Devise
|
# For Devise
|
||||||
config.action_mailer.default_url_options = { host: 'localhost:3000' }
|
config.action_mailer.default_url_options = { host: Rails.application.base_url }
|
||||||
|
|
||||||
# In the development environment your application's code is reloaded on
|
# In the development environment your application's code is reloaded on
|
||||||
# every request. This slows down response time but is perfect for development
|
# every request. This slows down response time but is perfect for development
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
en:
|
en:
|
||||||
errors:
|
errors:
|
||||||
|
unknown: 'An unknown error occurred'
|
||||||
unauthorized: 'You are not authorized'
|
unauthorized: 'You are not authorized'
|
||||||
not_logged_in: 'You must be logged in to access this page'
|
not_logged_in: 'You must be logged in to access this page'
|
||||||
not_enough_privileges: 'You do not have the privilegies to access this page'
|
not_enough_privileges: 'You do not have the privilegies to access this page'
|
||||||
user_blocked_or_deleted: 'You cannot access your account because it has been blocked or deleted.'
|
user_blocked_or_deleted: 'You cannot access your account because it has been blocked or deleted.'
|
||||||
|
o_auth_login_error: 'There was an error logging in with %{name}. Please contact the site administrator or try a different provider.'
|
||||||
board:
|
board:
|
||||||
update_order: 'There was an error in reordering boards'
|
update_order: 'There was an error in reordering boards'
|
||||||
post_status:
|
post_status:
|
||||||
update_order: 'There was an error in reordering statuses'
|
update_order: 'There was an error in reordering statuses'
|
||||||
|
defaults:
|
||||||
|
user_full_name: 'Anonymous User'
|
||||||
mailers:
|
mailers:
|
||||||
devise:
|
devise:
|
||||||
welcome_greeting: 'Welcome to %{site_name}, %{email}!'
|
welcome_greeting: 'Welcome to %{site_name}, %{email}!'
|
||||||
@@ -60,6 +64,18 @@ en:
|
|||||||
like:
|
like:
|
||||||
user_id: 'User'
|
user_id: 'User'
|
||||||
post_id: 'Post'
|
post_id: 'Post'
|
||||||
|
o_auth:
|
||||||
|
name: 'Name'
|
||||||
|
logo: 'Logo'
|
||||||
|
is_enabled: 'Enabled'
|
||||||
|
client_id: 'Client ID'
|
||||||
|
client_secret: 'Client secret'
|
||||||
|
authorize_url: 'Authorize URL'
|
||||||
|
token_url: 'Token URL'
|
||||||
|
profile_url: 'Profile URL'
|
||||||
|
scope: 'Scope'
|
||||||
|
json_user_name_path: 'JSON path to user name'
|
||||||
|
json_user_email_path: 'JSON path to user email'
|
||||||
post_status:
|
post_status:
|
||||||
name: 'Name'
|
name: 'Name'
|
||||||
color: 'Color'
|
color: 'Color'
|
||||||
@@ -71,9 +87,17 @@ en:
|
|||||||
board_id: 'Post board'
|
board_id: 'Post board'
|
||||||
user_id: 'Post author'
|
user_id: 'Post author'
|
||||||
post_status_id: 'Post status'
|
post_status_id: 'Post status'
|
||||||
|
tenant:
|
||||||
|
site_name: 'Site name'
|
||||||
|
site_logo: 'Site logo'
|
||||||
|
subdomain: 'Subdomain'
|
||||||
|
locale: 'Language'
|
||||||
|
brand_setting: 'Display'
|
||||||
user:
|
user:
|
||||||
email: 'Email'
|
email: 'Email'
|
||||||
full_name: 'Name and surname'
|
full_name: 'Full name'
|
||||||
|
password: 'Password'
|
||||||
|
password_confirmation: 'Password confirmation'
|
||||||
role: 'Role'
|
role: 'Role'
|
||||||
notifications_enabled: 'Notifications enabled'
|
notifications_enabled: 'Notifications enabled'
|
||||||
errors:
|
errors:
|
||||||
@@ -83,4 +107,4 @@ en:
|
|||||||
blank: 'cannot be blank'
|
blank: 'cannot be blank'
|
||||||
taken: 'is already in use'
|
taken: 'is already in use'
|
||||||
too_short: 'is too short (minimum %{count} characters)'
|
too_short: 'is too short (minimum %{count} characters)'
|
||||||
too_long: 'is too long (maximum ${count} characters)'
|
too_long: 'is too long (maximum %{count} characters)'
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
en:
|
en:
|
||||||
common:
|
common:
|
||||||
|
validations:
|
||||||
|
required: '%{attribute} is required'
|
||||||
|
email: 'Invalid email'
|
||||||
|
url: 'Invalid URL'
|
||||||
|
password: 'Password must have at least %{n} characters'
|
||||||
|
password_mismatch: 'Password and password confirmation must match'
|
||||||
forms:
|
forms:
|
||||||
auth:
|
auth:
|
||||||
email: 'Email'
|
email: 'Email'
|
||||||
@@ -14,6 +20,8 @@ en:
|
|||||||
remember_me: 'Remember me'
|
remember_me: 'Remember me'
|
||||||
log_in: 'Log in'
|
log_in: 'Log in'
|
||||||
sign_up: 'Sign up'
|
sign_up: 'Sign up'
|
||||||
|
log_in_with: 'Log in with %{o_auth}'
|
||||||
|
sign_up_with: 'Sign up with %{o_auth}'
|
||||||
profile_settings: 'Profile settings'
|
profile_settings: 'Profile settings'
|
||||||
update_profile: 'Update profile'
|
update_profile: 'Update profile'
|
||||||
cancel_account: 'Cancel account'
|
cancel_account: 'Cancel account'
|
||||||
@@ -31,14 +39,20 @@ en:
|
|||||||
no_status: 'No status'
|
no_status: 'No status'
|
||||||
loading: 'Loading...'
|
loading: 'Loading...'
|
||||||
confirmation: 'Are you sure?'
|
confirmation: 'Are you sure?'
|
||||||
|
unsaved_changes: 'Unsaved changes will be lost if you leave the page.'
|
||||||
edited: 'Edited'
|
edited: 'Edited'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
disabled: 'Disabled'
|
||||||
|
copied: 'Copied!'
|
||||||
buttons:
|
buttons:
|
||||||
|
new: 'New'
|
||||||
edit: 'Edit'
|
edit: 'Edit'
|
||||||
delete: 'Delete'
|
delete: 'Delete'
|
||||||
cancel: 'Cancel'
|
cancel: 'Cancel'
|
||||||
create: 'Create'
|
create: 'Create'
|
||||||
update: 'Save'
|
update: 'Save'
|
||||||
confirm: 'Confirm'
|
confirm: 'Confirm'
|
||||||
|
back: 'Back'
|
||||||
datetime:
|
datetime:
|
||||||
now: 'just now'
|
now: 'just now'
|
||||||
minutes:
|
minutes:
|
||||||
@@ -55,19 +69,10 @@ en:
|
|||||||
step1:
|
step1:
|
||||||
title: '1. Create user account'
|
title: '1. Create user account'
|
||||||
email_auth: 'Register with email'
|
email_auth: 'Register with email'
|
||||||
validations:
|
|
||||||
full_name: 'Full name is required'
|
|
||||||
email: 'Email is invalid'
|
|
||||||
password: 'Password must have at least %{n} characters'
|
|
||||||
password_mismatch: 'Password and password confirmation must match'
|
|
||||||
step2:
|
step2:
|
||||||
title: '2. Create feedback space'
|
title: '2. Create feedback space'
|
||||||
site_name: 'Site name'
|
|
||||||
subdomain: 'Subdomain'
|
|
||||||
create_button: 'Create feedback space'
|
create_button: 'Create feedback space'
|
||||||
validations:
|
validations:
|
||||||
site_name: 'Site name is required'
|
|
||||||
subdomain: 'Subdomain is required'
|
|
||||||
subdomain_already_taken: 'Sorry, this subdomain is not available'
|
subdomain_already_taken: 'Sorry, this subdomain is not available'
|
||||||
step3:
|
step3:
|
||||||
title: "You're almost done!"
|
title: "You're almost done!"
|
||||||
@@ -136,22 +141,17 @@ en:
|
|||||||
post_statuses: 'Statuses'
|
post_statuses: 'Statuses'
|
||||||
roadmap: 'Roadmap'
|
roadmap: 'Roadmap'
|
||||||
users: 'Users'
|
users: 'Users'
|
||||||
|
authentication: 'Authentication'
|
||||||
info_box:
|
info_box:
|
||||||
up_to_date: 'All changes saved'
|
up_to_date: 'All changes saved'
|
||||||
error: 'An error occurred: %{message}'
|
error: 'An error occurred: %{message}'
|
||||||
dirty: 'Changes not saved'
|
dirty: 'Changes not saved'
|
||||||
general:
|
general:
|
||||||
title: 'General'
|
title: 'General'
|
||||||
site_name: 'Site name'
|
|
||||||
site_logo: 'Site logo'
|
|
||||||
brand_setting: 'Display'
|
|
||||||
brand_setting_both: 'Both name and logo'
|
brand_setting_both: 'Both name and logo'
|
||||||
brand_setting_name: 'Name only'
|
brand_setting_name: 'Name only'
|
||||||
brand_setting_logo: 'Logo only'
|
brand_setting_logo: 'Logo only'
|
||||||
brand_setting_none: 'None'
|
brand_setting_none: 'None'
|
||||||
locale: 'Language'
|
|
||||||
validations:
|
|
||||||
site_name: 'Site name is required'
|
|
||||||
boards:
|
boards:
|
||||||
title: 'Boards'
|
title: 'Boards'
|
||||||
empty: 'There are no boards. Create one below!'
|
empty: 'There are no boards. Create one below!'
|
||||||
@@ -182,4 +182,22 @@ en:
|
|||||||
role_admin: 'Administrator'
|
role_admin: 'Administrator'
|
||||||
status_active: 'Active'
|
status_active: 'Active'
|
||||||
status_blocked: 'Blocked'
|
status_blocked: 'Blocked'
|
||||||
status_deleted: 'Deleted'
|
status_deleted: 'Deleted'
|
||||||
|
authentication:
|
||||||
|
title: 'Authentication'
|
||||||
|
oauth_subtitle: 'OAuth providers'
|
||||||
|
copy_url: 'Copy URL'
|
||||||
|
test_page:
|
||||||
|
title: '%{name} OAuth test results'
|
||||||
|
fetched_user_data: 'Fetched user data'
|
||||||
|
found: 'Found'
|
||||||
|
summary: 'Summary'
|
||||||
|
valid_configuration: 'This OAuth provider is configured correctly!'
|
||||||
|
warning_configuration: 'This OAuth provider if configured correctly, but a default name for users (%{name}) will be used.'
|
||||||
|
invalid_configuration: 'This OAuth provider is NOT configured correctly.'
|
||||||
|
form:
|
||||||
|
title_new: 'New OAuth provider'
|
||||||
|
title_edit: 'Edit OAuth provider'
|
||||||
|
subtitle_oauth_config: 'OAuth configuration'
|
||||||
|
subtitle_user_profile_config: 'User profile configuration'
|
||||||
|
client_secret_help: 'hidden for security purposes'
|
||||||
@@ -26,6 +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]
|
||||||
|
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
|
||||||
|
|
||||||
resources :posts, only: [:index, :create, :show, :update, :destroy] do
|
resources :posts, only: [:index, :create, :show, :update, :destroy] do
|
||||||
resource :follows, only: [:create, :destroy]
|
resource :follows, only: [:create, :destroy]
|
||||||
@@ -50,6 +53,7 @@ Rails.application.routes.draw do
|
|||||||
get 'post_statuses'
|
get 'post_statuses'
|
||||||
get 'roadmap'
|
get 'roadmap'
|
||||||
get 'users'
|
get 'users'
|
||||||
|
get 'authentication'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
22
db/migrate/20220727090932_create_o_auths.rb
Normal file
22
db/migrate/20220727090932_create_o_auths.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class CreateOAuths < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :o_auths do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :logo
|
||||||
|
t.boolean :is_enabled, default: false
|
||||||
|
t.string :client_id, null: false
|
||||||
|
t.string :client_secret, null: false
|
||||||
|
t.string :authorize_url, null: false
|
||||||
|
t.string :token_url, null: false
|
||||||
|
t.string :profile_url, null: false
|
||||||
|
t.string :scope, null: false
|
||||||
|
t.string :json_user_name_path
|
||||||
|
t.string :json_user_email_path, null: false
|
||||||
|
t.references :tenant, null: false, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :o_auths, [:name, :tenant_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddNotNullToOAuthIsEnabled < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
change_column_null :o_auths, :is_enabled, false
|
||||||
|
end
|
||||||
|
end
|
||||||
22
db/schema.rb
22
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: 2022_07_15_092725) do
|
ActiveRecord::Schema.define(version: 2022_07_27_094200) 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"
|
||||||
@@ -65,6 +65,25 @@ ActiveRecord::Schema.define(version: 2022_07_15_092725) do
|
|||||||
t.index ["user_id"], name: "index_likes_on_user_id"
|
t.index ["user_id"], name: "index_likes_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "o_auths", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.string "logo"
|
||||||
|
t.boolean "is_enabled", default: false, null: false
|
||||||
|
t.string "client_id", null: false
|
||||||
|
t.string "client_secret", null: false
|
||||||
|
t.string "authorize_url", null: false
|
||||||
|
t.string "token_url", null: false
|
||||||
|
t.string "profile_url", null: false
|
||||||
|
t.string "scope", null: false
|
||||||
|
t.string "json_user_name_path"
|
||||||
|
t.string "json_user_email_path", null: false
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["name", "tenant_id"], name: "index_o_auths_on_name_and_tenant_id", unique: true
|
||||||
|
t.index ["tenant_id"], name: "index_o_auths_on_tenant_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "post_status_changes", force: :cascade do |t|
|
create_table "post_status_changes", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
@@ -151,6 +170,7 @@ ActiveRecord::Schema.define(version: 2022_07_15_092725) do
|
|||||||
add_foreign_key "likes", "posts"
|
add_foreign_key "likes", "posts"
|
||||||
add_foreign_key "likes", "tenants"
|
add_foreign_key "likes", "tenants"
|
||||||
add_foreign_key "likes", "users"
|
add_foreign_key "likes", "users"
|
||||||
|
add_foreign_key "o_auths", "tenants"
|
||||||
add_foreign_key "post_status_changes", "post_statuses"
|
add_foreign_key "post_status_changes", "post_statuses"
|
||||||
add_foreign_key "post_status_changes", "posts"
|
add_foreign_key "post_status_changes", "posts"
|
||||||
add_foreign_key "post_status_changes", "tenants"
|
add_foreign_key "post_status_changes", "tenants"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ services:
|
|||||||
dockerfile: ./docker/app/Dockerfile
|
dockerfile: ./docker/app/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- UPDATE=0
|
- UPDATE=0
|
||||||
|
- BASE_URL
|
||||||
- ENVIRONMENT
|
- ENVIRONMENT
|
||||||
- SECRET_KEY_BASE
|
- SECRET_KEY_BASE
|
||||||
- POSTGRES_USER
|
- POSTGRES_USER
|
||||||
|
|||||||
15
spec/factories/o_auths.rb
Normal file
15
spec/factories/o_auths.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :o_auth do
|
||||||
|
sequence(:name) { |n| "OAuth#{n}" }
|
||||||
|
logo { "url_to_logo" }
|
||||||
|
is_enabled { false }
|
||||||
|
client_id { "123456" }
|
||||||
|
client_secret { "123456" }
|
||||||
|
authorize_url { "authorize_url" }
|
||||||
|
token_url { "token_url" }
|
||||||
|
profile_url { "profile_url" }
|
||||||
|
scope { "read" }
|
||||||
|
json_user_name_path { "user.name" }
|
||||||
|
json_user_email_path { "user.email" }
|
||||||
|
end
|
||||||
|
end
|
||||||
53
spec/helpers/o_auths_helper_spec.rb
Normal file
53
spec/helpers/o_auths_helper_spec.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe OAuthsHelper, type: :helper do
|
||||||
|
context 'query_path_from_hash method' do
|
||||||
|
it 'queries a path from hash' do
|
||||||
|
email = "admin@example.com"
|
||||||
|
name = "Admin"
|
||||||
|
surname = "Example"
|
||||||
|
hash = {
|
||||||
|
"email" => email,
|
||||||
|
"info" => {
|
||||||
|
"name" => name,
|
||||||
|
"additional_info" => {
|
||||||
|
"surnames" => [
|
||||||
|
["surname" => "Surname1"],
|
||||||
|
["surname" => "Surname2"],
|
||||||
|
["surname" => surname]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
email_path = "email"
|
||||||
|
name_path = "info.name"
|
||||||
|
surname_path = "info.additional_info.surnames[2][0].surname"
|
||||||
|
|
||||||
|
expect(helper.query_path_from_hash(hash, name_path)).to eq(name)
|
||||||
|
expect(helper.query_path_from_hash(hash, email_path)).to eq(email)
|
||||||
|
expect(helper.query_path_from_hash(hash, surname_path)).to eq(surname)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil if inputs are not of type Hash and String respectively' do
|
||||||
|
expect(helper.query_path_from_hash({"valid" => true}, ["invalid"])).to eq(nil)
|
||||||
|
expect(helper.query_path_from_hash("invalid", "valid")).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil if path not found' do
|
||||||
|
email = "admin@example.com"
|
||||||
|
name = "Admin"
|
||||||
|
hash = {
|
||||||
|
"email" => email,
|
||||||
|
"info" => {
|
||||||
|
"name" => name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name_path = "name"
|
||||||
|
expect(helper.query_path_from_hash(hash, name_path)).to eq(nil)
|
||||||
|
|
||||||
|
name_path = "info.names[0]"
|
||||||
|
expect(helper.query_path_from_hash(hash, name_path)).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
64
spec/models/o_auth_spec.rb
Normal file
64
spec/models/o_auth_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe OAuth, type: :model do
|
||||||
|
let(:o_auth) { FactoryBot.create(:o_auth) }
|
||||||
|
|
||||||
|
it 'should be valid' do
|
||||||
|
expect(o_auth).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a non-nil unique name' do
|
||||||
|
o_auth2 = FactoryBot.build_stubbed(:o_auth, name: o_auth.name)
|
||||||
|
|
||||||
|
expect(o_auth2).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is disabled by default' do
|
||||||
|
o_auth = OAuth.new
|
||||||
|
|
||||||
|
expect(o_auth.is_enabled).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a boolean enabled status' do
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, is_enabled: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, is_enabled: true)
|
||||||
|
expect(o_auth).to be_valid
|
||||||
|
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, is_enabled: false)
|
||||||
|
expect(o_auth).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has non-nil client credentials' do
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, client_id: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, client_secret: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has non-nil urls' do
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, authorize_url: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, token_url: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, profile_url: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a non-nil scope' do
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, scope: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a non-nil json user email path and a nullable name path' do
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, json_user_email_path: nil)
|
||||||
|
expect(o_auth).to be_invalid
|
||||||
|
|
||||||
|
o_auth = FactoryBot.build_stubbed(:o_auth, json_user_name_path: nil)
|
||||||
|
expect(o_auth).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user