FEATURE: Move security related user preferences to different tab (#12264)
This commit is contained in:
parent
b49b455e47
commit
039d0d3641
|
@ -4,19 +4,12 @@ import CanCheckEmails from "discourse/mixins/can-check-emails";
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import bootbox from "bootbox";
|
import bootbox from "bootbox";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { findAll } from "discourse/models/login-method";
|
import { findAll } from "discourse/models/login-method";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import logout from "discourse/lib/logout";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
import { userPath } from "discourse/lib/url";
|
|
||||||
|
|
||||||
// Number of tokens shown by default.
|
|
||||||
const DEFAULT_AUTH_TOKENS_COUNT = 2;
|
|
||||||
|
|
||||||
export default Controller.extend(CanCheckEmails, {
|
export default Controller.extend(CanCheckEmails, {
|
||||||
init() {
|
init() {
|
||||||
|
@ -33,10 +26,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
newTitleInput: null,
|
newTitleInput: null,
|
||||||
newPrimaryGroupInput: null,
|
newPrimaryGroupInput: null,
|
||||||
|
|
||||||
passwordProgress: null,
|
|
||||||
|
|
||||||
showAllAuthTokens: false,
|
|
||||||
|
|
||||||
revoking: null,
|
revoking: null,
|
||||||
|
|
||||||
cannotDeleteAccount: not("currentUser.can_delete_account"),
|
cannotDeleteAccount: not("currentUser.can_delete_account"),
|
||||||
|
@ -65,18 +54,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("model.is_anonymous")
|
|
||||||
canChangePassword(isAnonymous) {
|
|
||||||
if (isAnonymous) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
!this.siteSettings.enable_discourse_connect &&
|
|
||||||
this.siteSettings.enable_local_logins
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("model.associated_accounts")
|
@discourseComputed("model.associated_accounts")
|
||||||
associatedAccountsLoaded(associatedAccounts) {
|
associatedAccountsLoaded(associatedAccounts) {
|
||||||
return typeof associatedAccounts !== "undefined";
|
return typeof associatedAccounts !== "undefined";
|
||||||
|
@ -147,28 +124,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
return findAll().length > 0;
|
return findAll().length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("showAllAuthTokens", "model.user_auth_tokens")
|
|
||||||
authTokens(showAllAuthTokens, tokens) {
|
|
||||||
tokens.sort((a, b) => {
|
|
||||||
if (a.is_active) {
|
|
||||||
return -1;
|
|
||||||
} else if (b.is_active) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return b.seen_at.localeCompare(a.seen_at);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return showAllAuthTokens
|
|
||||||
? tokens
|
|
||||||
: tokens.slice(0, DEFAULT_AUTH_TOKENS_COUNT);
|
|
||||||
},
|
|
||||||
|
|
||||||
canShowAllAuthTokens: gt(
|
|
||||||
"model.user_auth_tokens.length",
|
|
||||||
DEFAULT_AUTH_TOKENS_COUNT
|
|
||||||
),
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
save() {
|
||||||
this.set("saved", false);
|
this.set("saved", false);
|
||||||
|
@ -205,31 +160,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
changePassword() {
|
|
||||||
if (!this.passwordProgress) {
|
|
||||||
this.set(
|
|
||||||
"passwordProgress",
|
|
||||||
I18n.t("user.change_password.in_progress")
|
|
||||||
);
|
|
||||||
return this.model
|
|
||||||
.changePassword()
|
|
||||||
.then(() => {
|
|
||||||
// password changed
|
|
||||||
this.setProperties({
|
|
||||||
changePasswordProgress: false,
|
|
||||||
passwordProgress: I18n.t("user.change_password.success"),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// password failed to change
|
|
||||||
this.setProperties({
|
|
||||||
changePasswordProgress: false,
|
|
||||||
passwordProgress: I18n.t("user.change_password.error"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
this.set("deleting", true);
|
this.set("deleting", true);
|
||||||
const message = I18n.t("user.delete_account_confirm"),
|
const message = I18n.t("user.delete_account_confirm"),
|
||||||
|
@ -282,32 +212,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
.finally(() => this.set(`revoking.${account.name}`, false));
|
.finally(() => this.set(`revoking.${account.name}`, false));
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleShowAllAuthTokens() {
|
|
||||||
this.toggleProperty("showAllAuthTokens");
|
|
||||||
},
|
|
||||||
|
|
||||||
revokeAuthToken(token) {
|
|
||||||
ajax(
|
|
||||||
userPath(
|
|
||||||
`${this.get("model.username_lower")}/preferences/revoke-auth-token`
|
|
||||||
),
|
|
||||||
{
|
|
||||||
type: "POST",
|
|
||||||
data: token ? { token_id: token.id } : {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
if (!token) {
|
|
||||||
logout();
|
|
||||||
} // All sessions revoked
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError);
|
|
||||||
},
|
|
||||||
|
|
||||||
showToken(token) {
|
|
||||||
showModal("auth-token", { model: token });
|
|
||||||
},
|
|
||||||
|
|
||||||
connectAccount(method) {
|
connectAccount(method) {
|
||||||
method.doLogin({ reconnect: true });
|
method.doLogin({ reconnect: true });
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
import Controller from "@ember/controller";
|
||||||
|
import { gt } from "@ember/object/computed";
|
||||||
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import logout from "discourse/lib/logout";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import { userPath } from "discourse/lib/url";
|
||||||
|
import CanCheckEmails from "discourse/mixins/can-check-emails";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
// Number of tokens shown by default.
|
||||||
|
const DEFAULT_AUTH_TOKENS_COUNT = 2;
|
||||||
|
|
||||||
|
export default Controller.extend(CanCheckEmails, {
|
||||||
|
passwordProgress: null,
|
||||||
|
|
||||||
|
showAllAuthTokens: false,
|
||||||
|
|
||||||
|
@discourseComputed("model.is_anonymous")
|
||||||
|
canChangePassword(isAnonymous) {
|
||||||
|
if (isAnonymous) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
!this.siteSettings.enable_discourse_connect &&
|
||||||
|
this.siteSettings.enable_local_logins
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed("showAllAuthTokens", "model.user_auth_tokens")
|
||||||
|
authTokens(showAllAuthTokens, tokens) {
|
||||||
|
tokens.sort((a, b) => {
|
||||||
|
if (a.is_active) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.is_active) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return b.seen_at.localeCompare(a.seen_at);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return showAllAuthTokens
|
||||||
|
? tokens
|
||||||
|
: tokens.slice(0, DEFAULT_AUTH_TOKENS_COUNT);
|
||||||
|
},
|
||||||
|
|
||||||
|
canShowAllAuthTokens: gt(
|
||||||
|
"model.user_auth_tokens.length",
|
||||||
|
DEFAULT_AUTH_TOKENS_COUNT
|
||||||
|
),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
this.set("saved", false);
|
||||||
|
|
||||||
|
return this.model
|
||||||
|
.then(() => this.set("saved", true))
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword() {
|
||||||
|
if (!this.passwordProgress) {
|
||||||
|
this.set(
|
||||||
|
"passwordProgress",
|
||||||
|
I18n.t("user.change_password.in_progress")
|
||||||
|
);
|
||||||
|
return this.model
|
||||||
|
.changePassword()
|
||||||
|
.then(() => {
|
||||||
|
// password changed
|
||||||
|
this.setProperties({
|
||||||
|
changePasswordProgress: false,
|
||||||
|
passwordProgress: I18n.t("user.change_password.success"),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// password failed to change
|
||||||
|
this.setProperties({
|
||||||
|
changePasswordProgress: false,
|
||||||
|
passwordProgress: I18n.t("user.change_password.error"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleShowAllAuthTokens() {
|
||||||
|
this.toggleProperty("showAllAuthTokens");
|
||||||
|
},
|
||||||
|
|
||||||
|
revokeAuthToken(token) {
|
||||||
|
ajax(
|
||||||
|
userPath(
|
||||||
|
`${this.get("model.username_lower")}/preferences/revoke-auth-token`
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "POST",
|
||||||
|
data: token ? { token_id: token.id } : {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (!token) {
|
||||||
|
logout();
|
||||||
|
} // All sessions revoked
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
showToken(token) {
|
||||||
|
showModal("auth-token", { model: token });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -158,6 +158,7 @@ export default function () {
|
||||||
|
|
||||||
this.route("preferences", { resetNamespace: true }, function () {
|
this.route("preferences", { resetNamespace: true }, function () {
|
||||||
this.route("account");
|
this.route("account");
|
||||||
|
this.route("security");
|
||||||
this.route("profile");
|
this.route("profile");
|
||||||
this.route("emails");
|
this.route("emails");
|
||||||
this.route("notifications");
|
this.route("notifications");
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||||
|
|
||||||
|
export default RestrictedUserRoute.extend({
|
||||||
|
showFooter: true,
|
||||||
|
});
|
|
@ -5,6 +5,11 @@
|
||||||
{{i18n "user.preferences_nav.account"}}
|
{{i18n "user.preferences_nav.account"}}
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-security">
|
||||||
|
{{#link-to "preferences.security"}}
|
||||||
|
{{i18n "user.preferences_nav.security"}}
|
||||||
|
{{/link-to}}
|
||||||
|
</li>
|
||||||
<li class="nav-profile">
|
<li class="nav-profile">
|
||||||
{{#link-to "preferences.profile"}}
|
{{#link-to "preferences.profile"}}
|
||||||
{{i18n "user.preferences_nav.profile"}}
|
{{i18n "user.preferences_nav.profile"}}
|
||||||
|
|
|
@ -26,22 +26,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{#if canEditName}}
|
|
||||||
<div class="control-group pref-name">
|
|
||||||
<label class="control-label">{{i18n "user.name.title"}}</label>
|
|
||||||
<div class="controls">
|
|
||||||
{{#if model.can_edit_name}}
|
|
||||||
{{text-field value=newNameInput classNames="input-xxlarge" maxlength="255"}}
|
|
||||||
{{else}}
|
|
||||||
<span class="static">{{model.name}}</span>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<div class="instructions">
|
|
||||||
{{nameInstructions}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if canCheckEmails}}
|
{{#if canCheckEmails}}
|
||||||
<div class="control-group pref-email">
|
<div class="control-group pref-email">
|
||||||
<label class="control-label">{{i18n "user.email.title"}}</label>
|
<label class="control-label">{{i18n "user.email.title"}}</label>
|
||||||
|
@ -107,40 +91,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if canChangePassword}}
|
|
||||||
<div class="control-group pref-password">
|
|
||||||
<label class="control-label">{{i18n "user.password.title"}}</label>
|
|
||||||
<div class="controls">
|
|
||||||
<a href {{action "changePassword"}} class="btn btn-default">
|
|
||||||
{{d-icon "envelope"}}
|
|
||||||
{{#if model.no_password}}
|
|
||||||
{{i18n "user.change_password.set_password"}}
|
|
||||||
{{else}}
|
|
||||||
{{i18n "user.change_password.action"}}
|
|
||||||
{{/if}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{passwordProgress}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group pref-second-factor">
|
|
||||||
<label class="control-label">{{i18n "user.second_factor.title"}}</label>
|
|
||||||
{{#unless model.second_factor_enabled}}
|
|
||||||
<label>
|
|
||||||
{{i18n "user.second_factor.short_description"}}
|
|
||||||
</label>
|
|
||||||
{{/unless}}
|
|
||||||
<div class="controls pref-second-factor">
|
|
||||||
{{#if isCurrentUser}}
|
|
||||||
{{#link-to "preferences.second-factor" class="btn btn-default"}}
|
|
||||||
{{d-icon "lock"}} <span>{{i18n "user.second_factor.enable"}}</span>
|
|
||||||
{{/link-to}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if canUpdateAssociatedAccounts}}
|
{{#if canUpdateAssociatedAccounts}}
|
||||||
<div class="control-group pref-associated-accounts">
|
<div class="control-group pref-associated-accounts">
|
||||||
<label class="control-label">{{i18n "user.associated_accounts.title"}}</label>
|
<label class="control-label">{{i18n "user.associated_accounts.title"}}</label>
|
||||||
|
@ -181,6 +131,22 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canEditName}}
|
||||||
|
<div class="control-group pref-name">
|
||||||
|
<label class="control-label">{{i18n "user.name.title"}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
{{#if model.can_edit_name}}
|
||||||
|
{{text-field value=newNameInput classNames="input-xxlarge" maxlength="255"}}
|
||||||
|
{{else}}
|
||||||
|
<span class="static">{{model.name}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="instructions">
|
||||||
|
{{nameInstructions}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if canSelectTitle}}
|
{{#if canSelectTitle}}
|
||||||
<div class="control-group pref-title">
|
<div class="control-group pref-title">
|
||||||
<label class="control-label">{{i18n "user.title.title"}}</label>
|
<label class="control-label">{{i18n "user.title.title"}}</label>
|
||||||
|
@ -207,47 +173,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if canCheckEmails}}
|
|
||||||
<div class="control-group pref-auth-tokens">
|
|
||||||
<label class="control-label">{{i18n "user.auth_tokens.title"}}</label>
|
|
||||||
|
|
||||||
<div class="auth-tokens">
|
|
||||||
{{#each authTokens as |token|}}
|
|
||||||
<div class="row auth-token">
|
|
||||||
<div class="auth-token-icon">{{d-icon token.icon}}</div>
|
|
||||||
{{#unless token.is_active}}
|
|
||||||
{{auth-token-dropdown token=token
|
|
||||||
revokeAuthToken=(action "revokeAuthToken")
|
|
||||||
showToken=(action "showToken")}}
|
|
||||||
{{/unless}}
|
|
||||||
<div class="auth-token-first">
|
|
||||||
{{html-safe (i18n "user.auth_tokens.device_location" device=token.device ip=token.client_ip location=token.location)}}
|
|
||||||
</div>
|
|
||||||
<div class="auth-token-second">
|
|
||||||
{{#if token.is_active}}
|
|
||||||
{{html-safe (i18n "user.auth_tokens.browser_active" browser=token.browser)}}
|
|
||||||
{{else}}
|
|
||||||
{{html-safe (i18n "user.auth_tokens.browser_last_seen" browser=token.browser date=(format-date token.seen_at))}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if canShowAllAuthTokens}}
|
|
||||||
<a href {{action "toggleShowAllAuthTokens"}}>
|
|
||||||
{{#if showAllAuthTokens}}
|
|
||||||
{{d-icon "caret-up"}} {{i18n "user.auth_tokens.show_few"}}
|
|
||||||
{{else}}
|
|
||||||
{{d-icon "caret-down"}} {{i18n "user.auth_tokens.show_all" count=model.user_auth_tokens.length}}
|
|
||||||
{{/if}}
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<a href {{action "revokeAuthToken"}} class="pull-right text-danger">{{d-icon "sign-out-alt"}} {{i18n "user.auth_tokens.log_out_all"}}</a>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{plugin-outlet name="user-preferences-account" args=(hash model=model save=(action "save"))}}
|
{{plugin-outlet name="user-preferences-account" args=(hash model=model save=(action "save"))}}
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
{{#if canChangePassword}}
|
||||||
|
<div class="control-group pref-password">
|
||||||
|
<label class="control-label">{{i18n "user.password.title"}}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<a href {{action "changePassword"}} class="btn btn-default">
|
||||||
|
{{d-icon "envelope"}}
|
||||||
|
{{#if model.no_password}}
|
||||||
|
{{i18n "user.change_password.set_password"}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n "user.change_password.action"}}
|
||||||
|
{{/if}}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{passwordProgress}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group pref-second-factor">
|
||||||
|
<label class="control-label">{{i18n "user.second_factor.title"}}</label>
|
||||||
|
{{#unless model.second_factor_enabled}}
|
||||||
|
<label>
|
||||||
|
{{i18n "user.second_factor.short_description"}}
|
||||||
|
</label>
|
||||||
|
{{/unless}}
|
||||||
|
<div class="controls pref-second-factor">
|
||||||
|
{{#if isCurrentUser}}
|
||||||
|
{{#link-to "preferences.second-factor" class="btn btn-default"}}
|
||||||
|
{{d-icon "lock"}} <span>{{i18n "user.second_factor.enable"}}</span>
|
||||||
|
{{/link-to}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canCheckEmails}}
|
||||||
|
<div class="control-group pref-auth-tokens">
|
||||||
|
<label class="control-label">{{i18n "user.auth_tokens.title"}}</label>
|
||||||
|
|
||||||
|
<div class="auth-tokens">
|
||||||
|
{{#each authTokens as |token|}}
|
||||||
|
<div class="row auth-token">
|
||||||
|
<div class="auth-token-icon">{{d-icon token.icon}}</div>
|
||||||
|
{{#unless token.is_active}}
|
||||||
|
{{auth-token-dropdown token=token
|
||||||
|
revokeAuthToken=(action "revokeAuthToken")
|
||||||
|
showToken=(action "showToken")}}
|
||||||
|
{{/unless}}
|
||||||
|
<div class="auth-token-first">
|
||||||
|
{{html-safe (i18n "user.auth_tokens.device_location" device=token.device ip=token.client_ip location=token.location)}}
|
||||||
|
</div>
|
||||||
|
<div class="auth-token-second">
|
||||||
|
{{#if token.is_active}}
|
||||||
|
{{html-safe (i18n "user.auth_tokens.browser_active" browser=token.browser)}}
|
||||||
|
{{else}}
|
||||||
|
{{html-safe (i18n "user.auth_tokens.browser_last_seen" browser=token.browser date=(format-date token.seen_at))}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if canShowAllAuthTokens}}
|
||||||
|
<a href {{action "toggleShowAllAuthTokens"}}>
|
||||||
|
{{#if showAllAuthTokens}}
|
||||||
|
{{d-icon "caret-up"}} {{i18n "user.auth_tokens.show_few"}}
|
||||||
|
{{else}}
|
||||||
|
{{d-icon "caret-down"}} {{i18n "user.auth_tokens.show_all" count=model.user_auth_tokens.length}}
|
||||||
|
{{/if}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<a href {{action "revokeAuthToken"}} class="pull-right text-danger">{{d-icon "sign-out-alt"}} {{i18n "user.auth_tokens.log_out_all"}}</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{plugin-outlet name="user-preferences-security" args=(hash model=model save=(action "save"))}}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
|
|
@ -329,47 +329,6 @@ acceptance("User Preferences when badges are disabled", function (needs) {
|
||||||
);
|
);
|
||||||
assert.ok(exists(".user-preferences"), "it shows the preferences");
|
assert.ok(exists(".user-preferences"), "it shows the preferences");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("recently connected devices", async function (assert) {
|
|
||||||
await visit("/u/eviltrout/preferences");
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
queryAll(".auth-tokens > .auth-token:nth-of-type(1) .auth-token-device")
|
|
||||||
.text()
|
|
||||||
.trim(),
|
|
||||||
"Linux Computer",
|
|
||||||
"it should display active token first"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
queryAll(".pref-auth-tokens > a:nth-of-type(1)").text().trim(),
|
|
||||||
I18n.t("user.auth_tokens.show_all", { count: 3 }),
|
|
||||||
"it should display two tokens"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
queryAll(".pref-auth-tokens .auth-token").length === 2,
|
|
||||||
"it should display two tokens"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".pref-auth-tokens > a:nth-of-type(1)");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
queryAll(".pref-auth-tokens .auth-token").length === 3,
|
|
||||||
"it should display three tokens"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".auth-token-dropdown button:nth-of-type(1)");
|
|
||||||
await click("li[data-value='notYou']");
|
|
||||||
|
|
||||||
assert.ok(queryAll(".d-modal:visible").length === 1, "modal should appear");
|
|
||||||
|
|
||||||
await click(".modal-footer .btn-primary");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
queryAll(".pref-password.highlighted").length === 1,
|
|
||||||
"it should highlight password preferences"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
acceptance(
|
acceptance(
|
||||||
|
@ -515,3 +474,49 @@ acceptance("Ignored users", function (needs) {
|
||||||
assert.ok(exists(".user-ignore"), "it shows the list of ignored users");
|
assert.ok(exists(".user-ignore"), "it shows the list of ignored users");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance("Security", function (needs) {
|
||||||
|
needs.user();
|
||||||
|
needs.pretender(preferencesPretender);
|
||||||
|
|
||||||
|
test("recently connected devices", async function (assert) {
|
||||||
|
await visit("/u/eviltrout/preferences/security");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
queryAll(".auth-tokens > .auth-token:nth-of-type(1) .auth-token-device")
|
||||||
|
.text()
|
||||||
|
.trim(),
|
||||||
|
"Linux Computer",
|
||||||
|
"it should display active token first"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
queryAll(".pref-auth-tokens > a:nth-of-type(1)").text().trim(),
|
||||||
|
I18n.t("user.auth_tokens.show_all", { count: 3 }),
|
||||||
|
"it should display two tokens"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
queryAll(".pref-auth-tokens .auth-token").length === 2,
|
||||||
|
"it should display two tokens"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".pref-auth-tokens > a:nth-of-type(1)");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
queryAll(".pref-auth-tokens .auth-token").length === 3,
|
||||||
|
"it should display three tokens"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".auth-token-dropdown button:nth-of-type(1)");
|
||||||
|
await click("li[data-value='notYou']");
|
||||||
|
|
||||||
|
assert.ok(queryAll(".d-modal:visible").length === 1, "modal should appear");
|
||||||
|
|
||||||
|
await click(".modal-footer .btn-primary");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
queryAll(".pref-password.highlighted").length === 1,
|
||||||
|
"it should highlight password preferences"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1139,6 +1139,7 @@ en:
|
||||||
|
|
||||||
preferences_nav:
|
preferences_nav:
|
||||||
account: "Account"
|
account: "Account"
|
||||||
|
security: "Security"
|
||||||
profile: "Profile"
|
profile: "Profile"
|
||||||
emails: "Emails"
|
emails: "Emails"
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
|
|
|
@ -450,6 +450,7 @@ Discourse::Application.routes.draw do
|
||||||
get "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||||
|
get "#{root_path}/:username/preferences/security" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||||
get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormat.username }
|
get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||||
put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email", format: :json, constraints: { username: RouteFormat.username }
|
put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email", format: :json, constraints: { username: RouteFormat.username }
|
||||||
|
|
Loading…
Reference in New Issue