mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
✨ Added api keys get/create/delete actions.
This commit is contained in:
parent
88ec0b5313
commit
c0f3bd3c17
|
@ -833,6 +833,9 @@ export interface ISettingsState {
|
|||
promptsData: IN8nPrompts;
|
||||
userManagement: IUserManagementConfig;
|
||||
templatesEndpointHealthy: boolean;
|
||||
api: {
|
||||
key: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITemplateState {
|
||||
|
|
|
@ -6,7 +6,7 @@ export function getApiKey(context: IRestApiContext): Promise<{ apiKey: string |
|
|||
}
|
||||
|
||||
export function createApiKey(context: IRestApiContext): Promise<{ apiKey: string | null }> {
|
||||
return makeRestApiRequest(context, 'POST', '/users/me/api-key');
|
||||
return makeRestApiRequest(context, 'POST', '/me/api-key');
|
||||
}
|
||||
|
||||
export function deleteApiKey(context: IRestApiContext): Promise<{ success: boolean }> {
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div>
|
||||
<n8n-input-label :label="label">
|
||||
<div :class="$style.copyText" @click="copy">
|
||||
<span>{{ copyContent }}</span>
|
||||
<div :class="$style.copyButton"><span>{{ copyButtonText }}</span></div>
|
||||
<span>{{ value }}</span>
|
||||
<div :class="$style.copyButton"><span>{{ copyButtonText || $locale.baseText('generic.copyToClipboard') }}</span></div>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
<div :class="$style.subtitle">{{ subtitle }}</div>
|
||||
<div v-if="hint" :class="$style.hint">{{ hint }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -20,26 +20,29 @@ export default mixins(copyPaste, showMessage).extend({
|
|||
label: {
|
||||
type: String,
|
||||
},
|
||||
subtitle: {
|
||||
hint: {
|
||||
type: String,
|
||||
},
|
||||
copyContent: {
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
copyButtonText: {
|
||||
type: String,
|
||||
},
|
||||
successMessage: {
|
||||
toastTitle: {
|
||||
type: String,
|
||||
},
|
||||
toastMessage: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copy(): void {
|
||||
this.copyToClipboard(this.$props.copyContent);
|
||||
this.copyToClipboard(this.value);
|
||||
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('credentialEdit.credentialEdit.showMessage.title'),
|
||||
message: this.$props.successMessage,
|
||||
title: this.toastTitle || this.$locale.baseText('generic.copiedToClipboard'),
|
||||
message: this.toastMessage,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
@ -54,6 +57,10 @@ export default mixins(copyPaste, showMessage).extend({
|
|||
font-family: Monaco, Consolas;
|
||||
line-height: 1.5;
|
||||
font-size: var(--font-size-s);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
padding: var(--spacing-xs);
|
||||
|
@ -86,7 +93,7 @@ export default mixins(copyPaste, showMessage).extend({
|
|||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
.hint {
|
||||
margin-top: var(--spacing-2xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: var(--font-line-height-loose);
|
||||
|
|
|
@ -48,10 +48,11 @@
|
|||
<CopyInput
|
||||
v-if="isOAuthType && credentialProperties.length"
|
||||
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
|
||||
:copyContent="oAuthCallbackUrl"
|
||||
:value="oAuthCallbackUrl"
|
||||
:copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
|
||||
:subtitle="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })"
|
||||
:successMessage="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')"
|
||||
:hint="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })"
|
||||
:toastTitle="$locale.baseText('credentialEdit.credentialEdit.showMessage.title')"
|
||||
:toastMessage="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')"
|
||||
/>
|
||||
|
||||
<CredentialInputs
|
||||
|
|
84
packages/editor-ui/src/components/DeleteApiKeyModal.vue
Normal file
84
packages/editor-ui/src/components/DeleteApiKeyModal.vue
Normal file
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="name"
|
||||
:title="$locale.baseText('settings.api.delete.title')"
|
||||
:center="true"
|
||||
:eventBus="modalBus"
|
||||
width="460px"
|
||||
@enter="deleteApiKey"
|
||||
>
|
||||
<template slot="content">
|
||||
<div>
|
||||
<n8n-text tag="p" color="text-base">
|
||||
{{$locale.baseText('settings.api.delete.description')}}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button type="outline" @click="cancel">
|
||||
{{ $locale.baseText('generic.cancel') }}
|
||||
</n8n-button>
|
||||
<n8n-button :loading="loading" @click="deleteApiKey">
|
||||
{{ $locale.baseText('settings.api.delete.button') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import mixins from "vue-typed-mixins";
|
||||
|
||||
import Modal from "./Modal.vue";
|
||||
import { N8nUserSelect } from 'n8n-design-system';
|
||||
import { showMessage } from "../components/mixins/showMessage";
|
||||
import {DELETE_API_KEY_MODAL_KEY} from "../constants";
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
components: {
|
||||
Modal,
|
||||
N8nUserSelect,
|
||||
},
|
||||
name: "DeleteApiKeyModal",
|
||||
data() {
|
||||
return {
|
||||
modalBus: new Vue(),
|
||||
name: DELETE_API_KEY_MODAL_KEY,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$store.dispatch('ui/closeModal', DELETE_API_KEY_MODAL_KEY);
|
||||
},
|
||||
async deleteApiKey() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
this.$store.dispatch('settings/deleteApiKey');
|
||||
this.$store.dispatch('ui/closeModal', DELETE_API_KEY_MODAL_KEY);
|
||||
this.$showMessage({ title: this.$locale.baseText("settings.api.delete.toast"), type: 'success' });
|
||||
} catch (error) {
|
||||
this.$showError(error, this.$locale.baseText('settings.api.delete.error'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button + button {
|
||||
margin-left: var(--spacing-xs)
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -88,6 +88,10 @@
|
|||
<ModalRoot :name="WORKFLOW_ACTIVE_MODAL_KEY">
|
||||
<ActivationModal />
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="DELETE_API_KEY_MODAL_KEY">
|
||||
<DeleteApiKeyModal />
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -95,6 +99,7 @@
|
|||
import Vue from "vue";
|
||||
import {
|
||||
ABOUT_MODAL_KEY,
|
||||
DELETE_API_KEY_MODAL_KEY,
|
||||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
|
@ -120,6 +125,7 @@ import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
|||
import CredentialsList from "./CredentialsList.vue";
|
||||
import InviteUsersModal from "./InviteUsersModal.vue";
|
||||
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
||||
import DeleteApiKeyModal from "./DeleteApiKeyModal.vue";
|
||||
import DuplicateWorkflowDialog from "./DuplicateWorkflowDialog.vue";
|
||||
import ModalRoot from "./ModalRoot.vue";
|
||||
import PersonalizationModal from "./PersonalizationModal.vue";
|
||||
|
@ -142,6 +148,7 @@ export default Vue.extend({
|
|||
CredentialEdit,
|
||||
CredentialsList,
|
||||
CredentialsSelectModal,
|
||||
DeleteApiKeyModal,
|
||||
DeleteUserModal,
|
||||
DuplicateWorkflowDialog,
|
||||
InviteUsersModal,
|
||||
|
@ -155,6 +162,7 @@ export default Vue.extend({
|
|||
WorkflowOpen,
|
||||
},
|
||||
data: () => ({
|
||||
DELETE_API_KEY_MODAL_KEY,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_LIST_MODAL_KEY,
|
||||
|
|
|
@ -22,6 +22,7 @@ export const CHANGE_PASSWORD_MODAL_KEY = 'changePassword';
|
|||
export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
|
||||
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
|
||||
export const DELETE_USER_MODAL_KEY = 'deleteUser';
|
||||
export const DELETE_API_KEY_MODAL_KEY = 'deleteApiKey';
|
||||
export const INVITE_USER_MODAL_KEY = 'inviteUser';
|
||||
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||
|
|
|
@ -12,6 +12,7 @@ import Vue from 'vue';
|
|||
import { CONTACT_PROMPT_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||
import { ITelemetrySettings } from 'n8n-workflow';
|
||||
import { testHealthEndpoint } from '@/api/templates';
|
||||
import {createApiKey, deleteApiKey, getApiKey} from "@/api/api-keys";
|
||||
|
||||
const module: Module<ISettingsState, IRootState> = {
|
||||
namespaced: true,
|
||||
|
@ -24,6 +25,9 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
smtpSetup: false,
|
||||
},
|
||||
templatesEndpointHealthy: false,
|
||||
api: {
|
||||
key: undefined,
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
versionCli(state: ISettingsState) {
|
||||
|
@ -68,6 +72,9 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
templatesHost: (state): string => {
|
||||
return state.settings.templates.host;
|
||||
},
|
||||
apiKey: (state): string | undefined => {
|
||||
return state.api.key;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setSettings(state: ISettingsState, settings: IN8nUISettings) {
|
||||
|
@ -85,6 +92,9 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
setTemplatesEndpointHealthy(state: ISettingsState) {
|
||||
state.templatesEndpointHealthy = true;
|
||||
},
|
||||
setApiKey(state: ISettingsState, apiKey: string | undefined) {
|
||||
state.api.key = apiKey;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getSettings(context: ActionContext<ISettingsState, IRootState>) {
|
||||
|
@ -153,6 +163,18 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
await Promise.race([testHealthEndpoint(context.getters.templatesHost), timeout]);
|
||||
context.commit('setTemplatesEndpointHealthy', true);
|
||||
},
|
||||
async getApiKey(context: ActionContext<ISettingsState, IRootState>) {
|
||||
const { apiKey } = await getApiKey(context.rootGetters['getRestApiContext']);
|
||||
context.commit('setApiKey', apiKey);
|
||||
},
|
||||
async createApiKey(context: ActionContext<ISettingsState, IRootState>) {
|
||||
const { apiKey } = await createApiKey(context.rootGetters['getRestApiContext']);
|
||||
context.commit('setApiKey', apiKey);
|
||||
},
|
||||
async deleteApiKey(context: ActionContext<ISettingsState, IRootState>) {
|
||||
await deleteApiKey(context.rootGetters['getRestApiContext']);
|
||||
context.commit('setApiKey', undefined);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
ABOUT_MODAL_KEY,
|
||||
DELETE_API_KEY_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
|
@ -32,6 +33,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
[ABOUT_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[DELETE_API_KEY_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[CHANGE_PASSWORD_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
|
@ -145,6 +149,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
closeModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
||||
context.commit('closeModal', modalKey);
|
||||
},
|
||||
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
||||
context.commit('openModal', modalKey);
|
||||
},
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
"_reusableBaseText.save": "Save",
|
||||
"_reusableDynamicText.oauth2.clientId": "Client ID",
|
||||
"_reusableDynamicText.oauth2.clientSecret": "Client Secret",
|
||||
"generic.learnMore": "Learn more",
|
||||
"generic.confirm": "Confirm",
|
||||
"generic.cancel": "Cancel",
|
||||
"generic.delete": "Delete",
|
||||
"generic.copyToClipboard": "Copy to clipboard",
|
||||
"generic.copiedToClipboard": "Copied to clipboard",
|
||||
"about.aboutN8n": "About n8n",
|
||||
"about.close": "Close",
|
||||
"about.license": "License",
|
||||
|
@ -653,12 +659,16 @@
|
|||
"settings.api.create.description.link": "REST API",
|
||||
"settings.api.create.button": "Create an API Key",
|
||||
"settings.api.create.button.loading": "Creating API Key...",
|
||||
"settings.api.error.title": "Something went wrong",
|
||||
"settings.api.error.get": "Could not check if an api key already exists.",
|
||||
"settings.api.error.create": "Creating the API Key failed.",
|
||||
"settings.api.error.delete": "Deleting the API Key failed.",
|
||||
"settings.api.create.error": "Creating the API Key failed.",
|
||||
"settings.api.delete.title": "Delete this API Key?",
|
||||
"settings.api.delete.description": "Any application using this API Key will no longer be granted access to n8n data. This operation cannot be undone.",
|
||||
"settings.api.delete.button": "Delete Forever",
|
||||
"settings.api.delete.error": "Deleting the API Key failed.",
|
||||
"settings.api.delete.toast": "API Key deleted",
|
||||
"settings.api.view.info": "Use your API Key to access n8n's REST API to build your own integrations.",
|
||||
"settings.api.view.myKey": "My API Key",
|
||||
"settings.api.view.copy.toast": "API Key copied to clipboard",
|
||||
"settings.api.view.error": "Could not check if an api key already exists.",
|
||||
"settings.version": "Version",
|
||||
"showMessage.cancel": "@:_reusableBaseText.cancel",
|
||||
"showMessage.ok": "OK",
|
||||
|
|
|
@ -6,18 +6,41 @@
|
|||
{{ $locale.baseText('settings.api') }}
|
||||
</n8n-heading>
|
||||
</div>
|
||||
<n8n-card v-if="apiKey">
|
||||
Hello
|
||||
</n8n-card>
|
||||
|
||||
<div v-if="apiKey">
|
||||
<p class="mb-s">
|
||||
<n8n-text color="text-base">
|
||||
<font-awesome-icon icon="info-circle" />
|
||||
{{ $locale.baseText('settings.api.view.info') }}
|
||||
<n8n-link to="https://docs.n8n.io/api/">
|
||||
{{ $locale.baseText('generic.learnMore') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</p>
|
||||
<n8n-card :class="$style.card">
|
||||
<span :class="$style.delete">
|
||||
<n8n-link @click="showDeleteModal">
|
||||
{{ $locale.baseText('generic.delete') }}
|
||||
</n8n-link>
|
||||
</span>
|
||||
<CopyInput
|
||||
:label="$locale.baseText('settings.api.view.myKey')"
|
||||
:value="apiKey"
|
||||
:toast-title="$locale.baseText('settings.api.view.copy.toast')"
|
||||
/>
|
||||
</n8n-card>
|
||||
</div>
|
||||
<div :class="$style.placeholder" v-else>
|
||||
<n8n-heading size="xlarge">
|
||||
{{ $locale.baseText('settings.api.create.title') }}
|
||||
</n8n-heading>
|
||||
<p class="mt-2xs mb-l">
|
||||
{{$locale.baseText('settings.api.create.description')}}
|
||||
<a href="https://docs.n8n.io/reference/glossary/#rest-api" target="_blank">
|
||||
{{$locale.baseText('settings.api.create.description.link')}}
|
||||
</a>
|
||||
<n8n-text color="text-base">
|
||||
{{$locale.baseText('settings.api.create.description')}}
|
||||
<n8n-link to="https://docs.n8n.io/api/">
|
||||
{{$locale.baseText('settings.api.create.description.link')}}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</p>
|
||||
<n8n-button :loading="loading" size="large" class="mt-l" @click="createApiKey">
|
||||
{{$locale.baseText(loading ? 'settings.api.create.button.loading' : 'settings.api.create.button')}}
|
||||
|
@ -34,6 +57,8 @@ import { IUser } from '@/Interface';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import SettingsView from './SettingsView.vue';
|
||||
import CopyInput from '../components/CopyInput.vue';
|
||||
import {DELETE_API_KEY_MODAL_KEY} from "../constants";
|
||||
|
||||
export default mixins(
|
||||
showMessage,
|
||||
|
@ -41,30 +66,35 @@ export default mixins(
|
|||
name: 'SettingsPersonalView',
|
||||
components: {
|
||||
SettingsView,
|
||||
CopyInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
apiKey: '' as string | null,
|
||||
loading: false,
|
||||
mounted: false,
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
mounted() {
|
||||
this.getApiKey();
|
||||
},
|
||||
computed: {
|
||||
currentUser() {
|
||||
return this.$store.getters['users/currentUser'] as IUser;
|
||||
},
|
||||
apiKey() {
|
||||
return this.$store.getters['settings/apiKey'];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showDeleteModal() {
|
||||
this.$store.dispatch('ui/openModal', DELETE_API_KEY_MODAL_KEY);
|
||||
},
|
||||
async getApiKey() {
|
||||
try {
|
||||
const { apiKey } = await getApiKey(this.$store.getters['getRestApiContext']);
|
||||
this.apiKey = apiKey;
|
||||
this.$store.dispatch('settings/getApiKey');
|
||||
} catch (error) {
|
||||
this.$showError(error, this.$locale.baseText('settings.api.error.get'));
|
||||
this.$showError(error, this.$locale.baseText('settings.api.view.error'));
|
||||
} finally {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
@ -73,23 +103,13 @@ export default mixins(
|
|||
this.loading = true;
|
||||
|
||||
try {
|
||||
const { apiKey } = await createApiKey(this.$store.getters['getRestApiContext']);
|
||||
this.apiKey = apiKey;
|
||||
this.$store.dispatch('settings/createApiKey');
|
||||
} catch (error) {
|
||||
this.$showError(error, this.$locale.baseText('settings.api.error.create'));
|
||||
this.$showError(error, this.$locale.baseText('settings.api.create.error'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async deleteApiKey() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
await deleteApiKey(this.$store.getters['getRestApiContext']);
|
||||
} catch (error) {
|
||||
this.$showError(error, this.$locale.baseText('settings.api.error.delete'));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -120,5 +140,16 @@ export default mixins(
|
|||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.delete {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
top: var(--spacing-s);
|
||||
right: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
Loading…
Reference in a new issue