diff --git a/package.json b/package.json index 00cbd6d8d..ea3eea58c 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@trpc/server": "^10.37.1", "@types/bcryptjs": "^2.4.2", "@vitejs/plugin-react": "^4.0.0", + "adm-zip": "^0.5.15", "axios": "^1.0.0", "bcryptjs": "^2.4.3", "better-sqlite3": "^8.6.0", @@ -120,6 +121,7 @@ "@next/eslint-plugin-next": "^13.4.5", "@testing-library/react": "^14.0.0", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/adm-zip": "^0.5.5", "@types/better-sqlite3": "^7.6.5", "@types/cookies": "^0.7.7", "@types/dockerode": "^3.3.9", diff --git a/src/pages/api/download.ts b/src/pages/api/download.ts new file mode 100644 index 000000000..915d35626 --- /dev/null +++ b/src/pages/api/download.ts @@ -0,0 +1,34 @@ +import AdmZip from 'adm-zip'; +import fs from 'fs'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { getServerAuthSession } from '~/server/auth'; +import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerAuthSession({ req, res }); + if (!session) { + return res.status(401).end(); + } + + if (!session.user.isAdmin) { + return res.status(403).end(); + } + + const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); + + const zip = new AdmZip(); + + for (const file of files) { + const data = await getFrontendConfig(file.replace('.json', '')); + const content = JSON.stringify(data, null, 2); + zip.addFile(file, Buffer.from(content, 'utf-8')); + } + + const zipBuffer = zip.toBuffer(); + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', 'attachment; filename=board-configs.zip'); + res.setHeader('Content-Length', zipBuffer.length.toString()); + res.status(200).end(zipBuffer); +}; + +export default handler; diff --git a/src/pages/manage/boards/index.tsx b/src/pages/manage/boards/index.tsx index 52e068dcd..212c2ce3b 100644 --- a/src/pages/manage/boards/index.tsx +++ b/src/pages/manage/boards/index.tsx @@ -13,6 +13,7 @@ import { Title, } from '@mantine/core'; import { useDisclosure, useListState } from '@mantine/hooks'; +import { notifications } from '@mantine/notifications'; import { IconBox, IconCategory, @@ -20,6 +21,7 @@ import { IconCursorText, IconDeviceFloppy, IconDotsVertical, + IconDownload, IconFolderFilled, IconLock, IconLockOff, @@ -32,6 +34,8 @@ import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useTranslation } from 'next-i18next'; import Head from 'next/head'; import Link from 'next/link'; +import { useState } from 'react'; +import { RenameBoardModal } from '~/components/Dashboard/Modals/RenameBoard/RenameBoardModal'; import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal'; import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; @@ -42,16 +46,14 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; import { manageNamespaces } from '~/tools/server/translation-namespaces'; import { api } from '~/utils/api'; -import { notifications } from '@mantine/notifications'; -import { RenameBoardModal } from '~/components/Dashboard/Modals/RenameBoard/RenameBoardModal'; -import { useState } from 'react'; // Infer return type from the `getServerSideProps` function export default function BoardsPage({ - boards, - session, + boards, + session, }: InferGetServerSidePropsType) { - const [openedRenameBoardModal, { open: openRenameBoardModal, close: closeRenameBoardModal }] = useDisclosure(false); + const [openedRenameBoardModal, { open: openRenameBoardModal, close: closeRenameBoardModal }] = + useDisclosure(false); const [renameBoardName, setRenameBoardName] = useState<{ boardName: string }>(); const { data, refetch } = api.boards.all.useQuery(undefined, { @@ -79,6 +81,11 @@ export default function BoardsPage({ }); const [deletingDashboards, { append, filter }] = useListState([]); + const downloadAllBoards = async () => { + const a = document.createElement('a'); + a.href = `/api/download`; + a.click(); + }; const { t } = useTranslation('manage/boards'); @@ -90,22 +97,37 @@ export default function BoardsPage({ {metaTitle} - - board.name)} - onClose={closeRenameBoardModal} /> + + board.name)} + onClose={closeRenameBoardModal} + /> {t('pageTitle')} {session?.user.isAdmin && ( - + + + + )} @@ -200,18 +222,20 @@ export default function BoardsPage({ boardName: board.name, }); }} - icon={}> + icon={} + > {t('cards.menu.duplicate')} { setRenameBoardName({ - boardName: board.name as string + boardName: board.name as string, }); openRenameBoardModal(); }} icon={} - disabled={board.name === 'default'}> + disabled={board.name === 'default'} + > {t('cards.menu.rename.label')} const result = checkForSessionOrAskForLogin( context, session, - () => session?.user.isAdmin == true, + () => session?.user.isAdmin == true ); if (result !== undefined) { return result; @@ -281,7 +305,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => manageNamespaces, context.locale, context.req, - context.res, + context.res ); return { diff --git a/yarn.lock b/yarn.lock index c9d89b889..de9fdc395 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3022,6 +3022,15 @@ __metadata: languageName: node linkType: hard +"@types/adm-zip@npm:^0.5.5": + version: 0.5.5 + resolution: "@types/adm-zip@npm:0.5.5" + dependencies: + "@types/node": "*" + checksum: 808c25b8a1c2e1c594cf9b1514e7953105cf96e19e38aa7dc109ff2537bda7345b950ef1f4e54a6e824e5503e29d24b0ff6d0aa1ff9bd4afb79ef0ef2df9ebab + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.4 resolution: "@types/aria-query@npm:5.0.4" @@ -4031,6 +4040,13 @@ __metadata: languageName: node linkType: hard +"adm-zip@npm:^0.5.15": + version: 0.5.15 + resolution: "adm-zip@npm:0.5.15" + checksum: 23fc108ba0ead637cf8f89431bd152017d3d2eccbbac5e77bcfa3d0209029a53921d9735c5110c06b51cf223184f4cf2fdade975f20266a64183e94717a535f4 + languageName: node + linkType: hard + "aes-decrypter@npm:4.0.1, aes-decrypter@npm:^4.0.1": version: 4.0.1 resolution: "aes-decrypter@npm:4.0.1" @@ -7429,6 +7445,7 @@ __metadata: "@trpc/next": ^10.37.1 "@trpc/react-query": ^10.37.1 "@trpc/server": ^10.37.1 + "@types/adm-zip": ^0.5.5 "@types/bcryptjs": ^2.4.2 "@types/better-sqlite3": ^7.6.5 "@types/cookies": ^0.7.7 @@ -7447,6 +7464,7 @@ __metadata: "@vitest/coverage-c8": ^0.33.0 "@vitest/coverage-v8": ^0.34.5 "@vitest/ui": ^0.34.4 + adm-zip: ^0.5.15 axios: ^1.0.0 bcryptjs: ^2.4.3 better-sqlite3: ^8.6.0