diff --git a/arke/config.ts b/arke/config.ts new file mode 100644 index 0000000..665c2d7 --- /dev/null +++ b/arke/config.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const acceptedRoles = ["super_admin"]; diff --git a/components/AppFormConfigProvider/AppFormConfigProvider.tsx b/components/AppFormConfigProvider/AppFormConfigProvider.tsx index d1baed3..76b5f9d 100644 --- a/components/AppFormConfigProvider/AppFormConfigProvider.tsx +++ b/components/AppFormConfigProvider/AppFormConfigProvider.tsx @@ -17,9 +17,8 @@ import { ReactNode } from "react"; import { FormConfigProvider as FCProvider } from "@arkejs/form"; import { Autocomplete, Checkbox, Input, Json } from "@arkejs/ui"; -import AutocompleteLink, { - LinkRef, -} from "@/components/AppFormConfigProvider/components/AutocompleteLink"; +import AutocompleteLink from "@/components/AppFormConfigProvider/components/AutocompleteLink"; +import Dropzone from "@/components/Dropzone/Dropzone"; export default function AppFormConfigProvider(props: { children: ReactNode }) { return ( @@ -94,13 +93,19 @@ export default function AppFormConfigProvider(props: { children: ReactNode }) { onChange={(value) => field.onChange(JSON.parse(value))} /> ), - link: ({ field }) => ( - - ), + link: ({ field }: any) => + field?.link_ref?.id === "arke_file" ? ( + <> + { + field?.onChange(files?.[0] ?? null); + }} + /> + + ) : ( + + ), default: () => <>, }} > diff --git a/components/AppFormConfigProvider/components/AutocompleteLink.tsx b/components/AppFormConfigProvider/components/AutocompleteLink.tsx index 86d38fa..926a98c 100644 --- a/components/AppFormConfigProvider/components/AutocompleteLink.tsx +++ b/components/AppFormConfigProvider/components/AutocompleteLink.tsx @@ -19,52 +19,19 @@ import { useEffect, useState } from "react"; import { TUnit } from "@arkejs/client"; import { Autocomplete } from "@arkejs/ui"; import toast from "react-hot-toast"; - -export type LinkRef = { id: string; arke_id: "group" | "arke" }; +import { LinkRef } from "@/types/link"; +import useLinkRef from "@/hooks/useLinkRef"; type AutocompleteLinkProps = { - refLink: LinkRef; + link_ref: LinkRef; onChange: (value: any) => void; value: string; }; export default function AutocompleteLink(props: AutocompleteLinkProps) { - const { refLink, onChange } = props; + const { link_ref, onChange } = props; const client = useClient(); - const [values, setValues] = useState([]); - - useEffect(() => { - // getAll: arke / group (id: se é gruppo o arke) - // filter_keys [OR] - // params: load_links: true => getAll - if (refLink?.arke_id === "group") { - // TODO: implement getAll by group and add filters with filter_keys - // client.unit.getAll(refLink.id).then((res) => { - client.group - .getAllUnits(refLink.id) - .then((res) => { - setValues(res.data.content.items); - }) - .catch(() => - toast.error("Something went wrong during group retrieval", { - id: "error_link_group", - }) - ); - } - if (refLink?.arke_id === "arke") { - client.unit - .getAll(refLink.id) - .then((res) => { - setValues(res.data.content.items); - }) - .catch(() => - toast.error("Something went wrong during arke retrieval", { - id: "error_link_arke", - }) - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { values } = useLinkRef(link_ref); function getValue() { if (Array.isArray(props.value)) { @@ -77,22 +44,24 @@ export default function AutocompleteLink(props: AutocompleteLinkProps) { } return ( - { - if (Array.isArray(value)) { - onChange((value as TUnit[]).map((item) => item.id)); - } else { - onChange((value as TUnit).id); - } - }} - renderValue={(value) => { - return `[${(value as TUnit).arke_id}] ${ - (value as TUnit).label ?? (value as TUnit).id - }`; - }} - values={values} - value={getValue()} - /> + <> + { + if (Array.isArray(value)) { + onChange((value as TUnit[]).map((item) => item.id)); + } else { + onChange((value as TUnit).id); + } + }} + renderValue={(value) => { + return `[${(value as TUnit).arke_id}] ${ + (value as TUnit).label ?? (value as TUnit).id + }`; + }} + values={values} + value={getValue()} + /> + ); } diff --git a/components/Dropzone/Dropzone.tsx b/components/Dropzone/Dropzone.tsx new file mode 100644 index 0000000..62a5aa2 --- /dev/null +++ b/components/Dropzone/Dropzone.tsx @@ -0,0 +1,115 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from "react"; +import { useDropzone } from "react-dropzone"; +import { ArrowUpTrayIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import toast from "react-hot-toast"; +import { formatBytes } from "@/utils/file"; +interface DropzoneProps { + value?: string; + label?: string; + onChange?(files: File[]): void; +} + +interface AcceptedFile extends File { + path?: string; +} + +const maxSize = 5243000; // 5Mb as bytes +export default function Dropzone(props: DropzoneProps) { + const { value, label, onChange } = props; + const [selectedFiles, setSelectedFiles] = useState([]); + const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ + multiple: false, + maxSize, + onDrop: (acceptedFiles, fileRejections) => { + setSelectedFiles([...selectedFiles, ...acceptedFiles]); + onChange?.([...selectedFiles, ...acceptedFiles]); + fileRejections.forEach((file) => { + file.errors.forEach((err) => { + if (err.code === "file-too-large") { + toast.error(`${err.message}`); + } + + if (err.code === "file-invalid-type") { + toast.error(`${err.message}`); + } + }); + }); + }, + }); + + const removeFile = (file: File) => { + const newFiles = [...selectedFiles]; + newFiles.splice(newFiles.indexOf(file), 1); + setSelectedFiles(newFiles); + onChange?.(newFiles); + }; + + const removeAll = () => { + setSelectedFiles([]); + onChange?.([]); + }; + + const File = (file: { path?: string; size: number }) => ( +
  • + removeFile(file as File)} + /> + {file.path} - {formatBytes(file.size)} +
  • + ); + + return ( + <> + {label} + {selectedFiles.length === 0 && !value && ( +
    +
    + + +

    Upload file

    +

    {formatBytes(maxSize)} max. file size

    +
    +
    + )} + + + ); +} diff --git a/components/Icon/ExpandIcon.tsx b/components/Icon/ExpandIcon.tsx new file mode 100644 index 0000000..c28bdf0 --- /dev/null +++ b/components/Icon/ExpandIcon.tsx @@ -0,0 +1,36 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function ExpandIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/components/Permissions/PermissionInput.tsx b/components/Permissions/PermissionInput.tsx new file mode 100644 index 0000000..806a84d --- /dev/null +++ b/components/Permissions/PermissionInput.tsx @@ -0,0 +1,47 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import useClient from "@/arke/useClient"; +import { Input } from "@arkejs/ui"; +import { useRef, useState } from "react"; +import useOnClickOutside from "@/hooks/useOnClickOutside"; + +interface PermissionInputProps { + role: string; + value: string; +} +export function PermissionInput(props: PermissionInputProps) { + const client = useClient(); + const [value, setValue] = useState(props.value); + const inputRef = useRef(null); + + function onUpdateData() { + console.log(value); + } + + useOnClickOutside(inputRef, onUpdateData); + return ( + setValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onUpdateData()} + onBlur={onUpdateData} + /> + ); +} diff --git a/components/Permissions/PermissionSwitch.tsx b/components/Permissions/PermissionSwitch.tsx new file mode 100644 index 0000000..60e43d5 --- /dev/null +++ b/components/Permissions/PermissionSwitch.tsx @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import useClient from "@/arke/useClient"; +import { Switch } from "@arkejs/ui"; +import { useState } from "react"; +import toast from "react-hot-toast"; + +interface PermissionSwitchProps { + role: string; + method: string; + checked: boolean; +} + +export function PermissionSwitch(props: PermissionSwitchProps) { + const client = useClient(); + const { role, method } = props; + const [checked, setChecked] = useState(props.checked); + function onChange(status: boolean) { + console.log(role, method, status); + /*client.unit.edit("arke", role, {}).then((res) => console.log(res.data)); + setChecked(!checked); + toast.success(`Permission ${role} (${method.toUpperCase()}) updated`);*/ + // TODO: reset check if api call failed + } + + return ( + onChange(!checked)} + color="primary" + /> + ); +} diff --git a/components/Permissions/PermissionTable.tsx b/components/Permissions/PermissionTable.tsx new file mode 100644 index 0000000..5524cb4 --- /dev/null +++ b/components/Permissions/PermissionTable.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useEffect, useState } from "react"; +import { Client, LinkDirection, TUnit } from "@arkejs/client"; +import useClient from "@/arke/useClient"; +import { CrudState } from "@/types/crud"; +import { Filter, Sort, useTable } from "@arkejs/table"; +import { columns } from "@/crud/permission/columns"; +import { Table } from "@/components/Table"; +import { AddIcon } from "@/components/Icon"; +import { Button } from "@arkejs/ui"; + +const PAGE_SIZE = 5; +const fetchArkePermission = async ( + role: string, + client: Client, + page?: number, + filters?: Filter[], + sort?: Sort[] +) => { + return client.unit.topology.getLinks( + { arkeId: "arke", id: role }, + LinkDirection.Child, + { + params: { + link_type: "permission", + depth: 0, + filter: + filters && filters?.length > 0 + ? `and(${filters.map( + (f) => `${f.operator}(${f.columnId},${f.value})` + )})` + : null, + offset: (page ?? 0) * PAGE_SIZE, + limit: PAGE_SIZE, + order: sort?.map((sort) => `${sort.columnId};${sort.type}`), + }, + } + ); +}; + +export function PermissionTable(props: { role: string }) { + const { role } = props; + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState([]); + const [count, setCount] = useState(0); + const client = useClient(); + + const [crud, setCrud] = useState({ + add: false, + edit: false, + delete: false, + }); + + const { + setFilters, + tableProps, + totalCount, + sort, + setSort, + filters, + goToPage, + currentPage, + } = useTable( + typeof count !== "undefined" + ? { + pagination: { + totalCount: count, + type: "custom", + pageSize: PAGE_SIZE, + }, + columns, + sorting: { + sortable: true, + type: "custom", + }, + } + : null + ); + + const loadData = useCallback( + (page?: number, filters?: Filter[], sort?: Sort[]) => { + setIsLoading(true); + fetchArkePermission(role, client, page, filters, sort).then((res) => { + setData(res.data.content.items); + setCount(res.data.content.count); + setIsLoading(false); + }); + }, + [] + ); + + useEffect(() => { + loadData(); + }, []); + + return ( +
    + { + goToPage(page); + loadData(page, filters, sort); + }} + onFiltersChange={(filters) => { + setFilters(filters); + loadData(currentPage, filters, sort); + }} + onSortChange={(sort) => { + setSort(sort); + loadData(currentPage, filters, sort); + }} + noResult={ +
    +
    + +
    + + Create your first Permission to get started. + + Do you need a hand? Check out our documentation. +
    + +
    +
    + } + totalCount={totalCount} + /> + + ); +} diff --git a/components/Sidebar/Sidebar.tsx b/components/Sidebar/Sidebar.tsx index c15874d..49f2c0e 100644 --- a/components/Sidebar/Sidebar.tsx +++ b/components/Sidebar/Sidebar.tsx @@ -21,6 +21,7 @@ import { TagIcon, UsersIcon, Squares2X2Icon, + KeyIcon, } from "@heroicons/react/24/outline"; import { useRouter } from "next/router"; import { twMerge } from "tailwind-merge"; @@ -84,6 +85,7 @@ function Sidebar() { + { + if (err.response.status === 403) { + toast.error(`You've not permission to get units`, { + id: "permission_error", + }); + } }); }, [arke.id] diff --git a/crud/common/CrudAddEdit.tsx b/crud/common/CrudAddEdit.tsx index 2b74da6..14c5622 100644 --- a/crud/common/CrudAddEdit.tsx +++ b/crud/common/CrudAddEdit.tsx @@ -21,6 +21,7 @@ import { TBaseParameter, TResponse, TUnit } from "@arkejs/client"; import { Button, Dialog, Spinner } from "@arkejs/ui"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; +import { containsFile } from "@/utils/file"; export interface CrudProps { unitId?: string; @@ -44,9 +45,16 @@ export function CrudAddEdit(props: CrudProps) { const onFormSubmit = useCallback( (data: Record) => { + const formData = new FormData(); + if (containsFile(data)) { + Object.keys(data).map((key) => { + formData.append(key, data[key] as Blob); + }); + } + const payload = containsFile(data) ? formData : data; const promise = !unitId - ? client.unit.create(arkeId, data) - : client.unit.edit(arkeId, unitId as string, data); + ? client.unit.create(arkeId, payload) + : client.unit.edit(arkeId, unitId as string, payload); promise.then( (res) => onSubmit(res), (err) => diff --git a/crud/permission/columns.tsx b/crud/permission/columns.tsx new file mode 100644 index 0000000..f52e2ec --- /dev/null +++ b/crud/permission/columns.tsx @@ -0,0 +1,86 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Column } from "@arkejs/table"; +import { PermissionSwitch } from "@/components/Permissions/PermissionSwitch"; +import { PermissionInput } from "@/components/Permissions/PermissionInput"; +import { TUnit } from "@arkejs/client"; + +export const columns: Column[] = [ + { + id: "id", + label: "Arke ID", + }, + { + id: "label", + label: "Label", + }, + { + id: "get", + label: "Get", + render: (rowData: any) => ( + + ), + }, + { + id: "post", + label: "Post", + render: (rowData: any) => ( + + ), + }, + { + id: "put", + label: "Put", + render: (rowData: any) => ( + + ), + }, + { + id: "delete", + label: "Delete", + render: (rowData: any) => ( + + ), + }, + { + id: "filter", + label: "Filter", + render: (rowData: any) => ( + + ), + }, +]; diff --git a/hooks/useLinkRef.ts b/hooks/useLinkRef.ts new file mode 100644 index 0000000..044bc1c --- /dev/null +++ b/hooks/useLinkRef.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import useClient from "@/arke/useClient"; +import { LinkRef } from "@/types/link"; +import { TUnit } from "@arkejs/client"; + +export default function useLinkRef(link_ref: LinkRef) { + const [values, setValues] = useState([]); + const client = useClient(); + + useEffect(() => { + if (link_ref?.arke_id === "group") { + client.group + .getAllUnits(link_ref.id) + .then((res) => { + setValues(res.data.content.items as TUnit[]); + }) + .catch(() => + toast.error("Something went wrong during group retrieval", { + id: "error_link_group", + }) + ); + } + if (link_ref?.arke_id === "arke") { + client.unit + .getAll(link_ref.id) + .then((res) => { + setValues(res.data.content.items as TUnit[]); + }) + .catch(() => + toast.error("Something went wrong during arke retrieval", { + id: "error_link_arke", + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { values }; +} diff --git a/package.json b/package.json index 5e5bc6e..f9fc184 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,18 @@ "test": "jest --silent" }, "dependencies": { - "@arkejs/client": "2.4.1", + "@arkejs/client": "2.6.2", "@arkejs/form": "1.6.0", "@arkejs/table": "0.7.2", "@arkejs/ui": "0.29.0", "@heroicons/react": "^2.0.16", "cookies-next": "^2.1.1", "next": "13.2.4", - "next-auth": "^4.20.1", + "next-auth": "^4.22.1", "prismjs": "^1.29.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.7", "react-hot-toast": "2.4.1", "reactflow": "^11.7.4" diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 9b54eab..7da08b1 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -83,6 +83,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { user: { id: user.id, email: user.email, + arke_id: user.arke_id, first_name: user.first_name, last_name: user.last_name, image: "", diff --git a/pages/arke/[arkeId]/[unitId].tsx b/pages/arke/[arkeId]/[unitId].tsx index 60dca4d..f4e1b59 100644 --- a/pages/arke/[arkeId]/[unitId].tsx +++ b/pages/arke/[arkeId]/[unitId].tsx @@ -29,6 +29,7 @@ import { Layout } from "@/components/Layout"; import { PageTitle } from "@/components/PageTitle"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; +import { acceptedRoles } from "@/arke/config"; function UnitDetail({ detail }: { detail: TUnit }) { const [crud, setCrud] = useState({ @@ -109,6 +110,7 @@ function UnitDetail({ detail }: { detail: TUnit }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); diff --git a/pages/arke/[arkeId]/index.tsx b/pages/arke/[arkeId]/index.tsx index 1ee8e5d..4418b7e 100644 --- a/pages/arke/[arkeId]/index.tsx +++ b/pages/arke/[arkeId]/index.tsx @@ -27,6 +27,7 @@ import { useRouter } from "next/router"; import { useMemo, useState } from "react"; import { CodeBracketIcon } from "@heroicons/react/24/outline"; import dynamic from "next/dynamic"; +import { acceptedRoles } from "@/arke/config"; const ApiDocsDrawer = dynamic( () => import("@/components/ApiDocs").then((mod) => mod.ApiDocsDrawer), { ssr: false } @@ -111,6 +112,7 @@ function ArkeDetail({ detail }: { detail: TUnit }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); diff --git a/pages/arke/index.tsx b/pages/arke/index.tsx index 539a734..cc15839 100644 --- a/pages/arke/index.tsx +++ b/pages/arke/index.tsx @@ -35,6 +35,7 @@ import { Layout } from "@/components/Layout"; import { Table } from "@/components/Table"; import { AddIcon, EditIcon } from "@/components/Icon"; import toast from "react-hot-toast"; +import { acceptedRoles } from "@/arke/config"; const PAGE_SIZE = 10; @@ -232,6 +233,7 @@ function Arke(props: { data: TUnit[]; count: number }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); const response = await fetchArke(client); diff --git a/pages/get-started.tsx b/pages/get-started.tsx index d4cbc5e..aa8b33d 100644 --- a/pages/get-started.tsx +++ b/pages/get-started.tsx @@ -28,6 +28,7 @@ import { useRouter } from "next/router"; import { cleanId } from "../utils/helper"; import { CubeIcon } from "@heroicons/react/24/outline"; import toast from "react-hot-toast"; +import { acceptedRoles } from "@/arke/config"; const fields: Array> = [ { @@ -139,6 +140,7 @@ function GetStarted({ projects }: { projects: TUnit[] }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); diff --git a/pages/groups/index.tsx b/pages/groups/index.tsx index 3efb31a..679bdf4 100644 --- a/pages/groups/index.tsx +++ b/pages/groups/index.tsx @@ -35,6 +35,7 @@ import { Layout } from "@/components/Layout"; import { Table } from "@/components/Table"; import { AddIcon, EditIcon, TrashIcon } from "@/components/Icon"; import toast from "react-hot-toast"; +import { acceptedRoles } from "@/arke/config"; const PAGE_SIZE = 10; @@ -257,6 +258,7 @@ function Groups(props: { groups: TUnit[]; count: number }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); diff --git a/pages/index.tsx b/pages/index.tsx index ab5a6bf..16e1a70 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -26,6 +26,7 @@ import { DocumentationIcon, SupportIcon, } from "@/components/Icon"; +import { acceptedRoles } from "@/arke/config"; export default function Home() { return ( @@ -67,6 +68,9 @@ export default function Home() { ); } -export const getServerSideProps: GetServerSideProps = withAuth(() => { - return { props: {} }; -}); +export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, + () => { + return { props: {} }; + } +); diff --git a/pages/login.tsx b/pages/login.tsx index c2e68ab..3c36ea8 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -112,7 +112,6 @@ export default function Login({ csrfToken }: { csrfToken: string }) { export const getServerSideProps: GetServerSideProps = withLoggedInRedirect( async (context) => { const providers = await getProviders(); - return { props: { csrfToken: await getCsrfToken(context), diff --git a/pages/logout.tsx b/pages/logout.tsx index 2531f1a..dd4a0f3 100644 --- a/pages/logout.tsx +++ b/pages/logout.tsx @@ -21,7 +21,7 @@ import { deleteCookie } from "cookies-next"; export default function Logout() { useEffect(() => { deleteCookie("arke_project"); - signOut({ callbackUrl: "/login" }); + void signOut({ callbackUrl: "/login" }); }, []); return ( diff --git a/pages/parameters.tsx b/pages/parameters.tsx index 731f269..923bd1e 100644 --- a/pages/parameters.tsx +++ b/pages/parameters.tsx @@ -34,6 +34,7 @@ import { Layout } from "@/components/Layout"; import { Table } from "@/components/Table"; import { AddIcon, EditIcon, TrashIcon } from "@/components/Icon"; import toast from "react-hot-toast"; +import { acceptedRoles } from "@/arke/config"; const PAGE_SIZE = 10; @@ -254,6 +255,7 @@ function Parameters(props: { parameters: TUnit[]; count: number }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); diff --git a/pages/permissions/index.tsx b/pages/permissions/index.tsx new file mode 100644 index 0000000..1aea5df --- /dev/null +++ b/pages/permissions/index.tsx @@ -0,0 +1,111 @@ +/** + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useEffect, useState } from "react"; +import { Client, LinkDirection, TUnit } from "@arkejs/client"; +import { CrudState } from "@/types/crud"; +import { PageTitle } from "@/components/PageTitle"; +import { Accordion, Button } from "@arkejs/ui"; +import { getClient } from "@/arke/getClient"; +import { GetServerSideProps } from "next"; +import { withAuth } from "@/server/withAuth"; +import { Layout } from "@/components/Layout"; +import { acceptedRoles } from "@/arke/config"; +import { Filter, Sort, useTable } from "@arkejs/table"; +import { columns } from "@/crud/permission/columns"; +import { ExpandIcon } from "@/components/Icon/ExpandIcon"; +import useClient from "@/arke/useClient"; +import { AddIcon } from "@/components/Icon"; +import { Table } from "@/components/Table"; +import { PermissionTable } from "@/components/Permissions/PermissionTable"; + +function Permissions(props: { data: TUnit[]; count: number }) { + const { data } = props; + const [crud, setCrud] = useState({ + add: false, + edit: false, + delete: false, + }); + const [expanded, setExpanded] = useState>( + data.reduce((accumulator, value) => { + return { ...accumulator, [value.id]: false }; + }, {}) + ); + + return ( + + + {data.map((item) => ( +
    + + setExpanded((p) => ({ ...p, [item.id]: !expanded[item.id] })) + } + > + }> +

    {item.label}

    +
    + +
    + +
    + +
    +
    +
    +
    + ))} +
    + ); +} + +export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, + async (context) => { + const client = getClient(context); + const fetchMembers = async () => { + return client.group.topology.getLinks( + { id: "arke_auth_member", groupId: "group" }, + LinkDirection.Child, + { + params: { + link_type: "group", + depth: 0, + }, + } + ); + }; + + const response = await fetchMembers(); + return { + props: { + data: response.data.content.items, + count: response.data.content.count, + }, + }; + } +); + +export default Permissions; diff --git a/pages/users/index.tsx b/pages/users/index.tsx index 20530f9..51f52e7 100644 --- a/pages/users/index.tsx +++ b/pages/users/index.tsx @@ -35,6 +35,7 @@ import { Layout } from "@/components/Layout"; import { Table } from "@/components/Table"; import { AddIcon, EditIcon } from "@/components/Icon"; import toast from "react-hot-toast"; +import { acceptedRoles } from "@/arke/config"; const PAGE_SIZE = 10; @@ -235,6 +236,7 @@ function Users(props: { data: TUnit[]; count: number }) { } export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, async (context) => { const client = getClient(context); const response = await fetchArke(client); diff --git a/pages/visual-schema/index.tsx b/pages/visual-schema/index.tsx index cc36f23..cd55301 100644 --- a/pages/visual-schema/index.tsx +++ b/pages/visual-schema/index.tsx @@ -44,6 +44,7 @@ import { CrudState } from "@/types/crud"; import { GetServerSideProps } from "next"; import { withAuth } from "@/server/withAuth"; import NoDataNode from "@/components/VisualSchema/NoDataNode"; +import { acceptedRoles } from "@/arke/config"; const PAGE_SIZE = 100; const fetchArke = async ( @@ -256,10 +257,13 @@ function VisualSchema() { ); } -export const getServerSideProps: GetServerSideProps = withAuth(() => { - return { - props: {}, - }; -}); +export const getServerSideProps: GetServerSideProps = withAuth( + acceptedRoles, + () => { + return { + props: {}, + }; + } +); export default VisualSchema; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa4e5bc..d3e8986 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@arkejs/client': - specifier: 2.4.1 - version: 2.4.1 + specifier: 2.6.2 + version: 2.6.2 '@arkejs/form': specifier: 1.6.0 version: 1.6.0(react-dom@18.2.0)(react@18.2.0) @@ -27,8 +27,8 @@ dependencies: specifier: 13.2.4 version: 13.2.4(@babel/core@7.21.4)(react-dom@18.2.0)(react@18.2.0) next-auth: - specifier: ^4.20.1 - version: 4.20.1(next@13.2.4)(react-dom@18.2.0)(react@18.2.0) + specifier: ^4.22.1 + version: 4.22.1(next@13.2.4)(react-dom@18.2.0)(react@18.2.0) prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -38,6 +38,9 @@ dependencies: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-dropzone: + specifier: ^14.2.3 + version: 14.2.3(react@18.2.0) react-hook-form: specifier: ^7.43.7 version: 7.43.7(react@18.2.0) @@ -132,8 +135,8 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.18 - /@arkejs/client@2.4.1: - resolution: {integrity: sha512-BMTeek4vrEuSBUrxF6hk138gMrAW4QadEkWimBD2k0mTkST4EudISTsaJo9HFMD0vFtuOln71BzMClFJzss/yg==} + /@arkejs/client@2.6.2: + resolution: {integrity: sha512-49IuNO8OBcJJrZMTfWa7o2kCARtB6iF35sj4rA5mCS+rywIVf7tME6BLxsBKxZc0CWU4iG9fYyfOQ4EhaVHJTQ==} dependencies: axios: 1.4.0 transitivePeerDependencies: @@ -1910,6 +1913,11 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + /attr-accept@2.2.2: + resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} + engines: {node: '>=4'} + dev: false + /autoprefixer@10.4.14(postcss@8.4.21): resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} engines: {node: ^10 || ^12 || >=14} @@ -3020,6 +3028,13 @@ packages: flat-cache: 3.0.4 dev: true + /file-selector@0.6.0: + resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} + engines: {node: '>= 12'} + dependencies: + tslib: 2.5.0 + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -4339,8 +4354,8 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /next-auth@4.20.1(next@13.2.4)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ZcTUN4qzzZ/zJYgOW0hMXccpheWtAol8QOMdMts+LYRcsPGsqf2hEityyaKyECQVw1cWInb9dF3wYwI5GZdEmQ==} + /next-auth@4.22.1(next@13.2.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==} peerDependencies: next: ^12.2.5 || ^13 nodemailer: ^6.6.5 @@ -4445,7 +4460,6 @@ packages: /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: true /object-hash@2.2.0: resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} @@ -4883,7 +4897,6 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: true /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -4932,6 +4945,18 @@ packages: react: 18.2.0 scheduler: 0.23.0 + /react-dropzone@14.2.3(react@18.2.0): + resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + dependencies: + attr-accept: 2.2.2 + file-selector: 0.6.0 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false @@ -4961,7 +4986,6 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} diff --git a/server/withAuth.ts b/server/withAuth.ts index 7a26ffc..9a62a72 100644 --- a/server/withAuth.ts +++ b/server/withAuth.ts @@ -1,11 +1,11 @@ -/** +/* * Copyright 2023 Arkemis S.r.l. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,13 +15,13 @@ */ import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; -import { getToken } from "next-auth/jwt"; -import { getClient } from "@/arke/getClient"; -import { getCookieName } from "../utils/auth"; +import { getSession } from "next-auth/react"; +import { acceptedRoles as defaultAcceptedRoles } from "@/arke/config"; export function withAuth< P extends { [key: string]: unknown } = { [key: string]: unknown } >( + acceptedRoles: string[] = defaultAcceptedRoles, handler: ( context: GetServerSidePropsContext ) => GetServerSidePropsResult

    | Promise> @@ -29,11 +29,8 @@ export function withAuth< return async function nextGetServerSidePropsHandlerWrappedWithLoggedInRedirect( context: GetServerSidePropsContext ) { - const session = await getToken({ - req: context?.req, - cookieName: `${getCookieName()}.session-token`, - }); - const client = getClient(context); + const session = await getSession(context); + const userRole = session?.user?.arke_id; if (!session) return { @@ -43,13 +40,14 @@ export function withAuth< }, }; - if (!client.project && context.req.url !== "/get-started") + if (userRole && !acceptedRoles.includes(userRole)) { return { redirect: { - destination: "/get-started", + destination: `/404`, permanent: false, }, }; + } return handler(context); }; diff --git a/styles/globals.css b/styles/globals.css index 237dafd..b39bfa6 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -55,6 +55,10 @@ @apply btn p-2 rounded-full inline-block } + .permission-table .arke__table__actions { + @apply h-16 + } + .tabs__list { @apply bg-background-400 w-auto inline-flex gap-2 p-2 rounded-theme } @@ -154,6 +158,10 @@ } } +html { + background: #000; +} + /*Change text in autofill textbox*/ input:-webkit-autofill, input:-webkit-autofill:hover, diff --git a/tsconfig.json b/tsconfig.json index 059925d..450ccdc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,8 @@ "@/pages/*": ["pages/*"], "@/server/*": ["server/*"], "@/styles/*": ["styles/*"], - "@/types/*": ["types/*"] + "@/types/*": ["types/*"], + "@/utils/*": ["utils/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], diff --git a/types/link.ts b/types/link.ts new file mode 100644 index 0000000..f8a5723 --- /dev/null +++ b/types/link.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type LinkRef = { id: string; arke_id: "group" | "arke" }; diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index 53399e7..20ed177 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import NextAuth from 'next-auth'; -import { JWT } from 'next-auth/jwt'; +import NextAuth from "next-auth"; +import { JWT } from "next-auth/jwt"; -declare module 'next-auth' { +declare module "next-auth" { interface Session { user?: User; access_token?: string; @@ -26,6 +26,7 @@ declare module 'next-auth' { interface User { id?: number; + arke_id: string; image?: string; email: string; first_name?: string; @@ -36,7 +37,7 @@ declare module 'next-auth' { } } -declare module 'next-auth/jwt' { +declare module "next-auth/jwt" { interface JWT { user?: User; access_token: string; diff --git a/utils/file.ts b/utils/file.ts new file mode 100644 index 0000000..68cb263 --- /dev/null +++ b/utils/file.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Arkemis S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function containsFile(obj: Record) { + if (typeof obj === "object" && obj !== null) { + for (const key in obj) { + if (obj[key] instanceof File) { + return true; + } + } + } + return false; +} + +export function formatBytes(bytes: number, decimals = 2) { + if (!+bytes) return "0 Bytes"; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["Bytes", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`; +}