mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Add record authorization checks
This commit is contained in:
@@ -26,7 +26,7 @@ export const ChatContainer = ({ nodeId }: ChatContainerProps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const role = extractNodeRole([node], workspace.userId);
|
||||
const role = extractNodeRole(node, workspace.userId);
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export const Database = ({ database, role, children }: DatabaseProps) => {
|
||||
value={{
|
||||
id: database.id,
|
||||
name: database.attributes.name,
|
||||
role,
|
||||
fields: Object.values(database.attributes.fields),
|
||||
views: Object.values(database.attributes.views),
|
||||
canEdit,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const SelectFieldOptions = ({
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const [color, setColor] = React.useState(getRandomSelectOptionColor());
|
||||
const showNewOption =
|
||||
database.canEdit &&
|
||||
allowAdd &&
|
||||
!selectOptions.some((option) => option.name === inputValue.trim());
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export const TableViewDateCell = ({ field }: TableViewDateCellProps) => {
|
||||
return (
|
||||
<DatePicker
|
||||
value={record.getDateValue(field)}
|
||||
readonly={!record.canEdit}
|
||||
onChange={(newValue) => {
|
||||
if (!record.canEdit) return;
|
||||
|
||||
|
||||
@@ -32,6 +32,21 @@ export const TableViewMultiSelectCell = ({
|
||||
selectedValues.includes(option.id)
|
||||
);
|
||||
|
||||
if (!record.canEdit) {
|
||||
return (
|
||||
<div className="flex h-full w-full cursor-pointer flex-wrap gap-1 p-1">
|
||||
{selectedOptions?.map((option) => (
|
||||
<SelectOptionBadge
|
||||
key={option.id}
|
||||
name={option.name}
|
||||
color={option.color}
|
||||
/>
|
||||
))}
|
||||
{selectedOptions?.length === 0 && ' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -29,6 +29,21 @@ export const TableViewSelectCell = ({ field }: TableViewSelectCellProps) => {
|
||||
(option) => option.id === selectedValue
|
||||
);
|
||||
|
||||
if (!record.canEdit) {
|
||||
return (
|
||||
<div className="h-full w-full cursor-pointer p-1">
|
||||
{selectedOption ? (
|
||||
<SelectOptionBadge
|
||||
name={selectedOption.name}
|
||||
color={selectedOption.color}
|
||||
/>
|
||||
) : (
|
||||
' '
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { TableViewNameCell } from '@/renderer/components/databases/tables/table-view-name-cell';
|
||||
import { TableViewFieldCell } from '@/renderer/components/databases/tables/table-view-field-cell';
|
||||
import { RecordNode } from '@colanode/core';
|
||||
import { extractNodeRole, RecordNode } from '@colanode/core';
|
||||
import { useView } from '@/renderer/contexts/view';
|
||||
import { RecordProvider } from '@/renderer/components/records/record-provider';
|
||||
import { useDatabase } from '@/renderer/contexts/database';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
|
||||
interface TableViewRowProps {
|
||||
index: number;
|
||||
@@ -10,10 +12,13 @@ interface TableViewRowProps {
|
||||
}
|
||||
|
||||
export const TableViewRow = ({ index, record }: TableViewRowProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const database = useDatabase();
|
||||
const view = useView();
|
||||
const role = extractNodeRole(record, workspace.userId) ?? database.role;
|
||||
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<RecordProvider record={record} role={role}>
|
||||
<div className="animate-fade-in flex flex-row items-center gap-0.5 border-b">
|
||||
<span
|
||||
className="flex cursor-pointer items-center justify-center text-sm text-muted-foreground"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
NodeRole,
|
||||
RecordNode,
|
||||
} from '@colanode/core';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
|
||||
interface RecordBodyProps {
|
||||
record: RecordNode;
|
||||
@@ -24,12 +25,14 @@ export const RecordBody = ({
|
||||
database,
|
||||
databaseRole,
|
||||
}: RecordBodyProps) => {
|
||||
const canEdit = hasEditorAccess(recordRole);
|
||||
const workspace = useWorkspace();
|
||||
const canEdit =
|
||||
record.createdBy === workspace.userId || hasEditorAccess(recordRole);
|
||||
|
||||
return (
|
||||
<Database database={database} role={databaseRole}>
|
||||
<ScrollArea className="h-full max-h-full w-full overflow-y-auto px-10 pb-12">
|
||||
<RecordProvider record={record}>
|
||||
<RecordProvider record={record} role={recordRole}>
|
||||
<RecordAttributes />
|
||||
</RecordProvider>
|
||||
<Separator className="my-4 w-full" />
|
||||
|
||||
@@ -10,14 +10,15 @@ import { Separator } from '@/renderer/components/ui/separator';
|
||||
import { FieldDeleteDialog } from '@/renderer/components/databases/fields/field-delete-dialog';
|
||||
import { FieldIcon } from '@/renderer/components/databases/fields/field-icon';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useDatabase } from '@/renderer/contexts/database';
|
||||
|
||||
interface RecordFieldProps {
|
||||
field: FieldAttributes;
|
||||
}
|
||||
|
||||
export const RecordField = ({ field }: RecordFieldProps) => {
|
||||
const database = useDatabase();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false);
|
||||
const canEditDatabase = true;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -31,7 +32,7 @@ export const RecordField = ({ field }: RecordFieldProps) => {
|
||||
<PopoverContent className="ml-1 flex w-72 flex-col gap-1 p-2 text-sm">
|
||||
<FieldRenameInput field={field} />
|
||||
<Separator />
|
||||
{canEditDatabase && (
|
||||
{database.canEdit && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import React from 'react';
|
||||
import { RecordContext } from '@/renderer/contexts/record';
|
||||
import { RecordNode } from '@colanode/core';
|
||||
import { hasEditorAccess, NodeRole, RecordNode } from '@colanode/core';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
|
||||
export const RecordProvider = ({
|
||||
record,
|
||||
role,
|
||||
children,
|
||||
}: {
|
||||
record: RecordNode;
|
||||
role: NodeRole;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const { mutate } = useMutation();
|
||||
|
||||
const canEdit =
|
||||
record.createdBy === workspace.userId || hasEditorAccess(role);
|
||||
|
||||
return (
|
||||
<RecordContext.Provider
|
||||
value={{
|
||||
@@ -26,7 +30,7 @@ export const RecordProvider = ({
|
||||
createdAt: record.createdAt,
|
||||
updatedBy: record.updatedBy,
|
||||
updatedAt: record.updatedAt,
|
||||
canEdit: true,
|
||||
canEdit,
|
||||
updateFieldValue: (field, value) => {
|
||||
mutate({
|
||||
input: {
|
||||
|
||||
@@ -32,6 +32,21 @@ export const RecordMultiSelectValue = ({
|
||||
selectedValues.includes(option.id)
|
||||
);
|
||||
|
||||
if (!record.canEdit) {
|
||||
return (
|
||||
<div className="flex h-full w-full cursor-pointer flex-wrap gap-1 p-1">
|
||||
{selectedOptions?.map((option) => (
|
||||
<SelectOptionBadge
|
||||
key={option.id}
|
||||
name={option.name}
|
||||
color={option.color}
|
||||
/>
|
||||
))}
|
||||
{selectedOptions?.length === 0 && ' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -27,6 +27,21 @@ export const RecordSelectValue = ({ field }: RecordSelectValueProps) => {
|
||||
|
||||
const selectedOption = field.options?.[selectedValue ?? ''];
|
||||
|
||||
if (!record.canEdit) {
|
||||
return (
|
||||
<div className="h-full w-full cursor-pointer p-1">
|
||||
{selectedOption ? (
|
||||
<SelectOptionBadge
|
||||
name={selectedOption.name}
|
||||
color={selectedOption.color}
|
||||
/>
|
||||
) : (
|
||||
' '
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SpaceSettingsDialog = ({
|
||||
defaultTab,
|
||||
}: SpaceSettingsDialogProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const role = extractNodeRole([space], workspace.userId);
|
||||
const role = extractNodeRole(space, workspace.userId);
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ interface DatePickerProps {
|
||||
className?: string;
|
||||
onChange: (date: Date | null) => void;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const DatePicker = ({
|
||||
@@ -19,11 +20,22 @@ export const DatePicker = ({
|
||||
className,
|
||||
onChange,
|
||||
placeholder,
|
||||
readonly,
|
||||
}: DatePickerProps) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const dateObj = value ? new Date(value) : undefined;
|
||||
const placeHolderText = placeholder ?? '';
|
||||
|
||||
if (readonly) {
|
||||
return (
|
||||
<div
|
||||
className={cn(!dateObj && 'text-sm text-muted-foreground', className)}
|
||||
>
|
||||
{dateObj ? dateObj.toLocaleDateString() : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
FieldAttributes,
|
||||
FieldType,
|
||||
SelectOptionAttributes,
|
||||
NodeRole,
|
||||
} from '@colanode/core';
|
||||
|
||||
interface DatabaseContext {
|
||||
@@ -13,6 +14,7 @@ interface DatabaseContext {
|
||||
views: ViewAttributes[];
|
||||
canEdit: boolean;
|
||||
canCreateRecord: boolean;
|
||||
role: NodeRole;
|
||||
createField: (type: FieldType, name: string) => void;
|
||||
renameField: (id: string, name: string) => void;
|
||||
deleteField: (id: string) => void;
|
||||
|
||||
@@ -19,12 +19,13 @@ export const extractNodeName = (attributes: NodeAttributes): string | null => {
|
||||
};
|
||||
|
||||
export const extractNodeRole = (
|
||||
ancestors: Node[],
|
||||
nodeTree: Node | Node[],
|
||||
collaboratorId: string
|
||||
): NodeRole | null => {
|
||||
const nodes = Array.isArray(nodeTree) ? nodeTree : [nodeTree];
|
||||
let role: NodeRole | null = null;
|
||||
for (const ancestor of ancestors) {
|
||||
const collaborators = extractNodeCollaborators(ancestor.attributes);
|
||||
for (const node of nodes) {
|
||||
const collaborators = extractNodeCollaborators(node.attributes);
|
||||
const collaboratorRole = collaborators[collaboratorId];
|
||||
if (collaboratorRole) {
|
||||
role = collaboratorRole;
|
||||
|
||||
@@ -30,9 +30,21 @@ export const recordModel: NodeModel = {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasCollaboratorAccess();
|
||||
if (node.createdBy === context.userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canDelete: async (context, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
canDelete: async (context, node) => {
|
||||
if (node.type !== 'record') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.createdBy === context.userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user