♻️ Update api endpoint authorization

This commit is contained in:
Meier Lukas
2023-08-06 15:40:19 +02:00
parent d281a2ee98
commit 3ef12cfe12
10 changed files with 124 additions and 109 deletions

View File

@@ -12,7 +12,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
const utils = api.useContext(); const utils = api.useContext();
const { isLoading, mutate } = api.config.save.useMutation({ const { isLoading, mutate } = api.config.save.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.config.all.invalidate(); await utils.boards.all.invalidate();
modals.close(id); modals.close(id);
}, },
}); });

View File

@@ -10,7 +10,7 @@ export const DeleteBoardModal = ({ id, innerProps }: ContextModalProps<InnerProp
const utils = api.useContext(); const utils = api.useContext();
const { isLoading, mutateAsync } = api.config.delete.useMutation({ const { isLoading, mutateAsync } = api.config.delete.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.config.all.invalidate(); await utils.boards.all.invalidate();
modals.close(id); modals.close(id);
}, },
}); });

View File

@@ -25,6 +25,7 @@ import {
IconTrash, IconTrash,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { GetServerSideProps } from 'next'; import { GetServerSideProps } from 'next';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
@@ -39,6 +40,7 @@ import { api } from '~/utils/api';
const BoardsPage = () => { const BoardsPage = () => {
const context = api.useContext(); const context = api.useContext();
const { data: sessionData } = useSession();
const { data } = api.boards.all.useQuery(); const { data } = api.boards.all.useQuery();
const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({ const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({
onSettled: () => { onSettled: () => {
@@ -60,13 +62,15 @@ const BoardsPage = () => {
<Group position="apart"> <Group position="apart">
<Title mb="xl">{t('pageTitle')}</Title> <Title mb="xl">{t('pageTitle')}</Title>
<Button {sessionData?.user.isAdmin && (
onClick={openCreateBoardModal} <Button
leftIcon={<IconPlus size="1rem" />} onClick={openCreateBoardModal}
variant="default" leftIcon={<IconPlus size="1rem" />}
> variant="default"
{t('buttons.create')} >
</Button> {t('buttons.create')}
</Button>
)}
</Group> </Group>
{data && ( {data && (
@@ -143,7 +147,7 @@ const BoardsPage = () => {
> >
{t('cards.buttons.view')} {t('cards.buttons.view')}
</Button> </Button>
<Menu width={240} withinPortal> <Menu width={240} withinPortal position="bottom-end">
<Menu.Target> <Menu.Target>
<ActionIcon h={34} w={34} variant="default"> <ActionIcon h={34} w={34} variant="default">
<IconDotsVertical size="1rem" /> <IconDotsVertical size="1rem" />
@@ -160,28 +164,32 @@ const BoardsPage = () => {
> >
<Text size="sm">{t('cards.menu.setAsDefault')}</Text> <Text size="sm">{t('cards.menu.setAsDefault')}</Text>
</Menu.Item> </Menu.Item>
<Menu.Divider /> {sessionData?.user.isAdmin && (
<Menu.Item <>
onClick={async () => { <Menu.Divider />
openDeleteBoardModal({ <Menu.Item
boardName: board.name, onClick={async () => {
onConfirm: async () => { openDeleteBoardModal({
append(board.name); boardName: board.name,
// give user feedback, that it's being deleted onConfirm: async () => {
await sleep(500); append(board.name);
filter((item, _) => item !== board.name); // give user feedback, that it's being deleted
}, await sleep(500);
}); filter((item, _) => item !== board.name);
}} },
disabled={board.name === 'default'} });
icon={<IconTrash size="1rem" />} }}
color="red" disabled={board.name === 'default'}
> icon={<IconTrash size="1rem" />}
<Text size="sm">{t('cards.menu.delete.label')}</Text> color="red"
{board.name === 'default' && ( >
<Text size="xs">{t('cards.menu.delete.disabled')}</Text> <Text size="sm">{t('cards.menu.delete.label')}</Text>
)} {board.name === 'default' && (
</Menu.Item> <Text size="xs">{t('cards.menu.delete.disabled')}</Text>
)}
</Menu.Item>
</>
)}
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
</Group> </Group>

View File

@@ -1,17 +1,16 @@
import fs from 'fs'; import fs from 'fs';
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
export const boardRouter = createTRPCRouter({ export const boardRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => { all: protectedProcedure.query(async ({ ctx }) => {
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
const userSettings = await ctx.prisma.userSettings.findUniqueOrThrow({ const userSettings = await ctx.prisma.userSettings.findUniqueOrThrow({
where: { where: {
userId: ctx.session?.user.id userId: ctx.session?.user.id,
} },
}); });
return await Promise.all( return await Promise.all(
@@ -26,7 +25,7 @@ export const boardRouter = createTRPCRouter({
countApps: countApps, countApps: countApps,
countWidgets: config.widgets.length, countWidgets: config.widgets.length,
countCategories: config.categories.length, countCategories: config.categories.length,
isDefaultForUser: name === userSettings.defaultBoard isDefaultForUser: name === userSettings.defaultBoard,
}; };
}) })
); );

View File

@@ -16,13 +16,6 @@ import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
const configNameSchema = z.string().regex(/^[a-zA-Z0-9-_]+$/); const configNameSchema = z.string().regex(/^[a-zA-Z0-9-_]+$/);
export const configRouter = createTRPCRouter({ export const configRouter = createTRPCRouter({
all: publicProcedure.query(async () => {
// Get all the configs in the /data/configs folder
// All the files that end in ".json"
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
// Strip the .json extension from the file name
return files.map((file) => file.replace('.json', ''));
}),
delete: adminProcedure delete: adminProcedure
.input( .input(
z.object({ z.object({

View File

@@ -2,13 +2,13 @@ import { TRPCError } from '@trpc/server';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { z } from 'zod'; import { z } from 'zod';
import { createTRPCRouter, publicProcedure } from '../../trpc'; import { adminProcedure, createTRPCRouter } from '../../trpc';
import DockerSingleton from './DockerSingleton'; import DockerSingleton from './DockerSingleton';
const dockerActionSchema = z.enum(['remove', 'start', 'stop', 'restart']); const dockerActionSchema = z.enum(['remove', 'start', 'stop', 'restart']);
export const dockerRouter = createTRPCRouter({ export const dockerRouter = createTRPCRouter({
containers: publicProcedure.query(async () => { containers: adminProcedure.query(async () => {
try { try {
const docker = DockerSingleton.getInstance(); const docker = DockerSingleton.getInstance();
const containers = await docker.listContainers({ all: true }); const containers = await docker.listContainers({ all: true });
@@ -20,7 +20,7 @@ export const dockerRouter = createTRPCRouter({
}); });
} }
}), }),
action: publicProcedure action: adminProcedure
.input( .input(
z.object({ z.object({
id: z.string(), id: z.string(),

View File

@@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { createTRPCRouter, publicProcedure } from '../trpc'; import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
const citySchema = z.object({ const citySchema = z.object({
id: z.number(), id: z.number(),
@@ -24,7 +24,7 @@ const weatherSchema = z.object({
}); });
export const weatherRouter = createTRPCRouter({ export const weatherRouter = createTRPCRouter({
findCity: publicProcedure findCity: adminProcedure
.input( .input(
z.object({ z.object({
query: z.string().min(2), query: z.string().min(2),

View File

@@ -1,11 +1,11 @@
import { Badge, Box, Button, Card, Group, Image, SimpleGrid, Stack, Text } from '@mantine/core'; import { Badge, Box, Button, Card, Group, Image, SimpleGrid, Stack, Text } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useElementSize } from '@mantine/hooks';
import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import { useConfigContext } from '../../config/provider'; import { useConfigContext } from '../../config/provider';
import { queryClient } from '../../tools/server/configurations/tanstack/queryClient.tool';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading'; import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
@@ -32,6 +32,8 @@ interface DnsHoleControlsWidgetProps {
} }
function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
const utils = api.useContext();
const { data: sessionData } = useSession();
const { isInitialLoading, data } = useDnsHoleSummeryQuery(); const { isInitialLoading, data } = useDnsHoleSummeryQuery();
const { mutateAsync } = useDnsHoleControlMutation(); const { mutateAsync } = useDnsHoleControlMutation();
const { width, ref } = useElementSize(); const { width, ref } = useElementSize();
@@ -45,38 +47,46 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
return ( return (
<Stack justify="space-between" h={'100%'} spacing="0.25rem"> <Stack justify="space-between" h={'100%'} spacing="0.25rem">
<SimpleGrid ref={ref} cols={width > 275 ? 2 : 1} verticalSpacing="0.25rem" spacing="0.25rem"> {sessionData?.user?.isAdmin && (
<Button <SimpleGrid
onClick={async () => { ref={ref}
await mutateAsync({ cols={width > 275 ? 2 : 1}
action: 'enable', verticalSpacing="0.25rem"
configName, spacing="0.25rem"
});
await queryClient.invalidateQueries({ queryKey: ['dns-hole-summary'] });
}}
leftIcon={<IconPlayerPlay size={20} />}
variant="light"
color="green"
h="2rem"
> >
{t('enableAll')} <Button
</Button> onClick={async () => {
<Button await mutateAsync({
onClick={async () => { action: 'enable',
await mutateAsync({ configName,
action: 'disable', });
configName,
}); await utils.dnsHole.summary.invalidate();
await queryClient.invalidateQueries({ queryKey: ['dns-hole-summary'] }); }}
}} leftIcon={<IconPlayerPlay size={20} />}
leftIcon={<IconPlayerStop size={20} />} variant="light"
variant="light" color="green"
color="red" h="2rem"
h="2rem" >
> {t('enableAll')}
{t('disableAll')} </Button>
</Button> <Button
</SimpleGrid> onClick={async () => {
await mutateAsync({
action: 'disable',
configName,
});
await utils.dnsHole.summary.invalidate();
}}
leftIcon={<IconPlayerStop size={20} />}
variant="light"
color="red"
h="2rem"
>
{t('disableAll')}
</Button>
</SimpleGrid>
)}
<Stack spacing="0.25rem"> <Stack spacing="0.25rem">
{data.status.map((status, index) => { {data.status.map((status, index) => {

View File

@@ -12,6 +12,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { IconCheck, IconGitPullRequest, IconThumbDown, IconThumbUp } from '@tabler/icons-react'; import { IconCheck, IconGitPullRequest, IconThumbDown, IconThumbUp } from '@tabler/icons-react';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider'; import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
@@ -95,6 +96,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
const { data, isLoading } = useMediaRequestQuery(); const { data, isLoading } = useMediaRequestQuery();
// Use mutation to approve or deny a pending request // Use mutation to approve or deny a pending request
const decideAsync = useMediaRequestDecisionMutation(); const decideAsync = useMediaRequestDecisionMutation();
const { data: sessionData } = useSession();
if (!data || isLoading) { if (!data || isLoading) {
return <WidgetLoading />; return <WidgetLoading />;
@@ -177,7 +179,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
</Text> </Text>
</Flex> </Flex>
{item.status === MediaRequestStatus.PendingApproval && ( {item.status === MediaRequestStatus.PendingApproval && sessionData?.user?.isAdmin && (
<Group> <Group>
<Tooltip label={t('tooltips.approve')} withArrow withinPortal> <Tooltip label={t('tooltips.approve')} withArrow withinPortal>
<ActionIcon <ActionIcon

View File

@@ -3,18 +3,19 @@ import { useElementSize } from '@mantine/hooks';
import { IconFileDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons-react'; import { IconFileDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons-react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'; import duration from 'dayjs/plugin/duration';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useConfigContext } from '../../config/provider'; import { useConfigContext } from '../../config/provider';
import { MIN_WIDTH_MOBILE } from '../../constants/constants'; import { MIN_WIDTH_MOBILE } from '../../constants/constants';
import { humanFileSize } from '../../tools/humanFileSize';
import { AppIntegrationType } from '../../types/app';
import { import {
useGetUsenetInfo, useGetUsenetInfo,
usePauseUsenetQueueMutation, usePauseUsenetQueueMutation,
useResumeUsenetQueueMutation, useResumeUsenetQueueMutation,
} from '../dashDot/api'; } from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize';
import { AppIntegrationType } from '../../types/app';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
import { UsenetHistoryList } from './UsenetHistoryList'; import { UsenetHistoryList } from './UsenetHistoryList';
@@ -49,7 +50,8 @@ function UseNetTile({ widget }: UseNetTileProps) {
const downloadApps = const downloadApps =
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ?? config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
[]; [];
const { ref, width, height } = useElementSize(); const { ref, width } = useElementSize();
const { data: sessionData } = useSession();
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id); const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
const { data } = useGetUsenetInfo({ appId: selectedAppId! }); const { data } = useGetUsenetInfo({ appId: selectedAppId! });
@@ -106,30 +108,31 @@ function UseNetTile({ widget }: UseNetTileProps) {
)} )}
<Tabs.Panel value="queue"> <Tabs.Panel value="queue">
<UsenetQueueList appId={selectedAppId} /> <UsenetQueueList appId={selectedAppId} />
{!data ? null : data.paused ? ( {sessionData?.user?.isAdmin &&
<Button (!data ? null : data.paused ? (
uppercase <Button
onClick={async () => resumeAsync({ appId: selectedAppId })} uppercase
radius="xl" onClick={async () => resumeAsync({ appId: selectedAppId })}
size="xs" radius="xl"
fullWidth size="xs"
mt="sm" fullWidth
> mt="sm"
<IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')} >
</Button> <IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')}
) : ( </Button>
<Button ) : (
uppercase <Button
onClick={async () => pauseAsync({ appId: selectedAppId })} uppercase
radius="xl" onClick={async () => pauseAsync({ appId: selectedAppId })}
size="xs" radius="xl"
fullWidth size="xs"
mt="sm" fullWidth
> mt="sm"
<IconPlayerPause size={12} style={{ marginRight: 5 }} />{' '} >
{dayjs.duration(data.eta, 's').format('HH:mm')} <IconPlayerPause size={12} style={{ marginRight: 5 }} />{' '}
</Button> {dayjs.duration(data.eta, 's').format('HH:mm')}
)} </Button>
))}
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="history" style={{ display: 'flex', flexDirection: 'column' }}> <Tabs.Panel value="history" style={{ display: 'flex', flexDirection: 'column' }}>
<UsenetHistoryList appId={selectedAppId} /> <UsenetHistoryList appId={selectedAppId} />