mirror of
https://github.com/discourse/discourse.git
synced 2025-09-06 10:50:21 +08:00
FEATURE: List, revoke and reconnect associated accounts. Phase 1 (#6099)
Listing connections is supported for all built-in auth providers. Revoke and reconnect is currently only implemented for Facebook.
This commit is contained in:
parent
32062864d3
commit
eda1462b3b
40 changed files with 836 additions and 240 deletions
|
@ -40,6 +40,18 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||||
.join(", ");
|
.join(", ");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed("model.associated_accounts")
|
||||||
|
associatedAccountsLoaded(associatedAccounts) {
|
||||||
|
return typeof associatedAccounts !== "undefined";
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model.associated_accounts")
|
||||||
|
associatedAccounts(associatedAccounts) {
|
||||||
|
return associatedAccounts
|
||||||
|
.map(provider => `${provider.name} (${provider.description})`)
|
||||||
|
.join(", ");
|
||||||
|
},
|
||||||
|
|
||||||
userFields: function() {
|
userFields: function() {
|
||||||
const siteUserFields = this.site.get("user_fields"),
|
const siteUserFields = this.site.get("user_fields"),
|
||||||
userFields = this.get("model.user_fields");
|
userFields = this.get("model.user_fields");
|
||||||
|
|
|
@ -109,10 +109,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='display-row associations'>
|
<div class='display-row associations'>
|
||||||
<div class='field'>{{i18n 'user.associated_accounts'}}</div>
|
<div class='field'>{{i18n 'user.associated_accounts.title'}}</div>
|
||||||
<div class='value'>
|
<div class='value'>
|
||||||
{{#if model.associated_accounts}}
|
{{#if associatedAccountsLoaded}}
|
||||||
{{model.associated_accounts}}
|
{{associatedAccounts}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}}
|
{{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -193,50 +193,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
},
|
},
|
||||||
|
|
||||||
externalLogin: function(loginMethod) {
|
externalLogin: function(loginMethod) {
|
||||||
const name = loginMethod.get("name");
|
loginMethod.doLogin();
|
||||||
const customLogin = loginMethod.get("customLogin");
|
|
||||||
|
|
||||||
if (customLogin) {
|
|
||||||
customLogin();
|
|
||||||
} else {
|
|
||||||
let authUrl =
|
|
||||||
loginMethod.get("customUrl") || Discourse.getURL("/auth/" + name);
|
|
||||||
if (loginMethod.get("fullScreenLogin")) {
|
|
||||||
document.cookie = "fsl=true";
|
|
||||||
window.location = authUrl;
|
|
||||||
} else {
|
|
||||||
this.set("authenticate", name);
|
|
||||||
const left = this.get("lastX") - 400;
|
|
||||||
const top = this.get("lastY") - 200;
|
|
||||||
|
|
||||||
const height = loginMethod.get("frameHeight") || 400;
|
|
||||||
const width = loginMethod.get("frameWidth") || 800;
|
|
||||||
|
|
||||||
if (loginMethod.get("displayPopup")) {
|
|
||||||
authUrl = authUrl + "?display=popup";
|
|
||||||
}
|
|
||||||
|
|
||||||
const w = window.open(
|
|
||||||
authUrl,
|
|
||||||
"_blank",
|
|
||||||
"menubar=no,status=no,height=" +
|
|
||||||
height +
|
|
||||||
",width=" +
|
|
||||||
width +
|
|
||||||
",left=" +
|
|
||||||
left +
|
|
||||||
",top=" +
|
|
||||||
top
|
|
||||||
);
|
|
||||||
const self = this;
|
|
||||||
const timer = setInterval(function() {
|
|
||||||
if (!w || w.closed) {
|
|
||||||
clearInterval(timer);
|
|
||||||
self.set("authenticate", null);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createAccount: function() {
|
createAccount: function() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import PreferencesTabController from "discourse/mixins/preferences-tab-controlle
|
||||||
import { setting } from "discourse/lib/computed";
|
import { setting } from "discourse/lib/computed";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import { findAll } from "discourse/models/login-method";
|
||||||
|
|
||||||
export default Ember.Controller.extend(
|
export default Ember.Controller.extend(
|
||||||
CanCheckEmails,
|
CanCheckEmails,
|
||||||
|
@ -54,6 +55,44 @@ export default Ember.Controller.extend(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed("model.associated_accounts")
|
||||||
|
associatedAccountsLoaded(associatedAccounts) {
|
||||||
|
return typeof associatedAccounts !== "undefined";
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model.associated_accounts.[]")
|
||||||
|
authProviders(accounts) {
|
||||||
|
const allMethods = findAll(
|
||||||
|
this.siteSettings,
|
||||||
|
this.capabilities,
|
||||||
|
this.site.isMobileDevice
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = allMethods.map(method => {
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
account: accounts.find(account => account.name === method.name) // Will be undefined if no account
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.filter(value => {
|
||||||
|
return value.account || value.method.get("canConnect");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model.id")
|
||||||
|
disableConnectButtons(userId) {
|
||||||
|
return userId !== this.get("currentUser.id");
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed()
|
||||||
|
canUpdateAssociatedAccounts() {
|
||||||
|
return (
|
||||||
|
findAll(this.siteSettings, this.capabilities, this.site.isMobileDevice)
|
||||||
|
.length > 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
save() {
|
||||||
this.set("saved", false);
|
this.set("saved", false);
|
||||||
|
@ -135,6 +174,28 @@ export default Ember.Controller.extend(
|
||||||
|
|
||||||
showTwoFactorModal() {
|
showTwoFactorModal() {
|
||||||
showModal("second-factor-intro");
|
showModal("second-factor-intro");
|
||||||
|
},
|
||||||
|
|
||||||
|
revokeAccount(account) {
|
||||||
|
const model = this.get("model");
|
||||||
|
this.set("revoking", true);
|
||||||
|
model
|
||||||
|
.revokeAssociatedAccount(account.name)
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
model.get("associated_accounts").removeObject(account);
|
||||||
|
} else {
|
||||||
|
bootbox.alert(result.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => {
|
||||||
|
this.set("revoking", false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
connectAccount(method) {
|
||||||
|
method.doLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,22 @@ const LoginMethod = Ember.Object.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.get("titleOverride") ||
|
this.get("titleOverride") || I18n.t(`login.${this.get("name")}.title`)
|
||||||
I18n.t("login." + this.get("name") + ".title")
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
prettyName() {
|
||||||
|
const prettyNameSetting = this.get("prettyNameSetting");
|
||||||
|
if (!Ember.isEmpty(prettyNameSetting)) {
|
||||||
|
const result = this.siteSettings[prettyNameSetting];
|
||||||
|
if (!Ember.isEmpty(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.get("prettyNameOverride") || I18n.t(`login.${this.get("name")}.name`)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -23,6 +37,52 @@ const LoginMethod = Ember.Object.extend({
|
||||||
this.get("messageOverride") ||
|
this.get("messageOverride") ||
|
||||||
I18n.t("login." + this.get("name") + ".message")
|
I18n.t("login." + this.get("name") + ".message")
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
doLogin() {
|
||||||
|
const name = this.get("name");
|
||||||
|
const customLogin = this.get("customLogin");
|
||||||
|
|
||||||
|
if (customLogin) {
|
||||||
|
customLogin();
|
||||||
|
} else {
|
||||||
|
let authUrl = this.get("customUrl") || Discourse.getURL("/auth/" + name);
|
||||||
|
if (this.get("fullScreenLogin")) {
|
||||||
|
document.cookie = "fsl=true";
|
||||||
|
window.location = authUrl;
|
||||||
|
} else {
|
||||||
|
this.set("authenticate", name);
|
||||||
|
const left = this.get("lastX") - 400;
|
||||||
|
const top = this.get("lastY") - 200;
|
||||||
|
|
||||||
|
const height = this.get("frameHeight") || 400;
|
||||||
|
const width = this.get("frameWidth") || 800;
|
||||||
|
|
||||||
|
if (this.get("displayPopup")) {
|
||||||
|
authUrl = authUrl + "?display=popup";
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = window.open(
|
||||||
|
authUrl,
|
||||||
|
"_blank",
|
||||||
|
"menubar=no,status=no,height=" +
|
||||||
|
height +
|
||||||
|
",width=" +
|
||||||
|
width +
|
||||||
|
",left=" +
|
||||||
|
left +
|
||||||
|
",top=" +
|
||||||
|
top
|
||||||
|
);
|
||||||
|
const self = this;
|
||||||
|
const timer = setInterval(function() {
|
||||||
|
if (!w || w.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
self.set("authenticate", null);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,6 +117,10 @@ export function findAll(siteSettings, capabilities, isMobileDevice) {
|
||||||
params.displayPopup = true;
|
params.displayPopup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["facebook"].includes(name)) {
|
||||||
|
params.canConnect = true;
|
||||||
|
}
|
||||||
|
|
||||||
params.siteSettings = siteSettings;
|
params.siteSettings = siteSettings;
|
||||||
methods.pushObject(LoginMethod.create(params));
|
methods.pushObject(LoginMethod.create(params));
|
||||||
}
|
}
|
||||||
|
|
|
@ -372,6 +372,16 @@ const User = RestModel.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
revokeAssociatedAccount(providerName) {
|
||||||
|
return ajax(
|
||||||
|
userPath(`${this.get("username")}/preferences/revoke-account`),
|
||||||
|
{
|
||||||
|
data: { provider_name: providerName },
|
||||||
|
type: "POST"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
loadUserAction(id) {
|
loadUserAction(id) {
|
||||||
const stream = this.get("stream");
|
const stream = this.get("stream");
|
||||||
return ajax(`/user_actions/${id}.json`, { cache: "false" }).then(result => {
|
return ajax(`/user_actions/${id}.json`, { cache: "false" }).then(result => {
|
||||||
|
|
|
@ -99,6 +99,45 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canUpdateAssociatedAccounts}}
|
||||||
|
<div class="control-group pref-associated-accounts">
|
||||||
|
<label class="control-label">{{i18n 'user.associated_accounts.title'}}</label>
|
||||||
|
{{#if associatedAccountsLoaded}}
|
||||||
|
<table>
|
||||||
|
{{#each authProviders as |authProvider|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{authProvider.method.prettyName}}</td>
|
||||||
|
|
||||||
|
{{#if authProvider.account}}
|
||||||
|
<td>{{authProvider.account.description}}</td>
|
||||||
|
<td>
|
||||||
|
{{#if authProvider.account.can_revoke}}
|
||||||
|
{{#conditional-loading-spinner condition=revoking size='small'}}
|
||||||
|
{{d-button action="revokeAccount" actionParam=authProvider.account title="user.associated_accounts.revoke" icon="times-circle" }}
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
{{else}}
|
||||||
|
<td colspan=2>
|
||||||
|
{{#if authProvider.method.canConnect}}
|
||||||
|
{{d-button action="connectAccount" actionParam=authProvider.method label="user.associated_accounts.connect" icon="plug" disabled=disableConnectButtons}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n 'user.associated_accounts.not_connected'}}
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="controls">
|
||||||
|
{{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#unless siteSettings.sso_overrides_avatar}}
|
{{#unless siteSettings.sso_overrides_avatar}}
|
||||||
<div class="control-group pref-avatar">
|
<div class="control-group pref-avatar">
|
||||||
<label class="control-label">{{i18n 'user.avatar.title'}}</label>
|
<label class="control-label">{{i18n 'user.avatar.title'}}</label>
|
||||||
|
|
|
@ -629,6 +629,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pref-associated-accounts table {
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginated-topics-list {
|
.paginated-topics-list {
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Users::OmniauthCallbacksController < ApplicationController
|
||||||
BUILTIN_AUTH = [
|
BUILTIN_AUTH = [
|
||||||
Auth::FacebookAuthenticator.new,
|
Auth::FacebookAuthenticator.new,
|
||||||
Auth::GoogleOAuth2Authenticator.new,
|
Auth::GoogleOAuth2Authenticator.new,
|
||||||
Auth::OpenIdAuthenticator.new("yahoo", "https://me.yahoo.com", trusted: true),
|
Auth::OpenIdAuthenticator.new("yahoo", "https://me.yahoo.com", 'enable_yahoo_logins', trusted: true),
|
||||||
Auth::GithubAuthenticator.new,
|
Auth::GithubAuthenticator.new,
|
||||||
Auth::TwitterAuthenticator.new,
|
Auth::TwitterAuthenticator.new,
|
||||||
Auth::InstagramAuthenticator.new
|
Auth::InstagramAuthenticator.new
|
||||||
|
@ -18,10 +18,6 @@ class Users::OmniauthCallbacksController < ApplicationController
|
||||||
|
|
||||||
layout 'no_ember'
|
layout 'no_ember'
|
||||||
|
|
||||||
def self.types
|
|
||||||
@types ||= Enum.new(:facebook, :instagram, :twitter, :google, :yahoo, :github, :persona, :cas)
|
|
||||||
end
|
|
||||||
|
|
||||||
# need to be able to call this
|
# need to be able to call this
|
||||||
skip_before_action :check_xhr
|
skip_before_action :check_xhr
|
||||||
|
|
||||||
|
@ -36,9 +32,13 @@ class Users::OmniauthCallbacksController < ApplicationController
|
||||||
auth[:session] = session
|
auth[:session] = session
|
||||||
|
|
||||||
authenticator = self.class.find_authenticator(params[:provider])
|
authenticator = self.class.find_authenticator(params[:provider])
|
||||||
provider = Discourse.auth_providers && Discourse.auth_providers.find { |p| p.name == params[:provider] }
|
provider = DiscoursePluginRegistry.auth_providers.find { |p| p.name == params[:provider] }
|
||||||
|
|
||||||
@auth_result = authenticator.after_authenticate(auth)
|
if authenticator.can_connect_existing_user? && current_user
|
||||||
|
@auth_result = authenticator.after_authenticate(auth, existing_account: current_user)
|
||||||
|
else
|
||||||
|
@auth_result = authenticator.after_authenticate(auth)
|
||||||
|
end
|
||||||
|
|
||||||
origin = request.env['omniauth.origin']
|
origin = request.env['omniauth.origin']
|
||||||
|
|
||||||
|
@ -91,23 +91,10 @@ class Users::OmniauthCallbacksController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_authenticator(name)
|
def self.find_authenticator(name)
|
||||||
BUILTIN_AUTH.each do |authenticator|
|
Discourse.enabled_authenticators.each do |authenticator|
|
||||||
if authenticator.name == name
|
return authenticator if authenticator.name == name
|
||||||
raise Discourse::InvalidAccess.new(I18n.t("provider_not_enabled")) unless SiteSetting.send("enable_#{name}_logins?")
|
|
||||||
return authenticator
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
raise Discourse::InvalidAccess.new(I18n.t('authenticator_not_found'))
|
||||||
Discourse.auth_providers.each do |provider|
|
|
||||||
next if provider.name != name
|
|
||||||
|
|
||||||
unless provider.enabled_setting.nil? || SiteSetting.send(provider.enabled_setting)
|
|
||||||
raise Discourse::InvalidAccess.new(I18n.t("provider_not_enabled"))
|
|
||||||
end
|
|
||||||
return provider.authenticator
|
|
||||||
end
|
|
||||||
|
|
||||||
raise Discourse::InvalidAccess.new(I18n.t("provider_not_found"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
|
@ -1072,6 +1072,32 @@ class UsersController < ApplicationController
|
||||||
render json: success_json
|
render json: success_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revoke_account
|
||||||
|
user = fetch_user_from_params
|
||||||
|
guardian.ensure_can_edit!(user)
|
||||||
|
provider_name = params.require(:provider_name)
|
||||||
|
|
||||||
|
# Using Discourse.authenticators rather than Discourse.enabled_authenticators so users can
|
||||||
|
# revoke permissions even if the admin has temporarily disabled that type of login
|
||||||
|
authenticator = Discourse.authenticators.find { |authenticator| authenticator.name == provider_name }
|
||||||
|
raise Discourse::NotFound if authenticator.nil?
|
||||||
|
|
||||||
|
skip_remote = params.permit(:skip_remote)
|
||||||
|
|
||||||
|
# We're likely going to contact the remote auth provider, so hijack request
|
||||||
|
hijack do
|
||||||
|
result = authenticator.revoke(user, skip_remote: skip_remote)
|
||||||
|
if result
|
||||||
|
return render json: success_json
|
||||||
|
else
|
||||||
|
return render json: {
|
||||||
|
success: false,
|
||||||
|
message: I18n.t("associated_accounts.revoke_failed", provider_name: provider_name)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def honeypot_value
|
def honeypot_value
|
||||||
|
|
|
@ -62,7 +62,7 @@ class User < ActiveRecord::Base
|
||||||
has_one :twitter_user_info, dependent: :destroy
|
has_one :twitter_user_info, dependent: :destroy
|
||||||
has_one :github_user_info, dependent: :destroy
|
has_one :github_user_info, dependent: :destroy
|
||||||
has_one :google_user_info, dependent: :destroy
|
has_one :google_user_info, dependent: :destroy
|
||||||
has_one :oauth2_user_info, dependent: :destroy
|
has_many :oauth2_user_infos, dependent: :destroy
|
||||||
has_one :instagram_user_info, dependent: :destroy
|
has_one :instagram_user_info, dependent: :destroy
|
||||||
has_many :user_second_factors, dependent: :destroy
|
has_many :user_second_factors, dependent: :destroy
|
||||||
has_one :user_stat, dependent: :destroy
|
has_one :user_stat, dependent: :destroy
|
||||||
|
@ -952,18 +952,18 @@ class User < ActiveRecord::Base
|
||||||
def associated_accounts
|
def associated_accounts
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
result << "Twitter(#{twitter_user_info.screen_name})" if twitter_user_info
|
Discourse.authenticators.each do |authenticator|
|
||||||
result << "Facebook(#{facebook_user_info.username})" if facebook_user_info
|
account_description = authenticator.description_for_user(self)
|
||||||
result << "Google(#{google_user_info.email})" if google_user_info
|
unless account_description.empty?
|
||||||
result << "GitHub(#{github_user_info.screen_name})" if github_user_info
|
result << {
|
||||||
result << "Instagram(#{instagram_user_info.screen_name})" if instagram_user_info
|
name: authenticator.name,
|
||||||
result << "#{oauth2_user_info.provider}(#{oauth2_user_info.email})" if oauth2_user_info
|
description: account_description,
|
||||||
|
can_revoke: authenticator.can_revoke?
|
||||||
user_open_ids.each do |oid|
|
}
|
||||||
result << "OpenID #{oid.url[0..20]}...(#{oid.email})"
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
result.empty? ? I18n.t("user.no_accounts_associated") : result.join(", ")
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_fields
|
def user_fields
|
||||||
|
|
|
@ -75,7 +75,8 @@ class UserSerializer < BasicUserSerializer
|
||||||
:staged,
|
:staged,
|
||||||
:second_factor_enabled,
|
:second_factor_enabled,
|
||||||
:second_factor_backup_enabled,
|
:second_factor_backup_enabled,
|
||||||
:second_factor_remaining_backup_codes
|
:second_factor_remaining_backup_codes,
|
||||||
|
:associated_accounts
|
||||||
|
|
||||||
has_one :invited_by, embed: :object, serializer: BasicUserSerializer
|
has_one :invited_by, embed: :object, serializer: BasicUserSerializer
|
||||||
has_many :groups, embed: :object, serializer: BasicGroupSerializer
|
has_many :groups, embed: :object, serializer: BasicGroupSerializer
|
||||||
|
@ -145,6 +146,10 @@ class UserSerializer < BasicUserSerializer
|
||||||
(scope.is_staff? && object.staged?)
|
(scope.is_staff? && object.staged?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_associated_accounts?
|
||||||
|
(object.id && object.id == scope.user.try(:id))
|
||||||
|
end
|
||||||
|
|
||||||
def include_second_factor_enabled?
|
def include_second_factor_enabled?
|
||||||
(object&.id == scope.user&.id) || scope.is_staff?
|
(object&.id == scope.user&.id) || scope.is_staff?
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,7 +58,7 @@ class UserAnonymizer
|
||||||
@user.github_user_info.try(:destroy)
|
@user.github_user_info.try(:destroy)
|
||||||
@user.facebook_user_info.try(:destroy)
|
@user.facebook_user_info.try(:destroy)
|
||||||
@user.single_sign_on_record.try(:destroy)
|
@user.single_sign_on_record.try(:destroy)
|
||||||
@user.oauth2_user_info.try(:destroy)
|
@user.oauth2_user_infos.try(:destroy_all)
|
||||||
@user.instagram_user_info.try(:destroy)
|
@user.instagram_user_info.try(:destroy)
|
||||||
@user.user_open_ids.find_each { |x| x.destroy }
|
@user.user_open_ids.find_each { |x| x.destroy }
|
||||||
@user.api_key.try(:destroy)
|
@user.api_key.try(:destroy)
|
||||||
|
|
|
@ -820,6 +820,12 @@ en:
|
||||||
one: "We'll only email you if we haven't seen you in the last minute."
|
one: "We'll only email you if we haven't seen you in the last minute."
|
||||||
other: "We'll only email you if we haven't seen you in the last {{count}} minutes."
|
other: "We'll only email you if we haven't seen you in the last {{count}} minutes."
|
||||||
|
|
||||||
|
associated_accounts:
|
||||||
|
title: "Associated Accounts"
|
||||||
|
connect: "Connect"
|
||||||
|
revoke: "Revoke"
|
||||||
|
not_connected: "(not connected)"
|
||||||
|
|
||||||
name:
|
name:
|
||||||
title: "Name"
|
title: "Name"
|
||||||
instructions: "your full name (optional)"
|
instructions: "your full name (optional)"
|
||||||
|
@ -1007,7 +1013,6 @@ en:
|
||||||
topics: "Topics"
|
topics: "Topics"
|
||||||
replies: "Replies"
|
replies: "Replies"
|
||||||
|
|
||||||
associated_accounts: "Logins"
|
|
||||||
ip_address:
|
ip_address:
|
||||||
title: "Last IP Address"
|
title: "Last IP Address"
|
||||||
registration_ip_address:
|
registration_ip_address:
|
||||||
|
@ -1198,25 +1203,28 @@ en:
|
||||||
preferences: "You need to be logged in to change your user preferences."
|
preferences: "You need to be logged in to change your user preferences."
|
||||||
forgot: "I don't recall my account details"
|
forgot: "I don't recall my account details"
|
||||||
not_approved: "Your account hasn't been approved yet. You will be notified by email when you are ready to log in."
|
not_approved: "Your account hasn't been approved yet. You will be notified by email when you are ready to log in."
|
||||||
google:
|
|
||||||
title: "with Google"
|
|
||||||
message: "Authenticating with Google (make sure pop up blockers are not enabled)"
|
|
||||||
google_oauth2:
|
google_oauth2:
|
||||||
|
name: "Google"
|
||||||
title: "with Google"
|
title: "with Google"
|
||||||
message: "Authenticating with Google (make sure pop up blockers are not enabled)"
|
message: "Authenticating with Google (make sure pop up blockers are not enabled)"
|
||||||
twitter:
|
twitter:
|
||||||
|
name: "Twitter"
|
||||||
title: "with Twitter"
|
title: "with Twitter"
|
||||||
message: "Authenticating with Twitter (make sure pop up blockers are not enabled)"
|
message: "Authenticating with Twitter (make sure pop up blockers are not enabled)"
|
||||||
instagram:
|
instagram:
|
||||||
|
name: "Instagram"
|
||||||
title: "with Instagram"
|
title: "with Instagram"
|
||||||
message: "Authenticating with Instagram (make sure pop up blockers are not enabled)"
|
message: "Authenticating with Instagram (make sure pop up blockers are not enabled)"
|
||||||
facebook:
|
facebook:
|
||||||
|
name: "Facebook"
|
||||||
title: "with Facebook"
|
title: "with Facebook"
|
||||||
message: "Authenticating with Facebook (make sure pop up blockers are not enabled)"
|
message: "Authenticating with Facebook (make sure pop up blockers are not enabled)"
|
||||||
yahoo:
|
yahoo:
|
||||||
|
name: "Yahoo"
|
||||||
title: "with Yahoo"
|
title: "with Yahoo"
|
||||||
message: "Authenticating with Yahoo (make sure pop up blockers are not enabled)"
|
message: "Authenticating with Yahoo (make sure pop up blockers are not enabled)"
|
||||||
github:
|
github:
|
||||||
|
name: "GitHub"
|
||||||
title: "with GitHub"
|
title: "with GitHub"
|
||||||
message: "Authenticating with GitHub (make sure pop up blockers are not enabled)"
|
message: "Authenticating with GitHub (make sure pop up blockers are not enabled)"
|
||||||
invites:
|
invites:
|
||||||
|
|
|
@ -197,6 +197,7 @@ en:
|
||||||
not_logged_in: "You need to be logged in to do that."
|
not_logged_in: "You need to be logged in to do that."
|
||||||
not_found: "The requested URL or resource could not be found."
|
not_found: "The requested URL or resource could not be found."
|
||||||
invalid_access: "You are not permitted to view the requested resource."
|
invalid_access: "You are not permitted to view the requested resource."
|
||||||
|
authenticator_not_found: "Authentication method does not exist, or has been disabled."
|
||||||
invalid_api_credentials: "You are not permitted to view the requested resource. The API username or key is invalid."
|
invalid_api_credentials: "You are not permitted to view the requested resource. The API username or key is invalid."
|
||||||
provider_not_enabled: "You are not permitted to view the requested resource. The authentication provider is not enabled."
|
provider_not_enabled: "You are not permitted to view the requested resource. The authentication provider is not enabled."
|
||||||
provider_not_found: "You are not permitted to view the requested resource. The authentication provider does not exist."
|
provider_not_found: "You are not permitted to view the requested resource. The authentication provider does not exist."
|
||||||
|
@ -701,6 +702,9 @@ en:
|
||||||
title: "Thanks for confirming your current email address"
|
title: "Thanks for confirming your current email address"
|
||||||
description: "We're now emailing your new address for confirmation."
|
description: "We're now emailing your new address for confirmation."
|
||||||
|
|
||||||
|
associated_accounts:
|
||||||
|
revoke_failed: "Failed to revoke your account with %{provider_name}."
|
||||||
|
|
||||||
activation:
|
activation:
|
||||||
action: "Click here to activate your account"
|
action: "Click here to activate your account"
|
||||||
already_done: "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?"
|
already_done: "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?"
|
||||||
|
@ -1950,7 +1954,6 @@ en:
|
||||||
backup_code: "Log in using a backup code"
|
backup_code: "Log in using a backup code"
|
||||||
|
|
||||||
user:
|
user:
|
||||||
no_accounts_associated: "No accounts associated"
|
|
||||||
deactivated: "Was deactivated due to too many bounced emails to '%{email}'."
|
deactivated: "Was deactivated due to too many bounced emails to '%{email}'."
|
||||||
deactivated_by_staff: "Deactivated by staff"
|
deactivated_by_staff: "Deactivated by staff"
|
||||||
activated_by_staff: "Activated by staff"
|
activated_by_staff: "Activated by staff"
|
||||||
|
|
|
@ -408,6 +408,7 @@ Discourse::Application.routes.draw do
|
||||||
delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username }
|
delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username }
|
||||||
put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username }
|
put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username }
|
||||||
put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", constraints: { username: RouteFormat.username }
|
put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", constraints: { username: RouteFormat.username }
|
||||||
|
post "#{root_path}/:username/preferences/revoke-account" => "users#revoke_account", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username }
|
||||||
|
|
|
@ -2,7 +2,17 @@
|
||||||
# an authentication system interacts with our database and middleware
|
# an authentication system interacts with our database and middleware
|
||||||
|
|
||||||
class Auth::Authenticator
|
class Auth::Authenticator
|
||||||
def after_authenticate(auth_options)
|
def name
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# run once the user has completed authentication on the third party system. Should return an instance of Auth::Result.
|
||||||
|
# If the user has requested to connect an existing account then `existing_account` will be set
|
||||||
|
def after_authenticate(auth_options, existing_account: nil)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -19,4 +29,31 @@ class Auth::Authenticator
|
||||||
def register_middleware(omniauth)
|
def register_middleware(omniauth)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# return a string describing the connected account
|
||||||
|
# for a given user (typically email address). Used to list
|
||||||
|
# connected accounts under the user's preferences. Empty string
|
||||||
|
# indicates not connected
|
||||||
|
def description_for_user(user)
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
# can authorisation for this provider be revoked?
|
||||||
|
def can_revoke?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# can exising discourse users connect this provider to their accounts
|
||||||
|
def can_connect_existing_user?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# optionally implement the ability for users to revoke
|
||||||
|
# their link with this authenticator.
|
||||||
|
# should ideally contact the third party to fully revoke
|
||||||
|
# permissions. If this fails, return :remote_failed.
|
||||||
|
# skip remote if skip_remote == true
|
||||||
|
def revoke(user, skip_remote: false)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,47 @@ class Auth::FacebookAuthenticator < Auth::Authenticator
|
||||||
"facebook"
|
"facebook"
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_authenticate(auth_token)
|
def enabled?
|
||||||
|
SiteSetting.enable_facebook_logins
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = FacebookUserInfo.find_by(user_id: user.id)
|
||||||
|
info&.email || info&.username || ""
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_revoke?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke(user, skip_remote: false)
|
||||||
|
info = FacebookUserInfo.find_by(user_id: user.id)
|
||||||
|
raise Discourse::NotFound if info.nil?
|
||||||
|
|
||||||
|
if skip_remote
|
||||||
|
info.destroy!
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
response = Excon.delete(revoke_url(info.facebook_user_id))
|
||||||
|
|
||||||
|
if response.status == 200
|
||||||
|
info.destroy!
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_url(fb_user_id)
|
||||||
|
"https://graph.facebook.com/#{fb_user_id}/permissions?access_token=#{SiteSetting.facebook_app_id}|#{SiteSetting.facebook_app_secret}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_connect_existing_user?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_authenticate(auth_token, existing_account: nil)
|
||||||
result = Auth::Result.new
|
result = Auth::Result.new
|
||||||
|
|
||||||
session_info = parse_auth_token(auth_token)
|
session_info = parse_auth_token(auth_token)
|
||||||
|
@ -20,9 +60,16 @@ class Auth::FacebookAuthenticator < Auth::Authenticator
|
||||||
|
|
||||||
user_info = FacebookUserInfo.find_by(facebook_user_id: facebook_hash[:facebook_user_id])
|
user_info = FacebookUserInfo.find_by(facebook_user_id: facebook_hash[:facebook_user_id])
|
||||||
|
|
||||||
result.user = user_info.try(:user)
|
if existing_account && (user_info.nil? || existing_account.id != user_info.user_id)
|
||||||
|
user_info.destroy! if user_info
|
||||||
|
result.user = existing_account
|
||||||
|
user_info = FacebookUserInfo.create!({ user_id: result.user.id }.merge(facebook_hash))
|
||||||
|
else
|
||||||
|
result.user = user_info&.user
|
||||||
|
end
|
||||||
|
|
||||||
if !result.user && !email.blank? && result.user = User.find_by_email(email)
|
if !result.user && !email.blank? && result.user = User.find_by_email(email)
|
||||||
FacebookUserInfo.create({ user_id: result.user.id }.merge(facebook_hash))
|
FacebookUserInfo.create!({ user_id: result.user.id }.merge(facebook_hash))
|
||||||
end
|
end
|
||||||
|
|
||||||
user_info.update_columns(facebook_hash) if user_info
|
user_info.update_columns(facebook_hash) if user_info
|
||||||
|
@ -42,7 +89,7 @@ class Auth::FacebookAuthenticator < Auth::Authenticator
|
||||||
|
|
||||||
def after_create_account(user, auth)
|
def after_create_account(user, auth)
|
||||||
extra_data = auth[:extra_data]
|
extra_data = auth[:extra_data]
|
||||||
FacebookUserInfo.create({ user_id: user.id }.merge(extra_data))
|
FacebookUserInfo.create!({ user_id: user.id }.merge(extra_data))
|
||||||
|
|
||||||
retrieve_avatar(user, extra_data)
|
retrieve_avatar(user, extra_data)
|
||||||
retrieve_profile(user, extra_data)
|
retrieve_profile(user, extra_data)
|
||||||
|
|
|
@ -6,6 +6,15 @@ class Auth::GithubAuthenticator < Auth::Authenticator
|
||||||
"github"
|
"github"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
SiteSetting.enable_github_logins
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = GithubUserInfo.find_by(user_id: user.id)
|
||||||
|
info&.screen_name || ""
|
||||||
|
end
|
||||||
|
|
||||||
class GithubEmailChecker
|
class GithubEmailChecker
|
||||||
include ::HasErrors
|
include ::HasErrors
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,15 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator
|
||||||
"google_oauth2"
|
"google_oauth2"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
SiteSetting.enable_google_oauth2_logins
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = GoogleUserInfo.find_by(user_id: user.id)
|
||||||
|
info&.email || info&.name || ""
|
||||||
|
end
|
||||||
|
|
||||||
def after_authenticate(auth_hash)
|
def after_authenticate(auth_hash)
|
||||||
session_info = parse_hash(auth_hash)
|
session_info = parse_hash(auth_hash)
|
||||||
google_hash = session_info[:google]
|
google_hash = session_info[:google]
|
||||||
|
|
|
@ -4,6 +4,15 @@ class Auth::InstagramAuthenticator < Auth::Authenticator
|
||||||
"instagram"
|
"instagram"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
SiteSetting.enable_instagram_logins
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = InstagramUserInfo.find_by(user_id: user.id)
|
||||||
|
info&.screen_name || ""
|
||||||
|
end
|
||||||
|
|
||||||
# TODO twitter provides all sorts of extra info, like website/bio etc.
|
# TODO twitter provides all sorts of extra info, like website/bio etc.
|
||||||
# it may be worth considering pulling some of it in.
|
# it may be worth considering pulling some of it in.
|
||||||
def after_authenticate(auth_token)
|
def after_authenticate(auth_token)
|
||||||
|
|
|
@ -52,4 +52,8 @@ class Auth::OAuth2Authenticator < Auth::Authenticator
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = Oauth2UserInfo.find_by(user_id: user.id, provider: @name)
|
||||||
|
info&.email || info&.name || info&.uid || ""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,22 @@ class Auth::OpenIdAuthenticator < Auth::Authenticator
|
||||||
|
|
||||||
attr_reader :name, :identifier
|
attr_reader :name, :identifier
|
||||||
|
|
||||||
def initialize(name, identifier, opts = {})
|
def initialize(name, identifier, enabled_site_setting, opts = {})
|
||||||
@name = name
|
@name = name
|
||||||
@identifier = identifier
|
@identifier = identifier
|
||||||
|
@enabled_site_setting = enabled_site_setting
|
||||||
@opts = opts
|
@opts = opts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
SiteSetting.send(@enabled_site_setting)
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = UserOpenId.find_by(user_id: user.id)
|
||||||
|
info&.email || ""
|
||||||
|
end
|
||||||
|
|
||||||
def after_authenticate(auth_token)
|
def after_authenticate(auth_token)
|
||||||
result = Auth::Result.new
|
result = Auth::Result.new
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,15 @@ class Auth::TwitterAuthenticator < Auth::Authenticator
|
||||||
"twitter"
|
"twitter"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
SiteSetting.enable_twitter_logins
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_for_user(user)
|
||||||
|
info = TwitterUserInfo.find_by(user_id: user.id)
|
||||||
|
info&.email || info&.screen_name || ""
|
||||||
|
end
|
||||||
|
|
||||||
def after_authenticate(auth_token)
|
def after_authenticate(auth_token)
|
||||||
result = Auth::Result.new
|
result = Auth::Result.new
|
||||||
|
|
||||||
|
|
|
@ -199,24 +199,15 @@ module Discourse
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.authenticators
|
def self.authenticators
|
||||||
# TODO: perhaps we don't need auth providers and authenticators maybe one object is enough
|
|
||||||
|
|
||||||
# NOTE: this bypasses the site settings and gives a list of everything, we need to register every middleware
|
# NOTE: this bypasses the site settings and gives a list of everything, we need to register every middleware
|
||||||
# for the cases of multisite
|
# for the cases of multisite
|
||||||
# In future we may change it so we don't include them all for cases where we are not a multisite, but we would
|
# In future we may change it so we don't include them all for cases where we are not a multisite, but we would
|
||||||
# require a restart after site settings change
|
# require a restart after site settings change
|
||||||
Users::OmniauthCallbacksController::BUILTIN_AUTH + auth_providers.map(&:authenticator)
|
Users::OmniauthCallbacksController::BUILTIN_AUTH + DiscoursePluginRegistry.auth_providers.map(&:authenticator)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.auth_providers
|
def self.enabled_authenticators
|
||||||
providers = []
|
authenticators.select { |authenticator| authenticator.enabled? }
|
||||||
plugins.each do |p|
|
|
||||||
next unless p.auth_providers
|
|
||||||
p.auth_providers.each do |prov|
|
|
||||||
providers << prov
|
|
||||||
end
|
|
||||||
end
|
|
||||||
providers
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cache
|
def self.cache
|
||||||
|
|
|
@ -5,6 +5,7 @@ class DiscoursePluginRegistry
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
attr_writer :javascripts
|
attr_writer :javascripts
|
||||||
|
attr_writer :auth_providers
|
||||||
attr_writer :service_workers
|
attr_writer :service_workers
|
||||||
attr_writer :admin_javascripts
|
attr_writer :admin_javascripts
|
||||||
attr_writer :stylesheets
|
attr_writer :stylesheets
|
||||||
|
@ -26,6 +27,10 @@ class DiscoursePluginRegistry
|
||||||
@javascripts ||= Set.new
|
@javascripts ||= Set.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auth_providers
|
||||||
|
@auth_providers ||= Set.new
|
||||||
|
end
|
||||||
|
|
||||||
def service_workers
|
def service_workers
|
||||||
@service_workers ||= Set.new
|
@service_workers ||= Set.new
|
||||||
end
|
end
|
||||||
|
@ -87,6 +92,10 @@ class DiscoursePluginRegistry
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.register_auth_provider(auth_provider)
|
||||||
|
self.auth_providers << auth_provider
|
||||||
|
end
|
||||||
|
|
||||||
def register_js(filename, options = {})
|
def register_js(filename, options = {})
|
||||||
# If we have a server side option, add that too.
|
# If we have a server side option, add that too.
|
||||||
self.class.javascripts << filename
|
self.class.javascripts << filename
|
||||||
|
@ -203,6 +212,10 @@ class DiscoursePluginRegistry
|
||||||
self.class.javascripts
|
self.class.javascripts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auth_providers
|
||||||
|
self.class.auth_providers
|
||||||
|
end
|
||||||
|
|
||||||
def service_workers
|
def service_workers
|
||||||
self.class.service_workers
|
self.class.service_workers
|
||||||
end
|
end
|
||||||
|
@ -229,6 +242,7 @@ class DiscoursePluginRegistry
|
||||||
|
|
||||||
def self.clear
|
def self.clear
|
||||||
self.javascripts = nil
|
self.javascripts = nil
|
||||||
|
self.auth_providers = nil
|
||||||
self.service_workers = nil
|
self.service_workers = nil
|
||||||
self.stylesheets = nil
|
self.stylesheets = nil
|
||||||
self.mobile_stylesheets = nil
|
self.mobile_stylesheets = nil
|
||||||
|
@ -240,6 +254,7 @@ class DiscoursePluginRegistry
|
||||||
|
|
||||||
def self.reset!
|
def self.reset!
|
||||||
javascripts.clear
|
javascripts.clear
|
||||||
|
auth_providers.clear
|
||||||
service_workers.clear
|
service_workers.clear
|
||||||
admin_javascripts.clear
|
admin_javascripts.clear
|
||||||
stylesheets.clear
|
stylesheets.clear
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
class Plugin::AuthProvider
|
class Plugin::AuthProvider
|
||||||
|
|
||||||
def self.auth_attributes
|
def self.auth_attributes
|
||||||
[:glyph, :background_color, :title, :message, :frame_width, :frame_height, :authenticator,
|
[:glyph, :background_color, :pretty_name, :title, :message, :frame_width, :frame_height, :authenticator,
|
||||||
:title_setting, :enabled_setting, :full_screen_login, :custom_url]
|
:pretty_name_setting, :title_setting, :enabled_setting, :full_screen_login, :custom_url]
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_accessor(*auth_attributes)
|
attr_accessor(*auth_attributes)
|
||||||
|
@ -14,8 +14,10 @@ class Plugin::AuthProvider
|
||||||
def to_json
|
def to_json
|
||||||
result = { name: name }
|
result = { name: name }
|
||||||
result['customUrl'] = custom_url if custom_url
|
result['customUrl'] = custom_url if custom_url
|
||||||
|
result['prettyNameOverride'] = pretty_name || name
|
||||||
result['titleOverride'] = title if title
|
result['titleOverride'] = title if title
|
||||||
result['titleSetting'] = title_setting if title_setting
|
result['titleSetting'] = title_setting if title_setting
|
||||||
|
result['prettyNameSetting'] = pretty_name_setting if pretty_name_setting
|
||||||
result['enabledSetting'] = enabled_setting if enabled_setting
|
result['enabledSetting'] = enabled_setting if enabled_setting
|
||||||
result['messageOverride'] = message if message
|
result['messageOverride'] = message if message
|
||||||
result['frameWidth'] = frame_width if frame_width
|
result['frameWidth'] = frame_width if frame_width
|
||||||
|
|
|
@ -451,6 +451,7 @@ JS
|
||||||
register_assets! unless assets.blank?
|
register_assets! unless assets.blank?
|
||||||
register_locales!
|
register_locales!
|
||||||
register_service_workers!
|
register_service_workers!
|
||||||
|
register_auth_providers!
|
||||||
|
|
||||||
seed_data.each do |key, value|
|
seed_data.each do |key, value|
|
||||||
DiscoursePluginRegistry.register_seed_data(key, value)
|
DiscoursePluginRegistry.register_seed_data(key, value)
|
||||||
|
@ -488,6 +489,20 @@ JS
|
||||||
Plugin::AuthProvider.auth_attributes.each do |sym|
|
Plugin::AuthProvider.auth_attributes.each do |sym|
|
||||||
provider.send "#{sym}=", opts.delete(sym)
|
provider.send "#{sym}=", opts.delete(sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
after_initialize do
|
||||||
|
begin
|
||||||
|
provider.authenticator.enabled?
|
||||||
|
rescue NotImplementedError
|
||||||
|
provider.authenticator.define_singleton_method(:enabled?) do
|
||||||
|
Rails.logger.warn("Auth::Authenticator subclasses should define an `enabled?` function. Patching for now.")
|
||||||
|
return SiteSetting.send(provider.enabled_setting) if provider.enabled_setting
|
||||||
|
Rails.logger.warn("Plugin::AuthProvider has not defined an enabled_setting. Defaulting to true.")
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
auth_providers << provider
|
auth_providers << provider
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -567,6 +582,12 @@ JS
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def register_auth_providers!
|
||||||
|
auth_providers.each do |auth_provider|
|
||||||
|
DiscoursePluginRegistry.register_auth_provider(auth_provider)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def register_locales!
|
def register_locales!
|
||||||
root_path = File.dirname(@path)
|
root_path = File.dirname(@path)
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,36 @@ describe Auth::FacebookAuthenticator do
|
||||||
expect(result.user.user_profile.location).to eq("America")
|
expect(result.user.user_profile.location).to eq("America")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'can connect to a different existing user account' do
|
||||||
|
authenticator = Auth::FacebookAuthenticator.new
|
||||||
|
user1 = Fabricate(:user)
|
||||||
|
user2 = Fabricate(:user)
|
||||||
|
|
||||||
|
FacebookUserInfo.create!(user_id: user1.id, facebook_user_id: 100)
|
||||||
|
|
||||||
|
hash = {
|
||||||
|
"extra" => {
|
||||||
|
"raw_info" => {
|
||||||
|
"username" => "bob"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"info" => {
|
||||||
|
"location" => "America",
|
||||||
|
"description" => "bio",
|
||||||
|
"urls" => {
|
||||||
|
"Website" => "https://awesome.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uid" => "100"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
||||||
|
|
||||||
|
expect(result.user.id).to eq(user2.id)
|
||||||
|
expect(FacebookUserInfo.exists?(user_id: user1.id)).to eq(false)
|
||||||
|
expect(FacebookUserInfo.exists?(user_id: user2.id)).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
it 'can create a proper result for non existing users' do
|
it 'can create a proper result for non existing users' do
|
||||||
|
|
||||||
hash = {
|
hash = {
|
||||||
|
@ -62,4 +92,58 @@ describe Auth::FacebookAuthenticator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'description_for_user' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:authenticator) { Auth::FacebookAuthenticator.new }
|
||||||
|
|
||||||
|
it 'returns empty string if no entry for user' do
|
||||||
|
expect(authenticator.description_for_user(user)).to eq("")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns correct information' do
|
||||||
|
FacebookUserInfo.create!(user_id: user.id, facebook_user_id: 12345, email: 'someuser@somedomain.tld')
|
||||||
|
expect(authenticator.description_for_user(user)).to eq('someuser@somedomain.tld')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'revoke' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:authenticator) { Auth::FacebookAuthenticator.new }
|
||||||
|
|
||||||
|
it 'raises exception if no entry for user' do
|
||||||
|
expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with valid record" do
|
||||||
|
before do
|
||||||
|
SiteSetting.facebook_app_id = '123'
|
||||||
|
SiteSetting.facebook_app_secret = 'abcde'
|
||||||
|
FacebookUserInfo.create!(user_id: user.id, facebook_user_id: 12345, email: 'someuser@somedomain.tld')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'revokes correctly' do
|
||||||
|
stub = stub_request(:delete, authenticator.revoke_url(12345)).to_return(body: "true")
|
||||||
|
|
||||||
|
expect(authenticator.can_revoke?).to eq(true)
|
||||||
|
expect(authenticator.revoke(user)).to eq(true)
|
||||||
|
|
||||||
|
expect(stub).to have_been_requested.once
|
||||||
|
expect(authenticator.description_for_user(user)).to eq("")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles errors correctly' do
|
||||||
|
stub = stub_request(:delete, authenticator.revoke_url(12345)).to_return(status: 404)
|
||||||
|
|
||||||
|
expect(authenticator.revoke(user)).to eq(false)
|
||||||
|
expect(stub).to have_been_requested.once
|
||||||
|
expect(authenticator.description_for_user(user)).to eq('someuser@somedomain.tld')
|
||||||
|
|
||||||
|
expect(authenticator.revoke(user, skip_remote: true)).to eq(true)
|
||||||
|
expect(stub).to have_been_requested.once
|
||||||
|
expect(authenticator.description_for_user(user)).to eq("")
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ load 'auth/open_id_authenticator.rb'
|
||||||
describe Auth::OpenIdAuthenticator do
|
describe Auth::OpenIdAuthenticator do
|
||||||
|
|
||||||
it "can lookup pre-existing user if trusted" do
|
it "can lookup pre-existing user if trusted" do
|
||||||
auth = Auth::OpenIdAuthenticator.new("test", "id", trusted: true)
|
auth = Auth::OpenIdAuthenticator.new("test", "id", "enable_yahoo_logins", trusted: true)
|
||||||
|
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
response = OpenStruct.new(identity_url: 'abc')
|
response = OpenStruct.new(identity_url: 'abc')
|
||||||
|
@ -18,7 +18,7 @@ describe Auth::OpenIdAuthenticator do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises an exception when email is missing" do
|
it "raises an exception when email is missing" do
|
||||||
auth = Auth::OpenIdAuthenticator.new("test", "id", trusted: true)
|
auth = Auth::OpenIdAuthenticator.new("test", "id", "enable_yahoo_logins", trusted: true)
|
||||||
response = OpenStruct.new(identity_url: 'abc')
|
response = OpenStruct.new(identity_url: 'abc')
|
||||||
expect { auth.after_authenticate(info: {}, extra: { response: response }) }.to raise_error(Discourse::InvalidParameters)
|
expect { auth.after_authenticate(info: {}, extra: { response: response }) }.to raise_error(Discourse::InvalidParameters)
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,13 @@ describe DiscoursePluginRegistry do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context '#auth_providers' do
|
||||||
|
it 'defaults to an empty Set' do
|
||||||
|
registry.auth_providers = nil
|
||||||
|
expect(registry.auth_providers).to eq(Set.new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context '#admin_javascripts' do
|
context '#admin_javascripts' do
|
||||||
it 'defaults to an empty Set' do
|
it 'defaults to an empty Set' do
|
||||||
registry.admin_javascripts = nil
|
registry.admin_javascripts = nil
|
||||||
|
@ -92,6 +99,28 @@ describe DiscoursePluginRegistry do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context '.register_auth_provider' do
|
||||||
|
let(:registry) { DiscoursePluginRegistry }
|
||||||
|
let(:auth_provider) do
|
||||||
|
provider = Plugin::AuthProvider.new
|
||||||
|
provider.authenticator = Auth::Authenticator.new
|
||||||
|
provider
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
registry.register_auth_provider(auth_provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
registry.reset!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is returned by DiscoursePluginRegistry.auth_providers' do
|
||||||
|
expect(registry.auth_providers.include?(auth_provider)).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
context '.register_service_worker' do
|
context '.register_service_worker' do
|
||||||
let(:registry) { DiscoursePluginRegistry }
|
let(:registry) { DiscoursePluginRegistry }
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,54 @@ describe Discourse do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'authenticators' do
|
||||||
|
it 'returns inbuilt authenticators' do
|
||||||
|
expect(Discourse.authenticators).to match_array(Users::OmniauthCallbacksController::BUILTIN_AUTH)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with authentication plugin installed' do
|
||||||
|
let(:plugin_auth_provider) do
|
||||||
|
authenticator_class = Class.new(Auth::Authenticator) do
|
||||||
|
def name
|
||||||
|
'pluginauth'
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
provider = Plugin::AuthProvider.new
|
||||||
|
provider.authenticator = authenticator_class.new
|
||||||
|
provider
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
DiscoursePluginRegistry.register_auth_provider(plugin_auth_provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
DiscoursePluginRegistry.reset!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns inbuilt and plugin authenticators' do
|
||||||
|
expect(Discourse.authenticators).to match_array(
|
||||||
|
Users::OmniauthCallbacksController::BUILTIN_AUTH + [plugin_auth_provider.authenticator])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'enabled_authenticators' do
|
||||||
|
it 'only returns enabled authenticators' do
|
||||||
|
expect(Discourse.enabled_authenticators.length).to be(0)
|
||||||
|
expect { SiteSetting.enable_twitter_logins = true }
|
||||||
|
.to change { Discourse.enabled_authenticators.length }.by(1)
|
||||||
|
expect(Discourse.enabled_authenticators.length).to be(1)
|
||||||
|
expect(Discourse.enabled_authenticators.first).to be_instance_of(Auth::TwitterAuthenticator)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context '#site_contact_user' do
|
context '#site_contact_user' do
|
||||||
|
|
||||||
let!(:admin) { Fabricate(:admin) }
|
let!(:admin) { Fabricate(:admin) }
|
||||||
|
|
|
@ -125,7 +125,40 @@ describe Plugin::Instance do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'patches the enabled? function for auth_providers if not defined' do
|
||||||
|
SiteSetting.stubs(:ubuntu_login_enabled).returns(false)
|
||||||
|
|
||||||
|
plugin = Plugin::Instance.new
|
||||||
|
|
||||||
|
# No enabled_site_setting
|
||||||
|
authenticator = Auth::Authenticator.new
|
||||||
|
plugin.auth_provider(authenticator: authenticator)
|
||||||
|
plugin.notify_after_initialize
|
||||||
|
expect(authenticator.enabled?).to eq(true)
|
||||||
|
|
||||||
|
# With enabled site setting
|
||||||
|
authenticator = Auth::Authenticator.new
|
||||||
|
plugin.auth_provider(enabled_setting: 'ubuntu_login_enabled', authenticator: authenticator)
|
||||||
|
plugin.notify_after_initialize
|
||||||
|
expect(authenticator.enabled?).to eq(false)
|
||||||
|
|
||||||
|
# Defines own method
|
||||||
|
SiteSetting.stubs(:ubuntu_login_enabled).returns(true)
|
||||||
|
authenticator = Class.new(Auth::Authenticator) do
|
||||||
|
def enabled?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end.new
|
||||||
|
plugin.auth_provider(enabled_setting: 'ubuntu_login_enabled', authenticator: authenticator)
|
||||||
|
plugin.notify_after_initialize
|
||||||
|
expect(authenticator.enabled?).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
context "activate!" do
|
context "activate!" do
|
||||||
|
before do
|
||||||
|
SiteSetting.stubs(:ubuntu_login_enabled).returns(false)
|
||||||
|
end
|
||||||
|
|
||||||
it "can activate plugins correctly" do
|
it "can activate plugins correctly" do
|
||||||
plugin = Plugin::Instance.new
|
plugin = Plugin::Instance.new
|
||||||
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
|
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
|
||||||
|
@ -135,10 +168,6 @@ describe Plugin::Instance do
|
||||||
File.open("#{plugin.auto_generated_path}/junk", "w") { |f| f.write("junk") }
|
File.open("#{plugin.auto_generated_path}/junk", "w") { |f| f.write("junk") }
|
||||||
plugin.activate!
|
plugin.activate!
|
||||||
|
|
||||||
expect(plugin.auth_providers.count).to eq(1)
|
|
||||||
auth_provider = plugin.auth_providers[0]
|
|
||||||
expect(auth_provider.authenticator.name).to eq('ubuntu')
|
|
||||||
|
|
||||||
# calls ensure_assets! make sure they are there
|
# calls ensure_assets! make sure they are there
|
||||||
expect(plugin.assets.count).to eq(1)
|
expect(plugin.assets.count).to eq(1)
|
||||||
plugin.assets.each do |a, opts|
|
plugin.assets.each do |a, opts|
|
||||||
|
@ -149,6 +178,17 @@ describe Plugin::Instance do
|
||||||
expect(File.exists?(junk_file)).to eq(false)
|
expect(File.exists?(junk_file)).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "registers auth providers correctly" do
|
||||||
|
plugin = Plugin::Instance.new
|
||||||
|
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
|
||||||
|
plugin.activate!
|
||||||
|
|
||||||
|
expect(plugin.auth_providers.count).to eq(1)
|
||||||
|
auth_provider = plugin.auth_providers[0]
|
||||||
|
expect(auth_provider.authenticator.name).to eq('ubuntu')
|
||||||
|
expect(DiscoursePluginRegistry.auth_providers.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
it "finds all the custom assets" do
|
it "finds all the custom assets" do
|
||||||
plugin = Plugin::Instance.new
|
plugin = Plugin::Instance.new
|
||||||
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
|
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
|
||||||
|
|
2
spec/fixtures/plugins/my_plugin/plugin.rb
vendored
2
spec/fixtures/plugins/my_plugin/plugin.rb
vendored
|
@ -4,7 +4,7 @@
|
||||||
# authors: Frank Zappa
|
# authors: Frank Zappa
|
||||||
|
|
||||||
auth_provider title: 'with Ubuntu',
|
auth_provider title: 'with Ubuntu',
|
||||||
authenticator: Auth::OpenIdAuthenticator.new('ubuntu', 'https://login.ubuntu.com', trusted: true),
|
authenticator: Auth::OpenIdAuthenticator.new('ubuntu', 'https://login.ubuntu.com', 'ubuntu_login_enabled', trusted: true),
|
||||||
message: 'Authenticating with Ubuntu (make sure pop up blockers are not enbaled)',
|
message: 'Authenticating with Ubuntu (make sure pop up blockers are not enbaled)',
|
||||||
frame_width: 1000, # the frame size used for the pop up window, overrides default
|
frame_width: 1000, # the frame size used for the pop up window, overrides default
|
||||||
frame_height: 800
|
frame_height: 800
|
||||||
|
|
|
@ -416,17 +416,16 @@ describe User do
|
||||||
describe 'associated_accounts' do
|
describe 'associated_accounts' do
|
||||||
it 'should correctly find social associations' do
|
it 'should correctly find social associations' do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
expect(user.associated_accounts).to eq(I18n.t("user.no_accounts_associated"))
|
expect(user.associated_accounts).to eq([])
|
||||||
|
|
||||||
TwitterUserInfo.create(user_id: user.id, screen_name: "sam", twitter_user_id: 1)
|
TwitterUserInfo.create(user_id: user.id, screen_name: "sam", twitter_user_id: 1)
|
||||||
FacebookUserInfo.create(user_id: user.id, username: "sam", facebook_user_id: 1)
|
FacebookUserInfo.create(user_id: user.id, username: "sam", facebook_user_id: 1)
|
||||||
GoogleUserInfo.create(user_id: user.id, email: "sam@sam.com", google_user_id: 1)
|
GoogleUserInfo.create(user_id: user.id, email: "sam@sam.com", google_user_id: 1)
|
||||||
GithubUserInfo.create(user_id: user.id, screen_name: "sam", github_user_id: 1)
|
GithubUserInfo.create(user_id: user.id, screen_name: "sam", github_user_id: 1)
|
||||||
Oauth2UserInfo.create(user_id: user.id, provider: "linkedin", email: "sam@sam.com", uid: 1)
|
|
||||||
InstagramUserInfo.create(user_id: user.id, screen_name: "sam", instagram_user_id: "examplel123123")
|
InstagramUserInfo.create(user_id: user.id, screen_name: "sam", instagram_user_id: "examplel123123")
|
||||||
|
|
||||||
user.reload
|
user.reload
|
||||||
expect(user.associated_accounts).to eq("Twitter(sam), Facebook(sam), Google(sam@sam.com), GitHub(sam), Instagram(sam), linkedin(sam@sam.com)")
|
expect(user.associated_accounts.map { |a| a[:name] }).to contain_exactly('twitter', 'facebook', 'google_oauth2', 'github', 'instagram')
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,13 +38,26 @@ RSpec.describe Users::OmniauthCallbacksController do
|
||||||
|
|
||||||
let :provider do
|
let :provider do
|
||||||
provider = Plugin::AuthProvider.new
|
provider = Plugin::AuthProvider.new
|
||||||
provider.authenticator = Auth::OpenIdAuthenticator.new('ubuntu', 'https://login.ubuntu.com', trusted: true)
|
provider.authenticator = Class.new(Auth::Authenticator) do
|
||||||
|
def name
|
||||||
|
'ubuntu'
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
SiteSetting.ubuntu_login_enabled
|
||||||
|
end
|
||||||
|
end.new
|
||||||
|
|
||||||
provider.enabled_setting = "ubuntu_login_enabled"
|
provider.enabled_setting = "ubuntu_login_enabled"
|
||||||
provider
|
provider
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Discourse.stubs(:auth_providers).returns [provider]
|
DiscoursePluginRegistry.register_auth_provider(provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
DiscoursePluginRegistry.reset!
|
||||||
end
|
end
|
||||||
|
|
||||||
it "finds an authenticator when enabled" do
|
it "finds an authenticator when enabled" do
|
||||||
|
@ -60,14 +73,6 @@ RSpec.describe Users::OmniauthCallbacksController do
|
||||||
expect { Users::OmniauthCallbacksController.find_authenticator("ubuntu") }
|
expect { Users::OmniauthCallbacksController.find_authenticator("ubuntu") }
|
||||||
.to raise_error(Discourse::InvalidAccess)
|
.to raise_error(Discourse::InvalidAccess)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "succeeds if an authenticator does not have a site setting" do
|
|
||||||
provider.enabled_setting = nil
|
|
||||||
SiteSetting.stubs(:ubuntu_login_enabled).returns(false)
|
|
||||||
|
|
||||||
expect(Users::OmniauthCallbacksController.find_authenticator("ubuntu"))
|
|
||||||
.to be(provider.authenticator)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1965,7 +1965,7 @@ describe UsersController do
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
expect(json["email"]).to eq(user.email)
|
expect(json["email"]).to eq(user.email)
|
||||||
expect(json["secondary_emails"]).to eq(user.secondary_emails)
|
expect(json["secondary_emails"]).to eq(user.secondary_emails)
|
||||||
expect(json["associated_accounts"]).to be_present
|
expect(json["associated_accounts"]).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "works on inactive users" do
|
it "works on inactive users" do
|
||||||
|
@ -1978,7 +1978,7 @@ describe UsersController do
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
expect(json["email"]).to eq(inactive_user.email)
|
expect(json["email"]).to eq(inactive_user.email)
|
||||||
expect(json["secondary_emails"]).to eq(inactive_user.secondary_emails)
|
expect(json["secondary_emails"]).to eq(inactive_user.secondary_emails)
|
||||||
expect(json["associated_accounts"]).to be_present
|
expect(json["associated_accounts"]).to eq([])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3068,4 +3068,46 @@ describe UsersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#revoke_account' do
|
||||||
|
let(:other_user) { Fabricate(:user) }
|
||||||
|
it 'errors for unauthorised users' do
|
||||||
|
post "/u/#{user.username}/preferences/revoke-account.json", params: {
|
||||||
|
provider_name: 'facebook'
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
|
||||||
|
sign_in(other_user)
|
||||||
|
|
||||||
|
post "/u/#{user.username}/preferences/revoke-account.json", params: {
|
||||||
|
provider_name: 'facebook'
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'while logged in' do
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error when there is no matching account' do
|
||||||
|
post "/u/#{user.username}/preferences/revoke-account.json", params: {
|
||||||
|
provider_name: 'facebook'
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'works' do
|
||||||
|
FacebookUserInfo.create!(user_id: user.id, facebook_user_id: 12345, email: 'someuser@somedomain.tld')
|
||||||
|
stub = stub_request(:delete, 'https://graph.facebook.com/12345/permissions?access_token=123%7Cabcde').to_return(body: "true")
|
||||||
|
|
||||||
|
post "/u/#{user.username}/preferences/revoke-account.json", params: {
|
||||||
|
provider_name: 'facebook'
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -181,7 +181,7 @@ describe UserAnonymizer do
|
||||||
user.github_user_info = GithubUserInfo.create(user_id: user.id, screen_name: "example", github_user_id: "examplel123123")
|
user.github_user_info = GithubUserInfo.create(user_id: user.id, screen_name: "example", github_user_id: "examplel123123")
|
||||||
user.facebook_user_info = FacebookUserInfo.create(user_id: user.id, facebook_user_id: "example")
|
user.facebook_user_info = FacebookUserInfo.create(user_id: user.id, facebook_user_id: "example")
|
||||||
user.single_sign_on_record = SingleSignOnRecord.create(user_id: user.id, external_id: "example", last_payload: "looks good")
|
user.single_sign_on_record = SingleSignOnRecord.create(user_id: user.id, external_id: "example", last_payload: "looks good")
|
||||||
user.oauth2_user_info = Oauth2UserInfo.create(user_id: user.id, uid: "example", provider: "example")
|
user.oauth2_user_infos = [Oauth2UserInfo.create(user_id: user.id, uid: "example", provider: "example")]
|
||||||
user.instagram_user_info = InstagramUserInfo.create(user_id: user.id, screen_name: "example", instagram_user_id: "examplel123123")
|
user.instagram_user_info = InstagramUserInfo.create(user_id: user.id, screen_name: "example", instagram_user_id: "examplel123123")
|
||||||
UserOpenId.create(user_id: user.id, email: user.email, url: "http://example.com/openid", active: true)
|
UserOpenId.create(user_id: user.id, email: user.email, url: "http://example.com/openid", active: true)
|
||||||
make_anonymous
|
make_anonymous
|
||||||
|
@ -191,7 +191,7 @@ describe UserAnonymizer do
|
||||||
expect(user.github_user_info).to eq(nil)
|
expect(user.github_user_info).to eq(nil)
|
||||||
expect(user.facebook_user_info).to eq(nil)
|
expect(user.facebook_user_info).to eq(nil)
|
||||||
expect(user.single_sign_on_record).to eq(nil)
|
expect(user.single_sign_on_record).to eq(nil)
|
||||||
expect(user.oauth2_user_info).to eq(nil)
|
expect(user.oauth2_user_infos).to be_empty
|
||||||
expect(user.instagram_user_info).to eq(nil)
|
expect(user.instagram_user_info).to eq(nil)
|
||||||
expect(user.user_open_ids.count).to eq(0)
|
expect(user.user_open_ids.count).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,43 +1,42 @@
|
||||||
import { acceptance } from "helpers/qunit-helpers";
|
import { acceptance } from "helpers/qunit-helpers";
|
||||||
acceptance("User Preferences", {
|
acceptance("User Preferences", {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
beforeEach() {
|
pretend(server, helper) {
|
||||||
const response = object => {
|
server.post("/u/second_factors.json", () => {
|
||||||
return [200, { "Content-Type": "application/json" }, object];
|
return helper.response({
|
||||||
};
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
server.post("/u/second_factors.json", () => { //eslint-disable-line
|
|
||||||
return response({
|
|
||||||
key: "rcyryaqage3jexfj",
|
key: "rcyryaqage3jexfj",
|
||||||
qr: '<div id="test-qr">qr-code</div>'
|
qr: '<div id="test-qr">qr-code</div>'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// prettier-ignore
|
server.put("/u/second_factor.json", () => {
|
||||||
server.put("/u/second_factor.json", () => { //eslint-disable-line
|
return helper.response({ error: "invalid token" });
|
||||||
return response({ error: "invalid token" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// prettier-ignore
|
server.put("/u/second_factors_backup.json", () => {
|
||||||
server.put("/u/second_factors_backup.json", () => { //eslint-disable-line
|
return helper.response({
|
||||||
return response({ backup_codes: ["dsffdsd", "fdfdfdsf", "fddsds"] });
|
backup_codes: ["dsffdsd", "fdfdfdsf", "fddsds"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.post("/u/eviltrout/preferences/revoke-account", () => {
|
||||||
|
return helper.response({
|
||||||
|
success: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("update some fields", assert => {
|
QUnit.test("update some fields", async assert => {
|
||||||
visit("/u/eviltrout/preferences");
|
await visit("/u/eviltrout/preferences");
|
||||||
|
|
||||||
andThen(() => {
|
assert.ok($("body.user-preferences-page").length, "has the body class");
|
||||||
assert.ok($("body.user-preferences-page").length, "has the body class");
|
assert.equal(
|
||||||
assert.equal(
|
currentURL(),
|
||||||
currentURL(),
|
"/u/eviltrout/preferences/account",
|
||||||
"/u/eviltrout/preferences/account",
|
"defaults to account tab"
|
||||||
"defaults to account tab"
|
);
|
||||||
);
|
assert.ok(exists(".user-preferences"), "it shows the preferences");
|
||||||
assert.ok(exists(".user-preferences"), "it shows the preferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
const savePreferences = () => {
|
const savePreferences = () => {
|
||||||
click(".save-user");
|
click(".save-user");
|
||||||
|
@ -48,25 +47,25 @@ QUnit.test("update some fields", assert => {
|
||||||
};
|
};
|
||||||
|
|
||||||
fillIn(".pref-name input[type=text]", "Jon Snow");
|
fillIn(".pref-name input[type=text]", "Jon Snow");
|
||||||
savePreferences();
|
await savePreferences();
|
||||||
|
|
||||||
click(".preferences-nav .nav-profile a");
|
click(".preferences-nav .nav-profile a");
|
||||||
fillIn("#edit-location", "Westeros");
|
fillIn("#edit-location", "Westeros");
|
||||||
savePreferences();
|
await savePreferences();
|
||||||
|
|
||||||
click(".preferences-nav .nav-emails a");
|
click(".preferences-nav .nav-emails a");
|
||||||
click(".pref-activity-summary input[type=checkbox]");
|
click(".pref-activity-summary input[type=checkbox]");
|
||||||
savePreferences();
|
await savePreferences();
|
||||||
|
|
||||||
click(".preferences-nav .nav-notifications a");
|
click(".preferences-nav .nav-notifications a");
|
||||||
selectKit(".control-group.notifications .combo-box.duration")
|
selectKit(".control-group.notifications .combo-box.duration")
|
||||||
.expand()
|
.expand()
|
||||||
.selectRowByValue(1440);
|
.selectRowByValue(1440);
|
||||||
savePreferences();
|
await savePreferences();
|
||||||
|
|
||||||
click(".preferences-nav .nav-categories a");
|
click(".preferences-nav .nav-categories a");
|
||||||
fillIn(".category-controls .category-selector", "faq");
|
fillIn(".category-controls .category-selector", "faq");
|
||||||
savePreferences();
|
await savePreferences();
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!exists(".preferences-nav .nav-tags a"),
|
!exists(".preferences-nav .nav-tags a"),
|
||||||
|
@ -84,83 +83,87 @@ QUnit.test("update some fields", assert => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("username", assert => {
|
QUnit.test("username", async assert => {
|
||||||
visit("/u/eviltrout/preferences/username");
|
await visit("/u/eviltrout/preferences/username");
|
||||||
andThen(() => {
|
assert.ok(exists("#change_username"), "it has the input element");
|
||||||
assert.ok(exists("#change_username"), "it has the input element");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("about me", assert => {
|
QUnit.test("about me", async assert => {
|
||||||
visit("/u/eviltrout/preferences/about-me");
|
await visit("/u/eviltrout/preferences/about-me");
|
||||||
andThen(() => {
|
assert.ok(exists(".raw-bio"), "it has the input element");
|
||||||
assert.ok(exists(".raw-bio"), "it has the input element");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("email", assert => {
|
QUnit.test("email", async assert => {
|
||||||
visit("/u/eviltrout/preferences/email");
|
await visit("/u/eviltrout/preferences/email");
|
||||||
andThen(() => {
|
|
||||||
assert.ok(exists("#change-email"), "it has the input element");
|
|
||||||
});
|
|
||||||
|
|
||||||
fillIn("#change-email", "invalidemail");
|
assert.ok(exists("#change-email"), "it has the input element");
|
||||||
|
|
||||||
andThen(() => {
|
await fillIn("#change-email", "invalidemail");
|
||||||
assert.equal(
|
|
||||||
find(".tip.bad")
|
assert.equal(
|
||||||
.text()
|
find(".tip.bad")
|
||||||
.trim(),
|
.text()
|
||||||
I18n.t("user.email.invalid"),
|
.trim(),
|
||||||
"it should display invalid email tip"
|
I18n.t("user.email.invalid"),
|
||||||
);
|
"it should display invalid email tip"
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("second factor", assert => {
|
QUnit.test("connected accounts", async assert => {
|
||||||
visit("/u/eviltrout/preferences/second-factor");
|
await visit("/u/eviltrout/preferences/account");
|
||||||
|
|
||||||
andThen(() => {
|
assert.ok(
|
||||||
assert.ok(exists("#password"), "it has a password input");
|
exists(".pref-associated-accounts"),
|
||||||
});
|
"it has the connected accounts section"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
find(".pref-associated-accounts table tr:first td:first")
|
||||||
|
.html()
|
||||||
|
.indexOf("Facebook") > -1,
|
||||||
|
"it lists facebook"
|
||||||
|
);
|
||||||
|
|
||||||
fillIn("#password", "secrets");
|
await click(".pref-associated-accounts table tr:first td:last button");
|
||||||
click(".user-preferences .btn-primary");
|
|
||||||
|
|
||||||
andThen(() => {
|
find(".pref-associated-accounts table tr:first td:last button")
|
||||||
assert.ok(exists("#test-qr"), "shows qr code");
|
.html()
|
||||||
assert.notOk(exists("#password"), "it hides the password input");
|
.indexOf("Connect") > -1;
|
||||||
});
|
|
||||||
|
|
||||||
fillIn("#second-factor-token", "111111");
|
|
||||||
click(".btn-primary");
|
|
||||||
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(
|
|
||||||
find(".alert-error")
|
|
||||||
.html()
|
|
||||||
.indexOf("invalid token") > -1,
|
|
||||||
"shows server validation error message"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("second factor backup", assert => {
|
QUnit.test("second factor", async assert => {
|
||||||
visit("/u/eviltrout/preferences/second-factor-backup");
|
await visit("/u/eviltrout/preferences/second-factor");
|
||||||
|
|
||||||
andThen(() => {
|
assert.ok(exists("#password"), "it has a password input");
|
||||||
assert.ok(
|
|
||||||
exists("#second-factor-token"),
|
|
||||||
"it has a authentication token input"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
fillIn("#second-factor-token", "111111");
|
await fillIn("#password", "secrets");
|
||||||
click(".user-preferences .btn-primary");
|
await click(".user-preferences .btn-primary");
|
||||||
|
|
||||||
andThen(() => {
|
assert.ok(exists("#test-qr"), "shows qr code");
|
||||||
assert.ok(exists(".backup-codes-area"), "shows backup codes");
|
assert.notOk(exists("#password"), "it hides the password input");
|
||||||
});
|
|
||||||
|
await fillIn("#second-factor-token", "111111");
|
||||||
|
await click(".btn-primary");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
find(".alert-error")
|
||||||
|
.html()
|
||||||
|
.indexOf("invalid token") > -1,
|
||||||
|
"shows server validation error message"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("second factor backup", async assert => {
|
||||||
|
await visit("/u/eviltrout/preferences/second-factor-backup");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists("#second-factor-token"),
|
||||||
|
"it has a authentication token input"
|
||||||
|
);
|
||||||
|
|
||||||
|
await fillIn("#second-factor-token", "111111");
|
||||||
|
await click(".user-preferences .btn-primary");
|
||||||
|
|
||||||
|
assert.ok(exists(".backup-codes-area"), "shows backup codes");
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("default avatar selector", assert => {
|
QUnit.test("default avatar selector", assert => {
|
||||||
|
@ -175,26 +178,23 @@ QUnit.test("default avatar selector", assert => {
|
||||||
acceptance("Avatar selector when selectable avatars is enabled", {
|
acceptance("Avatar selector when selectable avatars is enabled", {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
settings: { selectable_avatars_enabled: true },
|
settings: { selectable_avatars_enabled: true },
|
||||||
beforeEach() {
|
pretend(server) {
|
||||||
// prettier-ignore
|
server.get("/site/selectable-avatars.json", () => {
|
||||||
server.get("/site/selectable-avatars.json", () => { //eslint-disable-line
|
return [
|
||||||
return [200, { "Content-Type": "application/json" }, [
|
200,
|
||||||
"https://www.discourse.org",
|
{ "Content-Type": "application/json" },
|
||||||
"https://meta.discourse.org",
|
["https://www.discourse.org", "https://meta.discourse.org"]
|
||||||
]];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("selectable avatars", assert => {
|
QUnit.test("selectable avatars", async assert => {
|
||||||
visit("/u/eviltrout/preferences");
|
await visit("/u/eviltrout/preferences");
|
||||||
|
|
||||||
click(".pref-avatar .btn");
|
await click(".pref-avatar .btn");
|
||||||
andThen(() => {
|
|
||||||
assert.ok(
|
assert.ok(exists(".selectable-avatars", "opens the avatar selection modal"));
|
||||||
exists(".selectable-avatars", "opens the avatar selection modal")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
acceptance("User Preferences when badges are disabled", {
|
acceptance("User Preferences when badges are disabled", {
|
||||||
|
|
|
@ -114,6 +114,13 @@ export default {
|
||||||
"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
|
"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
|
||||||
name: "Robin Ward",
|
name: "Robin Ward",
|
||||||
email: "robin.ward@example.com",
|
email: "robin.ward@example.com",
|
||||||
|
associated_accounts: [
|
||||||
|
{
|
||||||
|
name: "facebook",
|
||||||
|
description: "robin.ward@example.com",
|
||||||
|
can_revoke: true
|
||||||
|
}
|
||||||
|
],
|
||||||
last_posted_at: "2015-05-07T15:23:35.074Z",
|
last_posted_at: "2015-05-07T15:23:35.074Z",
|
||||||
last_seen_at: "2015-05-13T14:34:23.188Z",
|
last_seen_at: "2015-05-13T14:34:23.188Z",
|
||||||
bio_raw:
|
bio_raw:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue