Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private sharing Support #428

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"vue-router": "^4.0.12",
"vue-sonner": "^1.0.3",
"vue-tsc": "^2.0.21",
"vuedraggable": "^4.1.0"
"vuedraggable": "^4.1.0",
"jwt-decode": "4.0.0"
}
}
4 changes: 2 additions & 2 deletions frontend/src2/charts/SharedChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import ChartRenderer from './components/ChartRenderer.vue'
import { getFormattedRows } from '../query/helpers'
import { showErrorToast } from '../helpers'

const props = defineProps<{ chart_name: string }>()
const props = defineProps<{ chart_name: string; query?: Record<string, string[]>}>()

const chart = reactive({
doc: {} as WorkbookChart,
result: {} as QueryResult,
})

const fetchingData = ref(true)
call('insights.api.workbooks.fetch_shared_chart_data', { chart_name: props.chart_name })
call('insights.api.workbooks.fetch_shared_chart_data', { chart_name: props.chart_name, filters: props.query })
.then((res: any) => {
fetchingData.value = false
chart.doc = res.chart
Expand Down
108 changes: 72 additions & 36 deletions frontend/src2/dashboard/DashboardShareDialog.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Globe } from 'lucide-vue-next'
import { computed, inject, ref, unref } from 'vue'
import { Globe, Lock } from 'lucide-vue-next'
import { computed, inject, ref, unref, watch } from 'vue'
import { Dashboard } from './dashboard'
import { copyToClipboard } from '../helpers'

Expand All @@ -9,39 +9,54 @@ const show = defineModel()
const dashboard = inject('dashboard') as Dashboard

const isPublic = ref(unref(dashboard.doc.is_public))
const isPrivate = ref(unref(dashboard.doc.secure_embed))

watch(isPublic, (newVal) => {
if (newVal) isPrivate.value = false;
});
watch(isPrivate, (newVal) => {
if (newVal) isPublic.value = false;
});
const shareLink = computed(() => dashboard.getShareLink())
const iFrameLink = computed(() => {
return `<iframe src="${shareLink.value}" width="100%" height="100%" frameborder="0"></iframe>`
})

const secureLink = computed(() => dashboard.getSecureLink())
const hasChanged = computed(() => {
const prev = Boolean(dashboard.doc.is_public)
const next = Boolean(isPublic.value)
return prev !== next
const prev_public = Boolean(dashboard.doc.is_public)
const next_public = Boolean(isPublic.value)
const prev_private = Boolean(dashboard.doc.secure_embed)
const next_private = Boolean(isPrivate.value)
return prev_public !== next_public || prev_private !== next_private
})

function saveChanges() {
dashboard.doc.is_public = isPublic.value
dashboard.doc.share_link = shareLink.value
show.value = false
if (isPublic.value && isPrivate.value && isPublic.value == isPrivate.value) {
alert('Public Sharing and Secure Embedding both are not possible at the same time');
}
else {
dashboard.doc.is_public = isPublic.value
dashboard.doc.secure_embed = isPrivate.value
dashboard.doc.share_link = shareLink.value
dashboard.doc.secure_link = secureLink.value
show.value = false
}

}
</script>

<template>
<Dialog
v-model="show"
:options="{
title: 'Share Dashboard',
actions: [
{
label: 'Done',
variant: 'solid',
disabled: !hasChanged,
onClick: saveChanges,
},
],
}"
>
<Dialog v-model="show" :options="{
title: 'Share Dashboard',
actions: [
{
label: 'Done',
variant: 'solid',
disabled: !hasChanged,
onClick: saveChanges,
},
],
}">
<template #body-content>
<div class="space-y-3 text-base">
<div class="space-y-4">
Expand All @@ -57,29 +72,50 @@ function saveChanges() {
</div>
<Checkbox v-model="isPublic" />
</div>
<div v-if="shareLink" class="flex overflow-hidden rounded bg-gray-100">
<div class="flex items-center gap-3 rounded border px-3 py-2">
<Lock class="h-6 w-6 text-red-500" stroke-width="1.5" />
<div class="flex flex-1 flex-col">
<div class="font-medium leading-5 text-gray-800">
Enable Private Access
</div>
<div class="text-sm text-gray-700">
Only users with auto-generated tokens can view this dashboard
</div>
</div>
<Checkbox v-model="isPrivate" />
</div>
<div v-if="shareLink && isPublic" class="flex overflow-hidden rounded bg-gray-100">
<div
class="font-code form-input flex-1 overflow-hidden text-ellipsis whitespace-nowrap rounded-r-none text-sm text-gray-600"
>
class="font-code form-input flex-1 overflow-hidden text-ellipsis whitespace-nowrap rounded-r-none text-sm text-gray-600">
{{ shareLink }}
</div>
<Tooltip text="Copy Link" :hoverDelay="0.1">
<Button
class="w-8 rounded-none bg-gray-200 hover:bg-gray-300"
icon="link-2"
@click="copyToClipboard(shareLink)"
>
<Button class="w-8 rounded-none bg-gray-200 hover:bg-gray-300" icon="link-2"
@click="copyToClipboard(shareLink)">
</Button>
</Tooltip>
<Tooltip text="Copy iFrame" :hoverDelay="0.1">
<Button
class="w-8 rounded-l-none bg-gray-200 hover:bg-gray-300"
icon="code"
@click="copyToClipboard(iFrameLink)"
>
<Button class="w-8 rounded-l-none bg-gray-200 hover:bg-gray-300" icon="code"
@click="copyToClipboard(iFrameLink)">
</Button>
</Tooltip>
</div>
<div v-if="secureLink && isPrivate" class="flex flex-col space-y-3 rounded bg-gray-100 p-4">
<div class="text-gray-700 text-base font-medium">
<div class="mb-2">
Private Access Link: Use the following snippet to create a payload to call
"generate_embed_link" API and get the sharable link.
</div>
<div class="mb-2">
<br>payload = {
<br>"resource" : "{{ secureLink }}",
<br>"params": { "column_name": 'value' },
<br>"user" : "session_user",
<br>"exp": round(time.time()) + (60 * (int(10))) // Token valid for 10 Mins
<br>}
</div>
</div>
</div>
</div>
</div>
</template>
Expand Down
77 changes: 77 additions & 0 deletions frontend/src2/dashboard/SecureEmbedDashboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { call } from 'frappe-ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import SharedChart from '../charts/SharedChart.vue'
import { WorkbookDashboard } from '../types/workbook.types'
import VueGridLayout from './VueGridLayout.vue'
import { jwtDecode } from "jwt-decode";


const props = defineProps<{ dashboard_name: string }>()


const dashboard = ref<WorkbookDashboard>()
const route = useRoute()
const keyValuePairs: Record<string, string[]> = {}

const jwtToken = route.query.token as string
let isAuthenticated = false


if (jwtToken) {
try {

const decodedToken = jwtDecode(jwtToken)
const currentTime = Math.floor(Date.now() / 1000)

if (decodedToken.exp && decodedToken.exp > currentTime) {
isAuthenticated = true


Object.entries(decodedToken.params || {}).forEach(([key, value]) => {
if (Array.isArray(value)) {
keyValuePairs[key] = value
} else {
keyValuePairs[key] = [value]
}
})
} else {
console.error('JWT token has expired')
}
} catch (error) {
console.error('Invalid JWT token', error)
}
} else {
console.error('JWT token is missing')
}



dashboard.value = await call('insights.api.dashboards.fetch_workbook_dashboard', {
dashboard_name: props.dashboard_name,
})


</script>


<template>
<div class="relative flex h-full w-full overflow-hidden">
<div class="flex-1 overflow-y-auto p-4">
<div v-if="dashboard && dashboard.secure_embed && isAuthenticated">
<VueGridLayout v-if="dashboard && dashboard.items.length > 0" class="h-fit w-full" :cols="20"
:disabled="true" :modelValue="dashboard.items.map((item) => item.layout)">
<template #item="{ index }">
<div class="relative h-full w-full rounded p-2">
<SharedChart :chart_name="dashboard.items[index].chart" :query="keyValuePairs" />
</div>
</template>
</VueGridLayout>
</div>
<div v-else class="flex-1 flex items-center justify-center">
<p class="text-red-500">Access Denied: Either token is invalid/expired or Sharing is Disabled</p>
</div>
</div>
</div>
</template>
40 changes: 27 additions & 13 deletions frontend/src2/dashboard/SharedDashboard.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
<script setup lang="ts">
import { call } from 'frappe-ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import SharedChart from '../charts/SharedChart.vue'
import { WorkbookDashboard } from '../types/workbook.types'
import VueGridLayout from './VueGridLayout.vue'

const props = defineProps<{ dashboard_name: string }>()

const dashboard = ref<WorkbookDashboard>()
const route = useRoute();
const queryString = new URLSearchParams(route.query);
const keyValuePairs: Record<string, string[]> = {};

Object.entries(route.query).forEach(([key, value]) => {
if (Array.isArray(value)) {
keyValuePairs[key] = value
} else {
keyValuePairs[key] = [value]
}
})



dashboard.value = await call('insights.api.dashboards.fetch_workbook_dashboard', {
dashboard_name: props.dashboard_name,
Expand All @@ -17,19 +31,19 @@ dashboard.value = await call('insights.api.dashboards.fetch_workbook_dashboard',
<template>
<div class="relative flex h-full w-full overflow-hidden">
<div class="flex-1 overflow-y-auto p-4">
<VueGridLayout
v-if="dashboard && dashboard.items.length > 0"
class="h-fit w-full"
:cols="20"
:disabled="true"
:modelValue="dashboard.items.map((item) => item.layout)"
>
<template #item="{ index }">
<div class="relative h-full w-full rounded p-2">
<SharedChart :chart_name="dashboard.items[index].chart" />
</div>
</template>
</VueGridLayout>
<div v-if="dashboard && dashboard.is_public">
<VueGridLayout v-if="dashboard && dashboard.items.length > 0" class="h-fit w-full" :cols="20"
:disabled="true" :modelValue="dashboard.items.map((item) => item.layout)">
<template #item="{ index }">
<div class="relative h-full w-full rounded p-2">
<SharedChart :chart_name="dashboard.items[index].chart" :query="keyValuePairs" />
</div>
</template>
</VueGridLayout>
</div>
<div v-else class="flex-1 flex items-center justify-center">
<p class="text-red-500">Access Denied: Public Sharing is Disabled</p>
</div>
</div>
</div>
</template>
9 changes: 8 additions & 1 deletion frontend/src2/dashboard/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function makeDashboard(workbookDashboard: WorkbookDashboard) {
}
})

chart.refresh(undefined, true)
chart.refresh()
},

getShareLink() {
Expand All @@ -94,6 +94,13 @@ function makeDashboard(workbookDashboard: WorkbookDashboard) {
`${window.location.origin}/insights/shared/dashboard/${dashboard.doc.name}`
)
},

getSecureLink() {
return (
dashboard.doc.secure_link ||
`${window.location.origin}/insights/secure/dashboard/${dashboard.doc.name}`
)
},
})

