Skip to content

Commit

Permalink
feat: database user and permission manager (#2245)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanmoysrt authored Nov 25, 2024
1 parent 21a3f76 commit 04ee6c5
Show file tree
Hide file tree
Showing 24 changed files with 1,641 additions and 148 deletions.
1 change: 1 addition & 0 deletions dashboard/src/components/global/Badge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default {
Running: 'blue',
Pending: 'orange',
Failure: 'red',
Failed: 'red',
'Update Available': 'blue',
Enabled: 'blue',
'Awaiting Approval': 'orange',
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src2/components/SiteActionCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function getSiteActionHandler(action) {
'Restore from an existing site': defineAsyncComponent(() =>
import('./site/SiteDatabaseRestoreFromURLDialog.vue')
),
'Access site database': defineAsyncComponent(() =>
'Manage database users': defineAsyncComponent(() =>
import('./SiteDatabaseAccessDialog.vue')
),
'Version upgrade': defineAsyncComponent(() =>
Expand Down
327 changes: 192 additions & 135 deletions dashboard/src2/components/SiteDatabaseAccessDialog.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<Dialog :options="{ title: 'Manage Database Access' }" v-model="show">
<Dialog
:options="{ title: 'Manage Database Users', size: '2xl' }"
v-model="show"
>
<template #body-content>
<!-- Not available on current plan, upsell higher plans -->
<div v-if="!planSupportsDatabaseAccess">
Expand All @@ -16,111 +19,45 @@
>
Upgrade Site Plan
</Button>
<ManageSitePlansDialog :site="site" v-if="showChangePlanDialog" />
<ManageSitePlansDialog
:site="site"
v-model="showChangePlanDialog"
v-if="showChangePlanDialog"
/>
</div>
</div>

<!-- Available on the current plan -->
<div v-else>
<div v-if="$site.doc.is_database_access_enabled">
<div v-if="databaseCredentials">
<p class="mb-2 text-base font-semibold text-gray-700">
Using an Analytics or Business Intelligence Tool
</p>
<p class="mb-2 text-base">
Use following credentials with your analytics or business
intelligence tool
</p>
<p class="ml-1 font-mono text-sm">
Host: {{ databaseCredentials.host }}
</p>
<p class="ml-1 font-mono text-sm">
Port: {{ databaseCredentials.port }}
</p>
<p class="ml-1 font-mono text-sm">
Database Name: {{ databaseCredentials.database }}
</p>
<p class="ml-1 font-mono text-sm">
Username: {{ databaseCredentials.username }}
</p>
<p class="ml-1 font-mono text-sm">
Password: {{ databaseCredentials.password }}
</p>
</div>
<div class="pb-2 pt-5">
<p class="mb-2 text-base font-semibold text-gray-700">
Using MariaDB Client
</p>
<p class="mb-2 text-base">
Run this command in your terminal to access MariaDB console
</p>
<ClickToCopyField class="ml-1" :textContent="dbAccessCommand" />
<p class="mt-3 text-sm">
Note: You should have a
<span class="font-mono">mariadb</span> client installed on your
computer.
</p>
</div>
</div>
<div v-else>
<p class="mb-2 text-sm">Database access is disabled for this site.</p>
</div>

<div class="mt-4">
<div
v-if="planSupportsDatabaseAccess && !databaseAccessEnabled"
class="mb-2"
>
<FormControl
label="Access type"
type="select"
:options="[
{ label: 'Read only', value: 'read_only' },
{ label: 'Read & Write', value: 'read_write' }
]"
v-model="mode"
/>
<p v-if="mode === 'read_write'" class="mt-2 text-base text-red-600">
Your credentials can be used to modify or wipe your database
</p>
</div>
<ErrorMessage
class="mt-2"
:message="
$site.enableDatabaseAccess.error ||
$site.disableDatabaseAccess.error ||
error
"
/>
<Button
v-if="planSupportsDatabaseAccess && !databaseAccessEnabled"
@click="enableDatabaseAccess"
:loading="
$site.enableDatabaseAccess.loading || pollingAgentJob?.loading
"
variant="solid"
class="mt-2 w-full"
>
Enable Access
</Button>
<Button
v-if="planSupportsDatabaseAccess && databaseAccessEnabled"
@click="$site.disableDatabaseAccess.submit()"
:loading="$site.disableDatabaseAccess.loading"
class="w-full"
>
Disable Access
</Button>
</div>
<ObjectList :options="listOptions" />
</div>
</template>
</Dialog>

<SiteDatabaseUserCredentialDialog
:name="selectedUser"
v-model="showDatabaseUserCredentialDialog"
v-if="showDatabaseUserCredentialDialog"
/>

<SiteDatabaseAddEditUserDialog
:site="site"
:key="selectedUser ? selectedUser : 'new'"
:db_user_name="selectedUser"
v-model="showDatabaseAddEditUserDialog"
v-if="showDatabaseAddEditUserDialog"
@success="this.hideSiteDatabaseAddEditUserDialog"
/>
</template>
<script>
import { defineAsyncComponent } from 'vue';
import { getCachedDocumentResource } from 'frappe-ui';
import ClickToCopyField from './ClickToCopyField.vue';
import { pollJobStatus } from '../utils/agentJob';
import ObjectList from './ObjectList.vue';
import { date } from '../utils/format';
import { confirmDialog, icon } from '../utils/components';
import SiteDatabaseUserCredentialDialog from './site_database_user/SiteDatabaseUserCredentialDialog.vue';
import SiteDatabaseAddEditUserDialog from './site_database_user/SiteDatabaseAddEditUserDialog.vue';
export default {
name: 'SiteDatabaseAccessDialog',
Expand All @@ -129,69 +66,189 @@ export default {
ManageSitePlansDialog: defineAsyncComponent(() =>
import('./ManageSitePlansDialog.vue')
),
ClickToCopyField
ClickToCopyField,
ObjectList,
SiteDatabaseUserCredentialDialog,
SiteDatabaseAddEditUserDialog
},
data() {
return {
mode: 'read_only',
show: true,
showChangePlanDialog: false,
error: null,
pollingAgentJob: null
selectedUser: '',
showDatabaseUserCredentialDialog: false,
showDatabaseAddEditUserDialog: false
};
},
mounted() {
this.fetchDatabaseCredentials();
},
methods: {
enableDatabaseAccess() {
return this.$site.enableDatabaseAccess.submit(
{ mode: this.mode },
{
onSuccess: result => {
let jobId = result.message;
this.pollingAgentJob = pollJobStatus(jobId, status => {
if (status === 'Success') {
this.fetchDatabaseCredentials();
return true;
} else if (status === 'Failure') {
this.error = 'Failed to enable database access';
return true;
}
});
}
}
);
watch: {
showDatabaseUserCredentialDialog(val) {
if (!val) {
this.show = true;
}
},
fetchDatabaseCredentials() {
if (this.planSupportsDatabaseAccess && this.databaseAccessEnabled) {
this.$site.getDatabaseCredentials.fetch();
showDatabaseAddEditUserDialog(val) {
if (!val) {
this.show = true;
}
}
},
resources: {
deleteSiteDatabaseUser() {
return {
url: 'press.api.client.run_doc_method',
onSuccess() {
toast.success('Database User will be deleted shortly');
},
onError(err) {
toast.error(
err.messages.length
? err.messages.join('\n')
: 'Failed to initiate database user deletion'
);
}
};
}
},
computed: {
dbAccessCommand() {
if (this.databaseCredentials) {
const credentials = this.databaseCredentials;
return `mysql -u ${credentials.username} -p -h ${credentials.host} -P ${credentials.port} --ssl --ssl-verify-server-cert`;
}
return null;
},
databaseCredentials() {
return this.$site.getDatabaseCredentials.data;
listOptions() {
return {
doctype: 'Site Database User',
filters: {
site: this.site,
status: ['!=', 'Archived']
},
searchField: 'username',
filterControls() {
return [
{
type: 'select',
label: 'Status',
fieldname: 'status',
options: ['', 'Pending', 'Active', 'Failed']
}
];
},
columns: [
{
label: 'Username',
fieldname: 'username',
width: 1
},
{
label: 'Status',
fieldname: 'status',
width: 0.5,
align: 'center',
type: 'Badge'
},
{
label: 'Mode',
fieldname: 'mode',
width: 0.5,
align: 'center',
format: (value, row) => {
return {
read_only: 'Read Only',
read_write: 'Read/Write',
granular: 'Granular'
}[value];
}
},
{
label: 'Created On',
fieldname: 'creation',
width: 0.5,
align: 'center',
format: value => date(value, 'll')
}
],
rowActions: ({ row, listResource, documentResource }) => {
if (row.status === 'Archived') {
return [];
}
return [
{
label: 'View Credential',
onClick: () => {
this.show = false;
this.selectedUser = row.name;
this.showDatabaseUserCredentialDialog = true;
}
},
{
label: 'Configure User',
onClick: () => {
this.selectedUser = row.name;
this.show = false;
this.showDatabaseAddEditUserDialog = true;
}
},
{
label: 'Delete User',
onClick: event => {
this.show = false;
event.stopPropagation();
confirmDialog({
title: 'Delete Database User',
message: `Are you sure you want to delete the database user ?<br>`,
primaryAction: {
label: 'Delete',
variant: 'solid',
theme: 'red',
onClick: ({ hide }) => {
this.$resources.deleteSiteDatabaseUser.submit({
dt: 'Site Database User',
dn: row.name,
method: 'archive'
});
this.$resources.deleteSiteDatabaseUser.promise.then(
() => {
hide();
this.show = true;
}
);
return this.$resources.deleteSiteDatabaseUser.promise;
}
},
onSuccess: () => {
listResource.refresh();
}
});
}
}
];
},
primaryAction: () => {
return {
label: 'Add User',
variant: 'solid',
slots: {
prefix: icon('plus')
},
onClick: () => {
this.show = false;
this.selectedUser = null;
this.showDatabaseAddEditUserDialog = true;
}
};
}
};
},
databaseAccessEnabled() {
return this.$site.doc.is_database_access_enabled;
sitePlan() {
return this.$site.doc.current_plan;
},
planSupportsDatabaseAccess() {
return this.sitePlan?.database_access;
},
sitePlan() {
return this.$site.doc.current_plan;
},
$site() {
return getCachedDocumentResource('Site', this.site);
}
},
methods: {
hideSiteDatabaseAddEditUserDialog() {
this.showDatabaseAddEditUserDialog = false;
}
}
};
</script>
Loading

0 comments on commit 04ee6c5

Please sign in to comment.