Add record authorization checks

This commit is contained in:
Hakan Shehu
2024-11-13 11:15:37 +01:00
parent 76219f0d89
commit af4f014a32
17 changed files with 120 additions and 17 deletions

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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());

View File

@@ -12,6 +12,7 @@ export const TableViewDateCell = ({ field }: TableViewDateCellProps) => {
return (
<DatePicker
value={record.getDateValue(field)}
readonly={!record.canEdit}
onChange={(newValue) => {
if (!record.canEdit) return;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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={() => {

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
},
};