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;
userManagement: IUserManagementConfig;
templatesEndpointHealthy: boolean;
api: {
key: string | undefined;
};
}
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 }> {
return makeRestApiRequest(context, 'POST', '/users/me/api-key');
return makeRestApiRequest(context, 'POST', '/me/api-key');
}
export function deleteApiKey(context: IRestApiContext): Promise<{ success: boolean }> {

View file

@ -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);

View file

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

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">
<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,

View file

@ -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';

View file

@ -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);
},
},
};

View file

@ -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);
},

View file

@ -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",

View file

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