Skip to content

Commit aa3a7b6

Browse files
committed
✨(frontend) integrate doc access request
When a user is redirected on the 403 page, they can now request access to the document.
1 parent 8376011 commit aa3a7b6

File tree

3 files changed

+196
-1
lines changed

3 files changed

+196
-1
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { UseQueryOptions, useMutation, useQuery } from '@tanstack/react-query';
2+
3+
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
4+
import { Access, Doc, Role } from '@/docs/doc-management';
5+
6+
import { OptionType } from '../types';
7+
8+
interface CreateDocAccessRequestParams {
9+
docId: Doc['id'];
10+
role?: Role;
11+
}
12+
13+
export const createDocAccessRequest = async ({
14+
docId,
15+
role,
16+
}: CreateDocAccessRequestParams): Promise<Access> => {
17+
const response = await fetchAPI(`documents/${docId}/ask-for-access/`, {
18+
method: 'POST',
19+
body: JSON.stringify({
20+
role,
21+
}),
22+
});
23+
24+
if (!response.ok) {
25+
throw new APIError(
26+
`Failed to create a request to access to the doc.`,
27+
await errorCauses(response, {
28+
type: OptionType.NEW_MEMBER,
29+
}),
30+
);
31+
}
32+
33+
return response.json() as Promise<Access>;
34+
};
35+
36+
export function useCreateDocAccessRequest() {
37+
return useMutation<Access, APIError, CreateDocAccessRequestParams>({
38+
mutationFn: createDocAccessRequest,
39+
});
40+
}
41+
42+
type AccessRequestResponse = APIList<Access>;
43+
44+
export const getDocAccessRequests = async (
45+
docId: Doc['id'],
46+
): Promise<AccessRequestResponse> => {
47+
const response = await fetchAPI(`documents/${docId}/ask-for-access/`);
48+
49+
if (!response.ok) {
50+
throw new APIError(
51+
'Failed to get the doc access requests',
52+
await errorCauses(response),
53+
);
54+
}
55+
56+
return response.json() as Promise<AccessRequestResponse>;
57+
};
58+
59+
export const KEY_LIST_DOC_REQUESTS_ACCESS = 'docs-requests-access';
60+
61+
export function useDocAccessRequests(
62+
params: Doc['id'],
63+
queryConfig?: UseQueryOptions<
64+
AccessRequestResponse,
65+
APIError,
66+
AccessRequestResponse
67+
>,
68+
) {
69+
return useQuery<AccessRequestResponse, APIError, AccessRequestResponse>({
70+
queryKey: [KEY_LIST_DOC_REQUESTS_ACCESS, params],
71+
queryFn: () => getDocAccessRequests(params),
72+
...queryConfig,
73+
});
74+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Button } from '@openfun/cunningham-react';
2+
import Head from 'next/head';
3+
import Image from 'next/image';
4+
import { useRouter } from 'next/router';
5+
import { useTranslation } from 'react-i18next';
6+
import styled from 'styled-components';
7+
8+
import img403 from '@/assets/icons/icon-403.png';
9+
import { Box, Icon, StyledLink, Text } from '@/components';
10+
import {
11+
useCreateDocAccessRequest,
12+
useDocAccessRequests,
13+
} from '@/features/docs/doc-share/api/useDocAccessRequest';
14+
import { MainLayout } from '@/layouts';
15+
import { NextPageWithLayout } from '@/types/next';
16+
17+
const StyledButton = styled(Button)`
18+
width: fit-content;
19+
`;
20+
21+
export function DocLayout() {
22+
const {
23+
query: { id },
24+
} = useRouter();
25+
26+
if (typeof id !== 'string') {
27+
return null;
28+
}
29+
30+
return (
31+
<>
32+
<Head>
33+
<meta name="robots" content="noindex" />
34+
</Head>
35+
36+
<MainLayout>
37+
<DocPage403 id={id} />
38+
</MainLayout>
39+
</>
40+
);
41+
}
42+
43+
interface DocProps {
44+
id: string;
45+
}
46+
47+
const DocPage403 = ({ id }: DocProps) => {
48+
const { t } = useTranslation();
49+
const { data: requests } = useDocAccessRequests(id);
50+
const { mutate: createRequest } = useCreateDocAccessRequest();
51+
52+
console.log('Access requests:', requests);
53+
54+
return (
55+
<>
56+
<Head>
57+
<title>
58+
{t('Access Denied - Error 403')} - {t('Docs')}
59+
</title>
60+
<meta
61+
property="og:title"
62+
content={`${t('Access Denied - Error 403')} - ${t('Docs')}`}
63+
key="title"
64+
/>
65+
</Head>
66+
<Box
67+
$align="center"
68+
$margin="auto"
69+
$gap="1rem"
70+
$padding={{ bottom: '2rem' }}
71+
>
72+
<Image
73+
className="c__image-system-filter"
74+
src={img403}
75+
alt={t('Image 403')}
76+
style={{
77+
maxWidth: '100%',
78+
height: 'auto',
79+
}}
80+
/>
81+
82+
<Box $align="center" $gap="0.8rem">
83+
<Text as="p" $textAlign="center" $maxWidth="350px" $theme="primary">
84+
{t('Insufficient access rights to view the document.')}
85+
</Text>
86+
87+
<Box $direction="row" $gap="0.7rem">
88+
<StyledLink href="/">
89+
<StyledButton
90+
icon={<Icon iconName="house" $theme="primary" />}
91+
color="tertiary"
92+
>
93+
{t('Home')}
94+
</StyledButton>
95+
</StyledLink>
96+
<Button
97+
onClick={() => createRequest({ docId: id })}
98+
disabled={!!requests?.count}
99+
>
100+
{t('Request access')}
101+
</Button>
102+
</Box>
103+
</Box>
104+
</Box>
105+
</>
106+
);
107+
};
108+
109+
const Page: NextPageWithLayout = () => {
110+
return null;
111+
};
112+
113+
Page.getLayout = function getLayout() {
114+
return <DocLayout />;
115+
};
116+
117+
export default Page;

src/frontend/apps/impress/src/pages/docs/[id]/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ const DocPage = ({ id }: DocProps) => {
104104

105105
if (isError && error) {
106106
if ([403, 404, 401].includes(error.status)) {
107+
let replacePath = `/${error.status}`;
108+
107109
if (error.status === 401) {
108110
if (authenticated) {
109111
queryClient.setQueryData([KEY_AUTH], {
@@ -112,9 +114,11 @@ const DocPage = ({ id }: DocProps) => {
112114
});
113115
}
114116
setAuthUrl();
117+
} else if (error.status === 403) {
118+
replacePath = `/docs/${id}/403`;
115119
}
116120

117-
void replace(`/${error.status}`);
121+
void replace(replacePath);
118122

119123
return (
120124
<Box $align="center" $justify="center" $height="100%">

0 commit comments

Comments
 (0)