DEV: Convert API keys to Admin UI guidelines
This commit is contained in:
parent
e04f535601
commit
358932c6d6
|
@ -0,0 +1,25 @@
|
|||
<div class="container admin-api_keys">
|
||||
{{#if @apiKeys}}
|
||||
<table class="d-admin-table admin-api_keys__items">
|
||||
<thead>
|
||||
<th>{{i18n "admin.api.key"}}</th>
|
||||
<th>{{i18n "admin.api.description"}}</th>
|
||||
<th>{{i18n "admin.api.user"}}</th>
|
||||
<th>{{i18n "admin.api.created"}}</th>
|
||||
<th>{{i18n "admin.api.last_used"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @apiKeys as |apiKey|}}
|
||||
<ApiKeyItem @apiKey={{apiKey}} />
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@ctaLabel="admin.api_keys.add"
|
||||
@ctaRoute="adminApiKeys.new"
|
||||
@ctaClass="admin-api_keys__add-api_key"
|
||||
@emptyLabel="admin.api_keys.no_api_keys"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,318 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { concat, fn, hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { eq } from "truth-helpers";
|
||||
import BackButton from "discourse/components/back-button";
|
||||
import ConditionalLoadingSection from "discourse/components/conditional-loading-section";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import Form from "discourse/components/form";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ApiKeyUrlsModal from "admin/components/modal/api-key-urls";
|
||||
import EmailGroupUserChooser from "select-kit/components/email-group-user-chooser";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
|
||||
export default class AdminConfigAreasApiKeysNew extends Component {
|
||||
@service router;
|
||||
@service modal;
|
||||
@service store;
|
||||
|
||||
@tracked username;
|
||||
@tracked loadingScopes = false;
|
||||
@tracked scopes = null;
|
||||
|
||||
userModes = [
|
||||
{ id: "all", name: i18n("admin.api.all_users") },
|
||||
{ id: "single", name: i18n("admin.api.single_user") },
|
||||
];
|
||||
|
||||
scopeModes = [
|
||||
{ id: "global", name: i18n("admin.api.scopes.global") },
|
||||
{ id: "read_only", name: i18n("admin.api.scopes.read_only") },
|
||||
{ id: "granular", name: i18n("admin.api.scopes.granular") },
|
||||
];
|
||||
|
||||
globalScopes = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.#loadScopes();
|
||||
}
|
||||
|
||||
@cached
|
||||
get formData() {
|
||||
let scopes = Object.keys(this.scopes).reduce((result, resource) => {
|
||||
result[resource] = this.scopes[resource].map((scope) => {
|
||||
const params = scope.params
|
||||
? scope.params.reduce((acc, param) => {
|
||||
acc[param] = undefined;
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
return {
|
||||
key: scope.key,
|
||||
enabled: undefined,
|
||||
urls: scope.urls,
|
||||
...(params && { params }),
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
user_mode: "all",
|
||||
scope_mode: "global",
|
||||
scopes,
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
updateUsername(field, selected) {
|
||||
this.username = selected[0];
|
||||
field.set(this.username);
|
||||
}
|
||||
|
||||
@action
|
||||
async save(data) {
|
||||
const payload = { description: data.description };
|
||||
|
||||
if (data.user_mode === "single") {
|
||||
payload.username = data.user;
|
||||
}
|
||||
|
||||
if (data.scope_mode === "granular") {
|
||||
payload.scopes = this.#selectedScopes(data.scopes);
|
||||
} else if (data.scope_mode === "read_only") {
|
||||
payload.scopes = this.globalScopes.filter(
|
||||
(scope) => scope.key === "read"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.store.createRecord("api-key").save(payload);
|
||||
this.router.transitionTo("adminApiKeys");
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
#selectedScopes(scopes) {
|
||||
const enabledScopes = [];
|
||||
|
||||
for (const [resource, resourceScopes] of Object.entries(scopes)) {
|
||||
enabledScopes.push(
|
||||
resourceScopes
|
||||
.filter((s) => s.enabled)
|
||||
.map((s) => {
|
||||
return {
|
||||
scope_id: `${resource}:${s.key}`,
|
||||
key: s.key,
|
||||
name: s.key,
|
||||
params: Object.keys(s.params),
|
||||
...s.params,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return enabledScopes.flat();
|
||||
}
|
||||
|
||||
@action
|
||||
showURLs(urls) {
|
||||
this.modal.show(ApiKeyUrlsModal, {
|
||||
model: { urls },
|
||||
});
|
||||
}
|
||||
|
||||
async #loadScopes() {
|
||||
try {
|
||||
this.loadingScopes = true;
|
||||
const data = await ajax("/admin/api/keys/scopes.json");
|
||||
|
||||
this.globalScopes = data.scopes.global;
|
||||
delete data.scopes.global;
|
||||
|
||||
this.scopes = data.scopes;
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loadingScopes = false;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<BackButton @route="adminApiKeys.index" @label="admin.api_keys.back" />
|
||||
|
||||
<div class="admin-config-area user-field">
|
||||
<div class="admin-config-area__primary-content">
|
||||
<div class="admin-config-area-card">
|
||||
<ConditionalLoadingSection @isLoading={{this.loadingScopes}}>
|
||||
<Form
|
||||
@onSubmit={{this.save}}
|
||||
@data={{this.formData}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Field
|
||||
@name="description"
|
||||
@title={{i18n "admin.api.description"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="user_mode"
|
||||
@title={{i18n "admin.api.user_mode"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Select as |select|>
|
||||
{{#each this.userModes as |userMode|}}
|
||||
<select.Option
|
||||
@value={{userMode.id}}
|
||||
>{{userMode.name}}</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
|
||||
{{#if (eq transientData.user_mode "single")}}
|
||||
<form.Field
|
||||
@name="user"
|
||||
@title={{i18n "admin.api.user"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<EmailGroupUserChooser
|
||||
@value={{this.username}}
|
||||
@onChange={{fn this.updateUsername field}}
|
||||
@options={{hash
|
||||
maximum=1
|
||||
filterPlaceholder="admin.api.user_placeholder"
|
||||
}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
<form.Field
|
||||
@name="scope_mode"
|
||||
@title={{i18n "admin.api.scope_mode"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Select as |select|>
|
||||
{{#each this.scopeModes as |scopeMode|}}
|
||||
<select.Option
|
||||
@value={{scopeMode.id}}
|
||||
>{{scopeMode.name}}</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
|
||||
{{#if (eq transientData.scope_mode "granular")}}
|
||||
<h2 class="scopes-title">{{i18n "admin.api.scopes.title"}}</h2>
|
||||
<p>{{i18n "admin.api.scopes.description"}}</p>
|
||||
<table class="scopes-table grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
|
||||
<td>{{i18n
|
||||
"admin.api.scopes.optional_allowed_parameters"
|
||||
}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<form.Object @name="scopes" as |scopesObject scopeName|>
|
||||
<tr class="scope-resource-name">
|
||||
<td><b>{{scopeName}}</b></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<scopesObject.Collection
|
||||
@name={{scopeName}}
|
||||
@tagName="tr"
|
||||
as |topicsCollection index collectionData|
|
||||
>
|
||||
<td>
|
||||
<topicsCollection.Field
|
||||
@name="enabled"
|
||||
@title={{collectionData.key}}
|
||||
@showTitle={{false}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</topicsCollection.Field>
|
||||
</td>
|
||||
<td>
|
||||
<div class="scope-name">{{collectionData.name}}</div>
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{i18n
|
||||
(concat
|
||||
"admin.api.scopes.descriptions."
|
||||
scopeName
|
||||
"."
|
||||
collectionData.key
|
||||
)
|
||||
class="scope-tooltip"
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DButton
|
||||
@icon="link"
|
||||
@action={{fn this.showURLs collectionData.urls}}
|
||||
class="btn-info"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<topicsCollection.Object
|
||||
@name="params"
|
||||
as |paramsObject name|
|
||||
>
|
||||
<paramsObject.Field
|
||||
@name={{name}}
|
||||
@title={{name}}
|
||||
@showTitle={{false}}
|
||||
as |field|
|
||||
>
|
||||
<field.Input placeholder={{name}} />
|
||||
</paramsObject.Field>
|
||||
</topicsCollection.Object>
|
||||
</td>
|
||||
</scopesObject.Collection>
|
||||
</form.Object>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
<form.Actions>
|
||||
<form.Submit class="save" @label="admin.api_keys.save" />
|
||||
<form.Button
|
||||
@route="adminApiKeys.index"
|
||||
@label="admin.api_keys.cancel"
|
||||
class="btn-default"
|
||||
/>
|
||||
</form.Actions>
|
||||
</Form>
|
||||
</ConditionalLoadingSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { concat, fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action, get } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import BackButton from "discourse/components/back-button";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import formatDate from "discourse/helpers/format-date";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AdminFormRow from "admin/components/admin-form-row";
|
||||
import ApiKeyUrlsModal from "admin/components/modal/api-key-urls";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
|
||||
export default class AdminConfigAreasApiKeysShow extends Component {
|
||||
@service modal;
|
||||
@service router;
|
||||
|
||||
@tracked editingDescription = false;
|
||||
@tracked scopes = this.args.apiKey.api_key_scopes;
|
||||
newDescription = "";
|
||||
|
||||
@action
|
||||
async revokeKey(key) {
|
||||
try {
|
||||
await key.revoke();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async undoRevokeKey(key) {
|
||||
try {
|
||||
await key.undoRevoke();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteKey(key) {
|
||||
try {
|
||||
await key.destroyRecord();
|
||||
this.router.transitionTo("adminApiKeys.index");
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
showURLs(urls) {
|
||||
this.modal.show(ApiKeyUrlsModal, {
|
||||
model: { urls },
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
toggleEditDescription() {
|
||||
this.editingDescription = !this.editingDescription;
|
||||
this.newDescription = this.args.apiKey.description;
|
||||
}
|
||||
|
||||
@action
|
||||
async saveDescription() {
|
||||
try {
|
||||
await this.args.apiKey.save({ description: this.newDescription });
|
||||
this.editingDescription = false;
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setNewDescription(event) {
|
||||
this.newDescription = event.currentTarget.value;
|
||||
}
|
||||
|
||||
<template>
|
||||
<BackButton @route="adminApiKeys.index" @label="admin.api_keys.back" />
|
||||
|
||||
<div class="api-key api-key-show">
|
||||
<AdminFormRow @label="admin.api.key">
|
||||
{{@apiKey.truncatedKey}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.description">
|
||||
{{#if this.editingDescription}}
|
||||
<Input
|
||||
@value={{@apiKey.description}}
|
||||
{{on "input" this.setNewDescription}}
|
||||
maxlength="255"
|
||||
placeholder={{i18n "admin.api.description_placeholder"}}
|
||||
/>
|
||||
{{else}}
|
||||
<span>
|
||||
{{if
|
||||
@apiKey.description
|
||||
@apiKey.description
|
||||
(i18n "admin.api.no_description")
|
||||
}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<div class="controls">
|
||||
{{#if this.editingDescription}}
|
||||
<DButton
|
||||
@action={{this.saveDescription}}
|
||||
@label="admin.api_keys.save"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.toggleEditDescription}}
|
||||
@label="admin.api_keys.cancel"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.toggleEditDescription}}
|
||||
@label="admin.api_keys.edit"
|
||||
class="btn-default"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.user">
|
||||
{{#if @apiKey.user}}
|
||||
<LinkTo @route="adminUser" @model={{@apiKey.user}}>
|
||||
{{avatar @apiKey.user imageSize="small"}}
|
||||
{{@apiKey.user.username}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{i18n "admin.api.all_users"}}
|
||||
{{/if}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.created">
|
||||
{{formatDate @apiKey.created_at leaveAgo="true"}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.updated">
|
||||
{{formatDate @apiKey.updated_at leaveAgo="true"}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.last_used">
|
||||
{{#if @apiKey.last_used_at}}
|
||||
{{formatDate @apiKey.last_used_at leaveAgo="true"}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.never_used"}}
|
||||
{{/if}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.revoked">
|
||||
{{#if @apiKey.revoked_at}}
|
||||
{{formatDate @apiKey.revoked_at leaveAgo="true"}}
|
||||
{{else}}
|
||||
<span>{{i18n "no_value"}}</span>
|
||||
{{/if}}
|
||||
<div class="controls">
|
||||
{{#if @apiKey.revoked_at}}
|
||||
<DButton
|
||||
@action={{fn this.undoRevokeKey @apiKey}}
|
||||
@label="admin.api.undo_revoke"
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn this.deleteKey @apiKey}}
|
||||
@label="admin.api.delete"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{fn this.revokeKey @apiKey}}
|
||||
@label="admin.api.revoke"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</AdminFormRow>
|
||||
|
||||
{{#if @apiKey.api_key_scopes.length}}
|
||||
<h2 class="scopes-title">{{i18n "admin.api.scopes.title"}}</h2>
|
||||
|
||||
<table class="scopes-table grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{i18n "admin.api.scopes.resource"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.action"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.allowed_parameters"}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @apiKey.api_key_scopes as |scope|}}
|
||||
<tr>
|
||||
<td>{{scope.resource}}</td>
|
||||
<td>
|
||||
{{scope.action}}
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{i18n
|
||||
(concat
|
||||
"admin.api.scopes.descriptions."
|
||||
scope.resource
|
||||
"."
|
||||
scope.key
|
||||
)
|
||||
}}
|
||||
class="scope-tooltip"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DButton
|
||||
@icon="link"
|
||||
@action={{fn this.showURLs scope.urls}}
|
||||
class="btn-info"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{#each scope.parameters as |p|}}
|
||||
<div>
|
||||
<b>{{p}}:</b>
|
||||
{{#if (get scope.allowed_parameters p)}}
|
||||
{{get scope.allowed_parameters p}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.scopes.any_parameter"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import formatDate from "discourse/helpers/format-date";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
export default class ApiKeysList extends Component {
|
||||
@service router;
|
||||
|
||||
@tracked apiKey = this.args.apiKey;
|
||||
|
||||
@action
|
||||
onRegisterApi(api) {
|
||||
this.dMenu = api;
|
||||
}
|
||||
|
||||
@action
|
||||
async revokeKey(key) {
|
||||
try {
|
||||
await key.revoke();
|
||||
this.dMenu.close();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async undoRevokeKey(key) {
|
||||
try {
|
||||
await key.undoRevoke();
|
||||
this.dMenu.close();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
edit() {
|
||||
this.router.transitionTo("adminApiKeys.show", this.apiKey);
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr class="d-admin-row__content">
|
||||
<td class="d-admin-row__overview key">
|
||||
{{this.apiKey.truncatedKey}}
|
||||
{{#if this.apiKey.revoked_at}}
|
||||
<span class="d-admin-table__badge">{{i18n
|
||||
"admin.api.revoked"
|
||||
}}</span>{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-description">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.description"
|
||||
}}</div>
|
||||
{{this.apiKey.shortDescription}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-user">
|
||||
<div class="d-admin-row__mobile-label">{{i18n "admin.api.user"}}</div>
|
||||
{{#if this.apiKey.user}}
|
||||
<LinkTo @route="adminUser" @model={{this.apiKey.user}}>
|
||||
{{avatar this.apiKey.user imageSize="small"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{i18n "admin.api.all_users"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-created">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.created"
|
||||
}}</div>
|
||||
{{formatDate this.apiKey.created_at}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-last-used">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.last_used"
|
||||
}}</div>
|
||||
{{#if this.apiKey.last_used_at}}
|
||||
{{formatDate this.apiKey.last_used_at}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.never_used"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls key-controls">
|
||||
<div class="d-admin-row__controls-options">
|
||||
<DButton
|
||||
@action={{this.edit}}
|
||||
@label="admin.api_keys.edit"
|
||||
@title="admin.api.show_details"
|
||||
class="btn-small"
|
||||
/>
|
||||
<DMenu
|
||||
@identifier="api_key-menu"
|
||||
@title={{i18n "admin.config_areas.user_fields.more_options.title"}}
|
||||
@icon="ellipsis-vertical"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#if this.apiKey.revoked_at}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@action={{fn this.undoRevokeKey this.apiKey}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="admin.api_keys.undo_revoke"
|
||||
@title="admin.api.undo_revoke"
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{else}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@action={{fn this.revokeKey this.apiKey}}
|
||||
@icon="xmark"
|
||||
@label="admin.api_keys.revoke"
|
||||
@title="admin.api.revoke"
|
||||
class="btn-danger"
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { empty } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
import ApiKeyUrlsModal from "../components/modal/api-key-urls";
|
||||
|
||||
export default class AdminApiKeysShowController extends Controller.extend(
|
||||
bufferedProperty("model")
|
||||
) {
|
||||
@service router;
|
||||
@service modal;
|
||||
|
||||
@empty("model.id") isNew;
|
||||
|
||||
@action
|
||||
saveDescription() {
|
||||
const buffered = this.buffered;
|
||||
const attrs = buffered.getProperties("description");
|
||||
|
||||
this.model
|
||||
.save(attrs)
|
||||
.then(() => {
|
||||
this.set("editingDescription", false);
|
||||
this.rollbackBuffer();
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const id = this.get("userField.id");
|
||||
if (isEmpty(id)) {
|
||||
this.destroyAction(this.userField);
|
||||
} else {
|
||||
this.rollbackBuffer();
|
||||
this.set("editing", false);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
editDescription() {
|
||||
this.toggleProperty("editingDescription");
|
||||
if (!this.editingDescription) {
|
||||
this.rollbackBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
revokeKey(key) {
|
||||
key.revoke().catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteKey(key) {
|
||||
key
|
||||
.destroyRecord()
|
||||
.then(() => this.router.transitionTo("adminApiKeys.index"))
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
undoRevokeKey(key) {
|
||||
key.undoRevoke().catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
showURLs(urls) {
|
||||
this.modal.show(ApiKeyUrlsModal, {
|
||||
model: {
|
||||
urls,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import discourseComputed from "discourse-common/utils/decorators";
|
|||
import AdminUser from "admin/models/admin-user";
|
||||
|
||||
export default class ApiKey extends RestModel {
|
||||
@fmt("truncated_key", "%@...") truncatedKey;
|
||||
@fmt("truncated_key", "%@ ...") truncatedKey;
|
||||
|
||||
@computed("_user")
|
||||
get user() {
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import Route from "@ember/routing/route";
|
||||
|
||||
export default class AdminApiKeysNewRoute extends Route {
|
||||
model() {
|
||||
return this.store.createRecord("api-key");
|
||||
}
|
||||
}
|
||||
export default class AdminApiKeysNewRoute extends Route {}
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
import { action } from "@ember/object";
|
||||
import Route from "@ember/routing/route";
|
||||
import { service } from "@ember/service";
|
||||
|
||||
export default class AdminApiKeysRoute extends Route {
|
||||
@service router;
|
||||
|
||||
@action
|
||||
show(apiKey) {
|
||||
this.router.transitionTo("adminApiKeys.show", apiKey.id);
|
||||
}
|
||||
|
||||
@action
|
||||
new() {
|
||||
this.router.transitionTo("adminApiKeys.new");
|
||||
}
|
||||
}
|
||||
export default class AdminApiKeysRoute extends Route {}
|
||||
|
|
|
@ -1,104 +1 @@
|
|||
<DButton
|
||||
@action={{route-action "new"}}
|
||||
@icon="plus"
|
||||
@label="admin.api.new_key"
|
||||
class="btn-primary"
|
||||
/>
|
||||
|
||||
{{#if this.model}}
|
||||
<LoadMore @selector=".api-keys tr" @action={{action "loadMore"}}>
|
||||
<table class="d-admin-table api-keys">
|
||||
<thead>
|
||||
<th>{{i18n "admin.api.key"}}</th>
|
||||
<th>{{i18n "admin.api.description"}}</th>
|
||||
<th>{{i18n "admin.api.user"}}</th>
|
||||
<th>{{i18n "admin.api.created"}}</th>
|
||||
<th>{{i18n "admin.api.last_used"}}</th>
|
||||
<th>{{i18n "admin.site_settings.table_column_heading.status"}}</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |k|}}
|
||||
<tr class="d-admin-row__content {{if k.revoked_at 'revoked'}}">
|
||||
<td class="d-admin-row__overview key">
|
||||
{{k.truncatedKey}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-description">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.description"
|
||||
}}</div>
|
||||
{{k.shortDescription}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-user">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.user"
|
||||
}}</div>
|
||||
{{#if k.user}}
|
||||
<LinkTo @route="adminUser" @model={{k.user}}>
|
||||
{{avatar k.user imageSize="small"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{i18n "admin.api.all_users"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-created">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.created"
|
||||
}}</div>
|
||||
{{format-date k.created_at}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-last-used">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.api.last_used"
|
||||
}}</div>
|
||||
{{#if k.last_used_at}}
|
||||
{{format-date k.last_used_at}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.never_used"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.site_settings.table_column_heading.status"
|
||||
}}</div>
|
||||
{{#if k.revoked_at}}
|
||||
<div role="status" class="status-label">
|
||||
<div class="status-label-indicator">
|
||||
</div>
|
||||
<div class="status-label-text">
|
||||
{{i18n "admin.api.revoked"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls key-controls">
|
||||
<DButton
|
||||
@action={{route-action "show" k}}
|
||||
@icon="far-eye"
|
||||
@title="admin.api.show_details"
|
||||
/>
|
||||
{{#if k.revoked_at}}
|
||||
<DButton
|
||||
@action={{fn this.undoRevokeKey k}}
|
||||
@icon="arrow-rotate-left"
|
||||
@title="admin.api.undo_revoke"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{fn this.revokeKey k}}
|
||||
@icon="xmark"
|
||||
@title="admin.api.revoke"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</LoadMore>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
{{else}}
|
||||
<p>{{i18n "admin.api.none"}}</p>
|
||||
{{/if}}
|
||||
<AdminConfigAreas::ApiKeysList @apiKeys={{this.model}} />
|
|
@ -1,132 +1 @@
|
|||
<LinkTo @route="adminApiKeys.index" class="go-back">
|
||||
{{d-icon "arrow-left"}}
|
||||
<span>{{i18n "admin.api.all_api_keys"}}</span>
|
||||
</LinkTo>
|
||||
|
||||
<div class="api-key api-key-new">
|
||||
{{#if this.model.id}}
|
||||
<AdminFormRow @label="admin.api.key">
|
||||
<div>{{this.model.key}}</div>
|
||||
</AdminFormRow>
|
||||
<AdminFormRow>
|
||||
{{i18n "admin.api.not_shown_again"}}
|
||||
</AdminFormRow>
|
||||
<AdminFormRow>
|
||||
<DButton
|
||||
@icon="angle-right"
|
||||
@label="admin.api.continue"
|
||||
@action={{this.continue}}
|
||||
class="btn-primary"
|
||||
/>
|
||||
</AdminFormRow>
|
||||
{{else}}
|
||||
<AdminFormRow @label="admin.api.description">
|
||||
<Input
|
||||
@value={{this.model.description}}
|
||||
maxlength="255"
|
||||
placeholder={{i18n "admin.api.description_placeholder"}}
|
||||
/>
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.user_mode">
|
||||
<ComboBox
|
||||
@content={{this.userModes}}
|
||||
@value={{this.userMode}}
|
||||
@onChange={{action "changeUserMode"}}
|
||||
/>
|
||||
</AdminFormRow>
|
||||
|
||||
{{#if this.showUserSelector}}
|
||||
<AdminFormRow @label="admin.api.user">
|
||||
<EmailGroupUserChooser
|
||||
@value={{this.model.username}}
|
||||
@onChange={{action "updateUsername"}}
|
||||
@options={{hash
|
||||
maximum=1
|
||||
filterPlaceholder="admin.api.user_placeholder"
|
||||
}}
|
||||
/>
|
||||
</AdminFormRow>
|
||||
{{/if}}
|
||||
|
||||
<AdminFormRow @label="admin.api.scope_mode">
|
||||
<ComboBox
|
||||
@content={{this.scopeModes}}
|
||||
@value={{this.scopeMode}}
|
||||
@onChange={{action "changeScopeMode"}}
|
||||
/>
|
||||
|
||||
{{#if (eq this.scopeMode "read_only")}}
|
||||
<p>{{i18n "admin.api.scopes.descriptions.global.read"}}</p>
|
||||
{{else if (eq this.scopeMode "global")}}
|
||||
<p>{{i18n "admin.api.scopes.global_description"}}</p>
|
||||
{{/if}}
|
||||
</AdminFormRow>
|
||||
|
||||
{{#if (eq this.scopeMode "granular")}}
|
||||
<h2 class="scopes-title">{{i18n "admin.api.scopes.title"}}</h2>
|
||||
<p>{{i18n "admin.api.scopes.description"}}</p>
|
||||
<table class="scopes-table grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.optional_allowed_parameters"}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each-in this.scopes as |resource actions|}}
|
||||
<tr class="scope-resource-name">
|
||||
<td><b>{{resource}}</b></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{{#each actions as |act|}}
|
||||
<tr>
|
||||
<td><Input @type="checkbox" @checked={{act.selected}} /></td>
|
||||
<td>
|
||||
<div class="scope-name">{{act.name}}</div>
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{i18n
|
||||
(concat
|
||||
"admin.api.scopes.descriptions." resource "." act.key
|
||||
)
|
||||
class="scope-tooltip"
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DButton
|
||||
@icon="link"
|
||||
@action={{fn this.showURLs act.urls}}
|
||||
class="btn-info"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{#each act.params as |p|}}
|
||||
<Input
|
||||
maxlength="255"
|
||||
@value={{get act p}}
|
||||
placeholder={{p}}
|
||||
/>
|
||||
{{/each}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@icon="check"
|
||||
@label="admin.api.save"
|
||||
@action={{this.save}}
|
||||
@disabled={{this.saveDisabled}}
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<AdminConfigAreas::ApiKeysNew />
|
|
@ -1,159 +1 @@
|
|||
<LinkTo @route="adminApiKeys.index" class="go-back">
|
||||
{{d-icon "arrow-left"}}
|
||||
{{i18n "admin.api.all_api_keys"}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="api-key api-key-show">
|
||||
<AdminFormRow @label="admin.api.key">
|
||||
{{#if this.model.revoked_at}}{{d-icon "circle-xmark"}}{{/if}}
|
||||
{{this.model.truncatedKey}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.description">
|
||||
{{#if this.editingDescription}}
|
||||
<Input
|
||||
@value={{this.buffered.description}}
|
||||
maxlength="255"
|
||||
placeholder={{i18n "admin.api.description_placeholder"}}
|
||||
/>
|
||||
{{else}}
|
||||
<span>
|
||||
{{if
|
||||
this.model.description
|
||||
this.model.description
|
||||
(i18n "admin.api.no_description")
|
||||
}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<div class="controls">
|
||||
{{#if this.editingDescription}}
|
||||
<DButton @action={{this.saveDescription}} @icon="check" class="ok" />
|
||||
<DButton
|
||||
@action={{this.editDescription}}
|
||||
@icon="xmark"
|
||||
class="cancel"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.editDescription}}
|
||||
@icon="pencil"
|
||||
class="btn-default"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.user">
|
||||
{{#if this.model.user}}
|
||||
<LinkTo @route="adminUser" @model={{this.model.user}}>
|
||||
{{avatar this.model.user imageSize="small"}}
|
||||
{{this.model.user.username}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{i18n "admin.api.all_users"}}
|
||||
{{/if}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.created">
|
||||
{{format-date this.model.created_at leaveAgo="true"}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.updated">
|
||||
{{format-date this.model.updated_at leaveAgo="true"}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.last_used">
|
||||
{{#if this.model.last_used_at}}
|
||||
{{format-date this.model.last_used_at leaveAgo="true"}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.never_used"}}
|
||||
{{/if}}
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @label="admin.api.revoked">
|
||||
{{#if this.model.revoked_at}}
|
||||
{{format-date this.model.revoked_at leaveAgo="true"}}
|
||||
{{else}}
|
||||
<span>{{i18n "no_value"}}</span>
|
||||
{{/if}}
|
||||
<div class="controls">
|
||||
{{#if this.model.revoked_at}}
|
||||
<DButton
|
||||
@action={{fn this.undoRevokeKey this.model}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="admin.api.undo_revoke"
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn this.deleteKey this.model}}
|
||||
@icon="trash-can"
|
||||
@label="admin.api.delete"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{fn this.revokeKey this.model}}
|
||||
@icon="xmark"
|
||||
@label="admin.api.revoke"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</AdminFormRow>
|
||||
|
||||
{{#if this.model.api_key_scopes.length}}
|
||||
<h2 class="scopes-title">{{i18n "admin.api.scopes.title"}}</h2>
|
||||
|
||||
<table class="scopes-table grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{i18n "admin.api.scopes.resource"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.action"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.allowed_urls"}}</td>
|
||||
<td>{{i18n "admin.api.scopes.allowed_parameters"}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model.api_key_scopes as |scope|}}
|
||||
<tr>
|
||||
<td>{{scope.resource}}</td>
|
||||
<td>
|
||||
{{scope.action}}
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{i18n
|
||||
(concat
|
||||
"admin.api.scopes.descriptions."
|
||||
scope.resource
|
||||
"."
|
||||
scope.key
|
||||
)
|
||||
}}
|
||||
class="scope-tooltip"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DButton
|
||||
@icon="link"
|
||||
@action={{fn this.showURLs scope.urls}}
|
||||
class="btn-info"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{#each scope.parameters as |p|}}
|
||||
<div>
|
||||
<b>{{p}}:</b>
|
||||
{{#if (get scope.allowed_parameters p)}}
|
||||
{{get scope.allowed_parameters p}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.scopes.any_parameter"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</div>
|
||||
<AdminConfigAreas::ApiKeysShow @apiKey={{this.model}} />
|
|
@ -1,3 +1,21 @@
|
|||
<PluginOutlet @name="admin-api-keys">
|
||||
{{outlet}}
|
||||
</PluginOutlet>
|
||||
<div class="admin-api-keys admin-config-page">
|
||||
<AdminPageHeader
|
||||
@titleLabel="admin.api_keys.title"
|
||||
@descriptionLabel="admin.api_keys.description"
|
||||
@hideTabs={{true}}
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/api/keys"
|
||||
@label={{i18n "admin.api_keys.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary @route="adminApiKeys.new" @label="admin.api_keys.add" />
|
||||
</:actions>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="admin-container admin-config-page__main-area">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
<AdminNav>
|
||||
<NavItem @route="adminApiKeys" @label="admin.api.title" />
|
||||
<NavItem @route="adminWebHooks" @label="admin.web_hooks.title" />
|
||||
</AdminNav>
|
||||
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
|
@ -92,6 +92,13 @@
|
|||
color: var(--primary-high);
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
&__badge {
|
||||
background-color: var(--primary-low);
|
||||
border-radius: var(--d-border-radius);
|
||||
font-size: var(--font-down-1);
|
||||
margin-left: var(--space-1);
|
||||
padding: var(--space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5320,6 +5320,19 @@ en:
|
|||
none_selected: "Select a group to get started"
|
||||
no_custom_groups: "Create a new custom group"
|
||||
|
||||
api_keys:
|
||||
title: "API Keys"
|
||||
description: "The API keys feature lets you securely integrate Discourse with external systems and automate actions. Admins can create keys with specific scopes to control access to resources and sensitive data. Scopes limit functionality, ensuring enhanced security."
|
||||
add: "Add API key"
|
||||
edit: "Edit"
|
||||
save: "Save"
|
||||
cancel: "Cancel"
|
||||
back: "Back to API keys"
|
||||
revoke: "Revoke"
|
||||
undo_revoke: "Undo revoke"
|
||||
revoked: "Revoked"
|
||||
no_api_keys: "You don't have any API keys yet."
|
||||
|
||||
api:
|
||||
generate_master: "Generate Master API Key"
|
||||
none: "There are no active API keys right now."
|
||||
|
@ -5332,25 +5345,25 @@ en:
|
|||
last_used: Last Used
|
||||
never_used: (never)
|
||||
generate: "Generate"
|
||||
undo_revoke: "Undo Revoke"
|
||||
undo_revoke: "Undo revoke"
|
||||
revoke: "Revoke"
|
||||
all_users: "All Users"
|
||||
all_users: "All users"
|
||||
active_keys: "Active API Keys"
|
||||
manage_keys: Manage Keys
|
||||
show_details: Details
|
||||
description: Description
|
||||
no_description: (no description)
|
||||
all_api_keys: All API Keys
|
||||
user_mode: User Level
|
||||
user_mode: User level
|
||||
scope_mode: Scope
|
||||
impersonate_all_users: Impersonate any user
|
||||
single_user: "Single User"
|
||||
single_user: "Single user"
|
||||
user_placeholder: Enter username
|
||||
description_placeholder: What will this key be used for?
|
||||
save: Save
|
||||
new_key: New API Key
|
||||
revoked: Revoked
|
||||
delete: Permanently Delete
|
||||
delete: Permanently delete
|
||||
not_shown_again: This key will not be displayed again. Make sure you take a copy before continuing.
|
||||
continue: Continue
|
||||
scopes:
|
||||
|
@ -5364,8 +5377,8 @@ en:
|
|||
global_description: API key has no restriction and all endpoints are accessible.
|
||||
resource: Resource
|
||||
action: Action
|
||||
allowed_parameters: Allowed Parameters
|
||||
optional_allowed_parameters: Allowed Parameters (optional)
|
||||
allowed_parameters: Allowed parameters
|
||||
optional_allowed_parameters: Allowed parameters (optional)
|
||||
any_parameter: (any parameter)
|
||||
allowed_urls: Allowed URLs
|
||||
descriptions:
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
#frozen_string_literal: true
|
||||
|
||||
describe "Admin API Keys Page", type: :system do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
|
||||
let(:api_keys_page) { PageObjects::Pages::AdminApiKeys.new }
|
||||
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||
|
||||
before do
|
||||
Fabricate(:api_key, description: "Integration")
|
||||
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
it "shows a list of API keys" do
|
||||
api_keys_page.visit_page
|
||||
|
||||
expect(api_keys_page).to have_api_key_listed("Integration")
|
||||
end
|
||||
|
||||
it "can add a new API key" do
|
||||
api_keys_page.visit_page
|
||||
api_keys_page.add_api_key(description: "Second Integration")
|
||||
|
||||
expect(api_keys_page).to have_api_key_listed("Second Integration")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminApiKeys < PageObjects::Pages::Base
|
||||
def visit_page
|
||||
page.visit "/admin/api/keys"
|
||||
self
|
||||
end
|
||||
|
||||
def has_api_key_listed?(name)
|
||||
page.has_css?(table_selector, text: name)
|
||||
end
|
||||
|
||||
def has_no_api_key_listed?(name)
|
||||
page.has_no_css?(table_selector, text: name)
|
||||
end
|
||||
|
||||
def add_api_key(description:)
|
||||
page.find(".admin-page-header__actions", text: "Add API key").click
|
||||
|
||||
form = page.find(".form-kit")
|
||||
form.find("input[name='description']").fill_in(with: description)
|
||||
form.find(".save").click
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def table_selector
|
||||
".admin-api_keys__items"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue