/* eslint-disable react-hooks/rules-of-hooks */ import { useCallback, useEffect, useState } from 'react'; import { Box, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Button, Divider, Flex, FormControl, FormHelperText, FormLabel, Heading, Input, InputGroup, NumberDecrementStepper, NumberIncrementStepper, NumberInput, NumberInputField, NumberInputStepper, Skeleton, Spacer, Stack, Switch, Tab, TabList, TabPanel, TabPanels, Tabs, Text, Textarea, chakra, useClipboard, useColorModeValue, useDisclosure, useToast } from '@chakra-ui/react'; import { ChevronRightIcon } from '@chakra-ui/icons'; import { IoIosRemoveCircle } from 'react-icons/io'; import { IoClipboard, IoSave } from 'react-icons/io5'; import { AccessGroup, Organization, OrganizationMember } from '@/types'; import Editor from '@monaco-editor/react'; import { CreatableSelect, Select } from 'chakra-react-select'; import { Field, Form, Formik } from 'formik'; import Head from 'next/head'; import NextLink from 'next/link'; import { useRouter } from 'next/router'; import { useAuthContext } from '@/contexts/AuthContext'; import Layout from '@/layouts/PlatformLayout'; import DeleteDialog from '@/components/DeleteDialog'; const ChakraEditor = chakra(Editor); export default function PlatformAccessPoint() { const { query, push } = useRouter(); const { user } = useAuthContext(); const [accessPoint, setAccessPoint] = useState(null); const themeBorderColor = useColorModeValue('gray.200', 'gray.700'); const [accessGroupOptions, setAccessGroupOptions] = useState([]); const [organization, setOrganization] = useState(null); const [memberChoices, setMemberChoices] = useState([]); const [tagsOptions, setTagsOptions] = useState([]); // [{ value: 'tag', label: 'tag' } const toast = useToast(); const { hasCopied: clipboardHasCopied, setValue: clipboardSetValue, value: clipboardValue, onCopy: clipboardOnCopy } = useClipboard(''); const { isOpen: isDeleteDialogOpen, onOpen: onDeleteDialogOpen, onClose: onDeleteDialogClose } = useDisclosure(); const onDelete = useCallback(async () => { await user.getIdToken().then(async (idToken: any) => { await fetch(`/api/v1/access-points/${query.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${idToken}` } }) .then((res) => { if (res.status === 200) { return res.json(); } else { return res.json().then((json: any) => { throw new Error(json.message); }); } }) .then((data) => { toast({ title: data.message, status: 'success', duration: 5000, isClosable: true }); push(`/locations/${accessPoint?.location?.id}/access-points`); }) .catch((err) => { toast({ title: 'Error', description: err.message, status: 'error', duration: 5000, isClosable: true }); }) .finally(() => { onDeleteDialogClose(); }); }); }, [accessPoint?.location?.id, onDeleteDialogClose, push, query.id, toast, user]); useEffect(() => { if (!accessPoint) return; user.getIdToken().then(async (idToken: any) => { await fetch(`/api/v1/organizations/${accessPoint.organizationId}`, { method: 'GET', headers: { Authorization: `Bearer ${idToken}` } }) .then((res) => res.json()) .then((data) => { setOrganization(data.organization); let options = [ { label: 'Users', options: [], type: 'user' }, { label: 'Roblox Users', options: [], type: 'roblox' }, { label: 'Roblox Groups', options: [], type: 'roblox-group' }, { label: 'Card Numbers', options: [], type: 'card' } ]; let optionsMap = { user: options[0], roblox: options[1], 'roblox-group': options[2], card: options[3] } as any; Object.values(data.organization.members || {}).map((member: any) => { let label; switch (member.type) { case 'user': label = `${member.displayName} (@${member.username})`; break; case 'roblox': label = `${member.displayName} (@${member.username})`; break; case 'roblox-group': label = member.groupName || member.displayName || member.name; break; case 'card': label = member.name; break; default: label = member.id; break; }; optionsMap[(member.type as string) || 'user'].options.push({ label, value: member.formattedId !== null ? member.formattedId : member.id }); }); setMemberChoices(options); }); }); }, [accessPoint, user]); let refreshData = useCallback(async () => { setAccessPoint(null); await user.getIdToken().then(async (idToken: any) => { await fetch(`/api/v1/access-points/${query.id}`, { method: 'GET', headers: { Authorization: `Bearer ${idToken}` } }) .then((res) => { if (res.status === 200) return res.json(); push('/organizations'); switch (res.status) { case 404: throw new Error('Access point not found.'); case 403: throw new Error('You do not have permission to view this access point.'); case 401: throw new Error('You do not have permission to view this access point.'); case 500: throw new Error('An internal server error occurred.'); default: throw new Error('An unknown error occurred.'); } }) .then((data) => { setAccessPoint(data.accessPoint); if (clipboardValue === '') { clipboardSetValue(data.accessPoint?.id); } }) .catch((err) => { toast({ title: 'There was an error fetching the access point.', description: err.message, status: 'error', duration: 5000, isClosable: true }); }); }); }, [clipboardSetValue, clipboardValue, push, query.id, toast, user]); // Fetch location data useEffect(() => { if (!user) return; if (!query.id) return; refreshData(); }, [query.id, user, refreshData]); const getAccessGroupType = useCallback((ag: AccessGroup) => { if (ag.type === 'organization') { return 'Organization'; } else if (ag.type === 'location') { return ag.locationName || ag.locationId || 'Unknown'; } else { return ag.type; } }, []); const getAccessGroupOptions = useCallback( (organization: Organization) => { if (!organization) return []; const ags = Object.values(organization?.accessGroups) || []; interface Group { label: string; options: { label: string; value: string; }[]; } let groups = [] as any; ags.forEach((ag: AccessGroup) => { if (ag.type === 'location' && ag.locationId !== accessPoint?.locationId) return; // check if the group is already in the groups object if (groups.find((g: Group) => g.label === getAccessGroupType(ag))) { // if it is, add the option to the options array groups .find((g: Group) => g.label === getAccessGroupType(ag)) .options.push({ label: ag.name, value: ag.id }); } else { // if it's not, add the group to the groups array groups.push({ label: getAccessGroupType(ag), options: [ { label: ag.name, value: ag.id } ] }); } }); // sort the groups so organizations are at the bottom groups.sort((a: Group, b: Group) => { if (a.label === 'Organization') return 1; if (b.label === 'Organization') return -1; return 0; }); setAccessGroupOptions(groups); return groups; }, [accessPoint?.locationId, getAccessGroupType] ); useEffect(() => { if (!accessPoint) return; if (!accessPoint?.organization) return; getAccessGroupOptions(accessPoint?.organization); }, [accessPoint, getAccessGroupOptions]); useEffect(() => { if (!accessPoint) return; if (!user) return; user.getIdToken().then(async (idToken: any) => { await fetch(`/api/v1/locations/${accessPoint.locationId}/access-points`, { method: 'GET', headers: { Authorization: `Bearer ${idToken}` } }) .then((res) => { if (res.status === 200) return res.json(); throw new Error(`Failed to fetch access points. (${res.status})`); }) .then((data) => { let accessPoints = data.accessPoints; let res = [] as string[]; accessPoints?.forEach((accessPoint: any) => { res = [...res, ...(accessPoint?.tags || [])]; }); res = [...(new Set(res as any) as any)]; setTagsOptions([ ...(new Set( res.map((value: string) => { return { value, label: value as string }; }) ) as any) ]); }) .catch((err) => { toast({ title: 'Error', description: err.message, status: 'error', duration: 5000, isClosable: true }); }); }); }, [accessPoint, toast, user]); return ( <> {accessPoint?.name} - Restrafes XCS } fontSize={{ base: 'sm', md: 'md' }} > Platform {accessPoint?.organization?.name} {accessPoint?.location?.name} {accessPoint?.name} {accessPoint?.name || 'Loading...'} {accessPoint?.organization.name} – {accessPoint?.location.name} ({ label: tag, value: tag })) || [], description: accessPoint?.description, active: accessPoint?.config?.active, armed: accessPoint?.config?.armed, unlockTime: accessPoint?.config?.unlockTime, accessGroups: accessPoint?.config?.alwaysAllowed?.groups.map((ag: AccessGroup) => ({ label: Object.values(accessPoint?.organization?.accessGroups as AccessGroup[]).find( (oag: any) => oag.id === ag )?.name, value: ag })) as { label: string; value: any; }[], members: (accessPoint?.config?.alwaysAllowed?.members || []).map((memberId: any) => { let label; let member = Object.values(organization?.members || {}).find( (m: any) => m.formattedId === memberId ) as OrganizationMember; switch (member?.type) { case 'user': label = `${member.displayName} (@${member.username})`; break; case 'roblox': label = `${member.displayName} (@${member.username})`; break; case 'roblox-group': label = member.groupName || member.displayName || member.name; break; case 'card': label = member.name; break; default: label = memberId; break; }; return { label, value: memberId }; }) as { label: string; value: any; }[], alwaysAllowedUsers: JSON.stringify(accessPoint?.config?.alwaysAllowed?.users), scanDataDisarmed: JSON.stringify(accessPoint?.config?.scanData?.disarmed || {}, null, 3), scanDataReady: JSON.stringify(accessPoint?.config?.scanData?.ready || {}, null, 3), webhookUrl: accessPoint?.config?.webhook?.url, webhookEventGranted: accessPoint?.config?.webhook?.eventGranted, webhookEventDenied: accessPoint?.config?.webhook?.eventDenied }} onSubmit={(values, actions) => { user.getIdToken().then((token: any) => { fetch(`/api/v1/access-points/${query.id}`, { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: values.name, description: values.description || '', tags: values.tags.map((tag: any) => tag.value), config: { active: values.active, armed: values.armed, unlockTime: values.unlockTime, alwaysAllowed: { // users: JSON.parse(values.alwaysAllowedUsers), members: values.members.map((m: any) => m.value), groups: values?.accessGroups?.map((ag: any) => ag?.value) }, webhook: { url: values.webhookUrl, eventGranted: values.webhookEventGranted, eventDenied: values.webhookEventDenied }, scanData: { disarmed: values.scanDataDisarmed, ready: values.scanDataReady } } }) }) .then((res: any) => { if (res.status === 200) { return res.json(); } else { return res.json().then((json: any) => { throw new Error(json.message); }); } }) .then((data) => { toast({ title: data.message, status: 'success', duration: 5000, isClosable: true }); actions.setSubmitting(false); refreshData(); }) .catch((error) => { toast({ title: 'There was an error updating the access point.', description: error.message, status: 'error', duration: 5000, isClosable: true }); }) .finally(() => { actions.setSubmitting(false); }); }); }} > {(props) => (
General {({ field, form }: any) => ( Name )} {({ field, form }: any) => ( Description