mirror of
https://github.com/discourse/discourse.git
synced 2025-01-16 06:22:41 +08:00
REVERT: "DEV: Convert admin API keys to conform to UI guidelines"
This reverts commit d9ddc25808
.
I noticed that Webhook admin UI is now inaccessible through the subheader
This commit is contained in:
parent
2ff511a4e4
commit
4da7904ffd
|
@ -1,25 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,318 +0,0 @@
|
||||||
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
|
|
||||||
async showURLs(urls) {
|
|
||||||
await 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>
|
|
||||||
}
|
|
|
@ -1,241 +0,0 @@
|
||||||
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
|
|
||||||
async showURLs(urls) {
|
|
||||||
await 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}}
|
|
||||||
name="description"
|
|
||||||
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>
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
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();
|
|
||||||
await this.dMenu.close();
|
|
||||||
} catch (error) {
|
|
||||||
popupAjaxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async undoRevokeKey(key) {
|
|
||||||
try {
|
|
||||||
await key.undoRevoke();
|
|
||||||
await 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">
|
|
||||||
<LinkTo @route="adminUser" @model={{this.apiKey.createdBy}}>
|
|
||||||
{{avatar this.apiKey.createdBy imageSize="small"}}
|
|
||||||
</LinkTo>
|
|
||||||
<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>
|
|
||||||
}
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
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";
|
import AdminUser from "admin/models/admin-user";
|
||||||
|
|
||||||
export default class ApiKey extends RestModel {
|
export default class ApiKey extends RestModel {
|
||||||
@fmt("truncated_key", "%@ ...") truncatedKey;
|
@fmt("truncated_key", "%@...") truncatedKey;
|
||||||
|
|
||||||
@computed("_user")
|
@computed("_user")
|
||||||
get user() {
|
get user() {
|
||||||
|
@ -21,19 +21,6 @@ export default class ApiKey extends RestModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("_created_by")
|
|
||||||
get createdBy() {
|
|
||||||
return this._created_by;
|
|
||||||
}
|
|
||||||
|
|
||||||
set created_by(value) {
|
|
||||||
if (value && !(value instanceof AdminUser)) {
|
|
||||||
this.set("_created_by", AdminUser.create(value));
|
|
||||||
} else {
|
|
||||||
this.set("_created_by", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("description")
|
@discourseComputed("description")
|
||||||
shortDescription(description) {
|
shortDescription(description) {
|
||||||
if (!description || description.length < 40) {
|
if (!description || description.length < 40) {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
import Route from "@ember/routing/route";
|
import Route from "@ember/routing/route";
|
||||||
|
|
||||||
export default class AdminApiKeysNewRoute extends Route {}
|
export default class AdminApiKeysNewRoute extends Route {
|
||||||
|
model() {
|
||||||
|
return this.store.createRecord("api-key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,17 @@
|
||||||
|
import { action } from "@ember/object";
|
||||||
import Route from "@ember/routing/route";
|
import Route from "@ember/routing/route";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
|
||||||
export default class AdminApiKeysRoute extends Route {}
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,104 @@
|
||||||
<AdminConfigAreas::ApiKeysList @apiKeys={{this.model}} />
|
<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}}
|
|
@ -1 +1,132 @@
|
||||||
<AdminConfigAreas::ApiKeysNew />
|
<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>
|
|
@ -1 +1,159 @@
|
||||||
<AdminConfigAreas::ApiKeysShow @apiKey={{this.model}} />
|
<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>
|
|
@ -1,21 +1,3 @@
|
||||||
<div class="admin-api-keys admin-config-page">
|
<PluginOutlet @name="admin-api-keys">
|
||||||
<DPageHeader
|
{{outlet}}
|
||||||
@titleLabel={{i18n "admin.api_keys.title"}}
|
</PluginOutlet>
|
||||||
@descriptionLabel={{i18n "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>
|
|
||||||
</DPageHeader>
|
|
||||||
|
|
||||||
<div class="admin-container admin-config-page__main-area">
|
|
||||||
{{outlet}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
8
app/assets/javascripts/admin/addon/templates/api.hbs
Normal file
8
app/assets/javascripts/admin/addon/templates/api.hbs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<AdminNav>
|
||||||
|
<NavItem @route="adminApiKeys" @label="admin.api.title" />
|
||||||
|
<NavItem @route="adminWebHooks" @label="admin.web_hooks.title" />
|
||||||
|
</AdminNav>
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
|
{{outlet}}
|
||||||
|
</div>
|
|
@ -96,14 +96,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success badge
|
// Success badge
|
||||||
.status-label.--success {
|
.status-label.--success {
|
||||||
background-color: var(--success-low);
|
background-color: var(--success-low);
|
||||||
|
|
|
@ -14,7 +14,6 @@ class Admin::ApiController < Admin::AdminController
|
||||||
ApiKey
|
ApiKey
|
||||||
.where(hidden: false)
|
.where(hidden: false)
|
||||||
.includes(:user)
|
.includes(:user)
|
||||||
.includes(:created_by)
|
|
||||||
.order("revoked_at DESC NULLS FIRST, created_at DESC")
|
.order("revoked_at DESC NULLS FIRST, created_at DESC")
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|
|
@ -11,7 +11,6 @@ class ApiKeySerializer < ApplicationSerializer
|
||||||
:revoked_at
|
:revoked_at
|
||||||
|
|
||||||
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
||||||
has_one :created_by, serializer: BasicUserSerializer, embed: :objects
|
|
||||||
has_many :api_key_scopes, serializer: ApiKeyScopeSerializer, embed: :objects
|
has_many :api_key_scopes, serializer: ApiKeyScopeSerializer, embed: :objects
|
||||||
|
|
||||||
def include_user_id?
|
def include_user_id?
|
||||||
|
|
|
@ -4,5 +4,4 @@ class BasicApiKeySerializer < ApplicationSerializer
|
||||||
attributes :id, :truncated_key, :description, :created_at, :last_used_at, :revoked_at
|
attributes :id, :truncated_key, :description, :created_at, :last_used_at, :revoked_at
|
||||||
|
|
||||||
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
||||||
has_one :created_by, serializer: BasicUserSerializer, embed: :objects
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5396,20 +5396,6 @@ en:
|
||||||
none_selected: "Select a group to get started"
|
none_selected: "Select a group to get started"
|
||||||
no_custom_groups: "Create a new custom group"
|
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"
|
|
||||||
delete: Permanently delete
|
|
||||||
no_api_keys: "You don't have any API keys yet."
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
generate_master: "Generate Master API Key"
|
generate_master: "Generate Master API Key"
|
||||||
none: "There are no active API keys right now."
|
none: "There are no active API keys right now."
|
||||||
|
@ -5417,30 +5403,30 @@ en:
|
||||||
title: "API"
|
title: "API"
|
||||||
key: "Key"
|
key: "Key"
|
||||||
keys: "Keys"
|
keys: "Keys"
|
||||||
created: Created by
|
created: Created
|
||||||
updated: Updated
|
updated: Updated
|
||||||
last_used: Last used
|
last_used: Last Used
|
||||||
never_used: Never
|
never_used: (never)
|
||||||
generate: "Generate"
|
generate: "Generate"
|
||||||
undo_revoke: "Undo revoke"
|
undo_revoke: "Undo Revoke"
|
||||||
revoke: "Revoke"
|
revoke: "Revoke"
|
||||||
all_users: "All users"
|
all_users: "All Users"
|
||||||
active_keys: "Active API Keys"
|
active_keys: "Active API Keys"
|
||||||
manage_keys: Manage Keys
|
manage_keys: Manage Keys
|
||||||
show_details: Details
|
show_details: Details
|
||||||
description: Description
|
description: Description
|
||||||
no_description: (no description)
|
no_description: (no description)
|
||||||
all_api_keys: All API Keys
|
all_api_keys: All API Keys
|
||||||
user_mode: User level
|
user_mode: User Level
|
||||||
scope_mode: Scope
|
scope_mode: Scope
|
||||||
impersonate_all_users: Impersonate any user
|
impersonate_all_users: Impersonate any user
|
||||||
single_user: "Single user"
|
single_user: "Single User"
|
||||||
user_placeholder: Enter username
|
user_placeholder: Enter username
|
||||||
description_placeholder: What will this key be used for?
|
description_placeholder: What will this key be used for?
|
||||||
save: Save
|
save: Save
|
||||||
new_key: New API Key
|
new_key: New API Key
|
||||||
revoked: Revoked
|
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.
|
not_shown_again: This key will not be displayed again. Make sure you take a copy before continuing.
|
||||||
continue: Continue
|
continue: Continue
|
||||||
scopes:
|
scopes:
|
||||||
|
@ -5454,8 +5440,8 @@ en:
|
||||||
global_description: API key has no restriction and all endpoints are accessible.
|
global_description: API key has no restriction and all endpoints are accessible.
|
||||||
resource: Resource
|
resource: Resource
|
||||||
action: Action
|
action: Action
|
||||||
allowed_parameters: Allowed parameters
|
allowed_parameters: Allowed Parameters
|
||||||
optional_allowed_parameters: Allowed parameters (optional)
|
optional_allowed_parameters: Allowed Parameters (optional)
|
||||||
any_parameter: (any parameter)
|
any_parameter: (any parameter)
|
||||||
allowed_urls: Allowed URLs
|
allowed_urls: Allowed URLs
|
||||||
descriptions:
|
descriptions:
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
#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
|
|
||||||
|
|
||||||
it "can edit existing API keys" do
|
|
||||||
api_keys_page.visit_page
|
|
||||||
api_keys_page.click_edit("Integration")
|
|
||||||
api_keys_page.edit_description("Old Integration")
|
|
||||||
api_keys_page.click_back
|
|
||||||
|
|
||||||
expect(api_keys_page).to have_api_key_listed("Old Integration")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "can revoke API keys" do
|
|
||||||
api_keys_page.visit_page
|
|
||||||
api_keys_page.click_edit("Integration")
|
|
||||||
api_keys_page.click_revoke
|
|
||||||
api_keys_page.click_back
|
|
||||||
|
|
||||||
expect(api_keys_page).to have_revoked_api_key_listed("Integration")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "can undo revokation of API keys" do
|
|
||||||
api_keys_page.visit_page
|
|
||||||
api_keys_page.click_edit("Integration")
|
|
||||||
api_keys_page.click_revoke
|
|
||||||
api_keys_page.click_unrevoke
|
|
||||||
api_keys_page.click_back
|
|
||||||
|
|
||||||
expect(api_keys_page).to have_unrevoked_api_key_listed("Integration")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "can permanently delete revoked API keys" do
|
|
||||||
api_keys_page.visit_page
|
|
||||||
api_keys_page.click_edit("Integration")
|
|
||||||
api_keys_page.click_revoke
|
|
||||||
api_keys_page.click_delete
|
|
||||||
|
|
||||||
expect(api_keys_page).to have_current_path("/admin/api/keys")
|
|
||||||
expect(api_keys_page).to have_no_api_key_listed("Integration")
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,87 +0,0 @@
|
||||||
# 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_revoked_api_key_listed?(name)
|
|
||||||
row = page.find(table_selector, text: name)
|
|
||||||
row.has_css?(badge_selector, text: I18n.t("admin_js.admin.api_keys.revoked"))
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_unrevoked_api_key_listed?(name)
|
|
||||||
row = page.find(table_selector, text: name)
|
|
||||||
row.has_no_css?(badge_selector, text: I18n.t("admin_js.admin.api_keys.revoked"))
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_no_api_key_listed?(name)
|
|
||||||
page.has_no_css?(table_selector, text: name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_api_key(description:)
|
|
||||||
page.find(header_actions_selector, text: I18n.t("admin_js.admin.api_keys.add")).click
|
|
||||||
|
|
||||||
form = page.find(".form-kit")
|
|
||||||
form.find(description_field_selector).fill_in(with: description)
|
|
||||||
form.find(".save").click
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_edit(description)
|
|
||||||
row = page.find(row_selector, text: description)
|
|
||||||
row.find("button", text: I18n.t("admin_js.admin.api_keys.edit")).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_revoke
|
|
||||||
page.find("button", text: I18n.t("admin_js.admin.api_keys.revoke")).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_unrevoke
|
|
||||||
page.find("button", text: I18n.t("admin_js.admin.api_keys.undo_revoke")).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_delete
|
|
||||||
page.find("button", text: I18n.t("admin_js.admin.api_keys.delete")).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit_description(new_description)
|
|
||||||
page.find("button", text: I18n.t("admin_js.admin.api_keys.edit")).click
|
|
||||||
page.find(description_field_selector).fill_in(with: new_description)
|
|
||||||
page.find("button", text: I18n.t("admin_js.admin.api_keys.save")).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_back
|
|
||||||
page.find("a.back-button").click
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def table_selector
|
|
||||||
".admin-api_keys__items"
|
|
||||||
end
|
|
||||||
|
|
||||||
def row_selector
|
|
||||||
".d-admin-row__content"
|
|
||||||
end
|
|
||||||
|
|
||||||
def badge_selector
|
|
||||||
".d-admin-table__badge"
|
|
||||||
end
|
|
||||||
|
|
||||||
def header_actions_selector
|
|
||||||
".d-page-header__actions"
|
|
||||||
end
|
|
||||||
|
|
||||||
def description_field_selector
|
|
||||||
"input[name='description']"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue
Block a user