🔐 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:
+2
@@ -30,6 +30,8 @@ description: Documents database models
|
|||||||
type AccessInformation = {
|
type AccessInformation = {
|
||||||
public: {
|
public: {
|
||||||
token: string;
|
token: string;
|
||||||
|
password: string;
|
||||||
|
expiration: number;
|
||||||
level: publicAccessLevel;
|
level: publicAccessLevel;
|
||||||
};
|
};
|
||||||
entities: AuthEntity[];
|
entities: AuthEntity[];
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export class DriveFile {
|
|||||||
export type AccessInformation = {
|
export type AccessInformation = {
|
||||||
public?: {
|
public?: {
|
||||||
token: string;
|
token: string;
|
||||||
|
password: string;
|
||||||
|
expiration: number;
|
||||||
level: publicAccessLevel;
|
level: publicAccessLevel;
|
||||||
};
|
};
|
||||||
entities: AuthEntity[];
|
entities: AuthEntity[];
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ export class DocumentsService {
|
|||||||
|
|
||||||
return item;
|
return item;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
this.logger.error("Failed to update drive item", error);
|
this.logger.error("Failed to update drive item", error);
|
||||||
throw new CrudException("Failed to update item", 500);
|
throw new CrudException("Failed to update item", 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export const getDefaultDriveItem = (
|
|||||||
],
|
],
|
||||||
public: {
|
public: {
|
||||||
level: "none",
|
level: "none",
|
||||||
|
password: "",
|
||||||
|
expiration: 0,
|
||||||
token: generateAccessToken(),
|
token: generateAccessToken(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -357,7 +359,7 @@ export const getAccessLevel = async (
|
|||||||
? "manage"
|
? "manage"
|
||||||
: "write";
|
: "write";
|
||||||
|
|
||||||
const publicToken = context.public_token;
|
let publicToken = context.public_token;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
item =
|
item =
|
||||||
@@ -379,7 +381,15 @@ export const getAccessLevel = async (
|
|||||||
//Public access
|
//Public access
|
||||||
if (publicToken) {
|
if (publicToken) {
|
||||||
if (!item.access_info.public.token) return "none";
|
if (!item.access_info.public.token) return "none";
|
||||||
const { token: itemToken, level: itemLevel } = item.access_info.public;
|
const { token: itemToken, level: itemLevel, password, expiration } = item.access_info.public;
|
||||||
|
if (expiration && expiration < Date.now()) return "none";
|
||||||
|
if (password) {
|
||||||
|
const data = publicToken.split("+");
|
||||||
|
if (data.length !== 2) return "none";
|
||||||
|
const [extractedPublicToken, publicTokenPassword] = data;
|
||||||
|
if (publicTokenPassword !== password) return "none";
|
||||||
|
publicToken = extractedPublicToken;
|
||||||
|
}
|
||||||
if (itemToken === publicToken) return itemLevel;
|
if (itemToken === publicToken) return itemLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ const documentSchema = {
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
token: { type: "string" },
|
token: { type: "string" },
|
||||||
|
password: { type: "string" },
|
||||||
|
expiration: { type: "number" },
|
||||||
level: { type: "string" },
|
level: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
||||||
|
import { deserialize } from "class-transformer";
|
||||||
|
import { AccessInformation, DriveFile } from "../../../src/services/documents/entities/drive-file";
|
||||||
|
import { FileVersion } from "../../../src/services/documents/entities/file-version";
|
||||||
|
import { DriveFileAccessLevel, DriveItemDetails } from "../../../src/services/documents/types";
|
||||||
|
import { init, TestPlatform } from "../setup";
|
||||||
|
import { TestDbService } from "../utils.prepare.db";
|
||||||
|
import { e2e_createDocument, e2e_updateDocument } from "./utils";
|
||||||
|
|
||||||
|
const url = "/internal/services/documents/v1";
|
||||||
|
|
||||||
|
describe("the public links feature", () => {
|
||||||
|
let platform: TestPlatform;
|
||||||
|
|
||||||
|
class DriveFileMockClass {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
added: string;
|
||||||
|
parent_id: string;
|
||||||
|
company_id: string;
|
||||||
|
access_info: AccessInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FullDriveInfoMockClass {
|
||||||
|
path: DriveFile[];
|
||||||
|
item?: DriveFile;
|
||||||
|
versions?: FileVersion[];
|
||||||
|
children: DriveFile[];
|
||||||
|
access: DriveFileAccessLevel | "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
platform = await init({
|
||||||
|
services: [
|
||||||
|
"webserver",
|
||||||
|
"database",
|
||||||
|
"applications",
|
||||||
|
"search",
|
||||||
|
"storage",
|
||||||
|
"message-queue",
|
||||||
|
"user",
|
||||||
|
"search",
|
||||||
|
"files",
|
||||||
|
"websocket",
|
||||||
|
"messages",
|
||||||
|
"auth",
|
||||||
|
"realtime",
|
||||||
|
"channels",
|
||||||
|
"counter",
|
||||||
|
"statistics",
|
||||||
|
"platform-services",
|
||||||
|
"documents",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await platform.tearDown();
|
||||||
|
await platform.app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createItem = async (): Promise<DriveFileMockClass> => {
|
||||||
|
await TestDbService.getInstance(platform, true);
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
name: "public file",
|
||||||
|
parent_id: "root",
|
||||||
|
company_id: platform.workspace.company_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const version = {};
|
||||||
|
|
||||||
|
const response = await e2e_createDocument(platform, item, version);
|
||||||
|
return deserialize<DriveFileMockClass>(DriveFileMockClass, response.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
let publicFile: DriveFileMockClass;
|
||||||
|
|
||||||
|
it("did create the drive item", async done => {
|
||||||
|
const result = await createItem();
|
||||||
|
publicFile = result;
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.name).toEqual("public file");
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.access_info).toBeDefined();
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unable to access non public file", async done => {
|
||||||
|
const res = await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should access public file", async done => {
|
||||||
|
const res = await e2e_updateDocument(platform, publicFile.id, {
|
||||||
|
...publicFile,
|
||||||
|
access_info: {
|
||||||
|
...publicFile.access_info,
|
||||||
|
public: {
|
||||||
|
...publicFile.access_info.public!,
|
||||||
|
level: "read",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const file = deserialize<DriveFileMockClass>(DriveFileMockClass, res.body);
|
||||||
|
expect(file.access_info.public?.level).toBe("read");
|
||||||
|
|
||||||
|
const resPublicRaw = await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
const resPublic = deserialize<DriveItemDetails>(FullDriveInfoMockClass, resPublicRaw.body);
|
||||||
|
expect(resPublicRaw.statusCode).toBe(200);
|
||||||
|
expect(resPublic.item?.id).toBe(publicFile.id);
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unable to access expired public file link", async done => {
|
||||||
|
await e2e_updateDocument(platform, publicFile.id, {
|
||||||
|
...publicFile,
|
||||||
|
access_info: {
|
||||||
|
...publicFile.access_info,
|
||||||
|
public: {
|
||||||
|
...publicFile.access_info.public!,
|
||||||
|
level: "read",
|
||||||
|
expiration: Date.now() + 1000 * 60, //In the future
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let resPublicRaw = await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
const resPublic = deserialize<DriveItemDetails>(FullDriveInfoMockClass, resPublicRaw.body);
|
||||||
|
expect(resPublicRaw.statusCode).toBe(200);
|
||||||
|
expect(resPublic.item?.id).toBe(publicFile.id);
|
||||||
|
|
||||||
|
await e2e_updateDocument(platform, publicFile.id, {
|
||||||
|
...publicFile,
|
||||||
|
access_info: {
|
||||||
|
...publicFile.access_info,
|
||||||
|
public: {
|
||||||
|
...publicFile.access_info.public!,
|
||||||
|
level: "read",
|
||||||
|
expiration: 123, //In the past
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
resPublicRaw = await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
expect(resPublicRaw.statusCode).toBe(401);
|
||||||
|
|
||||||
|
await e2e_updateDocument(platform, publicFile.id, {
|
||||||
|
...publicFile,
|
||||||
|
access_info: {
|
||||||
|
...publicFile.access_info,
|
||||||
|
public: {
|
||||||
|
...publicFile.access_info.public!,
|
||||||
|
level: "read",
|
||||||
|
expiration: 0, //Reset to default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("access public file link with password", async done => {
|
||||||
|
await e2e_updateDocument(platform, publicFile.id, {
|
||||||
|
...publicFile,
|
||||||
|
access_info: {
|
||||||
|
...publicFile.access_info,
|
||||||
|
public: {
|
||||||
|
...publicFile.access_info.public!,
|
||||||
|
level: "read",
|
||||||
|
password: "abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let resPublicRaw = await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${
|
||||||
|
publicFile.access_info.public?.token
|
||||||
|
}%2B${"abcdef"}`,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
let resPublic = deserialize<DriveItemDetails>(FullDriveInfoMockClass, resPublicRaw.body);
|
||||||
|
expect(resPublicRaw.statusCode).toBe(200);
|
||||||
|
expect(resPublic.item?.id).toBe(publicFile.id);
|
||||||
|
|
||||||
|
resPublicRaw = await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
expect(resPublicRaw.statusCode).toBe(401);
|
||||||
|
|
||||||
|
await e2e_updateDocument(platform, publicFile.id, {
|
||||||
|
...publicFile,
|
||||||
|
access_info: {
|
||||||
|
...publicFile.access_info,
|
||||||
|
public: {
|
||||||
|
...publicFile.access_info.public!,
|
||||||
|
level: "read",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
done?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,6 +37,8 @@ export type DriveFileAccessLevel = 'none' | 'read' | 'write' | 'manage';
|
|||||||
export type DriveItemAccessInfo = {
|
export type DriveItemAccessInfo = {
|
||||||
public?: {
|
public?: {
|
||||||
token: string;
|
token: string;
|
||||||
|
expiration?: number;
|
||||||
|
password?: string;
|
||||||
level: DriveFileAccessLevel;
|
level: DriveFileAccessLevel;
|
||||||
};
|
};
|
||||||
entities: AuthEntity[];
|
entities: AuthEntity[];
|
||||||
|
|||||||
+145
-30
@@ -1,9 +1,13 @@
|
|||||||
import A from '@atoms/link';
|
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 { useDriveItem, getPublicLink } from '@features/drive/hooks/use-drive-item';
|
||||||
import { ToasterService } from '@features/global/services/toaster-service';
|
import { ToasterService } from '@features/global/services/toaster-service';
|
||||||
import { copyToClipboard } from '@features/global/utils/CopyClipboard';
|
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 { AccessLevel } from './common';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boolean }) => {
|
export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boolean }) => {
|
||||||
const { item, loading, update } = useDriveItem(id);
|
const { item, loading, update } = useDriveItem(id);
|
||||||
@@ -11,44 +15,155 @@ export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boo
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Base className="block mt-2 mb-1">Public link access</Base>
|
<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="p-4 rounded-md border">
|
||||||
<div className="grow">
|
<div className="flex flex-row overflow-hidden w-full">
|
||||||
{item?.access_info?.public?.level !== 'none' && (
|
<div className="grow">
|
||||||
<Info>Anyone with this link will have access to this item.</Info>
|
{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' && (
|
||||||
)}
|
<Info>This item is not available by public link.</Info>
|
||||||
{item?.access_info?.public?.level !== 'none' && (
|
)}
|
||||||
<>
|
{item?.access_info?.public?.level !== 'none' && (
|
||||||
<br />
|
<>
|
||||||
<A
|
<br />
|
||||||
className="inline-block"
|
<A
|
||||||
onClick={() => {
|
className="inline-block"
|
||||||
copyToClipboard(publicLink);
|
onClick={() => {
|
||||||
ToasterService.success('Public link copied to clipboard');
|
copyToClipboard(publicLink);
|
||||||
}}
|
ToasterService.success('Public link copied to clipboard');
|
||||||
>
|
}}
|
||||||
Copy public link to clip board
|
>
|
||||||
</A>
|
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>
|
||||||
<div className="shrink-0">
|
{item?.access_info?.public?.level !== 'none' && (
|
||||||
<AccessLevel
|
<PublicLinkOptions
|
||||||
hiddenLevels={['manage', 'write']}
|
|
||||||
disabled={loading || disabled}
|
disabled={loading || disabled}
|
||||||
level={item?.access_info?.public?.level || null}
|
password={item?.access_info?.public?.password}
|
||||||
onChange={level => {
|
expiration={item?.access_info?.public?.expiration}
|
||||||
|
onChangePassword={(password: string) => {
|
||||||
update({
|
update({
|
||||||
access_info: {
|
access_info: {
|
||||||
entities: item?.access_info.entities || [],
|
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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import shortUUID from 'short-uuid';
|
|||||||
import Avatar from '../../../../atoms/avatar';
|
import Avatar from '../../../../atoms/avatar';
|
||||||
import { setPublicLinkToken } from '../../../../features/drive/api-client/api-client';
|
import { setPublicLinkToken } from '../../../../features/drive/api-client/api-client';
|
||||||
import useRouterCompany from '../../../../features/router/hooks/use-router-company';
|
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 () => {
|
export default () => {
|
||||||
const companyId = useRouterCompany();
|
const companyId = useRouterCompany();
|
||||||
@@ -62,9 +66,70 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="main-view public p-4">
|
<div className="main-view public p-4">
|
||||||
<Drive initialParentId={documentId} inPublicSharing />
|
<AccessChecker folderId={documentId} token={token}>
|
||||||
|
<Drive initialParentId={documentId} inPublicSharing />
|
||||||
|
</AccessChecker>
|
||||||
</div>
|
</div>
|
||||||
<MenusBodyLayer />
|
<MenusBodyLayer />
|
||||||
</div>
|
</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