Skip to content

Commit b0c00d7

Browse files
committed
work privilege system using ACL
1 parent f5d3e54 commit b0c00d7

File tree

16 files changed

+200
-116
lines changed

16 files changed

+200
-116
lines changed

client/admin/ControlButtons.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function ViewOriginalButton(props) {
3232

3333
function ReplaceFileButton(props) {
3434
const {settings} = props;
35+
const disabled = !settings.can_change;
3536
const inputRef = useRef(null);
3637

3738
function replaceFile() {
@@ -55,7 +56,7 @@ function ReplaceFileButton(props) {
5556
}
5657

5758
return (<>
58-
<button type="button" onClick={replaceFile}><UploadIcon/>{gettext("Replace File")}</button>
59+
<button type="button" disabled={disabled} onClick={replaceFile}><UploadIcon/>{gettext("Replace File")}</button>
5960
<input type="file" name="replaceFile" ref={inputRef} accept={settings.file_mime_type} onChange={handleFileSelect} />
6061
</>);
6162
}

client/admin/FolderAdmin.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {useAudioSettings, useCookie, useSessionStorage} from '../common/Storage'
1818
import FileUploader from '../common/FileUploader';
1919
import FinderSettings from './FinderSettings';
2020
import FolderTabs from './FolderTabs';
21-
import MenuBar from './MenuBar';
21+
import MenuBar, {VERBOSE_HTTP_ERROR_CODES} from './MenuBar';
2222
import SelectableArea from './SelectableArea';
2323
import InodeList from './InodeList';
2424
import DraggedInodes from './DraggedInodes';
@@ -38,7 +38,7 @@ export default function FolderAdmin() {
3838
const menuBarRef = useRef(null);
3939
const folderTabsRef = useRef(null);
4040
const uploaderRef = useRef(null);
41-
const columnRefs = Object.fromEntries(settings.ancestors.map(id => [id, useRef(null)]));
41+
const columnRefs = Object.fromEntries(settings.ancestors.map(ancestor => [ancestor.id, useRef(null)]));
4242
const overlayRef = useRef(null);
4343
const downloadLinkRef = useRef(null);
4444
const [currentFolderId, setCurrentFolderId] = useState(settings.folder_id);
@@ -267,9 +267,9 @@ export default function FolderAdmin() {
267267
folderTabsRef.current.setFavoriteFolders(body.favorite_folders);
268268
}
269269
if (sourceFolderId !== targetFolderId) {
270-
inodes = inodes.filter(inode => !inode.dragged);
270+
inodes = inodes.filter(inode => !(inode.can_change && inode.dragged));
271271
}
272-
} else if (response.status === 409) {
272+
} else if (VERBOSE_HTTP_ERROR_CODES.has(response.status)) {
273273
alert(await response.text());
274274
} else {
275275
console.error(response);
@@ -306,6 +306,7 @@ export default function FolderAdmin() {
306306
<InodeList
307307
ref={columnRefs[settings.folder_id]}
308308
folderId={settings.folder_id}
309+
sortingDisabled={true}
309310
setCurrentFolder={setCurrentFolder}
310311
listRef={columnRefs[settings.folder_id]}
311312
menuBarRef={menuBarRef}
@@ -334,35 +335,37 @@ export default function FolderAdmin() {
334335
}
335336
incomplete = ancestors.length < settings.ancestors.length;
336337
}
337-
let previousFolderId = null;
338-
return ancestors.map(folderId => {
338+
let previousAncestorId = null;
339+
return ancestors.map(ancestor => {
339340
const snippet = (
340341
<FileUploader
341-
key={folderId}
342-
ref={folderId === settings.folder_id ? uploaderRef : null}
343-
folderId={folderId}
342+
key={ancestor.id}
343+
ref={ancestor.id === settings.folder_id ? uploaderRef : null}
344+
folderId={ancestor.id}
345+
disabled={!ancestor.can_change}
344346
handleUpload={handleUpload}
345347
settings={settings}
346348
multiple
347349
>
348350
<SelectableArea
349-
folderId={folderId}
351+
folderId={ancestor.id}
350352
deselectAll={deselectAll}
351-
columnRef={columnRefs[folderId]}
353+
columnRef={columnRefs[ancestor.id]}
352354
dragging={dragging}
353355
>
354356
<DroppableArea
355-
id={`column:${folderId}`}
357+
id={`column:${ancestor.id}`}
356358
className="column-droppable"
357359
currentId={`column:${currentFolderId}`}
358360
dragging={dragging}
359361
>
360362
<InodeList
361-
ref={columnRefs[folderId]}
362-
folderId={folderId}
363-
previousFolderId={previousFolderId}
363+
ref={columnRefs[ancestor.id]}
364+
folderId={ancestor.id}
365+
ancestorFolderId={previousAncestorId}
366+
sortingDisabled={!ancestor.can_change}
364367
setCurrentFolder={setCurrentFolder}
365-
listRef={columnRefs[folderId]}
368+
listRef={columnRefs[ancestor.id]}
366369
menuBarRef={menuBarRef}
367370
folderTabsRef={folderTabsRef}
368371
layout={layout}
@@ -376,7 +379,7 @@ export default function FolderAdmin() {
376379
</SelectableArea>
377380
</FileUploader>
378381
);
379-
previousFolderId = folderId;
382+
previousAncestorId = ancestor.id;
380383
return snippet;
381384
});
382385
}

client/admin/InodeList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function InodeListHeader() {
2828
const InodeList = forwardRef((props: any, forwardedRef) => {
2929
const {
3030
folderId,
31-
previousFolderId,
31+
ancestorFolderId,
3232
setCurrentFolder,
3333
menuBarRef,
3434
layout,
@@ -168,7 +168,7 @@ const InodeList = forwardRef((props: any, forwardedRef) => {
168168
}
169169

170170
return inodes.map(inode => inode.is_folder
171-
? <Folder key={inode.id} {...inode} {...props} isParent={previousFolderId === inode.id} />
171+
? <Folder key={inode.id} {...inode} {...props} isParent={ancestorFolderId === inode.id} />
172172
: <File key={inode.id} {...inode} {...props} />
173173
);
174174
}

client/admin/Item.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const dateTimeFormatter = new Intl.DateTimeFormat(
1616

1717

1818
export function DraggableItem(props) {
19+
const {sortingDisabled} = props;
1920
const {
2021
attributes,
2122
listeners,
@@ -31,7 +32,7 @@ export function DraggableItem(props) {
3132
const [searchQuery] = useSearchParam('q');
3233
const ReOrdering = useMemo(() => {
3334
return (props) => {
34-
if (searchQuery || sorting || filter.some(v => v))
35+
if (sortingDisabled || searchQuery || sorting || filter.some(v => v))
3536
return <div className="reordering"></div>;
3637
return (
3738
<DroppableArea
@@ -188,6 +189,7 @@ export function ListItem(props) {
188189
return dateTimeFormatter.format(date);
189190
}
190191

192+
const contenteditable = !settings.is_trash && props.can_change;
191193
switch (props.layout) {
192194
case 'tiles':
193195
return (
@@ -198,7 +200,7 @@ export function ListItem(props) {
198200
</FigureLabels>
199201
</FigBody>
200202
<figcaption>
201-
<div className="inode-name" contentEditable={!settings.is_trash} suppressContentEditableWarning={true} onFocus={handleFocus} onBlur={updateName} onKeyDown={updateName}>
203+
<div className="inode-name" contentEditable={contenteditable} suppressContentEditableWarning={true} onFocus={handleFocus} onBlur={updateName} onKeyDown={updateName}>
202204
{props.name}
203205
</div>
204206
</figcaption>
@@ -229,7 +231,7 @@ export function ListItem(props) {
229231
</FigBody>
230232
</div>
231233
<div>
232-
<div className="inode-name" contentEditable={!settings.is_trash} suppressContentEditableWarning={true} onFocus={handleFocus} onBlur={updateName} onKeyDown={updateName}>
234+
<div className="inode-name" contentEditable={contenteditable} suppressContentEditableWarning={true} onFocus={handleFocus} onBlur={updateName} onKeyDown={updateName}>
233235
{props.name}
234236
</div>
235237
</div>
@@ -253,7 +255,7 @@ export function ListItem(props) {
253255
</FigBody>
254256
</div>
255257
<div>
256-
<div className="inode-name" contentEditable={!settings.is_trash} suppressContentEditableWarning={true} onFocus={handleFocus} onBlur={updateName} onKeyDown={updateName}>
258+
<div className="inode-name" contentEditable={contenteditable} suppressContentEditableWarning={true} onFocus={handleFocus} onBlur={updateName} onKeyDown={updateName}>
257259
{props.name}
258260
</div>
259261
</div>

client/admin/MenuBar.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ import UploadIcon from '../icons/upload.svg';
3939
import FolderUploadIcon from '../icons/folder-upload.svg';
4040

4141

42+
export const VERBOSE_HTTP_ERROR_CODES = new Set([403, 409]);
43+
44+
4245
function MenuExtension(props) {
4346
const MenuComponent = useMemo(() => {
4447
const component = `./components/menuextension/${props.extension.component}.js`;
@@ -92,7 +95,7 @@ function ExtraMenu(props) {
9295
const current = columnRefs[settings.folder_id].current;
9396
const body = await response.json();
9497
current.setInodes([...current.inodes, {...body.new_folder, elementRef: createRef()}]); // adds new folder to the end of the list
95-
} else if (response.status === 409) {
98+
} else if (VERBOSE_HTTP_ERROR_CODES.has(response.status)) {
9699
alert(await response.text());
97100
} else {
98101
console.error(response);
@@ -122,13 +125,13 @@ function ExtraMenu(props) {
122125
icon={<MoreVerticalIcon/>}
123126
tooltip={gettext("Extra options")}
124127
>
125-
<li role="option" onClick={addFolder}>
128+
<li role="option" aria-disabled={!settings.can_change} onClick={addFolder}>
126129
<AddFolderIcon/><span>{gettext("Add new folder")}</span>
127130
</li>
128-
<li role="option" onClick={() => openUploader(false)}>
131+
<li role="option" aria-disabled={!settings.can_change} onClick={() => openUploader(false)}>
129132
<UploadIcon/><span>{gettext("Upload local files")}</span>
130133
</li>
131-
<li role="option" onClick={() => openUploader(true)}>
134+
<li role="option" aria-disabled={!settings.can_change} onClick={() => openUploader(true)}>
132135
<FolderUploadIcon/><span>{gettext("Upload local folder")}</span>
133136
</li>
134137
<li role="option" aria-disabled={numSelectedFiles === 0} onClick={downloadSelectedFiles}>
@@ -262,15 +265,15 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
262265

263266
function cutInodes() {
264267
const current = columnRefs[currentFolderId].current;
265-
if (current.inodes.find(inode => inode.selected)) {
266-
const clipboard = current.inodes.filter(inode => inode.selected).map(inode => ({
268+
if (current.inodes.find(inode => inode.selected && inode.can_change)) {
269+
const clipboard = current.inodes.filter(inode => inode.selected && inode.can_change).map(inode => ({
267270
id: inode.id,
268271
parent: inode.parent,
269272
selected: false,
270-
cutted: true
273+
cutted: true,
271274
}));
272275
setClipboard(clipboard);
273-
current.setInodes(current.inodes.map(inode => ({...inode, selected: false, cutted: inode.selected})));
276+
current.setInodes(current.inodes.map(inode => ({...inode, selected: false, cutted: inode.selected && inode.can_change})));
274277
setNumSelectedInodes(0);
275278
setNumSelectedFiles(0);
276279
}
@@ -313,7 +316,7 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
313316
columnRefs[settings.folder_id].current.setInodes(
314317
body.inodes.map(inode => ({...inode, elementRef: createRef()}))
315318
);
316-
} else if (response.status === 409) {
319+
} else if (VERBOSE_HTTP_ERROR_CODES.has(response.status)) {
317320
alert(await response.text());
318321
} else {
319322
console.error(response);
@@ -417,7 +420,7 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
417420
</MenuItem>
418421
<SortingOptions refreshFilesList={refreshColumns} />
419422
{settings.labels && <FilterByLabel refreshFilesList={refreshColumns} labels={settings.labels} />}
420-
<MenuItem aria-disabled={numSelectedInodes === 0} onClick={cutInodes} tooltip={gettext("Cut selected to clipboard")}>
423+
<MenuItem aria-disabled={numSelectedInodes === 0 || !settings.can_change} onClick={cutInodes} tooltip={gettext("Cut selected to clipboard")}>
421424
<CutIcon/>
422425
</MenuItem>
423426
{settings.is_trash ? (<>
@@ -428,10 +431,10 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
428431
<EraseIcon/>
429432
</MenuItem>
430433
</>) : (<>
431-
<MenuItem aria-disabled={clipboard.length === 0} onClick={pasteInodes} tooltip={gettext("Paste from clipboard")}>
434+
<MenuItem aria-disabled={clipboard.length === 0 || !settings.can_change} onClick={pasteInodes} tooltip={gettext("Paste from clipboard")}>
432435
<PasteIcon/>
433436
</MenuItem>
434-
<MenuItem aria-disabled={numSelectedInodes === 0} onClick={deleteInodes} tooltip={gettext("Move selected to trash folder")}>
437+
<MenuItem aria-disabled={numSelectedInodes === 0 || !settings.can_change} onClick={deleteInodes} tooltip={gettext("Move selected to trash folder")}>
435438
<TrashIcon/>
436439
</MenuItem>
437440
<ExtraMenu

client/common/DropDownMenu.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import {Tooltip, TooltipTrigger, TooltipContent} from '../common/Tooltip';
33

44

55
export default function DropDownMenu(props) {
6+
const {disabled} = props;
67
const ref = useRef(null);
78
const WrapperElement = props.wrapperElement ?? 'li';
89

910
useEffect(() => {
11+
if (disabled)
12+
return;
1013
const handleClick = (event) => {
1114
const current = ref.current as HTMLElement;
1215
let target = event.target;
@@ -36,14 +39,15 @@ export default function DropDownMenu(props) {
3639
const rootNode = ref.current.getRootNode();
3740
rootNode.addEventListener('click', handleClick);
3841
return () => rootNode.removeEventListener('click', handleClick);
39-
}, []);
42+
}, [disabled]);
4043

4144
return (
4245
<WrapperElement
4346
ref={ref}
4447
role={props.role ? `combobox ${props.role}` : 'combobox'}
4548
aria-haspopup="listbox"
4649
aria-expanded="false"
50+
aria-disabled={disabled ? 'true' : 'false'}
4751
className={props.className}
4852
>{
4953
props.tooltip ? (

client/common/FileUploader.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ProgressOverlay, ProgressBar} from './UploadProgress';
33

44

55
const FileUploader = forwardRef((props: any, forwardedRef) => {
6-
const {folderId, handleUpload} = props;
6+
const {folderId, disabled, handleUpload} = props;
77
const multiple = 'multiple' in props;
88
const fileUploadRef = useRef(null);
99
const folderUploadRef = useRef(null);
@@ -47,6 +47,8 @@ const FileUploader = forwardRef((props: any, forwardedRef) => {
4747
}
4848

4949
function uploadFiles(files: FileList) {
50+
if (disabled)
51+
return;
5052
const uploadFile = file => new Promise<Response>((resolve, reject) => {
5153
file.resolve = resolve;
5254
file.reject = reject;
@@ -59,7 +61,8 @@ const FileUploader = forwardRef((props: any, forwardedRef) => {
5961
Promise.all(promises).then(responses => {
6062
handleUpload(folderId, responses);
6163
}).catch((error) => {
62-
alert(error);
64+
alert(gettext("An error occurred during file upload."));
65+
console.error(error);
6366
}).finally(() => {
6467
setUploading([]);
6568
});
@@ -78,12 +81,20 @@ const FileUploader = forwardRef((props: any, forwardedRef) => {
7881
{props.children}
7982
<input type="file" name={`file:${folderId}`} multiple={multiple} ref={fileUploadRef} onChange={handleFileSelect} />
8083
{multiple && <input type="file" name={`folder:${folderId}`} {...directory} ref={folderUploadRef} onChange={handleFileSelect}/>}
81-
{(dragging || uploading.length > 0) &&
84+
{(dragging || uploading.length > 0) && (
85+
disabled ?
86+
<div className="progress-overlay">
87+
<div className="progress-indicator">
88+
<p>{gettext("You don't have permission to upload files to this folder.")}</p>
89+
</div>
90+
</div>
91+
:
8292
<ProgressOverlay dragging={dragging}>{
8393
uploading.map((file, index) =>
8494
<ProgressBar key={index} file={file} targetId={folderId} settings={props.settings} />
8595
)
8696
}</ProgressOverlay>
97+
)
8798
}</div>
8899
)
89100
});

client/components/editor/Image.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface GravityOption {
2222

2323
export default function Image(props) {
2424
const {settings, children} = props;
25+
const disabled = !settings.can_change;
2526
const cropFields = {
2627
x: document.getElementById('id_crop_x') as HTMLInputElement,
2728
y: document.getElementById('id_crop_y') as HTMLInputElement,
@@ -31,7 +32,7 @@ export default function Image(props) {
3132
};
3233
const gravityField = document.getElementById('id_gravity') as HTMLInputElement;
3334
const [crop, setCrop] = useState<Crop>(null);
34-
const [gravity, setGravity] = useState<string>(gravityField?.value);
35+
const [gravity, setGravity] = useState<string>(disabled ? '' : gravityField.value);
3536
const ref = useRef(null);
3637
const gravityOptions: Record<string, GravityOption> = {
3738
'': {label: gettext("Center"), icon: <GravityCenterIcon/>},
@@ -99,8 +100,8 @@ export default function Image(props) {
99100

100101
const controlButtons = [
101102
<Fragment key="clear-crop">
102-
<button type="button" onClick={() => handleChange(null)}><ClearCropIcon/>{gettext("Clear selection")}</button>
103-
<DropDownMenu className="with-caret" wrapperElement="div" label={<Fragment>{gettext("Gravity")}: {gravityOptions[gravity]?.label}</Fragment>} tooltip={gettext("Align image before cropping")}>
103+
<button type="button" disabled={disabled} onClick={() => handleChange(null)}><ClearCropIcon/>{gettext("Clear selection")}</button>
104+
<DropDownMenu className="with-caret" disabled={disabled} wrapperElement="div" label={<Fragment>{gettext("Gravity")}: {gravityOptions[gravity]?.label}</Fragment>} tooltip={gettext("Align image before cropping")}>
104105
{Object.entries(gravityOptions).map(([value, record]) => (
105106
<li key={value} value={value} role="option" aria-selected={gravity === value} onClick={() => setGravityOption(value)}>
106107
{record.icon}{record.label}
@@ -113,7 +114,7 @@ export default function Image(props) {
113114
return (<>
114115
{children}
115116
<FileDetails controlButtons={controlButtons} {...props}>
116-
<ReactCrop crop={crop} aspect={1} onChange={handleChange}>
117+
<ReactCrop crop={crop} aspect={1} disabled={disabled} onChange={handleChange}>
117118
<img className="editable" src={settings.download_url} ref={ref} />
118119
</ReactCrop>
119120
</FileDetails>

0 commit comments

Comments
 (0)