const key = `insights:dashboard-filters-${workbookDashboard.name}`
Expand Down
10 changes: 10 additions & 0 deletions frontend/src2/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ const routes = [
isGuestView: true,
},
},
{
props: true,
name: 'SecureDashboard',
path: '/secure/dashboard/:dashboard_name',
component: () => import('./dashboard/SecureEmbedDashboard.vue'),
meta: {
hideSidebar: true,
isGuestView: true,
},
},
{
path: '/:pathMatch(.*)*',
component: () => import('./auth/NotFound.vue'),
Expand Down
2 changes: 2 additions & 0 deletions frontend/src2/types/workbook.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export type WorkbookDashboard = {
items: WorkbookDashboardItem[]
is_public?: boolean
share_link?: string
secure_embed? : boolean
secure_link?: string
}

export type WorkbookDashboardItem = WorkbookDashboardChart
Expand Down
17 changes: 17 additions & 0 deletions insights/api/embed_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import jwt
import frappe


@frappe.whitelist()
def generate_embed_link(payload):

secret_key = frappe.db.get_single_value("Insights Settings", "secret_key")

user_exists = frappe.db.exists("User",{"email": payload['user'], "enabled": 1})

if user_exists:
token = jwt.encode(payload, secret_key, algorithm="HS256")
protected_link = f"{payload['resource']}?token={token}"
return protected_link
else:
frappe.throw("No Access to User")
5 changes: 3 additions & 2 deletions insights/api/workbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ def update_share_permissions(


@frappe.whitelist(allow_guest=True)
def fetch_shared_chart_data(chart_name: str):
def fetch_shared_chart_data(chart_name: str, filters: dict = None):
filters = frappe.parse_json(filters) if filters else {}
workbooks = frappe.get_all(
"Insights Workbook",
filters={"charts": ["like", f"%{chart_name}%"]},
Expand All @@ -241,4 +242,4 @@ def fetch_shared_chart_data(chart_name: str):
frappe.throw("Chart not found")

workbook = frappe.get_doc("Insights Workbook", workbooks[0])
return workbook.get_shared_chart_data(chart_name)
return workbook.get_shared_chart_data(chart_name, filters)
Loading