commit
362b4e18fd
@ -1,79 +0,0 @@ |
||||
# Request custom OAuth credentials for the user |
||||
# @param options {optional} |
||||
# @param credentialRequestCompleteCallback {Function} Callback function to call on |
||||
# completion. Takes one argument, credentialToken on success, or Error on |
||||
# error. |
||||
class CustomOAuth |
||||
constructor: (@name, options) -> |
||||
if not Match.test @name, String |
||||
return throw new Meteor.Error 'CustomOAuth: Name is required and must be String' |
||||
|
||||
@configure options |
||||
|
||||
Accounts.oauth.registerService @name |
||||
|
||||
@configureLogin() |
||||
|
||||
configure: (options) -> |
||||
if not Match.test options, Object |
||||
return throw new Meteor.Error 'CustomOAuth: Options is required and must be Object' |
||||
|
||||
if not Match.test options.serverURL, String |
||||
return throw new Meteor.Error 'CustomOAuth: Options.serverURL is required and must be String' |
||||
|
||||
if not Match.test options.authorizePath, String |
||||
options.authorizePath = '/oauth/authorize' |
||||
|
||||
if not Match.test options.scope, String |
||||
options.scope = 'openid' |
||||
|
||||
@serverURL = options.serverURL |
||||
@authorizePath = options.authorizePath |
||||
@scope = options.scope |
||||
|
||||
if not /^https?:\/\/.+/.test @authorizePath |
||||
@authorizePath = @serverURL + @authorizePath |
||||
|
||||
configureLogin: -> |
||||
self = @ |
||||
loginWithService = "loginWith" + s.capitalize(@name) |
||||
|
||||
Meteor[loginWithService] = (options, callback) -> |
||||
# support a callback without options |
||||
if not callback and typeof options is "function" |
||||
callback = options |
||||
options = null |
||||
|
||||
credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback) |
||||
self.requestCredential(options, credentialRequestCompleteCallback) |
||||
|
||||
requestCredential: (options, credentialRequestCompleteCallback) -> |
||||
# support both (options, callback) and (callback). |
||||
if not credentialRequestCompleteCallback and typeof options is 'function' |
||||
credentialRequestCompleteCallback = options |
||||
options = {} |
||||
|
||||
config = ServiceConfiguration.configurations.findOne service: @name |
||||
if not config |
||||
credentialRequestCompleteCallback? new ServiceConfiguration.ConfigError() |
||||
return |
||||
|
||||
credentialToken = Random.secret() |
||||
loginStyle = OAuth._loginStyle @name, config, options |
||||
|
||||
loginUrl = @authorizePath + |
||||
'?client_id=' + config.clientId + |
||||
'&redirect_uri=' + OAuth._redirectUri(@name, config) + |
||||
'&response_type=code' + |
||||
'&state=' + OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl) + |
||||
'&scope=' + @scope |
||||
|
||||
OAuth.launchLogin |
||||
loginService: @name |
||||
loginStyle: loginStyle |
||||
loginUrl: loginUrl |
||||
credentialRequestCompleteCallback: credentialRequestCompleteCallback |
||||
credentialToken: credentialToken |
||||
popupOptions: |
||||
width: 900 |
||||
height: 450 |
||||
@ -0,0 +1,101 @@ |
||||
/*globals OAuth*/ |
||||
// Request custom OAuth credentials for the user
|
||||
// @param options {optional}
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
|
||||
export class CustomOAuth { |
||||
constructor(name, options) { |
||||
this.name = name; |
||||
if (!Match.test(this.name, String)) { |
||||
throw new Meteor.Error('CustomOAuth: Name is required and must be String'); |
||||
} |
||||
|
||||
this.configure(options); |
||||
|
||||
Accounts.oauth.registerService(this.name); |
||||
|
||||
this.configureLogin(); |
||||
} |
||||
|
||||
configure(options) { |
||||
if (!Match.test(options, Object)) { |
||||
throw new Meteor.Error('CustomOAuth: Options is required and must be Object'); |
||||
} |
||||
|
||||
if (!Match.test(options.serverURL, String)) { |
||||
throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String'); |
||||
} |
||||
|
||||
if (!Match.test(options.authorizePath, String)) { |
||||
options.authorizePath = '/oauth/authorize'; |
||||
} |
||||
|
||||
if (!Match.test(options.scope, String)) { |
||||
options.scope = 'openid'; |
||||
} |
||||
|
||||
this.serverURL = options.serverURL; |
||||
this.authorizePath = options.authorizePath; |
||||
this.scope = options.scope; |
||||
|
||||
if (!/^https?:\/\/.+/.test(this.authorizePath)) { |
||||
this.authorizePath = this.serverURL + this.authorizePath; |
||||
} |
||||
} |
||||
|
||||
configureLogin() { |
||||
self = this; |
||||
const loginWithService = 'loginWith' + s.capitalize(this.name); |
||||
|
||||
Meteor[loginWithService] = (options, callback) => { |
||||
// support a callback without options
|
||||
if (!callback && typeof options === 'function') { |
||||
callback = options; |
||||
options = null; |
||||
} |
||||
|
||||
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); |
||||
self.requestCredential(options, credentialRequestCompleteCallback); |
||||
}; |
||||
} |
||||
|
||||
requestCredential(options, credentialRequestCompleteCallback) { |
||||
// support both (options, callback) and (callback).
|
||||
if (!credentialRequestCompleteCallback && typeof options === 'function') { |
||||
credentialRequestCompleteCallback = options; |
||||
options = {}; |
||||
} |
||||
|
||||
const config = ServiceConfiguration.configurations.findOne({service: this.name}); |
||||
if (!config) { |
||||
if (credentialRequestCompleteCallback) { |
||||
credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError()); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
const credentialToken = Random.secret(); |
||||
const loginStyle = OAuth._loginStyle(this.name, config, options); |
||||
|
||||
const loginUrl = this.authorizePath + |
||||
'?client_id=' + config.clientId + |
||||
'&redirect_uri=' + OAuth._redirectUri(this.name, config) + |
||||
'&response_type=code' + |
||||
'&state=' + OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl) + |
||||
'&scope=' + this.scope; |
||||
|
||||
OAuth.launchLogin({ |
||||
loginService: this.name, |
||||
loginStyle: loginStyle, |
||||
loginUrl: loginUrl, |
||||
credentialRequestCompleteCallback: credentialRequestCompleteCallback, |
||||
credentialToken: credentialToken, |
||||
popupOptions: { |
||||
width: 900, |
||||
height: 450 |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
@ -1,162 +0,0 @@ |
||||
Services = {} |
||||
|
||||
class CustomOAuth |
||||
constructor: (@name, options) -> |
||||
if not Match.test @name, String |
||||
return throw new Meteor.Error 'CustomOAuth: Name is required and must be String' |
||||
|
||||
if Services[@name]? |
||||
Services[@name].configure options |
||||
return |
||||
|
||||
Services[@name] = @ |
||||
|
||||
@configure options |
||||
|
||||
@userAgent = "Meteor" |
||||
if Meteor.release |
||||
@userAgent += '/' + Meteor.release |
||||
|
||||
Accounts.oauth.registerService @name |
||||
@registerService() |
||||
|
||||
configure: (options) -> |
||||
if not Match.test options, Object |
||||
return throw new Meteor.Error 'CustomOAuth: Options is required and must be Object' |
||||
|
||||
if not Match.test options.serverURL, String |
||||
return throw new Meteor.Error 'CustomOAuth: Options.serverURL is required and must be String' |
||||
|
||||
if not Match.test options.tokenPath, String |
||||
options.tokenPath = '/oauth/token' |
||||
|
||||
if not Match.test options.identityPath, String |
||||
options.identityPath = '/me' |
||||
|
||||
@serverURL = options.serverURL |
||||
@tokenPath = options.tokenPath |
||||
@identityPath = options.identityPath |
||||
@tokenSentVia = options.tokenSentVia |
||||
|
||||
if not /^https?:\/\/.+/.test @tokenPath |
||||
@tokenPath = @serverURL + @tokenPath |
||||
|
||||
if not /^https?:\/\/.+/.test @identityPath |
||||
@identityPath = @serverURL + @identityPath |
||||
|
||||
if Match.test options.addAutopublishFields, Object |
||||
Accounts.addAutopublishFields options.addAutopublishFields |
||||
|
||||
getAccessToken: (query) -> |
||||
config = ServiceConfiguration.configurations.findOne service: @name |
||||
if not config? |
||||
throw new ServiceConfiguration.ConfigError() |
||||
|
||||
response = undefined |
||||
try |
||||
response = HTTP.post @tokenPath, |
||||
auth: config.clientId + ':' + OAuth.openSecret(config.secret) |
||||
headers: |
||||
Accept: 'application/json' |
||||
'User-Agent': @userAgent |
||||
params: |
||||
code: query.code |
||||
client_id: config.clientId |
||||
client_secret: OAuth.openSecret(config.secret) |
||||
redirect_uri: OAuth._redirectUri(@name, config) |
||||
grant_type: 'authorization_code' |
||||
state: query.state |
||||
|
||||
catch err |
||||
error = new Error("Failed to complete OAuth handshake with #{@name} at #{@tokenPath}. " + err.message) |
||||
throw _.extend error, {response: err.response} |
||||
|
||||
if response.data.error #if the http response was a json object with an error attribute |
||||
throw new Error("Failed to complete OAuth handshake with #{@name} at #{@tokenPath}. " + response.data.error) |
||||
else |
||||
return response.data.access_token |
||||
|
||||
getIdentity: (accessToken) -> |
||||
params = {} |
||||
headers = |
||||
'User-Agent': @userAgent # http://doc.gitlab.com/ce/api/users.html#Current-user |
||||
|
||||
if @tokenSentVia is 'header' |
||||
headers['Authorization'] = 'Bearer ' + accessToken |
||||
else |
||||
params['access_token'] = accessToken |
||||
|
||||
try |
||||
response = HTTP.get @identityPath, |
||||
headers: headers |
||||
params: params |
||||
|
||||
if response.data |
||||
return response.data |
||||
else |
||||
return JSON.parse response.content |
||||
|
||||
catch err |
||||
error = new Error("Failed to fetch identity from #{@name} at #{@identityPath}. " + err.message) |
||||
throw _.extend error, {response: err.response} |
||||
|
||||
registerService: -> |
||||
self = @ |
||||
OAuth.registerService @name, 2, null, (query) -> |
||||
accessToken = self.getAccessToken query |
||||
# console.log 'at:', accessToken |
||||
|
||||
identity = self.getIdentity accessToken |
||||
|
||||
# Fix for Reddit |
||||
if identity?.result |
||||
identity = identity.result |
||||
|
||||
# Fix WordPress-like identities having 'ID' instead of 'id' |
||||
if identity?.ID and not identity.id |
||||
identity.id = identity.ID |
||||
|
||||
# Fix Auth0-like identities having 'user_id' instead of 'id' |
||||
if identity?.user_id and not identity.id |
||||
identity.id = identity.user_id |
||||
|
||||
if identity?.CharacterID and not identity.id |
||||
identity.id = identity.CharacterID |
||||
|
||||
# Fix Dataporten having 'user.userid' instead of 'id' |
||||
if identity?.user?.userid and not identity.id |
||||
identity.id = identity.user.userid |
||||
identity.email = identity.user.email |
||||
|
||||
# Fix general 'phid' instead of 'id' from phabricator |
||||
if identity?.phid and not identity.id |
||||
identity.id = identity.phid |
||||
|
||||
# Fix Keycloak-like identities having 'sub' instead of 'id' |
||||
if identity?.sub and not identity.id |
||||
identity.id = identity.sub |
||||
|
||||
# Fix general 'userid' instead of 'id' from provider |
||||
if identity?.userid and not identity.id |
||||
identity.id = identity.userid |
||||
|
||||
# console.log 'id:', JSON.stringify identity, null, ' ' |
||||
|
||||
serviceData = |
||||
_OAuthCustom: true |
||||
accessToken: accessToken |
||||
|
||||
_.extend serviceData, identity |
||||
|
||||
data = |
||||
serviceData: serviceData |
||||
options: |
||||
profile: |
||||
name: identity.name or identity.username or identity.nickname or identity.CharacterName or identity.userName or identity.preferred_username or identity.user?.name |
||||
|
||||
# console.log data |
||||
|
||||
return data |
||||
|
||||
retrieveCredential: (credentialToken, credentialSecret) -> |
||||
return OAuth.retrieveCredential credentialToken, credentialSecret |
||||
@ -0,0 +1,293 @@ |
||||
/*globals OAuth*/ |
||||
|
||||
const logger = new Logger('CustomOAuth'); |
||||
|
||||
const Services = {}; |
||||
const BeforeUpdateOrCreateUserFromExternalService = []; |
||||
|
||||
export class CustomOAuth { |
||||
constructor(name, options) { |
||||
logger.debug('Init CustomOAuth', name, options); |
||||
|
||||
this.name = name; |
||||
if (!Match.test(this.name, String)) { |
||||
throw new Meteor.Error('CustomOAuth: Name is required and must be String'); |
||||
} |
||||
|
||||
if (Services[this.name]) { |
||||
Services[this.name].configure(options); |
||||
return; |
||||
} |
||||
|
||||
Services[this.name] = this; |
||||
|
||||
this.configure(options); |
||||
|
||||
this.userAgent = 'Meteor'; |
||||
if (Meteor.release) { |
||||
this.userAgent += '/' + Meteor.release; |
||||
} |
||||
|
||||
Accounts.oauth.registerService(this.name); |
||||
this.registerService(); |
||||
this.addHookToProcessUser(); |
||||
} |
||||
|
||||
configure(options) { |
||||
if (!Match.test(options, Object)) { |
||||
throw new Meteor.Error('CustomOAuth: Options is required and must be Object'); |
||||
} |
||||
|
||||
if (!Match.test(options.serverURL, String)) { |
||||
throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String'); |
||||
} |
||||
|
||||
if (!Match.test(options.tokenPath, String)) { |
||||
options.tokenPath = '/oauth/token'; |
||||
} |
||||
|
||||
if (!Match.test(options.identityPath, String)) { |
||||
options.identityPath = '/me'; |
||||
} |
||||
|
||||
this.serverURL = options.serverURL; |
||||
this.tokenPath = options.tokenPath; |
||||
this.identityPath = options.identityPath; |
||||
this.tokenSentVia = options.tokenSentVia; |
||||
this.usernameField = (options.usernameField || '').trim(); |
||||
this.mergeUsers = options.mergeUsers; |
||||
|
||||
if (!/^https?:\/\/.+/.test(this.tokenPath)) { |
||||
this.tokenPath = this.serverURL + this.tokenPath; |
||||
} |
||||
|
||||
if (!/^https?:\/\/.+/.test(this.identityPath)) { |
||||
this.identityPath = this.serverURL + this.identityPath; |
||||
} |
||||
|
||||
if (Match.test(options.addAutopublishFields, Object)) { |
||||
Accounts.addAutopublishFields(options.addAutopublishFields); |
||||
} |
||||
} |
||||
|
||||
getAccessToken(query) { |
||||
const config = ServiceConfiguration.configurations.findOne({service: this.name}); |
||||
if (!config) { |
||||
throw new ServiceConfiguration.ConfigError(); |
||||
} |
||||
|
||||
let response = undefined; |
||||
try { |
||||
response = HTTP.post(this.tokenPath, { |
||||
auth: config.clientId + ':' + OAuth.openSecret(config.secret), |
||||
headers: { |
||||
Accept: 'application/json', |
||||
'User-Agent': this.userAgent |
||||
}, |
||||
params: { |
||||
code: query.code, |
||||
client_id: config.clientId, |
||||
client_secret: OAuth.openSecret(config.secret), |
||||
redirect_uri: OAuth._redirectUri(this.name, config), |
||||
grant_type: 'authorization_code', |
||||
state: query.state |
||||
} |
||||
}); |
||||
} catch (err) { |
||||
const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`); |
||||
throw _.extend(error, {response: err.response}); |
||||
} |
||||
|
||||
if (response.data.error) { //if the http response was a json object with an error attribute
|
||||
throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.data.error}`); |
||||
} else { |
||||
return response.data.access_token; |
||||
} |
||||
} |
||||
|
||||
getIdentity(accessToken) { |
||||
const params = {}; |
||||
const headers = { |
||||
'User-Agent': this.userAgent // http://doc.gitlab.com/ce/api/users.html#Current-user
|
||||
}; |
||||
|
||||
if (this.tokenSentVia === 'header') { |
||||
headers['Authorization'] = 'Bearer ' + accessToken; |
||||
} else { |
||||
params['access_token'] = accessToken; |
||||
} |
||||
|
||||
try { |
||||
const response = HTTP.get(this.identityPath, { |
||||
headers: headers, |
||||
params: params |
||||
}); |
||||
|
||||
let data; |
||||
|
||||
if (response.data) { |
||||
data = response.data; |
||||
} else { |
||||
data = JSON.parse(response.content); |
||||
} |
||||
|
||||
logger.debug('Identity response', JSON.stringify(data, null, 2)); |
||||
|
||||
return data; |
||||
} catch (err) { |
||||
const error = new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}. ${err.message}`); |
||||
throw _.extend(error, {response: err.response}); |
||||
} |
||||
} |
||||
|
||||
registerService() { |
||||
const self = this; |
||||
OAuth.registerService(this.name, 2, null, (query) => { |
||||
const accessToken = self.getAccessToken(query); |
||||
// console.log 'at:', accessToken
|
||||
|
||||
let identity = self.getIdentity(accessToken); |
||||
|
||||
if (identity) { |
||||
// Fix for Reddit
|
||||
if (identity.result) { |
||||
identity = identity.result; |
||||
} |
||||
|
||||
// Fix WordPress-like identities having 'ID' instead of 'id'
|
||||
if (identity.ID && !identity.id) { |
||||
identity.id = identity.ID; |
||||
} |
||||
|
||||
// Fix Auth0-like identities having 'user_id' instead of 'id'
|
||||
if (identity.user_id && !identity.id) { |
||||
identity.id = identity.user_id; |
||||
} |
||||
|
||||
if (identity.CharacterID && !identity.id) { |
||||
identity.id = identity.CharacterID; |
||||
} |
||||
|
||||
// Fix Dataporten having 'user.userid' instead of 'id'
|
||||
if (identity.user && identity.user.userid && !identity.id) { |
||||
identity.id = identity.user.userid; |
||||
identity.email = identity.user.email; |
||||
} |
||||
|
||||
// Fix general 'phid' instead of 'id' from phabricator
|
||||
if (identity.phid && !identity.id) { |
||||
identity.id = identity.phid; |
||||
} |
||||
|
||||
// Fix Keycloak-like identities having 'sub' instead of 'id'
|
||||
if (identity.sub && !identity.id) { |
||||
identity.id = identity.sub; |
||||
} |
||||
|
||||
// Fix general 'userid' instead of 'id' from provider
|
||||
if (identity.userid && !identity.id) { |
||||
identity.id = identity.userid; |
||||
} |
||||
} |
||||
|
||||
// console.log 'id:', JSON.stringify identity, null, ' '
|
||||
|
||||
const serviceData = { |
||||
_OAuthCustom: true, |
||||
accessToken: accessToken |
||||
}; |
||||
|
||||
_.extend(serviceData, identity); |
||||
|
||||
const data = { |
||||
serviceData: serviceData, |
||||
options: { |
||||
profile: { |
||||
name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name) |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// console.log data
|
||||
|
||||
return data; |
||||
}); |
||||
} |
||||
|
||||
retrieveCredential(credentialToken, credentialSecret) { |
||||
return OAuth.retrieveCredential(credentialToken, credentialSecret); |
||||
} |
||||
|
||||
getUsername(data) { |
||||
let username = ''; |
||||
|
||||
if (this.usernameField.indexOf('#{') > -1) { |
||||
username = this.usernameField.replace(/#{(.+?)}/g, function(match, field) { |
||||
if (!data[field]) { |
||||
throw new Meteor.Error(`Username template item "${field}" not found in data`, data); |
||||
} |
||||
return data[field]; |
||||
}); |
||||
} else { |
||||
username = data[this.usernameField]; |
||||
if (!username) { |
||||
throw new Meteor.Error(`Username field "${this.usernameField}" not found in data`, data); |
||||
} |
||||
} |
||||
|
||||
return username; |
||||
} |
||||
|
||||
addHookToProcessUser() { |
||||
BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/*, options*/) => { |
||||
if (serviceName !== this.name) { |
||||
return; |
||||
} |
||||
|
||||
if (this.usernameField) { |
||||
const username = this.getUsername(serviceData); |
||||
|
||||
const user = RocketChat.models.Users.findOneByUsername(username); |
||||
if (!user) { |
||||
return; |
||||
} |
||||
|
||||
if (this.mergeUsers !== true) { |
||||
throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`); |
||||
} |
||||
|
||||
const serviceIdKey = `services.${serviceName}.id`; |
||||
const update = { |
||||
$set: { |
||||
[serviceIdKey]: serviceData.id |
||||
} |
||||
}; |
||||
|
||||
RocketChat.models.Users.update({_id: user._id}, update); |
||||
} |
||||
}); |
||||
|
||||
Accounts.validateNewUser((user) => { |
||||
if (!user.services || !user.services[this.name] || !user.services[this.name].id) { |
||||
return true; |
||||
} |
||||
|
||||
if (this.usernameField) { |
||||
user.username = this.getUsername(user.services[this.name]); |
||||
} |
||||
|
||||
return true; |
||||
}); |
||||
|
||||
} |
||||
} |
||||
|
||||
|
||||
const updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService; |
||||
Accounts.updateOrCreateUserFromExternalService = function(/*serviceName, serviceData, options*/) { |
||||
for (const hook of BeforeUpdateOrCreateUserFromExternalService) { |
||||
hook.apply(this, arguments); |
||||
} |
||||
|
||||
return updateOrCreateUserFromExternalService.apply(this, arguments); |
||||
}; |
||||
@ -0,0 +1,12 @@ |
||||
RocketChat.Migrations.add |
||||
version: 69 |
||||
up: -> |
||||
RocketChat.models.Settings.update { "_id": "theme-color-custom-scrollbar-color", "value": "rgba(255, 255, 255, 0.05)" }, { $set: { "editor": "expression", "value": "@transparent-darker" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-info-font-color", "value": "#aaaaaa" }, { $set: { "editor": "expression", "value": "@secondary-font-color" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-link-font-color", "value": "#008ce3" }, { $set: { "editor": "expression", "value": "@primary-action-color" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-status-away", "value": "#fcb316" }, { $set: { "editor": "expression", "value": "@pending-color" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-status-busy", "value": "#d30230" }, { $set: { "editor": "expression", "value": "@error-color" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-status-offline", "value": "rgba(150, 150, 150, 0.50)" }, { $set: { "editor": "expression", "value": "@transparent-darker" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-status-online", "value": "#35ac19" }, { $set: { "editor": "expression", "value": "@success-color" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-tertiary-background-color", "value": "#eaeaea" }, { $set: { "editor": "expression", "value": "@component-color" } } |
||||
RocketChat.models.Settings.update { "_id": "theme-color-tertiary-font-color", "value": "rgba(255, 255, 255, 0.6)" }, { $set: { "editor": "expression", "value": "@transparent-lightest" } } |
||||
@ -0,0 +1,14 @@ |
||||
RocketChat.Migrations.add({ |
||||
version: 70, |
||||
up: function() { |
||||
const settings = RocketChat.models.Settings.find({ _id: /^Accounts_OAuth_Custom_.+/ }).fetch(); |
||||
for (const setting of settings) { |
||||
const _id = setting._id; |
||||
setting._id = setting._id.replace(/Accounts_OAuth_Custom_([A-Za-z0-9]+)_(.+)/, 'Accounts_OAuth_Custom-$1-$2'); |
||||
setting._id = setting._id.replace(/Accounts_OAuth_Custom_([A-Za-z0-9]+)/, 'Accounts_OAuth_Custom-$1'); |
||||
|
||||
RocketChat.models.Settings.remove({ _id: _id }); |
||||
RocketChat.models.Settings.insert(setting); |
||||
} |
||||
} |
||||
}); |
||||
Loading…
Reference in new issue