<template> <div class="api-token"> <div class="api-token__head"> <div class="api-token__info"> <div class="api-token__name"> <div class="api-token__name-content"> <Editable ref="rename" :value="token.name" @change=" updateToken( token, { name: $event.value }, { name: $event.oldValue } ) " ></Editable> </div> <a ref="contextLink" class="api-token__more" @click.prevent=" $refs.context.toggle($refs.contextLink, 'bottom', 'right', 4) " > <i class="baserow-icon-more-horizontal"></i> </a> <Context ref="context" overflow-scroll max-height-if-outside-viewport> <div class="api-token__key"> <div class="api-token__key-name"> {{ $t('apiToken.tokenPrefix') }} </div> <div class="api-token__key-value"> <template v-if="tokenVisible"> {{ token.key }} </template> <template v-else>••••••••••••••••••••••••••••••••</template> </div> <a class="api-token__key-visible" :title="$t('apiToken.showOrHide')" @click.prevent="tokenVisible = !tokenVisible" > <i :class=" tokenVisible ? 'iconoir-eye-off' : 'iconoir-eye-empty' " ></i> </a> <a class="api-token__key-copy" :title="$t('apiToken.copyToClipboard')" @click=";[copyTokenToClipboard(), $refs.copied.show()]" > <i class="iconoir-copy"></i> <Copied ref="copied"></Copied> </a> </div> <ul class="context__menu"> <li class="context__menu-item"> <nuxt-link class="context__menu-item-link" :to="{ name: 'database-api-docs' }" > <i class="context__menu-item-icon iconoir-book"></i> {{ $t('apiToken.viewAPIDocs') }} </nuxt-link> </li> <li class="context__menu-item"> <a class="context__menu-item-link" :class="{ 'context__menu-item-link--loading': rotateLoading, }" @click="rotateKey(token)" > <i class="context__menu-item-icon iconoir-refresh-double"></i> {{ $t('apiToken.generateNewToken') }} </a> </li> <li class="context__menu-item"> <a class="context__menu-item-link" @click="enableRename()"> <i class="context__menu-item-icon iconoir-edit-pencil"></i> {{ $t('action.rename') }} </a> </li> <li class="context__menu-item"> <a :class="{ 'context__menu-item-link--loading': deleteLoading, }" class="context__menu-item-link" @click.prevent="deleteToken(token)" > <i class="context__menu-item-icon iconoir-bin"></i> {{ $t('action.delete') }} </a> </li> </ul> </Context> </div> <div class="api-token__details"> <div class="api-token__group">{{ workspace.name }}</div> <a class="api-token__expand" @click.prevent="open = !open"> {{ $t('apiToken.showDatabases') }} <i :class="{ 'iconoir-nav-arrow-down': !open, 'iconoir-nav-arrow-up': open, }" ></i> </a> </div> </div> <div class="api-token__permissions"> <div v-for="(operationName, operation) in operations" :key="operation" class="api-token__permission" > <span class="margin-bottom-1">{{ operationName }}</span> <SwitchInput :value="isActive(operation)" small @input="toggle(operation, $event)" ></SwitchInput> </div> </div> </div> <div class="api-token__body" :class="{ 'api-token__body--open': open }"> <div v-for="database in databases" :key="database.id"> <div class="api-token__row"> <div class="api-token__database"> {{ database.name }} {{ database.id }} </div> <div class="api-token__permissions"> <div v-for="(operationName, operation) in operations" :key="operation" class="api-token__permission" > <SwitchInput :value="isDatabaseActive(database, operation)" small @input="toggleDatabase(database, databases, operation, $event)" ></SwitchInput> </div> </div> </div> <div v-for="table in database.tables" :key="table.id" class="api-token__row" > <div class="api-token__table"> {{ table.name }} <small>(id: {{ table.id }})</small> </div> <div class="api-token__permissions"> <div v-for="(operationName, operation) in operations" :key="operation" class="api-token__permission" > <Checkbox v-if=" $hasPermission( `database.table.${operation}_row`, table, workspace.id ) " :checked="isTableActive(table, database, operation)" @input=" toggleTable(table, database, databases, operation, $event) " ></Checkbox> </div> </div> </div> </div> </div> </div> </template> <script> import { notifyIf } from '@baserow/modules/core/utils/error' import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes' import { copyToClipboard } from '@baserow/modules/database/utils/clipboard' import TokenService from '@baserow/modules/database/services/token' export default { name: 'APIToken', props: { token: { type: Object, required: true, }, }, data() { return { open: false, deleteLoading: false, rotateLoading: false, tokenVisible: false, operations: { create: this.$t('apiToken.create'), read: this.$t('apiToken.read'), update: this.$t('apiToken.update'), delete: this.$t('apiToken.delete'), }, } }, computed: { workspace() { return this.$store.getters['workspace/get'](this.token.workspace) }, databases() { return this.$store.getters['application/getAllOfWorkspace']( this.workspace ).filter( (application) => application.type === DatabaseApplicationType.getType() ) }, }, watch: { databases: { handler() { // if databases or tables change, we need to ensure that token permissions // are still valid this.removeInvalidPermissions() }, deep: true, }, }, methods: { copyTokenToClipboard() { copyToClipboard(this.token.key) }, enableRename() { this.$refs.context.hide() this.$refs.rename.edit() }, /** * Updates some token properties. If the request fails the changes are going to be * reverted. */ async updateToken(token, values, old) { Object.assign(token, values) try { await TokenService(this.$client).update(token.id, values) } catch (error) { Object.assign(token, old) notifyIf(error, 'token') } }, /** * Asks the backend to rotate the key of the token. */ async rotateKey(token) { this.rotateLoading = true try { const { data } = await TokenService(this.$client).rotateKey(token.id) this.token.key = data.key this.tokenVisible = true this.rotateLoading = false } catch (error) { this.rotateLoading = false notifyIf(error, 'token') } }, /** * Deletes the token and emits a signal to the parent component such that is can * be removed from the list. */ async deleteToken(token) { if (this.deleteLoading) { return } try { await TokenService(this.$client).delete(token.id) this.deleteLoading = false this.$emit('deleted') } catch (error) { this.deleteLoading = false notifyIf(error, 'field') } }, /** * Check if a type (database or table) with the given id exists in the * permissions of the provided operation. */ exists(operation, type, id) { const permissions = this.token.permissions[operation] if (Array.isArray(permissions)) { for (const i in permissions) { if (permissions[i][0] === type && permissions[i][1] === id) { return true } } } return false }, /** * Adds a type (database or table) to the permissions of the given operation. */ add(operation, type, id) { const permissions = this.token.permissions[operation] if (!Array.isArray(permissions)) { this.token.permissions[operation] = [] } if (!this.exists(operation, type, id)) { this.token.permissions[operation].push([type, id]) } }, /** * Removes a type (database or table) to the permissions of the given operation. */ remove(operation, type, id) { let permissions = this.token.permissions[operation] if (!Array.isArray(permissions)) { this.token.permissions[operation] = [] permissions = [] } this.token.permissions[operation] = permissions.filter((permission) => { return !(permission[0] === type && permission[1] === id) }) }, /** * Indicates if the token has permissions to all databases and tables for the given * operation. Returns 2 if there is only partially access. */ isActive(operation) { const value = this.token.permissions[operation] if (value === true) { return true } else if ( // If the value is false or if no permissions have been set we can show the // switch as if is empty. value === false || (Array.isArray(value) && value.length === 0) ) { return false } else { return 2 } }, /** * Indicates if the token has permissions to the given database for the given * operation. Returns 2 if there is only partially access. */ isDatabaseActive(database, operation) { if ( this.isActive(operation) === true || this.exists(operation, 'database', database.id) ) { return true } const tables = database.tables for (const i in tables) { if (this.exists(operation, 'table', tables[i].id)) { return 2 } } return false }, /** * Indicates if the token has permissions to the given table for the given * operation. */ isTableActive(table, database, operation) { return ( this.isActive(operation) === true || this.exists(operation, 'database', database.id) || this.exists(operation, 'table', table.id) ) }, /** * Indicates if the permission refer to a database or table still existent. * This fixes the problem that arises when user deletes a database or table from * another browser tab while this form is opened. * We need to delete the permissions that are pointing to the deleted database * before sending updates to the backend if we want to avoid errors. */ removeInvalidPermissions() { const tokenPermissions = JSON.parse( JSON.stringify(this.token.permissions) ) for (const [operation, permissions] of Object.entries(tokenPermissions)) { if (!Array.isArray(permissions)) { continue } permissions.forEach((permission) => { if (!this.isPermissionValid(permission)) { const [permType, permId] = permission this.remove(operation, permType, permId) } }) } }, isPermissionValid(permission) { const databases = this.databases const [permType, permId] = permission if (permType === 'database') { const database = databases.find((database) => database.id === permId) return database !== undefined } else if (permType === 'table') { return databases.find((database) => { const table = database.tables.find((table) => table.id === permId) return table !== undefined }) } }, /** * Changes the token permission state of all databases and tables of the given * operation. Also updates the permissions with the backend. */ toggle(operation, value) { const oldPermissions = JSON.parse(JSON.stringify(this.token.permissions)) // We can easily change the value to true or false because the permissions are // now going to be controlled on global (workspace) level. this.token.permissions[operation] = value this.updateToken( this.token, { permissions: this.token.permissions }, { permissions: oldPermissions } ) }, /** * Changes the token permission state of a provided database and his tables of the * given operation. Also updates the permissions with the backend. */ toggleDatabase(database, siblings, operation, value) { const oldPermissions = JSON.parse(JSON.stringify(this.token.permissions)) // First we want to add all the databases that already have an active state to // the permissions because the permissions are not going to controlled on // database level. siblings .filter( (database) => this.isDatabaseActive(database, operation) === true ) .forEach((database) => { this.add(operation, 'database', database.id) }) // Remove all the child table permissions of the database because the // permissions are not going to controlled on database level. database.tables.forEach((table) => { this.remove(operation, 'table', table.id) }) // Depending on the value we either need to remove the database from the list // or add it. if (value) { this.add(operation, 'database', database.id) } else { this.remove(operation, 'database', database.id) } // Updates the permissions with the backend. this.updateToken( this.token, { permissions: this.token.permissions }, { permissions: oldPermissions } ) }, /** * Changes the token permission state of a provided table of the given operation. * Also updates the permissions with the backend. */ toggleTable(table, database, databases, operation, value) { const oldPermissions = JSON.parse(JSON.stringify(this.token.permissions)) // First we want to add all the databases that already have an active state to // the permissions because the permissions are now going to be controlled on // table level. databases .filter( (database) => this.isDatabaseActive(database, operation) === true ) .forEach((database) => { this.add(operation, 'database', database.id) }) // We also want to add all the tables that already have an active state to the // permissions. database.tables .filter( (table) => this.isTableActive(table, database, operation) === true ) .forEach((table) => { this.add(operation, 'table', table.id) }) // Depending on the value we either need to remove the database from the list // or add it. this.remove(operation, 'database', database.id) if (value) { this.add(operation, 'table', table.id) } else { this.remove(operation, 'table', table.id) } // Updates the permissions with the backend. this.updateToken( this.token, { permissions: this.token.permissions }, { permissions: oldPermissions } ) }, }, } </script>