🔐 Implement password and expiration date (#53)
* Implemented password and expiration date on conf and backend side * Implemented password modal on shared side * Add public link related tests * Fix tests
This commit is contained in:
@@ -37,6 +37,8 @@ export type DriveFileAccessLevel = 'none' | 'read' | 'write' | 'manage';
|
||||
export type DriveItemAccessInfo = {
|
||||
public?: {
|
||||
token: string;
|
||||
expiration?: number;
|
||||
password?: string;
|
||||
level: DriveFileAccessLevel;
|
||||
};
|
||||
entities: AuthEntity[];
|
||||
|
||||
+145
-30
@@ -1,9 +1,13 @@
|
||||
import A from '@atoms/link';
|
||||
import { Base, Info } from '@atoms/text';
|
||||
import { Base, Info, Subtitle } from '@atoms/text';
|
||||
import { useDriveItem, getPublicLink } from '@features/drive/hooks/use-drive-item';
|
||||
import { ToasterService } from '@features/global/services/toaster-service';
|
||||
import { copyToClipboard } from '@features/global/utils/CopyClipboard';
|
||||
import { Checkbox } from 'app/atoms/input/input-checkbox';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AccessLevel } from './common';
|
||||
import moment from 'moment';
|
||||
|
||||
export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boolean }) => {
|
||||
const { item, loading, update } = useDriveItem(id);
|
||||
@@ -11,44 +15,155 @@ export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boo
|
||||
return (
|
||||
<>
|
||||
<Base className="block mt-2 mb-1">Public link access</Base>
|
||||
<div className="flex flex-row p-4 rounded-md border overflow-hidden">
|
||||
<div className="grow">
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<Info>Anyone with this link will have access to this item.</Info>
|
||||
)}
|
||||
{item?.access_info?.public?.level === 'none' && (
|
||||
<Info>This item is not available by public link.</Info>
|
||||
)}
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<>
|
||||
<br />
|
||||
<A
|
||||
className="inline-block"
|
||||
onClick={() => {
|
||||
copyToClipboard(publicLink);
|
||||
ToasterService.success('Public link copied to clipboard');
|
||||
}}
|
||||
>
|
||||
Copy public link to clip board
|
||||
</A>
|
||||
</>
|
||||
)}
|
||||
<div className="p-4 rounded-md border">
|
||||
<div className="flex flex-row overflow-hidden w-full">
|
||||
<div className="grow">
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<Info>Anyone with this link will have access to this item.</Info>
|
||||
)}
|
||||
{item?.access_info?.public?.level === 'none' && (
|
||||
<Info>This item is not available by public link.</Info>
|
||||
)}
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<>
|
||||
<br />
|
||||
<A
|
||||
className="inline-block"
|
||||
onClick={() => {
|
||||
copyToClipboard(publicLink);
|
||||
ToasterService.success('Public link copied to clipboard');
|
||||
}}
|
||||
>
|
||||
Copy public link to clip board
|
||||
</A>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<AccessLevel
|
||||
hiddenLevels={['manage', 'write']}
|
||||
disabled={loading || disabled}
|
||||
level={item?.access_info?.public?.level || null}
|
||||
onChange={level => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: { ...(item?.access_info?.public || { token: '' }), level },
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<AccessLevel
|
||||
hiddenLevels={['manage', 'write']}
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<PublicLinkOptions
|
||||
disabled={loading || disabled}
|
||||
level={item?.access_info?.public?.level || null}
|
||||
onChange={level => {
|
||||
password={item?.access_info?.public?.password}
|
||||
expiration={item?.access_info?.public?.expiration}
|
||||
onChangePassword={(password: string) => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: { ...(item?.access_info?.public || { token: '' }), level },
|
||||
public: {
|
||||
...item!.access_info!.public!,
|
||||
password: password || '',
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
onChangeExpiration={(expiration: number) => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: {
|
||||
...item!.access_info!.public!,
|
||||
expiration: expiration || 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>{' '}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PublicLinkOptions = (props: {
|
||||
disabled?: boolean;
|
||||
password?: string;
|
||||
expiration?: number;
|
||||
onChangePassword: Function;
|
||||
onChangeExpiration: Function;
|
||||
}) => {
|
||||
const [usePassword, setUsePassword] = useState(!!props.password);
|
||||
const [password, setPassword] = useState(props.password);
|
||||
const [useExpiration, setUseExpiration] = useState((props.expiration || 0) > 0);
|
||||
const [expiration, setExpiration] = useState(props.expiration);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChangePassword(usePassword ? password : '');
|
||||
}, [usePassword, password]);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChangeExpiration(useExpiration ? expiration : 0);
|
||||
}, [useExpiration, expiration]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle className="block mt-4 mb-1">Public link security</Subtitle>
|
||||
<div className="flex items-center justify-center w-full h-10">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
onChange={s => {
|
||||
setUsePassword(s);
|
||||
if (!password && s) setPassword(Math.random().toString(36).slice(-8));
|
||||
}}
|
||||
value={!!usePassword}
|
||||
label="Password"
|
||||
/>
|
||||
<div className="grow mr-2" />
|
||||
{!!usePassword && (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
className="max-w-xs"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onClick={() => {
|
||||
if (password) copyToClipboard(password);
|
||||
ToasterService.success('Password copied to clipboard');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-full h-10">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
onChange={s => {
|
||||
setUseExpiration(s);
|
||||
if (!expiration && s) setExpiration(Date.now() + 1000 * 60 * 60 * 24 * 7);
|
||||
}}
|
||||
value={!!useExpiration}
|
||||
label="Expiration"
|
||||
/>
|
||||
{useExpiration && (expiration || 0) < Date.now() && (
|
||||
<Info className="ml-2 text-red-500">(Expired)</Info>
|
||||
)}
|
||||
{useExpiration && (expiration || 0) > Date.now() && (
|
||||
<Info className="ml-2">({moment(expiration).fromNow(true)})</Info>
|
||||
)}{' '}
|
||||
<div className="grow mr-2" />
|
||||
{!!useExpiration && (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
type="date"
|
||||
className="max-w-xs"
|
||||
value={new Date(expiration || 0).toISOString().split('T')[0]}
|
||||
onChange={e => {
|
||||
e.target.value && setExpiration(new Date(e.target.value).getTime());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,10 @@ import shortUUID from 'short-uuid';
|
||||
import Avatar from '../../../../atoms/avatar';
|
||||
import { setPublicLinkToken } from '../../../../features/drive/api-client/api-client';
|
||||
import useRouterCompany from '../../../../features/router/hooks/use-router-company';
|
||||
import { useDriveItem } from 'app/features/drive/hooks/use-drive-item';
|
||||
import { Base, Subtitle, Title } from 'app/atoms/text';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
|
||||
export default () => {
|
||||
const companyId = useRouterCompany();
|
||||
@@ -62,9 +66,70 @@ export default () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="main-view public p-4">
|
||||
<Drive initialParentId={documentId} inPublicSharing />
|
||||
<AccessChecker folderId={documentId} token={token}>
|
||||
<Drive initialParentId={documentId} inPublicSharing />
|
||||
</AccessChecker>
|
||||
</div>
|
||||
<MenusBodyLayer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AccessChecker = ({
|
||||
children,
|
||||
folderId,
|
||||
token,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
token?: string;
|
||||
folderId: string;
|
||||
}) => {
|
||||
const { details, loading, refresh } = useDriveItem(folderId);
|
||||
const [password, setPassword] = useState((token || '').split('+')[1] || '');
|
||||
|
||||
useEffect(() => {
|
||||
refresh(folderId);
|
||||
}, []);
|
||||
|
||||
if (!details?.item?.id && loading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!details?.item?.id && !loading) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div style={{ height: '20vh' }} />
|
||||
<div className="inline-block text-left max-w-sm margin-auto bg-zinc-50 dark:bg-zinc-800 rounded-md p-4">
|
||||
<Title>You don't have access to this document or folder.</Title>
|
||||
<br />
|
||||
<Base>The public link you are using may be invalid or expired.</Base>
|
||||
<br />
|
||||
<br />
|
||||
<Subtitle>I have a password</Subtitle>
|
||||
<br />
|
||||
<div className="flex items-center mt-2">
|
||||
<Input
|
||||
theme="outline"
|
||||
placeholder="Password"
|
||||
className="-mr-px rounded-r-none border-r-none"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="rounded-l-none"
|
||||
theme="primary"
|
||||
onClick={() => {
|
||||
setPublicLinkToken(token ? encodeURIComponent(token + '+' + password) : null);
|
||||
refresh(folderId);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user