mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
feat: add permissions to routes and support adding and removing roles
This commit is contained in:
1 parent
b811e69158
commit
2e4cc9c2e9
13 files changed
+409
-151
No files matched your search
@@ -7,15 +7,20 @@ import { Typography } from '@supabase/ui'
|
||||
import { AutoField, LongTextField } from 'uniforms-bootstrap4'
|
||||
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { useProjectAuthConfig, useStore } from 'hooks'
|
||||
import { usePermissions, useProjectAuthConfig, useStore } from 'hooks'
|
||||
import { pluckJsonSchemaFields } from 'lib/helpers'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { authConfig } from 'stores/jsonSchema'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
const Templates: FC<any> = ({ project }) => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
const canUpdate = usePermissions(
|
||||
PermissionAction.SQL_UPDATE,
|
||||
'postgres.public.custom_config_gotrue'
|
||||
)
|
||||
const magicLinkEnable = semver.gte(
|
||||
// @ts-ignore
|
||||
semver.coerce(toJS(project?.kpsVersion) || 'kps-v0.0.1'),
|
||||
@@ -67,6 +72,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
readOnly={!IS_PLATFORM}
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an sms body."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
@@ -88,6 +94,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
name="MAILER_SUBJECTS_CONFIRMATION"
|
||||
readOnly={!IS_PLATFORM}
|
||||
errorMessage="Please enter a subject."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<LongTextField
|
||||
showInlineError
|
||||
@@ -96,6 +103,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
readOnly={!IS_PLATFORM}
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
@@ -117,6 +125,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
name="MAILER_SUBJECTS_RECOVERY"
|
||||
readOnly={!IS_PLATFORM}
|
||||
errorMessage="Please enter a subject."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<LongTextField
|
||||
showInlineError
|
||||
@@ -125,6 +134,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
readOnly={!IS_PLATFORM}
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
@@ -147,6 +157,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
name="MAILER_SUBJECTS_MAGIC_LINK"
|
||||
readOnly={!IS_PLATFORM}
|
||||
errorMessage="Please enter a subject."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<LongTextField
|
||||
showInlineError
|
||||
@@ -155,6 +166,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
readOnly={!IS_PLATFORM}
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
@@ -177,6 +189,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
name="MAILER_SUBJECTS_EMAIL_CHANGE"
|
||||
readOnly={!IS_PLATFORM}
|
||||
errorMessage="Please enter a subject."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<LongTextField
|
||||
showInlineError
|
||||
@@ -185,6 +198,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
readOnly={!IS_PLATFORM}
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
@@ -206,6 +220,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
name="MAILER_SUBJECTS_INVITE"
|
||||
readOnly={!IS_PLATFORM}
|
||||
errorMessage="Please enter a subject."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<LongTextField
|
||||
showInlineError
|
||||
@@ -214,6 +229,7 @@ const Templates: FC<any> = ({ project }) => {
|
||||
readOnly={!IS_PLATFORM}
|
||||
inputClassName="font-mono"
|
||||
errorMessage="Please enter an email body."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,17 @@ import { FC } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { sum } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Loading, Button } from '@supabase/ui'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { Button, Loading } from '@supabase/ui'
|
||||
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { useStore, useSubscriptionStats } from 'hooks'
|
||||
import { usePermissions, useStore, useSubscriptionStats } from 'hooks'
|
||||
import CostBreakdownRow from './CostBreakdownRow'
|
||||
import { StripeSubscription } from './Subscription.types'
|
||||
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
|
||||
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
|
||||
import { PaygStats, ProductFeature } from '../PAYGUsage/PAYGUsage.types'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
interface Props {
|
||||
project: any
|
||||
@@ -37,6 +37,11 @@ const Subscription: FC<Props> = ({
|
||||
const { ui } = useStore()
|
||||
const isOrgOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const canChangeSubscription = usePermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
|
||||
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
|
||||
const addOns = subscription?.addons ?? []
|
||||
const paid = subscription && subscription.tier.unit_amount > 0
|
||||
@@ -64,7 +69,7 @@ const Subscription: FC<Props> = ({
|
||||
</div>
|
||||
<div className="flex flex-col items-end space-y-2">
|
||||
<Button
|
||||
disabled={!isOrgOwner}
|
||||
disabled={!canChangeSubscription || !isOrgOwner}
|
||||
onClick={() => router.push(`/project/${project.ref}/settings/billing/update`)}
|
||||
type="primary"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FC } from 'react'
|
||||
import { Button } from '@supabase/ui'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useStore } from 'hooks'
|
||||
import { usePermissions, useStore } from 'hooks'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
interface Props {
|
||||
selectedSchema: string
|
||||
@@ -10,13 +11,18 @@ interface Props {
|
||||
|
||||
const EmptyState: FC<Props> = ({ selectedSchema, onAddTable }) => {
|
||||
const { meta } = useStore()
|
||||
const canCreate = usePermissions(PermissionAction.TENANT_SQL_CREATE_TABLE, 'postgres.public')
|
||||
const tables = meta.tables.list((table: PostgresTable) => table.schema === selectedSchema)
|
||||
|
||||
const renderNoTablesCTA = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<p className="text-sm">There are no tables available in this schema</p>
|
||||
{selectedSchema === 'public' && <Button onClick={onAddTable}>Create a new table</Button>}
|
||||
{selectedSchema === 'public' && (
|
||||
<Button onClick={onAddTable} disabled={!canCreate}>
|
||||
Create a new table
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -28,7 +34,9 @@ const EmptyState: FC<Props> = ({ selectedSchema, onAddTable }) => {
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<p className="text-sm">Select a table or create a new one</p>
|
||||
<Button onClick={onAddTable}>Create a new table</Button>
|
||||
<Button onClick={onAddTable} disabled={!canCreate}>
|
||||
Create a new table
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { FC, useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { isUndefined, find } from 'lodash'
|
||||
import { SupabaseGrid, SupabaseGridRef, parseSupaTable, Dictionary } from '@supabase/grid'
|
||||
import { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
|
||||
import { find, isUndefined } from 'lodash'
|
||||
import { Dictionary, parseSupaTable, SupabaseGrid, SupabaseGridRef } from '@supabase/grid'
|
||||
import { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { usePermissions, useStore } from 'hooks'
|
||||
import NotFoundState from './NotFoundState'
|
||||
import GridHeaderActions from './GridHeaderActions'
|
||||
import SidePanelEditor from './SidePanelEditor'
|
||||
import { SchemaView } from 'components/layouts/TableEditorLayout/TableEditorLayout.types'
|
||||
import { SidePanel } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
interface Props {
|
||||
/** Theme for the editor */
|
||||
@@ -64,6 +64,11 @@ const TableGridEditor: FC<Props> = ({
|
||||
}
|
||||
|
||||
const tableId = selectedTable?.id
|
||||
const canAdminWrite = usePermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, String(tableId))
|
||||
const canInsert = usePermissions(PermissionAction.TENANT_SQL_INSERT, String(tableId))
|
||||
const canUpdate = usePermissions(PermissionAction.TENANT_SQL_UPDATE, String(tableId))
|
||||
// const canEdit = canAdminWrite && canInsert && canUpdate
|
||||
const canEdit = true
|
||||
|
||||
const isViewSelected = Object.keys(selectedTable).length === 2
|
||||
const gridTable = !isViewSelected
|
||||
@@ -135,7 +140,7 @@ const TableGridEditor: FC<Props> = ({
|
||||
theme={theme}
|
||||
gridProps={{ height: '100%' }}
|
||||
storageRef={projectRef}
|
||||
editable={!isViewSelected && selectedTable.schema === 'public'}
|
||||
editable={!isViewSelected && selectedTable.schema === 'public' && canEdit}
|
||||
schema={selectedTable.schema}
|
||||
table={gridTable}
|
||||
headerActions={
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
Typography,
|
||||
Listbox,
|
||||
Menu,
|
||||
Input,
|
||||
IconCopy,
|
||||
IconChevronDown,
|
||||
IconCopy,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconSearch,
|
||||
IconX,
|
||||
IconLoader,
|
||||
IconRefreshCw,
|
||||
Alert,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
IconX,
|
||||
Input,
|
||||
Listbox,
|
||||
Menu,
|
||||
Typography,
|
||||
} from '@supabase/ui'
|
||||
import { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import Base64 from 'lib/base64'
|
||||
import { useStore } from 'hooks'
|
||||
import { usePermissions, useStore } from 'hooks'
|
||||
import { SchemaView } from './TableEditorLayout.types'
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
interface Props {
|
||||
selectedSchema?: string
|
||||
@@ -50,6 +51,7 @@ const TableEditorMenu: FC<Props> = ({
|
||||
const [searchText, setSearchText] = useState<string>('')
|
||||
const [schemaViews, setSchemaViews] = useState<SchemaView[]>([])
|
||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false)
|
||||
const canCreate = usePermissions(PermissionAction.TENANT_SQL_CREATE_TABLE, 'postgres.public.*')
|
||||
|
||||
// We may need to shift this to the schema store and do something like meta.schema.loadViews()
|
||||
// I don't need we need a separate store for views
|
||||
@@ -116,7 +118,8 @@ const TableEditorMenu: FC<Props> = ({
|
||||
// @ts-ignore
|
||||
label={
|
||||
<>
|
||||
<span className="text-scale-900">schema</span> <span>{schema.name}</span>
|
||||
<span className="text-scale-900">schema</span>
|
||||
<span>{schema.name}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -129,7 +132,7 @@ const TableEditorMenu: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{selectedSchema === 'public' && (
|
||||
{selectedSchema === 'public' && canCreate && (
|
||||
<div className="px-3">
|
||||
{/* Add new table button */}
|
||||
<Button
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { createContext, useContext, useCallback, useState, useEffect } from 'react'
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { debounce, isNil } from 'lodash'
|
||||
import { observer, useLocalObservable } from 'mobx-react-lite'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { Button, IconKey, IconLoader, IconUser, IconX, Input, Typography } from '@supabase/ui'
|
||||
import { Modal } from '@supabase/ui'
|
||||
import {
|
||||
Button,
|
||||
IconKey,
|
||||
IconLoader,
|
||||
IconUser,
|
||||
IconX,
|
||||
Input,
|
||||
Modal,
|
||||
Typography,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Transition } from '@headlessui/react'
|
||||
@@ -36,7 +44,7 @@ function InviteMemberModal({ organization, members = [] }) {
|
||||
if (profiles) {
|
||||
temp = profiles
|
||||
.map((profile) => {
|
||||
const foundMember = this.members.find((x) => x.profile.id == profile.id)
|
||||
const foundMember = this.members.find((x) => x.id == profile.id)
|
||||
return { ...profile, isMember: foundMember !== undefined }
|
||||
})
|
||||
.sort((a, b) => a.username.localeCompare(b.username))
|
||||
@@ -67,6 +75,7 @@ function InviteMemberModal({ organization, members = [] }) {
|
||||
async function addMember() {
|
||||
PageState.addMemberLoading = true
|
||||
const response = await post(`${API_URL}/organizations/${orgSlug}/members/add`, {
|
||||
gotrue_id: PageState.selectedProfile.gotrue_id,
|
||||
org_id: orgId,
|
||||
user_id: PageState.selectedProfile.id,
|
||||
})
|
||||
@@ -82,7 +91,10 @@ function InviteMemberModal({ organization, members = [] }) {
|
||||
const newMember = response
|
||||
mutateOrgMembers([...PageState.members, newMember], false)
|
||||
toggle()
|
||||
ui.setNotification({ category: 'success', message: 'Successfully added new member' })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully added new member',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +158,10 @@ const InputSearchWithResults = observer(({ className }) => {
|
||||
setLoading(true)
|
||||
const response = await post(`${API_URL}/profile/search`, { keywords: PageState.keywords })
|
||||
if (isNil(response)) {
|
||||
ui.setNotification({ category: 'error', message: 'Failed to search profile' })
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Failed to search profile',
|
||||
})
|
||||
} else if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
|
||||
@@ -8,6 +8,7 @@ interface Props {
|
||||
infoButtonLabel?: string
|
||||
infoButtonUrl?: string
|
||||
onClickCta?: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ProductEmptyState: FC<Props> = ({
|
||||
@@ -17,6 +18,7 @@ const ProductEmptyState: FC<Props> = ({
|
||||
infoButtonLabel = '',
|
||||
infoButtonUrl = '',
|
||||
onClickCta = () => {},
|
||||
disabled = false,
|
||||
}) => (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<div className="flex space-x-4 p-6 rounded border border-panel-border-light dark:border-panel-border-dark bg-panel-body-light dark:bg-panel-body-dark shadow-md">
|
||||
@@ -26,7 +28,7 @@ const ProductEmptyState: FC<Props> = ({
|
||||
<Typography.Title level={5}>{title}</Typography.Title>
|
||||
<div className="flex flex-col space-y-2">{children}</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button type="primary" onClick={onClickCta}>
|
||||
<Button type="primary" onClick={onClickCta} disabled={disabled}>
|
||||
{ctaButtonLabel}
|
||||
</Button>
|
||||
{infoButtonUrl && infoButtonLabel ? (
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Member } from 'types'
|
||||
import { API_URL } from 'lib/constants'
|
||||
|
||||
export function useOrganizationDetail(slug: string) {
|
||||
const url = `${API_URL}/props/org/${slug}`
|
||||
const url = `${API_URL}/props/org/${slug}?member_roles`
|
||||
const { data, error } = useSWR<any>(url, get)
|
||||
const { members, products } = data ?? []
|
||||
const members = data?.members ?? []
|
||||
const anyError = data?.error || error
|
||||
|
||||
function mutateOrgMembers(updatedMembers: Member[], revalidate?: boolean) {
|
||||
@@ -15,7 +15,6 @@ export function useOrganizationDetail(slug: string) {
|
||||
|
||||
return {
|
||||
members: members as Member[],
|
||||
products,
|
||||
isLoading: !anyError && !data,
|
||||
isError: !!anyError,
|
||||
mutateOrgMembers,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useEffect, useContext, useState } from 'react'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer, useLocalObservable } from 'mobx-react-lite'
|
||||
import { toJS } from 'mobx'
|
||||
@@ -6,23 +6,23 @@ import { pluckJsonSchemaFields, pluckObjectFields, timeout } from 'lib/helpers'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
import { organizations } from 'stores/jsonSchema'
|
||||
import {
|
||||
Loading,
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
Form,
|
||||
IconMoreHorizontal,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
Input,
|
||||
Loading,
|
||||
Modal,
|
||||
Tabs,
|
||||
Typography,
|
||||
IconTrash,
|
||||
Alert,
|
||||
Input,
|
||||
Dropdown,
|
||||
Modal,
|
||||
IconSearch,
|
||||
Form,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useOrganizationDetail, useStore, withAuth } from 'hooks'
|
||||
import { post, delete_, patch } from 'lib/common/fetch'
|
||||
import { useOrganizationDetail, usePermissions, useStore, withAuth } from 'hooks'
|
||||
import { delete_, get, patch, post } from 'lib/common/fetch'
|
||||
import { AccountLayoutWithoutAuth } from 'components/layouts'
|
||||
import { BillingSettings, InvoicesSettings } from 'components/interfaces/Organization'
|
||||
|
||||
@@ -33,6 +33,8 @@ import InviteMemberModal from 'components/to-be-cleaned/ModalsDeprecated/InviteM
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import { NextPageWithLayout, Project } from 'types'
|
||||
import useSWR from 'swr'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
// [Joshen] Low prio refactor: Bring out general and team settings into their own components too
|
||||
|
||||
@@ -47,22 +49,20 @@ const OrgSettingsLayout = withAuth(
|
||||
organization: {},
|
||||
projects: [],
|
||||
members: [],
|
||||
products: [],
|
||||
membersFilterString: '',
|
||||
get isOrgOwner() {
|
||||
return (
|
||||
this.members.find((x: any) => x.profile.id === this.user?.id && x.is_owner) != undefined
|
||||
)
|
||||
return this.members.find((x: any) => x.user_id === this.user?.id && x.is_owner) != undefined
|
||||
},
|
||||
get filteredMembers() {
|
||||
const temp = this.members.filter((x: any) => {
|
||||
let profile = x.profile
|
||||
return (
|
||||
profile.username.includes(this.membersFilterString) ||
|
||||
profile.primary_email.includes(this.membersFilterString)
|
||||
)
|
||||
})
|
||||
return temp.sort((a: any, b: any) => a.profile.username.localeCompare(b.profile.username))
|
||||
const temp = this.members.filter(
|
||||
({ username, primary_email }: { username: string; primary_email: string }) => {
|
||||
return (
|
||||
username.includes(this.membersFilterString) ||
|
||||
primary_email.includes(this.membersFilterString)
|
||||
)
|
||||
}
|
||||
)
|
||||
return temp.sort((a: any, b: any) => a.username.localeCompare(b.username))
|
||||
},
|
||||
initData(organization: any, user: any, projects: any) {
|
||||
this.organization = organization
|
||||
@@ -123,18 +123,15 @@ export default observer(OrgSettings)
|
||||
const OrganizationSettings = observer(() => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const { ui } = useStore()
|
||||
const {
|
||||
members,
|
||||
products,
|
||||
isError: isOrgDetailError,
|
||||
} = useOrganizationDetail(ui.selectedOrganization?.slug || '')
|
||||
const { members, isError: isOrgDetailError } = useOrganizationDetail(
|
||||
ui.selectedOrganization?.slug || ''
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOrgDetailError) {
|
||||
PageState.members = members ?? []
|
||||
PageState.products = products ?? []
|
||||
}
|
||||
}, [members, products, isOrgDetailError])
|
||||
}, [members, isOrgDetailError])
|
||||
|
||||
if (!PageState.organization) return <div />
|
||||
|
||||
@@ -198,6 +195,10 @@ const TabsView = observer(() => {
|
||||
const GeneralSettings = observer(() => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const { ui } = useStore()
|
||||
const canUpdateOrganization = usePermissions(
|
||||
PermissionAction.SQL_UPDATE,
|
||||
'postgres.public.organizations'
|
||||
)
|
||||
|
||||
const formModel = toJS(PageState.organization)
|
||||
// remove warning null value for controlled input
|
||||
@@ -214,7 +215,10 @@ const GeneralSettings = observer(() => {
|
||||
} else {
|
||||
const updatedOrg = response
|
||||
PageState.onOrgUpdated(updatedOrg)
|
||||
ui.setNotification({ category: 'success', message: 'Successfully saved settings' })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully saved settings',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,11 +235,13 @@ const GeneralSettings = observer(() => {
|
||||
name="name"
|
||||
showInlineError
|
||||
errorMessage="Please enter an organization name"
|
||||
disabled={!canUpdateOrganization}
|
||||
/>
|
||||
<AutoField
|
||||
name="billing_email"
|
||||
showInlineError
|
||||
errorMessage="Please enter an email address"
|
||||
disabled={!canUpdateOrganization}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
|
||||
@@ -410,7 +416,10 @@ const TeamSettings = observer(() => {
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
ui.setNotification({ category: 'error', message: `Error leaving: ${error?.message}` })
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Error leaving: ${error?.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsLeaving(false)
|
||||
}
|
||||
@@ -466,6 +475,10 @@ const MembersFilterInput = observer(() => {
|
||||
|
||||
const MembersView = observer(() => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const { data: roles, error: rolesError } = useSWR(
|
||||
`${API_URL}/organizations/${PageState.organization.slug}/roles`,
|
||||
get
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="rounded">
|
||||
@@ -478,32 +491,36 @@ const MembersView = observer(() => {
|
||||
]}
|
||||
body={[
|
||||
PageState.filteredMembers.map((x: any) => (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.tr key={x.member_id}>
|
||||
<Table.td>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<img
|
||||
src={`https://github.com/${x.profile.username}.png?size=80`}
|
||||
src={`https://github.com/${x.username}.png?size=80`}
|
||||
width="40"
|
||||
className="border-border-secondary-light dark:border-border-secondary-dark rounded-full border"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text>{x.profile.username}</Typography.Text>
|
||||
<Typography.Text>{x.username}</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text type="secondary">{x.profile.primary_email}</Typography.Text>
|
||||
<Typography.Text type="secondary">{x.primary_email}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text type="secondary">
|
||||
{x.is_owner ? 'Owner' : 'Developer'}
|
||||
{roles &&
|
||||
x.role_ids
|
||||
.map((roleId) => roles.find(({ id }) => roleId === id).name)
|
||||
.sort()
|
||||
.join(', ')}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{PageState.isOrgOwner && !x.is_owner && (
|
||||
// @ts-ignore
|
||||
<OwnerDropdown members={PageState.members} member={x} />
|
||||
<OwnerDropdown members={PageState.members} member={x} roles={roles} />
|
||||
)}
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
@@ -530,7 +547,7 @@ const MembersView = observer(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const OwnerDropdown = observer(({ members, member }: any) => {
|
||||
const OwnerDropdown = observer(({ members, member, roles }: any) => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const { ui } = useStore()
|
||||
const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '')
|
||||
@@ -552,11 +569,11 @@ const OwnerDropdown = observer(({ members, member }: any) => {
|
||||
|
||||
confirmAlert({
|
||||
title: 'Confirm to remove',
|
||||
message: `This is permanent! Are you sure you want to remove ${member.profile.username} ?`,
|
||||
message: `This is permanent! Are you sure you want to remove ${member.username} ?`,
|
||||
onAsyncConfirm: async () => {
|
||||
setLoading(true)
|
||||
const response = await delete_(`${API_URL}/organizations/${orgSlug}/members/remove`, {
|
||||
member_id: member.id,
|
||||
member_id: member.member_id,
|
||||
})
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
@@ -567,7 +584,10 @@ const OwnerDropdown = observer(({ members, member }: any) => {
|
||||
} else {
|
||||
const updatedMembers = members.filter((x: any) => x.id !== member.id)
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({ category: 'success', message: 'Successfully removed member' })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully removed member',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -595,10 +615,43 @@ const OwnerDropdown = observer(({ members, member }: any) => {
|
||||
if (newOwner) newOwner.is_owner = true
|
||||
mutateOrgMembers(updatedMembers)
|
||||
setOwnerTransferIsVisble(false)
|
||||
ui.setNotification({ category: 'success', message: 'Successfully transfered organization' })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully transfered organization',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRoleChecked(checked: boolean, roleId: number) {
|
||||
setLoading(true)
|
||||
const response = await (checked ? post : delete_)(
|
||||
`${API_URL}/users/${member.gotrue_id}/roles/${roleId}`,
|
||||
{}
|
||||
)
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to ${checked ? 'add' : 'remove'} member's role: ${response.error.message}`,
|
||||
})
|
||||
} else {
|
||||
const updatedMembers = [...members]
|
||||
const updatedMember = updatedMembers.find((x) => x.id == member.id)
|
||||
if (checked) {
|
||||
updatedMember.role_ids.push(roleId)
|
||||
} else {
|
||||
updatedMember.role_ids = updatedMember.role_ids.filter(
|
||||
(role_id: number) => role_id != roleId
|
||||
)
|
||||
}
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully ${checked ? 'added' : 'removed'} member's role`,
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Dropdown
|
||||
@@ -606,6 +659,18 @@ const OwnerDropdown = observer(({ members, member }: any) => {
|
||||
align="end"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Label>Roles</Dropdown.Label>
|
||||
{roles
|
||||
?.filter(({ name }: { name: string }) => name !== 'Owner')
|
||||
.map(({ id, name }: { id: number; name: string }) => (
|
||||
<Dropdown.Checkbox
|
||||
checked={member.role_ids.includes(id)}
|
||||
onChange={(checked) => handleRoleChecked(checked, id)}
|
||||
>
|
||||
{name}
|
||||
</Dropdown.Checkbox>
|
||||
))}
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item onClick={() => setOwnerTransferIsVisble(!ownerTransferIsVisble)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Make owner</p>
|
||||
@@ -641,8 +706,8 @@ const OwnerDropdown = observer(({ members, member }: any) => {
|
||||
text={
|
||||
<span>
|
||||
By transferring this organization, it will be solely owned by{' '}
|
||||
<span className="font-medium dark:text-white">{member.profile.username}</span>, they
|
||||
will also be able to remove you from the organization as a member
|
||||
<span className="font-medium dark:text-white">{member.username}</span>, they will also
|
||||
be able to remove you from the organization as a member
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import useSWR from 'swr'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { AutoField, NumField } from 'uniforms-bootstrap4'
|
||||
import { Button, Typography, Toggle as UIToggle } from '@supabase/ui'
|
||||
import { Button, Toggle as UIToggle, Typography } from '@supabase/ui'
|
||||
import Divider from 'components/ui/Divider'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { usePermissions, useStore } from 'hooks'
|
||||
import { authConfig } from 'stores/jsonSchema'
|
||||
import { get, patch } from 'lib/common/fetch'
|
||||
import { pluckJsonSchemaFields } from 'lib/helpers'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import Toggle from 'components/to-be-cleaned/forms/Toggle'
|
||||
import ToggleField from 'components/to-be-cleaned/forms/ToggleField'
|
||||
import SecretField from 'components/to-be-cleaned/forms/SecretField'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
const Auth: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -42,6 +42,10 @@ const Settings = () => {
|
||||
const [isCustomSMTPEnabled, setCustomSMTP] = useState<boolean>(false)
|
||||
const URL = `${API_URL}/auth/${router.query.ref}/config`
|
||||
const { data: config, error }: any = useSWR(URL, get)
|
||||
const canUpdate = usePermissions(
|
||||
PermissionAction.SQL_UPDATE,
|
||||
'postgres.public.custom_config_gotrue'
|
||||
)
|
||||
const { ref: projectRef } = router.query
|
||||
|
||||
if (error) {
|
||||
@@ -104,7 +108,10 @@ const Settings = () => {
|
||||
setModel({ ...updates })
|
||||
ui.setNotification({ category: 'success', message: 'Settings saved' })
|
||||
} catch (error: any) {
|
||||
ui.setNotification({ category: 'error', message: `Update config failed: ${error.message}` })
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Update config failed: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,16 +147,21 @@ const Settings = () => {
|
||||
name="SITE_URL"
|
||||
showInlineError
|
||||
errorMessage="Please enter a valid site url."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="URI_ALLOW_LIST"
|
||||
showInlineError
|
||||
errorMessage="Must be a comma separated list of exact URIs. No spaces."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<NumField name="JWT_EXP" step="1" />
|
||||
<NumField name="PASSWORD_MIN_LENGTH" step="1" />
|
||||
<ToggleField name="SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION" />
|
||||
<ToggleField name="DISABLE_SIGNUP" />
|
||||
<NumField name="JWT_EXP" step="1" disabled={!canUpdate} />
|
||||
<NumField name="PASSWORD_MIN_LENGTH" step="1" disabled={!canUpdate} />
|
||||
<ToggleField
|
||||
name="SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION"
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<ToggleField name="DISABLE_SIGNUP" disabled={!canUpdate} />
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
<div className="my-8 mt-0">
|
||||
@@ -190,6 +202,7 @@ const Settings = () => {
|
||||
}}
|
||||
checked={model.EXTERNAL_EMAIL_ENABLED}
|
||||
descriptionText={authConfig.properties.EXTERNAL_EMAIL_ENABLED.help}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
|
||||
<UIToggle
|
||||
@@ -201,6 +214,7 @@ const Settings = () => {
|
||||
}}
|
||||
checked={model.MAILER_SECURE_EMAIL_CHANGE_ENABLED}
|
||||
descriptionText={authConfig.properties.MAILER_SECURE_EMAIL_CHANGE_ENABLED.help}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
|
||||
<UIToggle
|
||||
@@ -214,6 +228,7 @@ const Settings = () => {
|
||||
}}
|
||||
checked={!model.MAILER_AUTOCONFIRM}
|
||||
descriptionText={authConfig.properties.MAILER_AUTOCONFIRM.help}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
|
||||
<UIToggle
|
||||
@@ -239,6 +254,7 @@ const Settings = () => {
|
||||
}
|
||||
setCustomSMTP(!isCustomSMTPEnabled)
|
||||
}}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{isCustomSMTPEnabled && (
|
||||
<>
|
||||
@@ -246,21 +262,43 @@ const Settings = () => {
|
||||
name="SMTP_ADMIN_EMAIL"
|
||||
showInlineError
|
||||
errorMessage="Please enter from email address."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="SMTP_HOST"
|
||||
showInlineError
|
||||
errorMessage="Please enter host."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="SMTP_PORT"
|
||||
showInlineError
|
||||
errorMessage="Please enter port."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="SMTP_USER"
|
||||
showInlineError
|
||||
errorMessage="Please enter user."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="SMTP_PASS"
|
||||
errorMessage="Please enter password."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField name="SMTP_HOST" showInlineError errorMessage="Please enter host." />
|
||||
<AutoField name="SMTP_PORT" showInlineError errorMessage="Please enter port." />
|
||||
<AutoField name="SMTP_USER" showInlineError errorMessage="Please enter user." />
|
||||
<SecretField name="SMTP_PASS" errorMessage="Please enter password." />
|
||||
<AutoField
|
||||
name="SMTP_SENDER_NAME"
|
||||
showInlineError
|
||||
errorMessage="Please enter from name."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<NumField
|
||||
showInlineError
|
||||
step="1"
|
||||
name="RATE_LIMIT_EMAIL_SENT"
|
||||
errorMessage="Please enter a value between 1 to 32767"
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -309,6 +347,7 @@ const Settings = () => {
|
||||
}}
|
||||
checked={model.EXTERNAL_PHONE_ENABLED}
|
||||
descriptionText={authConfig.properties.EXTERNAL_PHONE_ENABLED.help}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
|
||||
{model.EXTERNAL_PHONE_ENABLED && (
|
||||
@@ -317,6 +356,7 @@ const Settings = () => {
|
||||
name="SMS_PROVIDER"
|
||||
showInlineError
|
||||
errorMessage="Please enter the phone provider."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{smsProviderModel?.SMS_PROVIDER === 'messagebird' ? (
|
||||
<>
|
||||
@@ -324,11 +364,13 @@ const Settings = () => {
|
||||
name="SMS_MESSAGEBIRD_ACCESS_KEY"
|
||||
showInlineError
|
||||
errorMessage="Please enter the messagebird access key."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="SMS_MESSAGEBIRD_ORIGINATOR"
|
||||
showInlineError
|
||||
errorMessage="Please enter the messagebird originator."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
) : smsProviderModel?.SMS_PROVIDER === 'textlocal' ? (
|
||||
@@ -337,11 +379,13 @@ const Settings = () => {
|
||||
name="SMS_TEXTLOCAL_API_KEY"
|
||||
showInlineError
|
||||
errorMessage="Please enter the vonage account sid."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="SMS_TEXTLOCAL_SENDER"
|
||||
showInlineError
|
||||
errorMessage="Please enter the vonage auth token."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
) : smsProviderModel?.SMS_PROVIDER === 'vonage' ? (
|
||||
@@ -350,16 +394,19 @@ const Settings = () => {
|
||||
name="SMS_VONAGE_API_KEY"
|
||||
showInlineError
|
||||
errorMessage="Please enter the vonage account sid."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="SMS_VONAGE_API_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the vonage auth token."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="SMS_VONAGE_FROM"
|
||||
showInlineError
|
||||
errorMessage="Please enter the vonage message service sid."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -368,16 +415,19 @@ const Settings = () => {
|
||||
name="SMS_TWILIO_ACCOUNT_SID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the twilio account sid."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="SMS_TWILIO_AUTH_TOKEN"
|
||||
showInlineError
|
||||
errorMessage="Please enter the twilio auth token."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="SMS_TWILIO_MESSAGE_SERVICE_SID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the twilio message service sid."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -394,6 +444,7 @@ const Settings = () => {
|
||||
//
|
||||
checked={!model.SMS_AUTOCONFIRM}
|
||||
descriptionText={authConfig.properties.SMS_AUTOCONFIRM.help}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
</SchemaFormPanel>
|
||||
@@ -530,6 +581,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_APPLE_ENABLED && (
|
||||
<>
|
||||
@@ -537,11 +589,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_APPLE_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_APPLE_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -559,6 +613,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_AZURE_ENABLED && (
|
||||
<>
|
||||
@@ -566,16 +621,19 @@ const Settings = () => {
|
||||
name="EXTERNAL_AZURE_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_AZURE_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<AutoField
|
||||
name="EXTERNAL_AZURE_URL"
|
||||
showInlineError
|
||||
errorMessage="Please enter the azure tenant url."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -593,6 +651,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_BITBUCKET_ENABLED && (
|
||||
<>
|
||||
@@ -600,11 +659,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_BITBUCKET_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_BITBUCKET_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -622,6 +683,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_DISCORD_ENABLED && (
|
||||
<>
|
||||
@@ -629,11 +691,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_DISCORD_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_DISCORD_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -651,6 +715,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_FACEBOOK_ENABLED && (
|
||||
<>
|
||||
@@ -658,11 +723,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_FACEBOOK_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_FACEBOOK_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -680,6 +747,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_GITHUB_ENABLED && (
|
||||
<>
|
||||
@@ -687,11 +755,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_GITHUB_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_GITHUB_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -709,6 +779,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_GITLAB_ENABLED && (
|
||||
<>
|
||||
@@ -716,11 +787,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_GITLAB_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_GITLAB_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -738,6 +811,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_GOOGLE_ENABLED && (
|
||||
<>
|
||||
@@ -745,11 +819,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_GOOGLE_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_GOOGLE_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -797,6 +873,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_LINKEDIN_ENABLED && (
|
||||
<>
|
||||
@@ -804,11 +881,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_LINKEDIN_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_LINKEDIN_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -826,6 +905,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_NOTION_ENABLED && (
|
||||
<>
|
||||
@@ -833,11 +913,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_NOTION_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_NOTION_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -855,6 +937,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_TWITCH_ENABLED && (
|
||||
<>
|
||||
@@ -862,11 +945,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_TWITCH_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_TWITCH_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -884,6 +969,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_TWITTER_ENABLED && (
|
||||
<>
|
||||
@@ -891,11 +977,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_TWITTER_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_TWITTER_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -913,6 +1001,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_SLACK_ENABLED && (
|
||||
<>
|
||||
@@ -920,11 +1009,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_SLACK_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_SLACK_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -942,6 +1033,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_SPOTIFY_ENABLED && (
|
||||
<>
|
||||
@@ -949,11 +1041,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_SPOTIFY_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_SPOTIFY_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -1005,6 +1099,7 @@ const Settings = () => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
{externalProvidersModel.EXTERNAL_ZOOM_ENABLED && (
|
||||
<>
|
||||
@@ -1012,11 +1107,13 @@ const Settings = () => {
|
||||
name="EXTERNAL_ZOOM_CLIENT_ID"
|
||||
showInlineError
|
||||
errorMessage="Please enter the client id."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
<SecretField
|
||||
name="EXTERNAL_ZOOM_SECRET"
|
||||
showInlineError
|
||||
errorMessage="Please enter the secret."
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -1041,15 +1138,11 @@ const AuditLog = ({ interval, projectRef }: any) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
let query = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
auth.audit_log_entries
|
||||
WHERE
|
||||
created_at > CURRENT_TIMESTAMP - interval '${interval}'
|
||||
ORDER BY
|
||||
created_at DESC
|
||||
LIMIT 1000;
|
||||
select *
|
||||
from auth.audit_log_entries
|
||||
where created_at > current_timestamp - interval '${interval}'
|
||||
order by created_at desc
|
||||
limit 1000;
|
||||
`
|
||||
const response = await meta.query(query)
|
||||
if (response.error) throw response.error
|
||||
|
||||
@@ -6,19 +6,20 @@ import { groupBy, isNull } from 'lodash'
|
||||
import { toJS } from 'mobx'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
Dropdown,
|
||||
Button,
|
||||
IconSave,
|
||||
Badge,
|
||||
IconSettings,
|
||||
Button,
|
||||
Dropdown,
|
||||
IconArrowRight,
|
||||
IconHome,
|
||||
IconChevronRight,
|
||||
IconHome,
|
||||
IconPlus,
|
||||
IconSave,
|
||||
IconSettings,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { usePermissions } from 'hooks'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { METRICS, METRIC_CATEGORIES, TIME_PERIODS_REPORTS } from 'lib/constants'
|
||||
import { METRIC_CATEGORIES, METRICS, TIME_PERIODS_REPORTS } from 'lib/constants'
|
||||
import { useProjectContentStore } from 'stores/projectContentStore'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts'
|
||||
import Loading from 'components/ui/Loading'
|
||||
@@ -26,6 +27,7 @@ import EditReportModal from 'components/to-be-cleaned/Reports/EditReportModal'
|
||||
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
|
||||
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
const ReactGridLayout = WidthProvider(RGL)
|
||||
|
||||
@@ -64,6 +66,13 @@ const Reports = () => {
|
||||
const [startDate, setStartDate] = useState<any>(null)
|
||||
const [endDate, setEndDate] = useState<any>(null)
|
||||
|
||||
const canSaveReport = usePermissions(
|
||||
PermissionAction.SQL_UPDATE,
|
||||
'postgres.public.user_content',
|
||||
{
|
||||
resource: { type: 'report' },
|
||||
}
|
||||
)
|
||||
const contentStore = useProjectContentStore(ref)
|
||||
|
||||
/*
|
||||
@@ -293,7 +302,12 @@ const Reports = () => {
|
||||
<Dropdown.Checkbox
|
||||
key={metric.key}
|
||||
checked={config.layout?.find((x: any) => x.attribute === metric.key)}
|
||||
onChange={(e) => handleChartSelection({ metric, value: e })}
|
||||
onChange={(e) =>
|
||||
handleChartSelection({
|
||||
metric,
|
||||
value: e,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col space-y-0">
|
||||
<span>{metric.label}</span>
|
||||
@@ -354,7 +368,7 @@ const Reports = () => {
|
||||
<Badge color="green">There are unsaved changes</Badge>
|
||||
</div>
|
||||
)}
|
||||
{hasEdits && (
|
||||
{canSaveReport && hasEdits && (
|
||||
<Button
|
||||
type={!hasEdits ? 'default' : 'primary'}
|
||||
disabled={!hasEdits}
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useEffect, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { usePermissions, useStore, withAuth } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts'
|
||||
import Loading from 'components/ui/Loading'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
@@ -13,6 +12,7 @@ import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import { useProjectContentStore } from 'stores/projectContentStore'
|
||||
import { createReport } from 'components/to-be-cleaned/Reports/Reports.utils'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -23,6 +23,15 @@ const PageLayout: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const canCreateReport = usePermissions(
|
||||
PermissionAction.SQL_INSERT,
|
||||
'postgres.public.user_content',
|
||||
{
|
||||
resource: {
|
||||
type: 'report',
|
||||
},
|
||||
}
|
||||
)
|
||||
const contentStore = useProjectContentStore(ref)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,6 +75,7 @@ const PageLayout: NextPageWithLayout = () => {
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!canCreateReport}
|
||||
>
|
||||
<p className="text-scale-1100 text-sm">Create custom reports for your projects.</p>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
|
||||
@@ -4,18 +4,19 @@ import { observer } from 'mobx-react-lite'
|
||||
import { toJS } from 'mobx'
|
||||
import { projects } from 'stores/jsonSchema'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
import { Alert, Button, Input, IconRefreshCcw } from '@supabase/ui'
|
||||
import { Alert, Button, IconRefreshCcw, Input } from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers'
|
||||
import { post, delete_ } from 'lib/common/fetch'
|
||||
import { useStore } from 'hooks'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import { usePermissions, useStore } from 'hooks'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -54,7 +55,11 @@ const RestartServerButton: FC<RestartServerButtonProps> = observer(({ projectRef
|
||||
ui.setNotification({ category: 'success', message: 'Restarting server' })
|
||||
router.push(`/project/${projectRef}`)
|
||||
} catch (error) {
|
||||
ui.setNotification({ error, category: 'error', message: 'Unable to restart server' })
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: 'Unable to restart server',
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
closeModal()
|
||||
@@ -85,6 +90,9 @@ const GeneralSettings = observer(() => {
|
||||
const formModel = toJS(project)
|
||||
const BASIC_FIELDS = ['name']
|
||||
|
||||
const canReboot = usePermissions(PermissionAction.TENANT_INFRA_EXECUTE, 'reboot')
|
||||
const canUpdateProject = usePermissions(PermissionAction.SQL_UPDATE, 'postgres.public.projects')
|
||||
|
||||
const handleUpdateProject = async (model: any) => {
|
||||
const response = await post(`${API_URL}/projects/${project?.ref}/update`, model)
|
||||
if (response.error) {
|
||||
@@ -95,7 +103,10 @@ const GeneralSettings = observer(() => {
|
||||
} else {
|
||||
const updatedProject = response
|
||||
app.onProjectUpdated(updatedProject)
|
||||
ui.setNotification({ category: 'success', message: 'Successfully saved settings' })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully saved settings',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +119,12 @@ const GeneralSettings = observer(() => {
|
||||
model={formModel}
|
||||
onSubmit={(model: any) => handleUpdateProject(pluckObjectFields(model, BASIC_FIELDS))}
|
||||
>
|
||||
<AutoField name="name" showInlineError errorMessage="Please enter a project name" />
|
||||
<AutoField
|
||||
name="name"
|
||||
showInlineError
|
||||
errorMessage="Please enter a project name"
|
||||
disabled={!canUpdateProject}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</section>
|
||||
|
||||
@@ -132,40 +148,44 @@ const GeneralSettings = observer(() => {
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<Input readOnly disabled value={project?.region} label="Region" layout="horizontal" />
|
||||
</Panel.Content>
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p>Restart server</p>
|
||||
<div style={{ maxWidth: '420px' }}>
|
||||
<p className="text-sm opacity-50">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
{canRebot && (
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p>Restart server</p>
|
||||
<div style={{ maxWidth: '420px' }}>
|
||||
<p className="text-sm opacity-50">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel.Content>
|
||||
)}
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Panel title={<p className="uppercase">Danger Zone</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
variant="danger"
|
||||
withIcon
|
||||
title="Deleting this project will also remove your database."
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-4 block">
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</p>
|
||||
<ProjectDeleteModal project={project} />
|
||||
</div>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
{canUpdateProject && (
|
||||
<section>
|
||||
<Panel title={<p className="uppercase">Danger Zone</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
variant="danger"
|
||||
withIcon
|
||||
title="Deleting this project will also remove your database."
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<p className="block mb-4">
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</p>
|
||||
<ProjectDeleteModal project={project} />
|
||||
</div>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})
|
||||
@@ -188,7 +208,10 @@ const ProjectDeleteModal = ({ project }: any) => {
|
||||
const response = await delete_(`${API_URL}/projects/${project.ref}`)
|
||||
if (response.error) throw response.error
|
||||
app.onProjectDeleted(response)
|
||||
ui.setNotification({ category: 'success', message: `Successfully deleted ${project.name}` })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully deleted ${project.name}`,
|
||||
})
|
||||
router.push(`/`)
|
||||
} catch (error: any) {
|
||||
setLoading(false)
|
||||
|
||||
Reference in new issue
Block a user