Added api keys get/create/delete actions.

This commit is contained in:
Alex Grozav 2022-04-28 16:57:49 +03:00
parent 88ec0b5313
commit c0f3bd3c17
11 changed files with 216 additions and 42 deletions

View file

@ -833,6 +833,9 @@ export interface ISettingsState {
promptsData: IN8nPrompts; promptsData: IN8nPrompts;
userManagement: IUserManagementConfig; userManagement: IUserManagementConfig;
templatesEndpointHealthy: boolean; templatesEndpointHealthy: boolean;
api: {
key: string | undefined;
};
} }
export interface ITemplateState { export interface ITemplateState {

View file

@ -6,7 +6,7 @@ export function getApiKey(context: IRestApiContext): Promise<{ apiKey: string |
} }
export function createApiKey(context: IRestApiContext): Promise<{ apiKey: string | null }> { 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 }> { export function deleteApiKey(context: IRestApiContext): Promise<{ success: boolean }> {

View file

@ -2,11 +2,11 @@
<div> <div>
<n8n-input-label :label="label"> <n8n-input-label :label="label">
<div :class="$style.copyText" @click="copy"> <div :class="$style.copyText" @click="copy">
<span>{{ copyContent }}</span> <span>{{ value }}</span>
<div :class="$style.copyButton"><span>{{ copyButtonText }}</span></div> <div :class="$style.copyButton"><span>{{ copyButtonText || $locale.baseText('generic.copyToClipboard') }}</span></div>
</div> </div>
</n8n-input-label> </n8n-input-label>
<div :class="$style.subtitle">{{ subtitle }}</div> <div v-if="hint" :class="$style.hint">{{ hint }}</div>
</div> </div>
</template> </template>
@ -20,26 +20,29 @@ export default mixins(copyPaste, showMessage).extend({
label: { label: {
type: String, type: String,
}, },
subtitle: { hint: {
type: String, type: String,
}, },
copyContent: { value: {
type: String, type: String,
}, },
copyButtonText: { copyButtonText: {
type: String, type: String,
}, },
successMessage: { toastTitle: {
type: String,
},
toastMessage: {
type: String, type: String,
}, },
}, },
methods: { methods: {
copy(): void { copy(): void {
this.copyToClipboard(this.$props.copyContent); this.copyToClipboard(this.value);
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('credentialEdit.credentialEdit.showMessage.title'), title: this.toastTitle || this.$locale.baseText('generic.copiedToClipboard'),
message: this.$props.successMessage, message: this.toastMessage,
type: 'success', type: 'success',
}); });
}, },
@ -54,6 +57,10 @@ export default mixins(copyPaste, showMessage).extend({
font-family: Monaco, Consolas; font-family: Monaco, Consolas;
line-height: 1.5; line-height: 1.5;
font-size: var(--font-size-s); font-size: var(--font-size-s);
overflow: hidden;
width: 100%;
display: block;
text-overflow: ellipsis;
} }
padding: var(--spacing-xs); padding: var(--spacing-xs);
@ -86,7 +93,7 @@ export default mixins(copyPaste, showMessage).extend({
} }
} }
.subtitle { .hint {
margin-top: var(--spacing-2xs); margin-top: var(--spacing-2xs);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
line-height: var(--font-line-height-loose); line-height: var(--font-line-height-loose);

View file

@ -48,10 +48,11 @@
<CopyInput <CopyInput
v-if="isOAuthType && credentialProperties.length" v-if="isOAuthType && credentialProperties.length"
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')" :label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
:copyContent="oAuthCallbackUrl" :value="oAuthCallbackUrl"
:copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')" :copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
:subtitle="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })" :hint="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })"
:successMessage="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')" :toastTitle="$locale.baseText('credentialEdit.credentialEdit.showMessage.title')"
:toastMessage="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')"
/> />
<CredentialInputs <CredentialInputs

View 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>

View file

@ -88,6 +88,10 @@
<ModalRoot :name="WORKFLOW_ACTIVE_MODAL_KEY"> <ModalRoot :name="WORKFLOW_ACTIVE_MODAL_KEY">
<ActivationModal /> <ActivationModal />
</ModalRoot> </ModalRoot>
<ModalRoot :name="DELETE_API_KEY_MODAL_KEY">
<DeleteApiKeyModal />
</ModalRoot>
</div> </div>
</template> </template>
@ -95,6 +99,7 @@
import Vue from "vue"; import Vue from "vue";
import { import {
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
DELETE_API_KEY_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY,
CONTACT_PROMPT_MODAL_KEY, CONTACT_PROMPT_MODAL_KEY,
CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY,
@ -120,6 +125,7 @@ import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
import CredentialsList from "./CredentialsList.vue"; import CredentialsList from "./CredentialsList.vue";
import InviteUsersModal from "./InviteUsersModal.vue"; import InviteUsersModal from "./InviteUsersModal.vue";
import CredentialsSelectModal from "./CredentialsSelectModal.vue"; import CredentialsSelectModal from "./CredentialsSelectModal.vue";
import DeleteApiKeyModal from "./DeleteApiKeyModal.vue";
import DuplicateWorkflowDialog from "./DuplicateWorkflowDialog.vue"; import DuplicateWorkflowDialog from "./DuplicateWorkflowDialog.vue";
import ModalRoot from "./ModalRoot.vue"; import ModalRoot from "./ModalRoot.vue";
import PersonalizationModal from "./PersonalizationModal.vue"; import PersonalizationModal from "./PersonalizationModal.vue";
@ -142,6 +148,7 @@ export default Vue.extend({
CredentialEdit, CredentialEdit,
CredentialsList, CredentialsList,
CredentialsSelectModal, CredentialsSelectModal,
DeleteApiKeyModal,
DeleteUserModal, DeleteUserModal,
DuplicateWorkflowDialog, DuplicateWorkflowDialog,
InviteUsersModal, InviteUsersModal,
@ -155,6 +162,7 @@ export default Vue.extend({
WorkflowOpen, WorkflowOpen,
}, },
data: () => ({ data: () => ({
DELETE_API_KEY_MODAL_KEY,
CONTACT_PROMPT_MODAL_KEY, CONTACT_PROMPT_MODAL_KEY,
CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY,
CREDENTIAL_LIST_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY,

View file

@ -22,6 +22,7 @@ export const CHANGE_PASSWORD_MODAL_KEY = 'changePassword';
export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential'; export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential'; export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
export const DELETE_USER_MODAL_KEY = 'deleteUser'; export const DELETE_USER_MODAL_KEY = 'deleteUser';
export const DELETE_API_KEY_MODAL_KEY = 'deleteApiKey';
export const INVITE_USER_MODAL_KEY = 'inviteUser'; export const INVITE_USER_MODAL_KEY = 'inviteUser';
export const DUPLICATE_MODAL_KEY = 'duplicate'; export const DUPLICATE_MODAL_KEY = 'duplicate';
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager'; export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';

View file

@ -12,6 +12,7 @@ import Vue from 'vue';
import { CONTACT_PROMPT_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants'; import { CONTACT_PROMPT_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
import { ITelemetrySettings } from 'n8n-workflow'; import { ITelemetrySettings } from 'n8n-workflow';
import { testHealthEndpoint } from '@/api/templates'; import { testHealthEndpoint } from '@/api/templates';
import {createApiKey, deleteApiKey, getApiKey} from "@/api/api-keys";
const module: Module<ISettingsState, IRootState> = { const module: Module<ISettingsState, IRootState> = {
namespaced: true, namespaced: true,
@ -24,6 +25,9 @@ const module: Module<ISettingsState, IRootState> = {
smtpSetup: false, smtpSetup: false,
}, },
templatesEndpointHealthy: false, templatesEndpointHealthy: false,
api: {
key: undefined,
},
}, },
getters: { getters: {
versionCli(state: ISettingsState) { versionCli(state: ISettingsState) {
@ -68,6 +72,9 @@ const module: Module<ISettingsState, IRootState> = {
templatesHost: (state): string => { templatesHost: (state): string => {
return state.settings.templates.host; return state.settings.templates.host;
}, },
apiKey: (state): string | undefined => {
return state.api.key;
},
}, },
mutations: { mutations: {
setSettings(state: ISettingsState, settings: IN8nUISettings) { setSettings(state: ISettingsState, settings: IN8nUISettings) {
@ -85,6 +92,9 @@ const module: Module<ISettingsState, IRootState> = {
setTemplatesEndpointHealthy(state: ISettingsState) { setTemplatesEndpointHealthy(state: ISettingsState) {
state.templatesEndpointHealthy = true; state.templatesEndpointHealthy = true;
}, },
setApiKey(state: ISettingsState, apiKey: string | undefined) {
state.api.key = apiKey;
},
}, },
actions: { actions: {
async getSettings(context: ActionContext<ISettingsState, IRootState>) { async getSettings(context: ActionContext<ISettingsState, IRootState>) {
@ -153,6 +163,18 @@ const module: Module<ISettingsState, IRootState> = {
await Promise.race([testHealthEndpoint(context.getters.templatesHost), timeout]); await Promise.race([testHealthEndpoint(context.getters.templatesHost), timeout]);
context.commit('setTemplatesEndpointHealthy', true); 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);
},
}, },
}; };

View file

@ -1,5 +1,6 @@
import { import {
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
DELETE_API_KEY_MODAL_KEY,
CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY,
CREDENTIAL_SELECT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY,
@ -32,6 +33,9 @@ const module: Module<IUiState, IRootState> = {
[ABOUT_MODAL_KEY]: { [ABOUT_MODAL_KEY]: {
open: false, open: false,
}, },
[DELETE_API_KEY_MODAL_KEY]: {
open: false,
},
[CHANGE_PASSWORD_MODAL_KEY]: { [CHANGE_PASSWORD_MODAL_KEY]: {
open: false, open: false,
}, },
@ -145,6 +149,9 @@ const module: Module<IUiState, IRootState> = {
}, },
}, },
actions: { actions: {
closeModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
context.commit('closeModal', modalKey);
},
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => { openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
context.commit('openModal', modalKey); context.commit('openModal', modalKey);
}, },

View file

@ -4,6 +4,12 @@
"_reusableBaseText.save": "Save", "_reusableBaseText.save": "Save",
"_reusableDynamicText.oauth2.clientId": "Client ID", "_reusableDynamicText.oauth2.clientId": "Client ID",
"_reusableDynamicText.oauth2.clientSecret": "Client Secret", "_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.aboutN8n": "About n8n",
"about.close": "Close", "about.close": "Close",
"about.license": "License", "about.license": "License",
@ -653,12 +659,16 @@
"settings.api.create.description.link": "REST API", "settings.api.create.description.link": "REST API",
"settings.api.create.button": "Create an API Key", "settings.api.create.button": "Create an API Key",
"settings.api.create.button.loading": "Creating API Key...", "settings.api.create.button.loading": "Creating API Key...",
"settings.api.error.title": "Something went wrong", "settings.api.create.error": "Creating the API Key failed.",
"settings.api.error.get": "Could not check if an api key already exists.", "settings.api.delete.title": "Delete this API Key?",
"settings.api.error.create": "Creating the API Key failed.", "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.error.delete": "Deleting the API Key failed.", "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.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.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", "settings.version": "Version",
"showMessage.cancel": "@:_reusableBaseText.cancel", "showMessage.cancel": "@:_reusableBaseText.cancel",
"showMessage.ok": "OK", "showMessage.ok": "OK",

View file

@ -6,18 +6,41 @@
{{ $locale.baseText('settings.api') }} {{ $locale.baseText('settings.api') }}
</n8n-heading> </n8n-heading>
</div> </div>
<n8n-card v-if="apiKey">
Hello <div v-if="apiKey">
</n8n-card> <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> <div :class="$style.placeholder" v-else>
<n8n-heading size="xlarge"> <n8n-heading size="xlarge">
{{ $locale.baseText('settings.api.create.title') }} {{ $locale.baseText('settings.api.create.title') }}
</n8n-heading> </n8n-heading>
<p class="mt-2xs mb-l"> <p class="mt-2xs mb-l">
{{$locale.baseText('settings.api.create.description')}} <n8n-text color="text-base">
<a href="https://docs.n8n.io/reference/glossary/#rest-api" target="_blank"> {{$locale.baseText('settings.api.create.description')}}
{{$locale.baseText('settings.api.create.description.link')}} <n8n-link to="https://docs.n8n.io/api/">
</a> {{$locale.baseText('settings.api.create.description.link')}}
</n8n-link>
</n8n-text>
</p> </p>
<n8n-button :loading="loading" size="large" class="mt-l" @click="createApiKey"> <n8n-button :loading="loading" size="large" class="mt-l" @click="createApiKey">
{{$locale.baseText(loading ? 'settings.api.create.button.loading' : 'settings.api.create.button')}} {{$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 mixins from 'vue-typed-mixins';
import SettingsView from './SettingsView.vue'; import SettingsView from './SettingsView.vue';
import CopyInput from '../components/CopyInput.vue';
import {DELETE_API_KEY_MODAL_KEY} from "../constants";
export default mixins( export default mixins(
showMessage, showMessage,
@ -41,30 +66,35 @@ export default mixins(
name: 'SettingsPersonalView', name: 'SettingsPersonalView',
components: { components: {
SettingsView, SettingsView,
CopyInput,
}, },
data() { data() {
return { return {
apiKey: '' as string | null,
loading: false, loading: false,
mounted: false, mounted: false,
error: '', error: '',
}; };
}, },
async mounted() { mounted() {
this.getApiKey(); this.getApiKey();
}, },
computed: { computed: {
currentUser() { currentUser() {
return this.$store.getters['users/currentUser'] as IUser; return this.$store.getters['users/currentUser'] as IUser;
}, },
apiKey() {
return this.$store.getters['settings/apiKey'];
},
}, },
methods: { methods: {
showDeleteModal() {
this.$store.dispatch('ui/openModal', DELETE_API_KEY_MODAL_KEY);
},
async getApiKey() { async getApiKey() {
try { try {
const { apiKey } = await getApiKey(this.$store.getters['getRestApiContext']); this.$store.dispatch('settings/getApiKey');
this.apiKey = apiKey;
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('settings.api.error.get')); this.$showError(error, this.$locale.baseText('settings.api.view.error'));
} finally { } finally {
this.mounted = true; this.mounted = true;
} }
@ -73,23 +103,13 @@ export default mixins(
this.loading = true; this.loading = true;
try { try {
const { apiKey } = await createApiKey(this.$store.getters['getRestApiContext']); this.$store.dispatch('settings/createApiKey');
this.apiKey = apiKey;
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('settings.api.error.create')); this.$showError(error, this.$locale.baseText('settings.api.create.error'));
} finally { } finally {
this.loading = false; 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> </script>
@ -120,5 +140,16 @@ export default mixins(
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
} }
.card {
position: relative;
}
.delete {
position: absolute;
display: inline-block;
top: var(--spacing-s);
right: var(--spacing-s);
}
</style> </style>