Move metamigo assets to metamigo-add

This commit is contained in:
Darren Clarke 2023-08-25 11:04:38 +02:00
parent aab5b7f5d5
commit 28f7f0f47b
71 changed files with 3 additions and 2 deletions

View file

@ -1,16 +0,0 @@
"use client";
import { FC } from "react";
import { ApolloProvider } from "@apollo/client";
import { apolloClient } from "../_lib/apollo-client";
import dynamic from "next/dynamic";
const MetamigoAdmin = dynamic(() => import("./MetamigoAdmin"), {
ssr: false,
});
export const Admin: FC = () => (
<ApolloProvider client={apolloClient}>
<MetamigoAdmin />
</ApolloProvider>
);

View file

@ -1,88 +0,0 @@
"use client";
import { FC, useEffect } from "react";
import { CircularProgress, Typography, Grid } from "@mui/material";
import { signIn, signOut, getSession } from "next-auth/react";
import { useLogin, useTranslate } from "react-admin";
export const authProvider = {
login(o: any) {
if (o.ok) return Promise.resolve();
return Promise.reject();
},
async logout() {
const session = await getSession();
if (session) {
await signOut();
}
},
checkError(e: any) {
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
const permDenied = e.graphQLErrors.some((e: any) =>
e.message.match(/.*permission denied.*/),
);
if (permDenied)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({ message: "auth.permissionDenied" });
}
if (e.networkError && e.networkError.statusCode === 401) {
return Promise.reject();
}
return Promise.resolve();
},
async checkAuth() {
const session = await getSession();
if (!session) {
throw new Error("Invalid session");
}
},
async getIdentity() {
const session = await getSession();
if (!session) throw new Error("Invalid session");
return {
id: session.user?.email,
fullName: session.user?.name,
avatar: session.user?.image,
};
},
getPermissions: () => Promise.resolve(),
};
export const AdminLogin: FC = () => {
const reactAdminLogin = useLogin();
const translate = useTranslate();
useEffect(() => {
(async () => {
const session = await getSession();
if (session) {
reactAdminLogin({ ok: true });
} else {
signIn();
}
})();
});
return (
<Grid
container
spacing={5}
direction="column"
alignItems="center"
justifyContent="center"
style={{ minHeight: "100vh" }}
>
<Grid item xs={3}>
<Typography variant="h4" color="textSecondary">
{translate("auth.loggingIn")}
</Typography>
</Grid>
<Grid item xs={3}>
<CircularProgress size={80} />
</Grid>
</Grid>
);
};

View file

@ -1,27 +0,0 @@
.input {
width: 40px;
height: 60px;
margin: 5px;
font-size: 1.4rem;
padding: 0 9px 0 12px;
border: none;
outline: none;
background: white;
font-weight: 400;
color: rgba(59, 59, 59, 0.788);
-webkit-box-shadow: 2px 2px 2px 1px #d6d6d6, -1px -1px 1px #e6e6e6;
box-shadow: 2px 2px 2px 1px #d6d6d6, -1px -1px 1px #e6e6e6;
border-radius: 5px;
}
.group {
margin-top: 1.6rem;
}
.hyphen {
background: black;
height: 0.1em;
width: 1em;
margin: 0 0.5em;
display: inline-block;
}

View file

@ -1,62 +0,0 @@
"use client";
/* eslint-disable react/display-name */
import { forwardRef } from "react";
import useDigitInput, { InputAttributes } from "react-digit-input";
import styles from "./DigitInput.module.css";
const DigitInputElement = forwardRef<
HTMLInputElement,
Omit<InputAttributes, "ref"> & {
autoFocus?: boolean;
}
>(({ ...props }, ref): any => (
<>
<input
aria-label="verification code"
className={styles.input}
{...props}
ref={ref}
inputMode="decimal"
/>
</>
));
const DigitSeparator = forwardRef<
HTMLInputElement,
Omit<InputAttributes, "ref"> & {
autoFocus?: boolean;
}
>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ ...props }, ref): any => (
<>
<span className={styles.hyphen} ref={ref} />
</>
)
);
export const SixDigitInput = ({ value, onChange }: any) => {
const digits = useDigitInput({
acceptedCharacters: /^\d$/,
length: 6,
value,
onChange,
});
return (
<div>
<div className={styles.group}>
<DigitInputElement autoFocus {...digits[0]} />
<DigitInputElement {...digits[1]} />
<DigitInputElement {...digits[2]} />
<DigitSeparator />
<DigitInputElement {...digits[3]} />
<DigitInputElement {...digits[4]} />
<DigitInputElement {...digits[5]} />
</div>
<pre hidden>
<code>{value}</code>
</pre>
</div>
);
};

View file

@ -1,66 +0,0 @@
// @ts-nocheck
"use client";
import { FC, useEffect, useState } from "react";
import { Admin, Resource } from "react-admin";
import { useApolloClient } from "@apollo/client";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { metamigoDataProvider } from "../_lib/dataprovider";
import { theme } from "./layout/themes";
import { Layout } from "./layout";
import englishMessages from "../_i18n/en";
import whatsappBots from "./whatsapp/bots";
import whatsappMessages from "./whatsapp/messages";
import whatsappAttachments from "./whatsapp/attachments";
import voiceLines from "./voice/voicelines";
import signalBots from "./signal/bots";
import voiceProviders from "./voice/providers";
import webhooks from "./webhooks";
import { AdminLogin, authProvider } from "./AdminLogin";
const i18nProvider = polyglotI18nProvider(
(_locale: any) => englishMessages,
"en",
);
const MetamigoAdmin: FC = () => {
// eslint-disable-next-line unicorn/no-null
const [dataProvider, setDataProvider] = useState(null);
const client = useApolloClient();
const muiTheme = createTheme(theme);
useEffect(() => {
(async () => {
const dataProvider = await metamigoDataProvider(client);
// @ts-ignore
setDataProvider(() => dataProvider);
})();
}, [client]);
return (
dataProvider && (
<Admin
disableTelemetry
dataProvider={dataProvider}
layout={Layout}
i18nProvider={i18nProvider}
// @ts-ignore
loginPage={AdminLogin}
// @ts-ignore
authProvider={authProvider}
>
<Resource name="webhooks" {...webhooks} />
<Resource name="whatsappBots" {...whatsappBots} />
<Resource name="whatsappMessages" {...whatsappMessages} />
<Resource name="whatsappAttachments" {...whatsappAttachments} />
<Resource name="signalBots" {...signalBots} />
<Resource name="voiceProviders" {...voiceProviders} />
<Resource name="voiceLines" {...voiceLines} />
<Resource name="languages" />
</Admin>
)
);
};
export default MetamigoAdmin;

View file

@ -1,8 +0,0 @@
"use client";
import { FC, PropsWithChildren } from "react";
import { SessionProvider } from "next-auth/react";
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => (
<SessionProvider>{children}</SessionProvider>
);

View file

@ -1,63 +0,0 @@
"use client";
import { FC } from "react";
// import { makeStyles } from "@mui/styles";
import {
SimpleForm,
TextInput,
Edit,
ReferenceInput,
SelectInput,
DateInput,
Toolbar,
DeleteButton,
EditProps,
} from "react-admin";
import { useSession } from "next-auth/react";
/*
const useStyles = makeStyles((_theme: any) => ({
defaultToolbar: {
flex: 1,
display: "flex",
justifyContent: "space-between",
},
}));
*/
type AccountEditToolbarProps = {
record?: any;
};
const AccountEditToolbar: FC<AccountEditToolbarProps> = (props) => {
const { data: session } = useSession();
const classes: any = {}; // useStyles(props);
return (
<Toolbar className={classes.defaultToolbar} {...props}>
<DeleteButton disabled={session?.user?.email === props.record?.userId} />
</Toolbar>
);
};
const AccountTitle = ({ record }: { record?: any }) => {
let title = "";
if (record) title = record.name ?? record.email;
return <span>Account {title}</span>;
};
export const AccountEdit: FC<EditProps> = (props) => (
<Edit title={<AccountTitle />} {...props}>
<SimpleForm toolbar={<AccountEditToolbar />}>
<TextInput disabled source="id" />
<ReferenceInput source="userId" reference="users">
<SelectInput disabled optionText="email" />
</ReferenceInput>
<TextInput disabled source="providerType" />
<TextInput disabled source="providerId" />
<TextInput disabled source="providerAccountId" />
<DateInput disabled source="createdAt" />
<DateInput disabled source="updatedAt" />
</SimpleForm>
</Edit>
);
export default AccountEdit;

View file

@ -1,46 +0,0 @@
"use client";
import { FC } from "react";
import {
List,
Datagrid,
DateField,
TextField,
ReferenceField,
DeleteButton,
ListProps,
} from "react-admin";
import { useSession } from "next-auth/react";
type DeleteNotSelfButtonProps = {
record?: any;
};
const DeleteNotSelfButton: FC<DeleteNotSelfButtonProps> = (props) => {
const { data: session } = useSession();
return (
// @ts-ignore
<DeleteButton
disabled={session?.user?.email === props.record.userId}
{...props}
/>
);
};
export const AccountList: FC<ListProps> = (props) => (
<List {...props} exporter={false}>
<Datagrid rowClick="edit">
<ReferenceField source="userId" reference="users">
<TextField source="email" />
</ReferenceField>
<TextField source="providerType" />
<TextField source="providerId" />
<TextField source="providerAccountId" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
<DeleteNotSelfButton />
</Datagrid>
</List>
);
export default AccountList;

View file

@ -1,13 +0,0 @@
"use client";
/* eslint-disable import/no-named-as-default */
/* eslint-disable import/no-anonymous-default-export */
import AccountIcon from "@mui/icons-material/AccountTree";
import AccountList from "./AccountList";
import AccountEdit from "./AccountEdit";
export default {
list: AccountList,
edit: AccountEdit,
icon: AccountIcon,
};

View file

@ -1,58 +0,0 @@
"use client";
import { forwardRef } from "react";
import { AppBar, UserMenu, MenuItemLink, useTranslate } from "react-admin";
import Typography from "@mui/material/Typography";
import SettingsIcon from "@mui/icons-material/Settings";
import { colors } from "app/_styles/theme";
// import { makeStyles } from "@mui/styles";
/*
const useStyles = makeStyles({
title: {
flex: 1,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
},
spacer: {
flex: 1,
},
});
*/
// eslint-disable-next-line react/display-name
const ConfigurationMenu = forwardRef<any, any>((props, ref) => {
const translate = useTranslate();
return (
<MenuItemLink
ref={ref}
to="/configuration"
primaryText={translate("pos.configuration")}
leftIcon={<SettingsIcon />}
onClick={props.onClick}
sidebarIsOpen
/>
);
});
const CustomAppBar = (props: any) => {
const classes: any = {}; // useStyles();
const { darkLavender } = colors;
return (
<AppBar
{...props}
elevation={0}
userMenu={<div />}
position="sticky"
sx={{ mt: -1, backgroundColor: darkLavender }}
>
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
</AppBar>
);
};
export default CustomAppBar;

View file

@ -1,19 +0,0 @@
"use client";
import { Layout as RaLayout, LayoutProps, Sidebar } from "react-admin";
import AppBar from "./AppBar";
import { Menu } from "./Menu";
import { theme } from "./themes";
const CustomSidebar = (props: any) => <Sidebar {...props} size={200} />;
export const Layout = (props: LayoutProps) => (
<RaLayout
{...props}
appBar={AppBar}
menu={Menu as any}
sidebar={CustomSidebar}
// @ts-ignore
theme={theme}
/>
);

View file

@ -1,106 +0,0 @@
"use client";
import { SVGProps } from "react";
const Logo = (props: SVGProps<SVGSVGElement>) => (
<svg width="220.001" height="43.659" {...props}>
<path d="M59.39 24.586h4.6v8.512c-1.058.2-3.743.57-5.742.57-6.398 0-7.74-3.77-7.74-11.452 0-7.827 1.4-11.54 7.797-11.54 3.627 0 8.597.828 8.597.828l.115-2.542s-4.885-1.056-9.083-1.056c-8.312 0-10.626 5.112-10.626 14.31 0 8.968 2.228 14.167 10.711 14.167 3.028 0 8.17-.8 8.998-.971V21.816H59.39zm13.14 11.397h2.998V21.302s3.514-1.943 7.284-2.714V15.56c-3.828.743-7.312 3.142-7.312 3.142v-2.713h-2.97zm27.962-13.967c0-4.342-1.913-6.427-6.455-6.427-3.427 0-7.826.885-7.826.885l.114 2.285s4.77-.542 7.57-.542c2.4 0 3.598 1 3.598 3.799v1.742l-6.284.6c-4.113.4-6.112 2.056-6.112 5.912 0 4.028 2 6.113 5.627 6.113 3.6 0 7.198-1.6 7.198-1.6 1.2 1.2 2.656 1.6 4.77 1.6l.114-2.37c-1.285-.144-2.228-.6-2.314-1.743zm-2.999 3.998v6.599s-3.313 1.256-6.284 1.256c-2.028 0-3.027-1.37-3.027-3.684 0-2.2.942-3.37 3.4-3.6zm17.738-10.425c-2.828 0-5.855 1.4-5.855 1.4V7.277h-2.97v28.677s4.283.429 6.683.429c7.283 0 9.425-2.77 9.425-10.711 0-7.198-1.828-10.083-7.283-10.083zm-2.2 18.109c-1.056 0-3.655-.2-3.655-.2V19.416s2.8-1.142 5.54-1.142c3.514 0 4.57 2.228 4.57 7.398 0 5.598-.97 8.026-6.454 8.026zm28.535-11.682c0-4.342-1.942-6.427-6.455-6.427-3.428 0-7.826.885-7.826.885l.114 2.285s4.77-.542 7.57-.542c2.4 0 3.598 1 3.598 3.799v1.742l-6.284.6c-4.113.4-6.112 2.056-6.112 5.912 0 4.028 2 6.113 5.626 6.113 3.6 0 7.198-1.6 7.198-1.6 1.2 1.2 2.628 1.6 4.77 1.6l.115-2.37c-1.286-.144-2.257-.6-2.314-1.743zm-3 3.998v6.599s-3.34 1.256-6.283 1.256c-2.057 0-3.056-1.37-3.056-3.684 0-2.2.97-3.37 3.4-3.6zm24.25-18.737h-2.94v8.826c-.6-.114-3.2-.514-4.914-.514-6.084 0-8.369 3.513-8.369 10.568 0 8.626 3.285 10.226 7.198 10.226 3 0 6.084-1.771 6.084-1.771v1.37h2.942zm-8.654 26.42c-2.37 0-4.484-1.084-4.484-7.54 0-5.198 1.228-7.97 5.427-7.97 1.657 0 4.113.373 4.77.487v13.539s-2.885 1.485-5.713 1.485zM176.3 15.59c-6.313 0-8.54 3.285-8.54 10.168 0 7.255 1.827 10.626 8.54 10.626 6.77 0 8.57-3.37 8.57-10.626 0-6.883-2.2-10.168-8.57-10.168zm0 18.195c-4.713 0-5.484-2.371-5.484-8.027 0-5.57 1.256-7.57 5.484-7.57 4.284 0 5.484 2 5.484 7.57 0 5.656-.714 8.027-5.484 8.027zm13.453 2.199h3V21.303s3.512-1.943 7.254-2.714V15.56c-3.828.743-7.312 3.142-7.312 3.142V15.99h-2.942zm27.934-13.967c0-4.342-1.913-6.427-6.426-6.427-3.456 0-7.855.885-7.855.885l.143 2.285s4.741-.542 7.54-.542c2.4 0 3.6 1 3.6 3.799v1.742l-6.285.6c-4.113.4-6.112 2.056-6.112 5.912 0 4.028 2 6.113 5.655 6.113 3.6 0 7.198-1.6 7.198-1.6 1.2 1.2 2.628 1.6 4.742 1.6l.114-2.37c-1.257-.144-2.228-.6-2.314-1.743zm-2.999 3.998v6.599s-3.313 1.256-6.284 1.256c-2.028 0-3.027-1.37-3.027-3.684 0-2.2.97-3.37 3.4-3.6z" />
<defs>
<linearGradient
gradientTransform="rotate(25)"
id="a"
x1="0"
y1="0"
x2="1"
y2="0"
>
<stop offset="0%" stopColor="#8C48D2" />
<stop offset="100%" stopColor="#CF705A" />
</linearGradient>
<linearGradient
xlinkHref="#a"
id="c"
gradientTransform="scale(.7746 1.291)"
x1="15.492"
y1="4.648"
x2="23.238"
y2="4.648"
gradientUnits="userSpaceOnUse"
/>
<linearGradient
xlinkHref="#a"
id="d"
gradientTransform="scale(1.27 .7874)"
x1="7.874"
y1="15.24"
x2="15.748"
y2="15.24"
gradientUnits="userSpaceOnUse"
/>
<linearGradient
xlinkHref="#a"
id="e"
gradientTransform="scale(.91287 1.09545)"
x1="10.954"
y1="7.303"
x2="21.909"
y2="7.303"
gradientUnits="userSpaceOnUse"
/>
<linearGradient
xlinkHref="#a"
id="f"
gradientTransform="scale(1.13606 .88024)"
x1="3.521"
y1="13.576"
x2="22.886"
y2="13.576"
gradientUnits="userSpaceOnUse"
/>
<linearGradient
xlinkHref="#a"
id="g"
gradientTransform="scale(1.029 .97183)"
x1="5.831"
y1="1.029"
x2="23.324"
y2="1.029"
gradientUnits="userSpaceOnUse"
/>
<linearGradient
xlinkHref="#a"
id="b"
gradientTransform="scale(.88647 1.12807)"
x1="4.512"
y1=".886"
x2="29.33"
y2=".886"
gradientUnits="userSpaceOnUse"
/>
</defs>
<g transform="translate(-6.238 -1.56) scale(1.55946)" fill="url(#b)">
<path
d="M12 9v4a3 3 0 006 0V9a3 3 0 00-6 0zm3-2a2 2 0 012 2v4a2 2 0 11-4 0V9a2 2 0 012-2z"
fill="url(#c)"
/>
<path
d="M10 13.2a5 5 0 0010 0v-.7a.5.5 0 10-1 0v.7a4 4 0 11-8 0v-.7a.5.5 0 10-1 0z"
fill="url(#d)"
/>
<path
d="M19.5 13a.5.5 0 100-1h-9a.5.5 0 100 1zm-3 6a.5.5 0 110 1h-3a.5.5 0 110-1h1v-1h1v1zm-3-10a.5.5 0 000-1h-1v1zm0 2a.5.5 0 000-1h-1v1zm3 0a.5.5 0 110-1h1v1zm0-2a.5.5 0 110-1h1v1z"
fill="url(#e)"
/>
<path
d="M25.947 14.272a.51.51 0 01.053.23v13.994a.5.5 0 01-.5.5h-21a.5.5 0 01-.5-.5V14.502a.502.502 0 01.2-.406L7 11.95v1.26l-2 1.533v1.253l6.667 5h6.666l6.667-5v-1.253l-2-1.533v-1.26l2.8 2.146a.502.502 0 01.147.176zM10.739 21.55L5 27.29V17.245l5.739 4.304zm.968.446h6.586l6 6H5.707zm7.554-.446L25 17.246V27.29l-5.739-5.739z"
fill="url(#f)"
/>
<path
d="M24 6.2a.5.5 0 00-.146-.354l-4.7-4.7A.5.5 0 0018.8 1H6.5a.5.5 0 00-.5.5V18h1V2h11v4.5a.5.5 0 00.5.5H23v11h1zM19 6V2.41L22.59 6z"
fill="url(#g)"
/>
</g>
</svg>
);
export default Logo;

View file

@ -1,100 +0,0 @@
"use client";
/* eslint-disable camelcase */
import { FC, useState } from "react";
import VoiceIcon from "@mui/icons-material/PhoneInTalk";
import { Box } from "@mui/material";
// import { useTheme } from "@mui/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTranslate, MenuItemLink } from "react-admin";
import webhooks from "../webhooks";
import voiceLines from "../voice/voicelines";
import voiceProviders from "../voice/providers";
import whatsappBots from "../whatsapp/bots";
import signalBots from "../signal/bots";
import { SubMenu } from "./SubMenu";
type MenuName = "menuVoice" | "menuSecurity";
export const Menu: FC = ({ onMenuClick, logout, dense = false }: any) => {
const [state, setState] = useState({
menuVoice: false,
menuSecurity: false,
});
const translate = useTranslate();
const theme: any = {}; // useTheme();
// @ts-ignore
const isXSmall = false; // useMediaQuery(theme?.breakpoints?.down("xs"));
const open = true; // useSelector((state: any) => state.admin.ui.sidebarOpen);
const handleToggle = (menu: MenuName) => {
setState((state: any) => ({ ...state, [menu]: !state[menu] }));
};
return (
<Box mt={1}>
<MenuItemLink
to={`/whatsappbots`}
primaryText={translate(`pos.menu.whatsapp`, {
smart_count: 2,
})}
leftIcon={<whatsappBots.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<MenuItemLink
to={`/signalbots`}
primaryText={translate(`pos.menu.signal`, {
smart_count: 2,
})}
leftIcon={<signalBots.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<SubMenu
handleToggle={() => handleToggle("menuVoice")}
isOpen={state.menuVoice}
sidebarIsOpen={open}
name="pos.menu.voice"
icon={<VoiceIcon />}
dense={dense}
>
<MenuItemLink
to={`/voiceproviders`}
primaryText={translate(`resources.providers.name`, {
smart_count: 2,
})}
leftIcon={<voiceProviders.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<MenuItemLink
to={`/voicelines`}
primaryText={translate(`resources.voicelines.name`, {
smart_count: 2,
})}
leftIcon={<voiceLines.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
</SubMenu>
<MenuItemLink
to={`/webhooks`}
primaryText={translate(`resources.webhooks.name`, {
smart_count: 2,
})}
leftIcon={<webhooks.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
{isXSmall && logout}
</Box>
);
};
export default Menu;

View file

@ -1,87 +0,0 @@
"use client";
import { FC, PropsWithChildren, Fragment, ReactElement } from "react";
import ExpandMore from "@mui/icons-material/ExpandMore";
import List from "@mui/material/List";
import MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import Typography from "@mui/material/Typography";
import Collapse from "@mui/material/Collapse";
import Tooltip from "@mui/material/Tooltip";
// import { makeStyles } from "@mui/styles";
import { useTranslate } from "react-admin";
/*
const useStyles = makeStyles((theme: any) => ({
icon: { minWidth: theme.spacing(5) },
sidebarIsOpen: {
"& a": {
paddingLeft: theme.spacing(4),
transition: "padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms",
},
},
sidebarIsClosed: {
"& a": {
paddingLeft: theme.spacing(2),
transition: "padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms",
},
},
}));
*/
type SubMenuProps = PropsWithChildren<{
dense: boolean;
handleToggle: () => void;
icon: ReactElement;
isOpen: boolean;
name: string;
sidebarIsOpen: boolean;
}>;
export const SubMenu: FC<SubMenuProps> = ({
handleToggle,
sidebarIsOpen,
isOpen,
name,
icon,
children,
dense,
}: any) => {
const translate = useTranslate();
const classes: any = {}; // = useStyles();
const header = (
// @ts-ignore
<MenuItem dense={dense} button onClick={handleToggle}>
<ListItemIcon className={classes.icon}>
{isOpen ? <ExpandMore /> : icon}
</ListItemIcon>
<Typography variant="inherit" color="textSecondary">
{translate(name)}
</Typography>
</MenuItem>
);
return (
<Fragment>
{sidebarIsOpen || isOpen ? (
header
) : (
<Tooltip title={translate(name)} placement="right">
{header}
</Tooltip>
)}
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List
dense={dense}
component="div"
disablePadding
className={
sidebarIsOpen ? classes.sidebarIsOpen : classes.sidebarIsClosed
}
>
{children}
</List>
</Collapse>
</Fragment>
);
};

View file

@ -1,5 +0,0 @@
"use client";
export { default as AppBar } from "./AppBar";
export { Layout } from "./Layout";
export { default as Menu } from "./Menu";

View file

@ -1,103 +0,0 @@
export const theme = {
spacing: () => 8,
palette: {
primary: {
main: "#337799",
},
secondary: {
light: "#5f5fc4",
main: "#283593",
dark: "#001064",
contrastText: "#fff",
},
background: {
default: "#fff",
},
getContrastText(color: string) {
return color === "#ffffff" ? "#000" : "#fff";
},
},
shape: {
borderRadius: 5,
},
breakpoints: {
up: (key: any) => `@media (min-width:${key})`,
down: (key: any) => `@media (max-width:${key})`,
},
transitions: {
create(props: any) {
return `all ${props.duration}ms ${props.easing}`;
},
easing: {
easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
easeOut: "cubic-bezier(0.0, 0, 0.2, 1)",
easeIn: "cubic-bezier(0.4, 0, 1, 1)",
sharp: "cubic-bezier(0.4, 0, 0.6, 1)",
},
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
},
typography: {
fontFamily: "Poppins, sans-serif",
fontWeight: 900,
h6: { fontSize: 16, fontWeight: 600, color: "red" },
},
/*
overrides: {
RaMenuItemLink: {
root: {
borderLeft: "3px solid #fff",
},
active: {
borderLeft: "3px solid #ef7706",
},
},
MuiPaper: {
elevation1: {
boxShadow: "none",
},
root: {
border: "1px solid #e0e0e3",
backgroundClip: "padding-box",
},
},
MuiButton: {
contained: {
backgroundColor: "#fff",
color: "#4f3cc9",
boxShadow: "none",
},
},
MuiAppBar: {
colorSecondary: {
color: "#fff",
backgroundColor: "#337799",
border: 0,
},
},
MuiLinearProgress: {
colorPrimary: {
backgroundColor: "#f5f5f5",
},
barColorPrimary: {
backgroundColor: "#d7d7d7",
},
},
MuiFilledInput: {
root: {
backgroundColor: "rgba(0, 0, 0, 0.04)",
"&$disabled": {
backgroundColor: "rgba(0, 0, 0, 0.04)",
},
},
},
},
*/
};

View file

@ -1,27 +0,0 @@
.input {
width: 40px;
height: 60px;
margin: 5px;
font-size: 1.4rem;
padding: 0 9px 0 12px;
border: none;
outline: none;
background: white;
font-weight: 400;
color: rgba(59, 59, 59, 0.788);
-webkit-box-shadow: 2px 2px 2px 1px #d6d6d6, -1px -1px 1px #e6e6e6;
box-shadow: 2px 2px 2px 1px #d6d6d6, -1px -1px 1px #e6e6e6;
border-radius: 5px;
}
.group {
margin-top: 1.6rem;
}
.hyphen {
background: black;
height: 0.1em;
width: 1em;
margin: 0 0.5em;
display: inline-block;
}

View file

@ -1,37 +0,0 @@
"use client";
import { FC } from "react";
import {
SimpleForm,
Create,
TextInput,
required,
CreateProps,
} from "react-admin";
import { useSession } from "next-auth/react";
import { validateE164Number } from "../../../_lib/phone-numbers";
const SignalBotCreate: FC<CreateProps> = (props) => {
const { data: session } = useSession();
return (
<Create {...props} title="Create Signal Bot">
<SimpleForm>
<TextInput
source="userId"
defaultValue={
// @ts-expect-error: ID does exist
session.user.id
}
/>
<TextInput
source="phoneNumber"
validate={[validateE164Number, required()]}
/>
<TextInput source="description" />
</SimpleForm>
</Create>
);
};
export default SignalBotCreate;

View file

@ -1,15 +0,0 @@
"use client";
import { FC } from "react";
import { SimpleForm, Edit, TextInput, required, EditProps } from "react-admin";
const SignalBotEdit: FC<EditProps> = (props) => (
<Edit {...props} title="Edit Bot">
<SimpleForm>
<TextInput disabled source="phoneNumber" validate={[required()]} />
<TextInput source="description" />
</SimpleForm>
</Edit>
);
export default SignalBotEdit;

View file

@ -1,26 +0,0 @@
"use client";
import { FC } from "react";
import {
List,
Datagrid,
DateField,
TextField,
BooleanField,
ListProps,
} from "react-admin";
const SignalBotList: FC<ListProps> = (props) => (
<List {...props} exporter={false}>
<Datagrid rowClick="show">
<TextField source="phoneNumber" />
<TextField source="description" />
<BooleanField source="isVerified" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
<TextField source="createdBy" />
</Datagrid>
</List>
);
export default SignalBotList;

View file

@ -1,475 +0,0 @@
"use client";
import { FC, useEffect, useState } from "react";
import {
Show,
SimpleShowLayout,
BooleanField,
TextField,
ShowProps,
EditButton,
TopToolbar,
useTranslate,
useRefresh,
} from "react-admin";
import {
TextField as MuiTextField,
Button,
Card,
Grid,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Typography,
Box,
CircularProgress,
} from "@mui/material";
import { SixDigitInput } from "../../DigitInput";
import {
sanitizeE164Number,
isValidE164Number,
} from "../../../_lib/phone-numbers";
const Sidebar = ({ record }: any) => {
const [phoneNumber, setPhoneNumber] = useState("");
const [errorNumber, setErrorNumber] = useState(false);
const handlePhoneNumberChange = (event: any) => {
setPhoneNumber(event.target.value);
};
const [message, setMessage] = useState("");
const handleMessageChange = (event: any) => {
setMessage(event.target.value);
};
const sendMessage = async (phoneNumber: string, message: string) => {
await fetch(`/api/v1/signal/bots/${record.token}/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber, message }),
});
};
const resetSession = async (phoneNumber: string) => {
await fetch(`/api/v1/signal/bots/${record.token}/resetSession`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber }),
});
};
const handleBlurNumber = () => {
setErrorNumber(!isValidE164Number(sanitizeE164Number(phoneNumber)));
};
const handleSend = () => {
const sanitized = sanitizeE164Number(phoneNumber);
if (isValidE164Number(sanitized)) {
setErrorNumber(false);
sendMessage(sanitized, message);
} else setErrorNumber(false);
};
const handleResetSession = () => {
const sanitized = sanitizeE164Number(phoneNumber);
if (isValidE164Number(sanitized)) {
setErrorNumber(false);
resetSession(sanitized);
} else setErrorNumber(false);
};
return (
<Card style={{ width: "33%", marginLeft: 20, padding: 14 }}>
<Grid container direction="column" spacing={2}>
<Grid item>
<Typography variant="h6">Send message</Typography>
</Grid>
<Grid item>
<MuiTextField
variant="outlined"
label="Phone number"
fullWidth
size="small"
error={errorNumber}
onBlur={handleBlurNumber}
value={phoneNumber}
onChange={handlePhoneNumberChange}
/>
</Grid>
<Grid item>
<MuiTextField
variant="outlined"
label="Message"
multiline
minRows={3}
fullWidth
size="small"
value={message}
onChange={handleMessageChange}
/>
</Grid>
<Grid item container direction="row-reverse">
<Button
variant="contained"
color="primary"
onClick={() => handleSend()}
>
Send
</Button>
<Button variant="contained" onClick={() => handleResetSession()}>
Reset Session
</Button>
</Grid>
</Grid>
</Card>
);
};
const MODE = {
SMS: "SMS",
VOICE: "VOICE",
};
const handleRequestCode = async ({
verifyMode,
id,
onSuccess,
onError,
captchaCode = undefined,
}: any) => {
if (verifyMode === MODE.SMS) console.log("REQUESTING sms");
else if (verifyMode === MODE.VOICE) console.log("REQUESTING voice");
let response: Response;
let url = `/api/v1/signal/bots/${id}/requestCode?mode=${verifyMode.toLowerCase()}`;
if (captchaCode) {
url += `&captcha=${captchaCode}`;
}
try {
response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (response && response.ok) {
onSuccess();
} else {
onError(response.status || 400);
}
} catch (error: any) {
console.error("Failed to request verification code:", error);
}
};
const VerificationCodeRequest = ({
verifyMode,
data,
onSuccess,
onError,
}: any) => {
useEffect(() => {
(async () => {
await handleRequestCode({
verifyMode,
id: data.id,
onSuccess,
onError,
});
})();
}, [data.id, onError, onSuccess, verifyMode]);
return (
<>
<DialogTitle id="form-dialog-title">
Requesting code for {data.phoneNumber}
</DialogTitle>
<DialogContent>
<Box display="flex">
<Box m="auto">
<CircularProgress />
</Box>
</Box>
</DialogContent>
</>
);
};
const VerificationCaptcha = ({
verifyMode,
data,
onSuccess,
onError,
handleClose,
}: any) => {
const [code, setCode] = useState(undefined);
const [isSubmitting, setSubmitting] = useState(false);
const handleSubmitVerification = async () => {
setSubmitting(true);
await handleRequestCode({
verifyMode,
id: data.id,
onSuccess,
onError,
captchaCode: code,
});
setSubmitting(false);
};
const handleCaptchaChange = (value: any) => {
if (value)
setCode(
value
.replace(/signalcaptcha:\/\//, "")
.replace("“", "")
.replace("”", "")
.trim(),
);
else setCode(value);
};
return (
<>
<DialogTitle id="form-dialog-title">
Captcha for {data.phoneNumber}
</DialogTitle>
<DialogContent>
<MuiTextField
value={code}
onChange={(ev) => handleCaptchaChange(ev.target.value)}
/>
</DialogContent>
<DialogActions>
{isSubmitting && <CircularProgress />}
{!isSubmitting && (
<Button onClick={handleClose} color="primary">
Cancel
</Button>
)}
{!isSubmitting && (
<Button onClick={handleSubmitVerification} color="primary">
Request
</Button>
)}
</DialogActions>
</>
);
};
const VerificationCodeInput = ({
data,
verifyMode,
handleClose,
handleRestartVerification,
confirmVerification,
}: any) => {
const [code, setValue] = useState("");
const [isSubmitting, setSubmitting] = useState(false);
const [isValid, setValid] = useState(false);
const [submissionError, setSubmissionError] = useState(undefined);
const translate = useTranslate();
const validator = (v: any) => v.trim().length === 6;
const handleValueChange = (newValue: any) => {
setValue(newValue);
setValid(validator(newValue));
};
const handleSubmitVerification = async () => {
setSubmitting(true);
// await sleep(2000)
const response = await fetch(
`/api/v1/signal/bots/${data.id}/register?code=${code}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
setSubmitting(false);
const responseBody = await response.json();
console.log(responseBody);
if (response.status === 200) {
confirmVerification();
} else if (responseBody.message)
setSubmissionError(`Error: ${responseBody.message}`);
else {
setSubmissionError(
"There was an error, sorry about that. Please try again later or contact support.",
);
}
};
const title =
verifyMode === MODE.SMS
? translate("resources.signalBots.verifyDialog.sms", {
phoneNumber: data.phoneNumber,
})
: translate("resources.signalBots.verifyDialog.voice", {
phoneNumber: data.phoneNumber,
});
return (
<>
<DialogTitle id="form-dialog-title">
Verify {data.phoneNumber}
</DialogTitle>
<DialogContent>
<DialogContentText>{title}</DialogContentText>
<SixDigitInput value={code} onChange={handleValueChange} />
{submissionError && (
<Typography variant="body1" gutterBottom color="error">
{submissionError}
</Typography>
)}
</DialogContent>
<DialogActions>
{isSubmitting && <CircularProgress />}
{!isSubmitting && (
<Button onClick={handleClose} color="primary">
Cancel
</Button>
)}
{!isSubmitting && (
<Button onClick={handleRestartVerification} color="primary">
Restart
</Button>
)}
{!isSubmitting && (
<Button
onClick={handleSubmitVerification}
color="primary"
disabled={!isValid}
>
Verify
</Button>
)}
</DialogActions>
</>
);
};
const VerificationCodeDialog = (props: any) => {
const [stage, setStage] = useState("request");
const onRequestSuccess = () => setStage("verify");
const onRestartVerification = () => setStage("request");
const handleClose = () => {
setStage("request");
props.handleClose();
};
const onError = (code: number) => {
if (code === 402 || code === 500) {
setStage("captcha");
} else {
setStage("request");
}
};
return (
<Dialog
open={props.open}
onClose={props.handleClose}
aria-labelledby="form-dialog-title"
>
{props.open && stage === "request" && (
<VerificationCodeRequest
mode={props.verifyMode}
onSuccess={onRequestSuccess}
onError={onError}
{...props}
/>
)}
{props.open && stage === "verify" && (
<VerificationCodeInput
{...props}
handleRestartVerification={onRestartVerification}
handleClose={handleClose}
/>
)}
{props.open && stage === "captcha" && (
<VerificationCaptcha
mode={props.verifyMode}
onSuccess={onRequestSuccess}
onError={onRestartVerification}
handleClose={handleClose}
{...props}
/>
)}
</Dialog>
);
};
const SignalBotShowActions = ({ data }: any) => {
const [open, setOpen] = useState(false);
const [verifyMode, setVerifyMode] = useState("");
const refresh = useRefresh();
const handleOpenSMS = () => {
setVerifyMode(MODE.SMS);
setOpen(true);
};
const handleOpenVoice = () => {
setVerifyMode(MODE.VOICE);
setOpen(true);
};
const handleClose = () => setOpen(false);
const confirmVerification = () => {
setOpen(false);
refresh();
};
return (
<TopToolbar>
<EditButton record={data} />
{data && !data.isVerified && (
<Button onClick={handleOpenSMS} color="primary">
Verify with SMS
</Button>
)}
{data && !data.isVerified && (
<Button onClick={handleOpenVoice} color="primary">
Verify with Voice
</Button>
)}
{data && !data.isVerified && (
<VerificationCodeDialog
data={data}
verifyMode={verifyMode}
handleClose={handleClose}
open={open}
confirmVerification={confirmVerification}
/>
)}
</TopToolbar>
);
};
const SignalBotShow: FC<ShowProps> = (props) => (
<Show
actions={<SignalBotShowActions />}
{...props}
title="Signal Bot"
aside={<Sidebar />}
>
<SimpleShowLayout>
<TextField source="phoneNumber" />
<BooleanField source="isVerified" />
<TextField source="description" />
<TextField source="token" />
</SimpleShowLayout>
</Show>
);
export default SignalBotShow;

View file

@ -1,16 +0,0 @@
"use client";
import SignalBotIcon from "@mui/icons-material/ChatOutlined";
import SignalBotList from "./SignalBotList";
import SignalBotEdit from "./SignalBotEdit";
import SignalBotCreate from "./SignalBotCreate";
import SignalBotShow from "./SignalBotShow";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: SignalBotList,
create: SignalBotCreate,
edit: SignalBotEdit,
show: SignalBotShow,
icon: SignalBotIcon,
};

View file

@ -1,34 +0,0 @@
"use client";
import { FC } from "react";
/* eslint-disable react/display-name */
import {
SelectInput,
required,
ReferenceInput,
ReferenceField,
TextField,
} from "react-admin";
export const SignalBotSelectInput =
(source: string): FC =>
() =>
(
<ReferenceInput
label="Signal Bot"
source={source}
reference="signalBots"
validate={[required()]}
>
<SelectInput optionText="phoneNumber" />
</ReferenceInput>
);
export const SignalBotField =
(source: string): FC =>
() =>
(
<ReferenceField label="Signal Bot" reference="signalBots" source={source}>
<TextField source="phoneNumber" />
</ReferenceField>
);

View file

@ -1,29 +0,0 @@
"use client";
import { FC } from "react";
import {
SimpleForm,
TextInput,
BooleanInput,
Create,
CreateProps,
} from "react-admin";
import { useSession } from "next-auth/react";
import { UserRoleInput } from "./shared";
const UserCreate: FC<CreateProps> = () => {
const { data: session } = useSession();
return (
<Create title="Create Users">
<SimpleForm>
<TextInput source="email" />
<TextInput source="name" />
<UserRoleInput session={session} initialValue="NONE" />
<BooleanInput source="isActive" defaultValue={true} />
<TextInput source="createdBy" defaultValue={session?.user?.name} />
</SimpleForm>
</Create>
);
};
export default UserCreate;

View file

@ -1,73 +0,0 @@
"use client";
// import { makeStyles } from "@mui/styles";
import {
SimpleForm,
TextInput,
BooleanInput,
DateInput,
Edit,
Toolbar,
SaveButton,
DeleteButton,
useRedirect,
useRecordContext,
} from "react-admin";
import { useSession } from "next-auth/react";
import { UserRoleInput } from "./shared";
/*
const useStyles = makeStyles((_theme: any) => ({
defaultToolbar: {
flex: 1,
display: "flex",
justifyContent: "space-between",
},
}));
*/
const UserEditToolbar = (props: any) => {
const classes: any = {}; // = useStyles();
const redirect = useRedirect();
const record = useRecordContext();
const { session } = props;
const shouldDisableDelete =
!session || !session.user || session.user.id === record.id;
return (
<Toolbar className={classes.defaultToolbar}>
<SaveButton
label="save"
mutationOptions={{ onSuccess: () => redirect("/users") }}
/>
<DeleteButton disabled={shouldDisableDelete} />
</Toolbar>
);
};
const UserTitle = ({ record }: { record?: any }) => {
let title = "";
if (record) title = record.name ?? record.email;
return <span>User {title}</span>;
};
const UserEdit = () => {
const { data: session } = useSession();
return (
<Edit title={<UserTitle />}>
<SimpleForm toolbar={<UserEditToolbar session={session} />}>
<TextInput disabled source="id" />
<TextInput source="email" />
<TextInput source="name" />
<UserRoleInput session={session} />
<DateInput source="emailVerified" />
<BooleanInput source="isActive" />
<TextInput source="createdBy" />
</SimpleForm>
</Edit>
);
};
export default UserEdit;

View file

@ -1,30 +0,0 @@
"use client";
import { FC } from "react";
import {
List,
Datagrid,
ImageField,
DateField,
TextField,
EmailField,
BooleanField,
} from "react-admin";
const UserList: FC = () => (
<List exporter={false}>
<Datagrid rowClick="edit">
<EmailField source="email" />
<DateField source="emailVerified" />
<TextField source="name" />
<ImageField source="avatar" />
<TextField source="userRole" />
<BooleanField source="isActive" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
<TextField source="createdBy" />
</Datagrid>
</List>
);
export default UserList;

View file

@ -1,14 +0,0 @@
"use client";
import UserIcon from "@mui/icons-material/People";
import UserList from "./UserList";
import UserEdit from "./UserEdit";
import UserCreate from "./UserCreate";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: UserList,
create: UserCreate,
edit: UserEdit,
icon: UserIcon,
};

View file

@ -1,19 +0,0 @@
"use client";
import { SelectInput, useRecordContext } from "react-admin";
export const UserRoleInput = (props: any) => {
const record = useRecordContext();
return (
<SelectInput
source="userRole"
choices={[
{ id: "NONE", name: "None" },
{ id: "USER", name: "User" },
{ id: "ADMIN", name: "Admin" },
]}
disabled={props.session.user.id === record.id}
{...props}
/>
);
};

View file

@ -1,33 +0,0 @@
"use client";
import {
SimpleForm,
TextInput,
Create,
PasswordInput,
CreateProps,
} from "react-admin";
import { ProviderKindInput } from "./shared";
// import TextField from "@mui/material/TextField";
/* const TwilioCredentialsInput = () => (
<span>
<TextField name="accountSid" label="Account Sid" />
<TextField name="authToken" label="Auth Token" />
</span>
); */
const ProviderCreate = (props: CreateProps) => (
<Create {...props} title="Create Providers">
<SimpleForm>
<ProviderKindInput />
<TextInput source="name" />
<TextInput source="credentials.accountSid" />
<TextInput source="credentials.apiKeySid" />
<PasswordInput source="credentials.apiKeySecret" />
</SimpleForm>
</Create>
);
export default ProviderCreate;

View file

@ -1,31 +0,0 @@
"use client";
import {
SimpleForm,
TextInput,
PasswordInput,
Edit,
EditProps,
} from "react-admin";
import { ProviderKindInput } from "./shared";
const ProviderTitle = ({ record }: { record?: any }) => {
let title = "";
if (record) title = record.name ?? record.email;
return <span>Provider {title}</span>;
};
const ProviderEdit = (props: EditProps) => (
<Edit title={<ProviderTitle />} {...props}>
<SimpleForm>
<TextInput disabled source="id" />
<ProviderKindInput disabled />
<TextInput source="name" />
<TextInput source="credentials.accountSid" />
<TextInput source="credentials.apiKeySid" />
<PasswordInput source="credentials.apiKeySecret" />
</SimpleForm>
</Edit>
);
export default ProviderEdit;

View file

@ -1,16 +0,0 @@
"use client";
import { List, Datagrid, DateField, TextField, ListProps } from "react-admin";
const ProviderList = (props: ListProps) => (
<List {...props} exporter={false}>
<Datagrid rowClick="edit">
<TextField source="kind" />
<TextField source="name" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
</Datagrid>
</List>
);
export default ProviderList;

View file

@ -1,14 +0,0 @@
"use client";
/* eslint-disable import/no-anonymous-default-export */
import ProviderIcon from "@mui/icons-material/Business";
import ProviderList from "./ProviderList";
import ProviderEdit from "./ProviderEdit";
import ProviderCreate from "./ProviderCreate";
export default {
list: ProviderList,
create: ProviderCreate,
edit: ProviderEdit,
icon: ProviderIcon,
};

View file

@ -1,11 +0,0 @@
"use client";
import { SelectInput } from "react-admin";
export const ProviderKindInput = (props: any) => (
<SelectInput
source="kind"
choices={[{ id: "TWILIO", name: "Twilio" }]}
{...props}
/>
);

View file

@ -1,43 +0,0 @@
.voiceWaveWrapper {
width: 100%;
max-height: 50px;
display: flex;
justify-content: center;
}
.hidden {
display: none;
}
.visible {
display: block;
}
.buttonWrapper {
display: flex;
align-items: center;
justify-content: center;
}
.playerWrapper {
display: flex;
justify-content: center;
margin-top: 20px;
}
.recordTime {
align-self: center;
width: 66px;
height: 18px;
margin-top: 10px;
font-family: "sans";
font-style: normal;
font-weight: normal;
font-size: 15px;
line-height: 18px;
color: #000;
}
.content {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}

View file

@ -1,152 +0,0 @@
"use client";
import { useInput } from "react-admin";
import React, { useState } from "react";
import dynamic from "next/dynamic";
import MicIcon from "@mui/icons-material/Mic";
import StopIcon from "@mui/icons-material/Stop";
import Button from "@mui/material/Button";
// import { useTheme } from "@mui/styles"; // makeStyles,
// import AudioPlayer from "material-ui-audio-player";
import { useStopwatch } from "react-timer-hook";
import style from "./MicInput.module.css";
// import type { ReactMicProps } from "react-mic";
const ReactMic = dynamic<any>(
() => {
throw new Error(
"MIC INPUT FEATURE IS DISABLED",
); /* return import("react-mic").then((mod) => mod.ReactMic); */
},
{ ssr: false },
);
const blobToDataUri = (blob: Blob) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
return new Promise((resolve) => {
reader.onloadend = () => {
resolve(reader.result);
};
});
};
const dataUriToObj = (dataUri: string) => {
const [prefix, base64] = dataUri.split(",");
const mime = prefix.slice(5, prefix.indexOf(";"));
const result: any = {};
result[mime] = base64;
return result;
};
const blobToResult = async (blob: Blob) => {
const result = dataUriToObj((await blobToDataUri(blob)) as string);
return result;
};
/* const resultToDataUri = (result: Record<string, any>): string => {
if (!result || !result["audio/webm"]) return "";
const base64 = result["audio/webm"];
const r = `data:audio/webm;base64,${base64}`;
return r;
}; */
const MicInput = (props: any) => {
const { seconds, minutes, hours, start, reset, pause } = useStopwatch();
const theme: any = {}; // useTheme();
const {
field: { onChange }, // value
} = useInput(props);
const [record, setRecorder] = useState({ record: false });
// const decodedValue = resultToDataUri(value);
const startRecording = () => {
setRecorder({ record: true });
reset();
start();
};
const stopRecording = () => {
setRecorder({ record: false });
pause();
};
async function onData(recordedBlob: any) {
console.log({ recordedBlob });
}
async function onStop(recordedBlob: any) {
const result = await blobToResult(recordedBlob.blob);
onChange(result);
}
const isRecording = record.record;
// const canPlay = !isRecording && decodedValue;
const duration = `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
/*
const useStyles = makeStyles(() => ({
volumeIcon: {
display: "none",
},
mainSlider: {
display: "none",
},
}));
*/
return (
<div className="MuiFormControl-marginDense RaFormInput-input-40">
<div className={style.content}>
<div className={style.voiceWaveWrapper}>
<ReactMic
record={record.record}
className={isRecording ? style.visible : style.hidden}
onStop={onStop}
onData={onData}
// @ts-ignore
strokeColor={theme.palette.primary.main}
backgroundColor="white"
mimeType="audio/webm"
visualSetting="frequencyBars"
/>
</div>
<div>{isRecording ? <p>Recording... {duration}</p> : ""}</div>
<div className={style.buttonWrapper}>
{isRecording ? (
<Button variant="contained" color="primary" onClick={stopRecording}>
<StopIcon />
Stop
</Button>
) : (
<Button
variant="contained"
color="primary"
onClick={startRecording}
style={{ marginRight: "20px" }}
>
<MicIcon />
Record
</Button>
)}
</div>
<div className={style.playerWrapper}>
{/* canPlay && (
<AudioPlayer
elevation={0}
src={decodedValue}
variation="secondary"
volume={false}
useStyles={useStyles}
/>
) */}
</div>
</div>
</div>
);
};
export default MicInput;

View file

@ -1,54 +0,0 @@
"use client";
import {
SimpleForm,
Create,
FormDataConsumer,
SelectInput,
BooleanInput,
ReferenceInput,
required,
CreateProps,
} from "react-admin";
import TwilioLanguages from "./twilio-languages";
import {
PromptInput,
VoiceInput,
AvailableNumbersInput,
populateNumber,
} from "./shared";
import MicInput from "./MicInput";
const VoiceLineCreate = (props: CreateProps) => (
<Create {...props} title="Create Voice Line" transform={populateNumber}>
<SimpleForm>
<ReferenceInput
label="Provider"
source="providerId"
reference="voiceProviders"
validate={[required()]}
>
<SelectInput optionText={(p) => `${p.kind}: ${p.name}`} />
</ReferenceInput>
<FormDataConsumer subscription={{ values: true }}>
{AvailableNumbersInput}
</FormDataConsumer>
<SelectInput
source="language"
choices={TwilioLanguages.languages}
validate={[required()]}
/>
<FormDataConsumer subscription={{ values: true }}>
{VoiceInput}
</FormDataConsumer>
<FormDataConsumer subscription={{ values: true }}>
{PromptInput}
</FormDataConsumer>
<BooleanInput source="audioPromptEnabled" />
<MicInput source="promptAudio" />
</SimpleForm>
</Create>
);
export default VoiceLineCreate;

View file

@ -1,51 +0,0 @@
"use client";
import {
SimpleForm,
TextInput,
Edit,
FormDataConsumer,
SelectInput,
BooleanInput,
ReferenceInput,
required,
EditProps,
} from "react-admin";
import TwilioLanguages from "./twilio-languages";
import { VoiceInput, PromptInput } from "./shared";
import MicInput from "./MicInput";
const VoiceLineTitle = ({ record }: { record?: any }) => {
let title = "";
if (record) title = record.name ?? record.email;
return <span>VoiceLine {title}</span>;
};
const VoiceLineEdit = (props: EditProps) => (
<Edit title={<VoiceLineTitle />} {...props}>
<SimpleForm>
<ReferenceInput
disabled
label="Provider"
source="providerId"
reference="providers"
validate={[required()]}
>
<SelectInput optionText={(p) => `${p.kind}: ${p.name}`} />
</ReferenceInput>
<TextInput disabled source="providerLineSid" />
<TextInput disabled source="number" />
<SelectInput source="language" choices={TwilioLanguages.languages} />
<FormDataConsumer subscription={{ values: true }}>
{VoiceInput}
</FormDataConsumer>
<FormDataConsumer subscription={{ values: true }}>
{PromptInput}
</FormDataConsumer>
<BooleanInput source="audioPromptEnabled" />
<MicInput source="promptAudio" />
</SimpleForm>
</Edit>
);
export default VoiceLineEdit;

View file

@ -1,32 +0,0 @@
"use client";
import {
List,
ListProps,
Datagrid,
DateField,
FunctionField,
TextField,
ReferenceField,
} from "react-admin";
const VoiceLineList = (props: ListProps) => (
<List {...props} exporter={false}>
<Datagrid rowClick="edit">
<ReferenceField
label="Provider"
source="providerId"
reference="providers"
>
<FunctionField render={(p: any) => `${p.kind}: ${p.name}`} />
</ReferenceField>
<TextField source="number" />
<TextField source="language" />
<TextField source="voice" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
</Datagrid>
</List>
);
export default VoiceLineList;

View file

@ -1,14 +0,0 @@
"use client";
import VoiceLineIcon from "@mui/icons-material/PhoneCallback";
import VoiceLineList from "./VoiceLineList";
import VoiceLineEdit from "./VoiceLineEdit";
import VoiceLineCreate from "./VoiceLineCreate";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: VoiceLineList,
create: VoiceLineCreate,
edit: VoiceLineEdit,
icon: VoiceLineIcon,
};

View file

@ -1,149 +0,0 @@
/* add css module styles here (optional) */
@import url("https://fonts.googleapis.com/css?family=Lato:400,700&display=swap");
.recorder_library_box,
.recorder_library_box * {
box-sizing: border-box;
padding: 0;
margin: 0;
font-family: "Lato", sans-serif;
}
.recorder_library_box .recorder_box {
width: 100%;
margin: 0 auto;
padding: 30px 0;
}
.recorder_library_box .recorder_box_inner {
min-height: 400px;
background: #212121;
border-radius: 0 0 3px 3px;
display: flex;
flex-direction: column;
}
.recorder_library_box .mic_icon {
width: 60px;
display: flex;
height: 60px;
position: fixed;
justify-content: center;
align-items: center;
background: rgb(245, 0, 87);
border-radius: 50%;
bottom: 65px;
right: 20%;
color: #fff;
font-size: 25px;
}
.recorder_library_box .reco_header {
display: flex;
justify-content: space-between;
background: #bd9f61;
align-items: center;
padding: 20px 20px;
color: #fff;
border-radius: 3px 3px 0 0;
}
.recorder_library_box .reco_header .h2 {
font-weight: 400;
}
.recorder_library_box .reco_header .close_icons {
font-size: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
justify-content: center;
align-items: center;
display: flex;
cursor: pointer;
transition: 0.5s ease all;
}
.recorder_library_box .reco_header .close_icons:hover {
background: rgba(123, 118, 106, 0.21);
}
.recorder_library_box .record_section {
position: relative;
flex: 1;
}
.recorder_library_box .record_section .mic_icon {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 20px;
}
.recorder_library_box .record_section .duration_section {
position: absolute;
left: 50%;
transform: translate(-50%);
bottom: 100px;
}
.recorder_library_box .btn_wrapper {
margin: 20px 30px;
}
.recorder_library_box .btn_wrapper .btn {
border: 0;
outline: 0;
padding: 10px 20px;
border-radius: 20px;
background: #185fec;
color: #fff;
cursor: pointer;
border: 1px solid #185fec;
transition: 0.3s ease all;
}
.recorder_library_box .btn_wrapper .btn:hover {
background: #fff;
color: #185fec;
}
.recorder_library_box .btn_wrapper .clear_btn {
background: #fff;
color: #185fec;
margin-left: 15px;
}
.recorder_library_box .btn_wrapper .clear_btn:hover {
background: #185fec;
color: #fff;
}
.recorder_library_box .duration {
text-align: center;
}
.recorder_library_box .recorder_page_box {
min-height: calc(100vh - 128px);
background: #fff;
}
.recorder_library_box .duration * {
color: #fff;
font-size: 60px;
}
.recorder_library_box .duration_section .help {
color: #fff;
}
.recorder_library_box .record_controller {
position: absolute;
left: 50%;
transform: translate(-50%);
bottom: 0px;
padding: 20px 0;
display: flex;
}
.recorder_library_box .record_controller .icons {
width: 50px;
display: flex;
height: 50px;
justify-content: center;
align-items: center;
border-radius: 50%;
color: #fff;
margin-right: 15px;
font-size: 20px;
}
.recorder_library_box .record_controller .stop {
background: #940505;
}
.recorder_library_box .record_controller .pause {
background: #9c6702;
}

View file

@ -1,303 +0,0 @@
"use client";
/* eslint-disable react/display-name */
import React, { useState, useEffect } from "react";
import PlayIcon from "@mui/icons-material/PlayCircleFilled";
import {
TextInput,
SelectInput,
required,
useTranslate,
useNotify,
ReferenceInput,
ReferenceField,
TextField,
} from "react-admin";
import { IconButton, CircularProgress } from "@mui/material";
import absoluteUrl from "../../../_lib/absolute-url";
import TwilioLanguages from "./twilio-languages";
type TTSProvider = (voice: any, language: any, prompt: any) => Promise<void>;
const tts = async (providerId: any): Promise<TTSProvider> => {
const r = await fetch(
`/api/v1/voice/twilio/text-to-speech-token/${providerId}`,
);
const { token } = await r.json();
const twilioClient = await import("twilio-client");
return (voice, language, prompt): Promise<void> =>
new Promise((resolve) => {
if (!voice || !language || !prompt) resolve();
const { Device } = twilioClient;
const device = new Device();
const silence = `${absoluteUrl().origin}/static/silence.mp3`;
device.setup(token, {
codecPrefences: ["opus", "pcmu"],
enableRingingState: false,
fakeLocalDTMF: true,
disableAudioContextSounds: true,
sounds: {
disconnect: silence,
incoming: silence,
outgoing: silence,
},
});
device.on("ready", (device: any) => {
device.connect({ language, voice, prompt });
});
device.on("disconnect", () => resolve());
device.on("error", () => resolve());
});
};
export const TextToSpeechButton = ({ form }: any) => {
const { providerId, language, voice, promptText: prompt } = form.formData;
const [loading, setLoading] = useState<boolean>(false);
const [ttsProvider, setTTSProvider] = useState<
undefined | { provider: TTSProvider }
>(undefined);
const [playText, setPlayText] = useState<
undefined | { func: () => Promise<void> }
>(undefined);
useEffect(() => {
(async () => {
if (providerId) {
setLoading(true);
setTTSProvider({ provider: await tts(providerId) });
setLoading(false);
}
})();
}, [providerId]);
useEffect(() => {
(async () => {
setPlayText({
async func() {
setLoading(true);
if (ttsProvider) await ttsProvider.provider(voice, language, prompt);
setLoading(false);
},
});
})();
}, [prompt, language, voice, ttsProvider, ttsProvider?.provider]);
const disabled = !(providerId && prompt?.length >= 2 && voice && language);
/* TODO add this back to IconButtonwhen we know how to extend MUI theme and appease typescript
variant="contained"
*/
return (
<IconButton onClick={playText?.func} disabled={disabled} color="primary">
{!loading && <PlayIcon />}
{loading && <CircularProgress size={20} />}
</IconButton>
);
};
export const PromptInput = (form: any, ...rest: any[]) => (
<TextInput
source="promptText"
multiline
// options={{ fullWidth: true }}
InputProps={{ endAdornment: <TextToSpeechButton form={form} /> }}
{...rest}
/>
);
const validateVoice = (_args: any, values: any) => {
if (!values.language) return "validation.language";
if (!values.voice) return "validation.voice";
const availableVoices = TwilioLanguages.voices[values.language];
const found =
availableVoices.filter((v: any) => v.id === values.voice).length === 1;
if (!found) return "validation.voice";
return undefined;
};
export const VoiceInput = (form: any, ...rest: any[]) => {
const voice = TwilioLanguages.voices[form.formData.language] || [];
return (
// @ts-expect-error
<SelectInput
source="voice"
choices={voice}
validate={[required(), validateVoice]}
{...rest}
/>
);
};
let noAvailableNumbers = false;
let availableNumbers: any[] = [];
const getAvailableNumbers = async (providerId: string) => {
try {
const r = await fetch(`/api/v1/voice/providers/${providerId}/freeNumbers`);
availableNumbers = await r.json();
noAvailableNumbers = availableNumbers.length === 0;
return availableNumbers;
} catch (error) {
console.error(
`Could not fetch available numbers for provider ${providerId} - ${error}`,
);
return [];
}
};
const sidToNumber = (sid: any) =>
availableNumbers.filter(({ id }) => id === sid).map(({ name }) => name)[0];
export const populateNumber = (data: any) => ({
...data,
number: sidToNumber(data.providerLineSid),
});
const hasNumbers = (
_args: any,
_value: any,
_values: any,
_translate: any,
..._props: any[]
) => {
if (noAvailableNumbers) return "validation.noAvailableNumbers";
return undefined;
};
export const AvailableNumbersInput = (form: any, ...rest: any[]) => {
const {
// @ts-expect-error: non-existent property
meta: { touched, error } = {},
// @ts-expect-error: non-existent property
input: { ...inputProps },
...props
} = rest;
const translate = useTranslate();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const [choices, setChoices] = useState({});
// @ts-expect-error: Invalid return type
useEffect(async () => {
if (form && form.formData && form.formData.providerId) {
setLoading(true);
const choices = await getAvailableNumbers(form.formData.providerId);
setChoices({
choices,
helperText: noAvailableNumbers
? translate("validation.noAvailableNumbers")
: "",
});
if (noAvailableNumbers)
notify("validation.noAvailableNumbers", { type: "error" });
setLoading(false);
}
}, [form, notify, translate]);
return (
<>
<SelectInput
label="Number"
source="providerLineSid"
// @ts-expect-error: non-existent property
choices={choices.choices}
disabled={loading}
validate={[hasNumbers, required()]}
// @ts-expect-error: non-existent property
error={Boolean(touched && error) || Boolean(choices.helperText)}
// @ts-expect-error: non-existent property
helperText={choices.helperText}
{...inputProps}
{...props}
/>
{loading && <CircularProgress />}
</>
);
};
/*
const voiceLineName = voiceLine => {
return voiceLine.number
}
const getVoiceLineChoices = async ():Promise<any[]> => {
try {
const r = await fetch(`/api/v1/voice/voice-line`);
const lines = await r.json();
if(lines.data?.length > 0) {
return lines.data.map(voiceLine => ({"id": voiceLine.id, "name": voiceLineName(voiceLine)}))
}
return [];
} catch (error) {
console.error(
`Could not fetch voice lines error: ${error}`
);
return [];
}
}
export const AsyncSelectInput = (choiceLoader: () => Promise<any[]>, label, source, translationEmpty,) => (form, ...rest) => {
const {
meta: { touched, error } = {},
input: { ...inputProps },
...props
} = rest;
const translate = useTranslate();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const [choices, setChoices] = useState({choices: []});
useEffect(() => {
(async () => {
setLoading(true);
//const items = await choiceLoader()
const items = [{"id": "testing", "name": "OMG"}]
setChoices({
choices: items,
helperText: items.length === 0
? translate(translationEmpty)
: "",
});
if (items.length === 0) notify(translationEmpty, "error");
setLoading(false);
})()}, [form && form.formData ? form.formData.providerId : undefined]);
const isNotEmpty = () => {
if (choices.choices.length === 0) return translationEmpty;
return undefined;
};
return (
<>
{choices.choices.length > 0 &&
<SelectInput
label={label}
source={source}
choices={choices.choices}
disabled={loading}
validate={[isNotEmpty, required()]}
error={Boolean(touched && error) || Boolean(choices.helperText)}
helperText={choices.helperText}
{...inputProps}
{...props}
/>}
{loading && <CircularProgress />}
</>
)
}
export const VoiceLineSelectInput = AsyncSelectInput(getVoiceLineChoices, "Voice Line", "backendId", "validation.noVoiceLines" )
*/
export const VoiceLineSelectInput = (source: string) => () => (
<ReferenceInput
label="Voice Line"
source={source}
reference="voiceLines"
validate={[required()]}
>
<SelectInput optionText="number" />
</ReferenceInput>
);
export const VoiceLineField = (source: string) => () => (
<ReferenceField label="Voice Line" source={source} reference="voiceLines">
<TextField source="number" />
</ReferenceField>
);

View file

@ -1,65 +0,0 @@
const languages = {
languages: [
{ id: "arb", name: "Arabic" },
{ id: "cy-GB", name: "Welsh" },
{ id: "da-DK", name: "Danish" },
{ id: "de-DE", name: "German" },
{ id: "en-US", name: "English (US)" },
{ id: "en-AU", name: "English (Australian)" },
{ id: "en-GB", name: "English (British)" },
{ id: "en-GB-WLS", name: "English (Welsh)" },
{ id: "en-IN", name: "English (Indian)" },
{ id: "es-ES", name: "Spanish (Castilian)" },
{ id: "es-MX", name: "Spanish (Mexico)" },
{ id: "es-US", name: "Spanish (Latin American)" },
{ id: "fr-CA", name: "French (Canadian)" },
{ id: "fr-FR", name: "French" },
{ id: "hi-IN", name: "Hindi" },
{ id: "is-IS", name: "Icelandic" },
{ id: "it-IT", name: "Italian" },
{ id: "ja-JP", name: "Japanese" },
{ id: "ko-KR", name: "Korean" },
{ id: "nb-NO", name: "Norwegian" },
{ id: "nl-NL", name: "Dutch" },
{ id: "pl-PL", name: "Polish" },
{ id: "pt-BR", name: "Portuguese (Brazilian)" },
{ id: "pt-PT", name: "Portuguese (European)" },
{ id: "ro-RO", name: "Romanian" },
{ id: "ru-RU", name: "Russian" },
{ id: "sv-SE", name: "Swedish" },
{ id: "tr-TR", name: "Turkish" },
{ id: "zh-CN", name: "Chinese (Mandarin)" },
],
voices: {
arb: [{ id: "Polly.Zeina", name: "Zeina" }],
"cy-GB": [{ id: "Polly.Gwyneth", name: "Gwyneth" }],
"da-DK": [{ id: "Polly.Naja", name: "Naja" }],
"de-DE": [{ id: "Polly.Marlene", name: "Marlene" }],
"en-US": [{ id: "Polly.Salli", name: "Salli" }],
"en-AU": [{ id: "Polly.Nicole", name: "Nicole" }],
"en-GB": [{ id: "Polly.Amy", name: "Amy" }],
"en-GB-WLS": [{ id: "Polly.Geraint", name: "Geraint" }],
"en-IN": [{ id: "Polly.Aditi", name: "Aditi" }],
"es-ES": [{ id: "Polly.Conchita", name: "Conchita" }],
"es-MX": [{ id: "Polly.Mia", name: "Mia" }],
"es-US": [{ id: "Polly.Penelope", name: "Penelope" }],
"fr-CA": [{ id: "Polly.Chantal", name: "Chantal" }],
"fr-FR": [{ id: "Polly.Celine", name: "Celine" }],
"hi-IN": [{ id: "Polly.Aditi", name: "Aditi" }],
"is-IS": [{ id: "Polly.Dora", name: "Dora" }],
"it-IT": [{ id: "Polly.Carla", name: "Carla" }],
"ja-JP": [{ id: "Polly.Mizuki", name: "Mizuki" }],
"ko-KR": [{ id: "Polly.Seoyeon", name: "Seoyeon" }],
"nb-NO": [{ id: "Polly.Liv", name: "Liv" }],
"nl-NL": [{ id: "Polly.Lotte", name: "Lotte" }],
"pl-PL": [{ id: "Polly.Ewa", name: "Ewa" }],
"pt-BR": [{ id: "Polly.Vitoria", name: "Vitoria" }],
"pt-PT": [{ id: "Polly.Ines", name: "Ines" }],
"ro-RO": [{ id: "Polly.Carmen", name: "Carmen" }],
"ru-RU": [{ id: "Polly.Tatyana", name: "Tatyana" }],
"sv-SE": [{ id: "Polly.Astrid", name: "Astrid" }],
"tr-TR": [{ id: "Polly.Filiz", name: "Filiz" }],
"zh-CN": [{ id: "Polly.Zhiyu", name: "Zhiyu" }],
},
};
export default languages;

View file

@ -1,53 +0,0 @@
"use client";
import {
SimpleForm,
FormDataConsumer,
TextInput,
Create,
ArrayInput,
SimpleFormIterator,
regex,
required,
CreateProps,
} from "react-admin";
import { BackendTypeInput, BackendIdInput, HttpMethodInput } from "./shared";
/*
<ReferenceInput
label="Voice Line"
source="voiceLineId"
reference="voiceLines"
validate={[required()]}
>
<SelectInput optionText="number" />
</ReferenceInput>
*/
const WebhookCreate = (props: CreateProps) => (
<Create {...props} title="Create Webhooks">
<SimpleForm>
<TextInput source="name" validate={[required()]} />
<BackendTypeInput />
<FormDataConsumer subscription={{ values: true }}>
{BackendIdInput}
</FormDataConsumer>
<TextInput
source="endpointUrl"
validate={[required(), regex(/^https?:\/\/[^/]+/, "validation.url")]}
/>
<HttpMethodInput />
<ArrayInput source="headers">
<SimpleFormIterator>
<TextInput
source="header"
validate={[required(), regex(/^[\w-]+$/, "validation.headerName")]}
/>
<TextInput source="value" validate={[required()]} />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Create>
);
export default WebhookCreate;

View file

@ -1,48 +0,0 @@
"use client";
import {
SimpleForm,
TextInput,
Edit,
ArrayInput,
SimpleFormIterator,
regex,
required,
EditProps,
FormDataConsumer,
} from "react-admin";
import { BackendTypeInput, BackendIdInput, HttpMethodInput } from "./shared";
const WebhookTitle = ({ record }: any) => {
let title = "";
if (record) title = record.name ?? record.email;
return <span>Webhook {title}</span>;
};
const WebhookEdit = (props: EditProps) => (
<Edit title={<WebhookTitle />} {...props}>
<SimpleForm>
<TextInput source="name" validate={[required()]} />
<BackendTypeInput />
<FormDataConsumer subscription={{ values: true }}>
{BackendIdInput}
</FormDataConsumer>
<TextInput
source="endpointUrl"
validate={[required(), regex(/^https?:\/\/[^/]+/, "validation.url")]}
/>
<HttpMethodInput />
<ArrayInput source="headers">
<SimpleFormIterator>
<TextInput
source="header"
validate={[required(), regex(/^[\w-]+$/, "validation.headerName")]}
/>
<TextInput source="value" validate={[required()]} />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Edit>
);
export default WebhookEdit;

View file

@ -1,18 +0,0 @@
"use client";
import { List, Datagrid, DateField, TextField, ListProps } from "react-admin";
import { BackendIdField } from "./shared";
const WebhookList = (props: ListProps) => (
<List {...props} exporter={false}>
<Datagrid rowClick="edit">
<TextField source="name" />
<TextField source="backendType" />
<BackendIdField source="backendId" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
</Datagrid>
</List>
);
export default WebhookList;

View file

@ -1,14 +0,0 @@
"use client";
import WebhookIcon from "@mui/icons-material/Send";
import WebhookList from "./WebhookList";
import WebhookEdit from "./WebhookEdit";
import WebhookCreate from "./WebhookCreate";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: WebhookList,
create: WebhookCreate,
edit: WebhookEdit,
icon: WebhookIcon,
};

View file

@ -1,70 +0,0 @@
"use client";
import { SelectInput, required } from "react-admin";
import {
VoiceLineField,
VoiceLineSelectInput,
} from "../voice/voicelines/shared";
import {
WhatsAppBotField,
WhatsAppBotSelectInput,
} from "../whatsapp/bots/shared";
import { SignalBotField, SignalBotSelectInput } from "../signal/bots/shared";
const httpChoices = [
{ id: "post", name: "POST" },
{ id: "put", name: "PUT" },
];
export const HttpMethodInput = (props: any) => (
<SelectInput
source="httpMethod"
choices={httpChoices}
validate={[required()]}
initialValue="post"
{...props}
/>
);
const backendChoices = [
{ id: "signal", name: "Signal" },
{ id: "whatsapp", name: "WhatsApp" },
{ id: "voice", name: "Voice" },
];
const backendInputComponents = {
whatsapp: WhatsAppBotSelectInput("backendId"),
signal: SignalBotSelectInput("backendId"),
voice: VoiceLineSelectInput("backendId"),
};
const backendFieldComponents = {
whatsapp: WhatsAppBotField("backendId"),
signal: SignalBotField("backendId"),
voice: VoiceLineField("backendId"),
};
export const BackendTypeInput = (props: any) => (
<SelectInput
source="backendType"
choices={backendChoices}
validate={[required()]}
{...props}
/>
);
export const BackendIdInput = (form: any, ...rest: any[]) => {
const Component = form.formData.backendType
? backendInputComponents[form.formData.backendType]
: false;
return <>{Component && <Component form={form} {...rest} />}</>;
};
export const BackendIdField = (form: any, ...rest: any[]) => {
console.log(form);
const Component = form.record.backendType
? backendFieldComponents[form.record.backendType]
: false;
return <>{Component && <Component form={form} {...rest} />}</>;
};

View file

@ -1,13 +0,0 @@
"use client";
import { List, Datagrid, TextField } from "react-admin";
const WhatsappAttachmentList = (props: any) => (
<List {...props} exporter={false}>
<Datagrid rowClick="show">
<TextField source="id" />
</Datagrid>
</List>
);
export default WhatsappAttachmentList;

View file

@ -1,13 +0,0 @@
"use client";
import { Show, ShowProps, SimpleShowLayout, TextField } from "react-admin";
const WhatsappAttachmentShow = (props: ShowProps) => (
<Show {...props} title="Whatsapp Attachment">
<SimpleShowLayout>
<TextField source="id" />
</SimpleShowLayout>
</Show>
);
export default WhatsappAttachmentShow;

View file

@ -1,12 +0,0 @@
"use client";
import WhatsappAttachmentIcon from "@mui/icons-material/AttachFile";
import WhatsappAttachmentList from "./WhatsappAttachmentList";
import WhatsappAttachmentShow from "./WhatsappAttachmentShow";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: WhatsappAttachmentList,
show: WhatsappAttachmentShow,
icon: WhatsappAttachmentIcon,
};

View file

@ -1,47 +0,0 @@
"use client";
// import dynamic from "next/dynamic";
import { FC } from "react";
import {
SimpleForm,
Create,
TextInput,
required,
CreateProps,
} from "react-admin";
import { useSession } from "next-auth/react";
import { validateE164Number } from "../../../_lib/phone-numbers";
const WhatsappBotCreate: FC<CreateProps> = (props) => {
// const MuiPhoneNumber = dynamic(() => import("material-ui-phone-number"), {
// ssr: false,
// });
const { data: session } = useSession();
return (
<Create {...props} title="Create Whatsapp Bot" redirect="show">
<SimpleForm>
<TextInput
source="userId"
defaultValue={
// @ts-expect-error: non-existent property
session.user.id
}
/>
<TextInput
source="phoneNumber"
validate={[validateE164Number, required()]}
/>
{/* <MuiPhoneNumber
defaultCountry={"us"}
fullWidth
onChange={(e: any) => setFieldValue("phoneNumber", e)}
/> */}
<TextInput source="description" />
</SimpleForm>
</Create>
);
};
export default WhatsappBotCreate;

View file

@ -1,14 +0,0 @@
"use client";
import { SimpleForm, Edit, TextInput, required, EditProps } from "react-admin";
const WhatsappBotEdit = (props: EditProps) => (
<Edit {...props} title="Edit Bot">
<SimpleForm>
<TextInput source="phoneNumber" validate={[required()]} />
<TextInput source="description" />
</SimpleForm>
</Edit>
);
export default WhatsappBotEdit;

View file

@ -1,24 +0,0 @@
"use client";
import {
List,
Datagrid,
DateField,
TextField,
BooleanField,
} from "react-admin";
const WhatsappBotList = (props: any) => (
<List {...props} exporter={false}>
<Datagrid rowClick="show">
<TextField source="phoneNumber" />
<TextField source="description" />
<BooleanField source="isVerified" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
<TextField source="createdBy" />
</Datagrid>
</List>
);
export default WhatsappBotList;

View file

@ -1,177 +0,0 @@
"use client";
import React, { useEffect, useState } from "react";
import {
Card,
Typography,
Grid,
Button,
TextField as MaterialTextField,
IconButton,
} from "@mui/material";
import {
Show,
SimpleShowLayout,
TextField,
ShowProps,
useGetOne,
useRefresh,
BooleanField,
} from "react-admin";
import QRCode from "react-qr-code";
import useSWR from "swr";
import RefreshIcon from "@mui/icons-material/Refresh";
const Sidebar = ({ record }: any) => {
const [receivedMessages, setReceivedMessages] = useState([]);
const [phoneNumber, setPhoneNumber] = useState("");
const handlePhoneNumberChange = (event: any) => {
setPhoneNumber(event.target.value);
};
const [message, setMessage] = useState("");
const handleMessageChange = (event: any) => {
setMessage(event.target.value);
};
const sendMessage = async (phoneNumber: string, message: string) => {
await fetch(`/api/v1/whatsapp/bots/${record.token}/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber, message }),
});
};
const receiveMessages = async () => {
const result = await fetch(`/api/v1/whatsapp/bots/${record.token}/receive`);
const msgs = await result.json();
console.log(msgs);
setReceivedMessages(msgs);
};
return (
<Card style={{ width: "33%", marginLeft: 20, padding: 14 }}>
<Grid container direction="column" spacing={2}>
<Grid item>
<Typography variant="h6">Send message</Typography>
</Grid>
<Grid item>
<MaterialTextField
variant="outlined"
label="Phone number"
fullWidth
size="small"
value={phoneNumber}
onChange={handlePhoneNumberChange}
/>
</Grid>
<Grid item>
<MaterialTextField
variant="outlined"
label="Message"
multiline
rows={3}
fullWidth
size="small"
value={message}
onChange={handleMessageChange}
/>
</Grid>
<Grid item container direction="row-reverse">
<Button
variant="contained"
color="primary"
onClick={() => sendMessage(phoneNumber, message)}
>
Send
</Button>
</Grid>
<Grid item container direction="row">
<Grid item>
<Typography variant="h6">Receive messages</Typography>
</Grid>
<Grid item>
<IconButton
onClick={receiveMessages}
color="primary"
style={{ marginTop: -12 }}
>
<RefreshIcon />
</IconButton>
</Grid>
</Grid>
{receivedMessages.map((receivedMessage: any, index: number) => (
<Grid key={index} item container direction="column" spacing={1}>
<Grid item style={{ fontWeight: "bold", color: "#999" }}>
{receivedMessage.key.remoteJid.replace("@s.whatsapp.net", "")}
</Grid>
<Grid item style={{ borderBottom: "1px solid #999" }}>
{receivedMessage.message.conversation}
</Grid>
</Grid>
))}
</Grid>
</Card>
);
};
const WhatsappBotShow = (props: ShowProps) => {
const refresh = useRefresh();
const { data } = useGetOne("whatsappBots", {id: props.id});
const { data: registerData, error: registerError } = useSWR(
data && !data?.isVerified
? `/api/v1/whatsapp/bots/${props.id}/register`
: undefined,
{ refreshInterval: 59000 }
);
const unverifyBot = async () => {
await fetch(`/api/v1/whatsapp/bots/${props.id}/unverify`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ verified: false }),
});
};
console.log({ registerData, registerError });
useEffect(() => {
if (data && !data?.isVerified) {
const interval = setInterval(() => {
refresh();
}, 10000);
return () => clearInterval(interval);
}
return undefined;
}, [refresh, data]);
return (
<Show {...props} title="Bot" aside={<Sidebar />}>
<SimpleShowLayout>
<TextField source="phoneNumber" />
<BooleanField source="isVerified" />
<Button
color="primary"
size="small"
style={{ color: "black", backgroundColor: "#ddd" }}
onClick={async () => unverifyBot()}
>
Unverify
</Button>
<TextField source="description" />
<TextField source="token" />
{!data?.isVerified && data?.qrCode && data?.qrCode !== "" && (
<QRCode value={data.qrCode} />
)}
</SimpleShowLayout>
</Show>
);
};
export default WhatsappBotShow;

View file

@ -1,16 +0,0 @@
"use client";
import WhatsappBotIcon from "@mui/icons-material/WhatsApp";
import WhatsappBotList from "./WhatsappBotList";
import WhatsappBotEdit from "./WhatsappBotEdit";
import WhatsappBotCreate from "./WhatsappBotCreate";
import WhatsappBotShow from "./WhatsappBotShow";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: WhatsappBotList,
create: WhatsappBotCreate,
edit: WhatsappBotEdit,
show: WhatsappBotShow,
icon: WhatsappBotIcon,
};

View file

@ -1,33 +0,0 @@
"use client";
/* eslint-disable react/display-name */
import {
SelectInput,
required,
ReferenceInput,
ReferenceField,
TextField,
} from "react-admin";
export const WhatsAppBotSelectInput = (source: string) => () =>
(
<ReferenceInput
label="WhatsApp Bot"
reference="whatsappBots"
source={source}
validate={[required()]}
>
<SelectInput optionText="phoneNumber" />
</ReferenceInput>
);
export const WhatsAppBotField = (source: string) => () =>
(
<ReferenceField
label="WhatsApp Bot"
reference="whatsappBots"
source={source}
>
<TextField source="phoneNumber" />
</ReferenceField>
);

View file

@ -1,25 +0,0 @@
"use client";
import {
List,
ListProps,
Datagrid,
DateField,
TextField,
BooleanField,
} from "react-admin";
const WhatsappMessageList = (props: ListProps) => (
<List {...props} exporter={false}>
<Datagrid rowClick="show">
<TextField source="phoneNumber" />
<TextField source="description" />
<BooleanField source="isVerified" />
<DateField source="createdAt" />
<DateField source="updatedAt" />
<TextField source="createdBy" />
</Datagrid>
</List>
);
export default WhatsappMessageList;

View file

@ -1,31 +0,0 @@
"use client";
import {
Show,
ShowProps,
SimpleShowLayout,
TextField,
ReferenceManyField,
Datagrid,
} from "react-admin";
const WhatsappMessageShow = (props: ShowProps) => (
<Show {...props} title="Whatsapp Message">
<SimpleShowLayout>
<TextField source="waMessage" />
<TextField source="createdAt" />
<ReferenceManyField
label="Attachments"
reference="whatsappAttachments"
target="whatsappMessageId"
>
<Datagrid rowClick="show">
<TextField source="id" />
<TextField source="createdAt" />
</Datagrid>
</ReferenceManyField>
</SimpleShowLayout>
</Show>
);
export default WhatsappMessageShow;

View file

@ -1,12 +0,0 @@
"use client";
import WhatsappMessageIcon from "@mui/icons-material/Message";
import WhatsappMessageList from "./WhatsappMessageList";
import WhatsappMessageShow from "./WhatsappMessageShow";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
list: WhatsappMessageList,
show: WhatsappMessageShow,
icon: WhatsappMessageIcon,
};

View file

@ -1,81 +0,0 @@
import { TranslationMessages } from "react-admin";
import englishMessages from "ra-language-english";
const customEnglishMessages: TranslationMessages = {
...englishMessages,
auth: {
loggingIn: "Logging in...",
permissionDenied: "Permission denied",
},
pos: {
configuration: "Configuration",
menu: {
security: "Security",
accounts: "Accounts",
voicelines: "Voice Lines",
providers: "Voice Provider",
webhooks: "Webhooks",
voice: "Voice",
whatsapp: "WhatsApp",
signal: "Signal",
},
},
resources: {
signalBots: {
name: "Signal Bot |||| Signal Bots",
verifyDialog: {
sms: "Please enter the verification code sent via SMS to %{phoneNumber}",
voice:
"Please answer the call from Signal to %{phoneNumber} and enter the verification code",
},
},
whatsappBots: {
name: "WhatsApp Bot |||| WhatsApp Bots",
},
users: {
name: "User |||| Users",
},
accounts: {
name: "OAuth Account |||| OAuth Accounts",
},
voicelines: {
name: "Voice Line |||| Voice Lines",
fields: {
providerLineSid: "Provider Line SID",
},
},
providers: {
name: "Voice Provider |||| Voice Providers",
fields: {
credentials: {
accountSid: "Twilio Account SID",
apiKeySid: "Twilio API Key SID",
apiKeySecret: "Twilio API Key Secret",
},
},
},
webhooks: {
name: "Webhook |||| Webhooks",
fields: {
endpointUrl: "Endpoint URL",
httpMethod: "HTTP Method",
headers: "HTTP Headers",
header: "Header Name",
value: "Header Value",
},
},
},
validation: {
url: "a valid url starting with https:// is required",
voice: "a voice is required",
language: "a language is required",
headerName: "a valid http header name has only letters, numbers and dashes",
noAvailableNumbers:
"There are no available numbers to assign. Please visit the provider and purchase more numbers.",
noVoiceLines:
"There are no configured voice lines. Visit the Voice Lines admin page to create some.",
},
};
export default customEnglishMessages;

View file

@ -1,35 +0,0 @@
import { IncomingMessage } from "node:http";
function absoluteUrl(
req?: IncomingMessage,
localhostAddress = "localhost:3000"
) {
let host =
(req?.headers ? req.headers.host : window.location.host) ||
localhostAddress;
let protocol = /^localhost(:\d+)?$/.test(host) ? "http:" : "https:";
if (
req &&
req.headers["x-forwarded-host"] &&
typeof req.headers["x-forwarded-host"] === "string"
) {
host = req.headers["x-forwarded-host"];
}
if (
req &&
req.headers["x-forwarded-proto"] &&
typeof req.headers["x-forwarded-proto"] === "string"
) {
protocol = `${req.headers["x-forwarded-proto"]}:`;
}
return {
protocol,
host,
origin: protocol + "//" + host,
};
}
export default absoluteUrl;

View file

@ -1,40 +0,0 @@
import {
ApolloClient,
InMemoryCache,
ApolloLink,
HttpLink,
} from "@apollo/client";
import { onError } from "@apollo/client/link/error";
const errorLink = onError(
({ operation, graphQLErrors, networkError, forward }) => {
console.log("ERROR LINK", operation);
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path, ...rest }) =>
console.log(
`[GraphQL error]: Message: ${message}`,
locations,
path,
rest
)
);
if (networkError) console.log(`[Network error]: ${networkError}`);
forward(operation);
}
);
export const apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, new HttpLink({ uri: "/proxy/metamigo/graphql" })]),
cache: new InMemoryCache(),
/*
defaultOptions: {
watchQuery: {
fetchPolicy: "no-cache",
errorPolicy: "ignore",
},
query: {
fetchPolicy: "no-cache",
errorPolicy: "all",
},
}, */
});

View file

@ -1,213 +0,0 @@
import { promisify } from "node:util";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
import * as Boom from "@hapi/boom";
import * as Wreck from "@hapi/wreck";
import Credentials from "next-auth/providers/credentials";
import type { Adapter } from "next-auth/adapters";
import type { IncomingMessage } from "node:http";
const CF_JWT_HEADER_NAME = "cf-access-jwt-assertion";
const CF_JWT_ALGOS = ["RS256"];
export type VerifyFn = (token: string) => Promise<any>;
/**
* Returns a function that will accept a jwt and verify it against the cloudflare access details
*
* @param audience the cloudflare access audience id
* @param domain the cloudflare access domain
*/
export const cfVerifier = (audience: string, domain: string): VerifyFn => {
if (!audience || !domain)
throw Boom.badImplementation(
"Cloudflare configuration is missing. See project documentation."
);
const issuer = `https://${domain}`;
const client = jwksClient({
jwksUri: `${issuer}/cdn-cgi/access/certs`,
});
return async (token) => {
const getKey = (header: any, callback: any) => {
client.getSigningKey(header.kid, (err: any, key: any) => {
if (err)
throw Boom.serverUnavailable(
"failed to fetch cloudflare access jwks"
);
callback(undefined, key.getPublicKey());
});
};
const opts = {
algorithms: CF_JWT_ALGOS,
audience,
issuer,
};
// @ts-expect-error: Too many args
return promisify(jwt.verify)(token, getKey, opts);
};
};
/**
* Verifies the Cloudflare Access JWT and returns the decoded token's contents.
* Throws if the token is missing or invalid.
*
* @param verifier the verification function
* @param req the incoming http request to verify
* @return the original token and the decoded contents.
*/
export const verifyRequest = async (
verifier: VerifyFn,
req: IncomingMessage
): Promise<{ token: string; decoded: any }> => {
const token = req.headers[CF_JWT_HEADER_NAME] as string;
if (token) {
try {
const decoded = await verifier(token);
return { token, decoded };
} catch (error) {
console.error(error);
throw Boom.unauthorized("invalid cloudflare access token");
}
}
throw Boom.unauthorized("cloudflare access token missing");
};
/**
* Fetches user identity information from cloudflare.
*
* @param domain the cloudflare access domain
* @param token the encoded jwt token for the user
* @see https://developers.cloudflare.com/access/setting-up-access/json-web-token#groups-within-a-jwt
*/
export const getIdentity = async (
domain: string,
token: string
): Promise<any> => {
const { payload } = await Wreck.get(
`https://${domain}/cdn-cgi/access/get-identity`,
{
headers: {
Cookie: `CF_Authorization=${token}`,
},
json: true,
}
);
return payload;
};
const cloudflareAccountProvider = "cloudflare-access";
const cloudflareAuthorizeCallback =
(
req: IncomingMessage,
domain: string,
verifier: VerifyFn,
adapter: Adapter
): (() => Promise<any>) =>
async () => {
/*
lots of little variables in here.
token: the encoded jwt from cloudflare access
decoded: the decoded jwt containing the content cloudflare gives us
identity: we call the cloudflare access identity endpoint to retrieve more user identity information
this data is identity provider specific, so the format is unknown
it would be possible to support specific identity providers and have roles/groups
profile: this is the accumulated user information we have that we will fetch/build the user record with
*/
const { token, decoded } = await verifyRequest(verifier, req);
const profile = {
email: undefined,
name: undefined,
avatar: undefined,
};
if (decoded.email) profile.email = decoded.email;
if (decoded.name) profile.name = decoded.name;
const identity = await getIdentity(domain, token);
if (identity.email) profile.email = identity.email;
if (identity.name) profile.name = identity.name;
if (!profile.email)
throw new Error("cloudflare access authorization: email not found");
const providerId = `cfaccess|${identity.idp.type}|${identity.idp.id}`;
const providerAccountId = identity.user_uuid;
if (!providerAccountId)
throw new Error(
"cloudflare access authorization: missing provider account id"
);
const {
getUserByProviderAccountId,
getUserByEmail,
createUser,
linkAccount,
} =
// @ts-expect-error: non-existent property
await adapter.getAdapter({} as any);
const userByProviderAccountId = await getUserByProviderAccountId(
providerId,
providerAccountId
);
if (userByProviderAccountId) {
return userByProviderAccountId;
}
const userByEmail = await getUserByEmail(profile.email);
if (userByEmail) {
// we will not explicitly link accounts
throw new Error(
"cloudflare access authorization: user exists for email address, but is not linked."
);
}
const user = await createUser(profile);
// between the previous line and the next line exists a transactional bug
// https://github.com/nextauthjs/next-auth/issues/876
// hopefully we don't experience it
await linkAccount(
user.id,
providerId,
cloudflareAccountProvider,
providerAccountId,
// the following are unused but are specified for completness
undefined,
undefined,
undefined
);
return user;
};
/**
* @param audience the cloudflare access audience id
* @param domain the cloudflare access domain (including the .cloudflareaccess.com bit)
* @param adapter the next-auth adapter used to talk to the backend
* @param req the incoming request object used to parse the jwt from
*/
export const CloudflareAccessProvider = (
audience: string,
domain: string,
adapter: Adapter,
req: IncomingMessage
) => {
const verifier = cfVerifier(audience, domain);
return Credentials({
id: cloudflareAccountProvider,
name: "Cloudflare Access",
credentials: {},
authorize: cloudflareAuthorizeCallback(req, domain, verifier, adapter),
});
};

View file

@ -1,12 +0,0 @@
import pgDataProvider from "ra-postgraphile";
import schema from "./graphql-schema.json";
export const metamigoDataProvider = async (client: any) => {
const graphqlDataProvider: any = await pgDataProvider(
client,
// @ts-expect-error: Missing property
{},
{ introspection: { schema: schema.data.__schema } }
);
return graphqlDataProvider;
};

File diff suppressed because one or more lines are too long

View file

@ -1,230 +0,0 @@
/* eslint-disable unicorn/no-null */
import type {
Adapter,
AdapterAccount,
AdapterSession,
AdapterUser,
} from "next-auth/adapters";
import * as Wreck from "@hapi/wreck";
import * as Boom from "@hapi/boom";
import type { IAppConfig } from "@digiresilience/metamigo-config";
export interface Profile {
name: string;
email: string;
emailVerified: string;
userRole: string;
avatar?: string;
image?: string;
createdBy: string;
}
export type User = Profile & { id: string; createdAt: Date; updatedAt: Date };
export interface Session {
userId: string;
expires: Date;
sessionToken: string;
accessToken: string;
createdAt: Date;
updatedAt: Date;
}
// from https://github.com/nextauthjs/next-auth/blob/main/src/lib/errors.js
class UnknownError extends Error {
constructor(message: any) {
super(message);
this.name = "UnknownError";
this.message = message;
}
toJSON() {
return {
error: {
name: this.name,
message: this.message,
// stack: this.stack
},
};
}
}
class CreateUserError extends UnknownError {
constructor(message: any) {
super(message);
this.name = "CreateUserError";
this.message = message;
}
}
const basicHeader = (secret: any) =>
"Basic " + Buffer.from(secret + ":", "utf8").toString("base64");
export const MetamigoAdapter = (config: IAppConfig): Adapter => {
if (!config) throw new Error("MetamigoAdapter: config is not defined.");
const wreck = Wreck.defaults({
headers: {
authorization: basicHeader(config.nextAuth.secret),
},
baseUrl: `${config.frontend.apiUrl}/api/nextauth/`,
maxBytes: 1024 * 1024,
json: "force",
});
function getAdapter(): Adapter {
async function createUser(profile: Profile) {
try {
if (!profile.createdBy) profile = { ...profile, createdBy: "nextauth" };
profile.avatar = profile.image;
delete profile.image;
const { payload } = await wreck.post("createUser", {
payload: profile,
});
return payload;
} catch {
throw new CreateUserError("CREATE_USER_ERROR");
}
}
async function getUser(id: string) {
try {
const { payload } = await wreck.get(`getUser/${id}`);
return payload;
} catch (error) {
if (Boom.isBoom(error, 404)) return null;
throw new Error("GET_USER_BY_ID_ERROR");
}
}
async function getUserByEmail(email: string) {
try {
const { payload } = await wreck.get(`getUserByEmail/${email}`);
return payload;
} catch (error) {
if (Boom.isBoom(error, 404)) return null;
throw new Error("GET_USER_BY_EMAIL_ERROR");
}
}
async function getUserByAccount({
providerAccountId,
provider,
}: {
providerAccountId: string;
provider: string;
}) {
try {
const { payload } = await wreck.get(
`getUserByAccount/${provider}/${providerAccountId}`
);
return payload;
} catch (error) {
if (Boom.isBoom(error, 404)) return null;
console.log(error);
throw new Error("GET_USER_BY_ACCOUNT");
}
}
async function updateUser(user: User) {
try {
const { payload } = await wreck.put("updateUser", {
payload: user,
});
return payload;
} catch {
throw new Error("UPDATE_USER");
}
}
async function linkAccount(account: AdapterAccount) {
try {
await wreck.put("linkAccount", { payload: account } as any);
} catch (error) {
console.log(error);
throw new Error("LINK_ACCOUNT_ERROR");
}
}
async function createSession(user: User) {
try {
const { payload }: { payload: AdapterSession } = await wreck.post(
"createSession",
{
payload: user,
}
);
payload.expires = new Date(payload.expires);
return payload;
} catch (error) {
console.log(error);
throw new Error("CREATE_SESSION_ERROR");
}
}
async function getSessionAndUser(sessionToken: string) {
try {
const { payload }: { payload: any } = await wreck.get(
`getSessionAndUser/${sessionToken}`
);
const {
session,
user,
}: { session: AdapterSession; user: AdapterUser } = payload;
session.expires = new Date(session.expires);
return { session, user };
} catch (error) {
console.log(error);
if (Boom.isBoom(error, 404)) return null;
throw new Error("GET_SESSION_AND_USER_ERROR");
}
}
async function updateSession(session: Session, force: boolean) {
try {
const payload = {
...session,
expires: new Date(session.expires).getTime(),
};
const { payload: result } = await wreck.put(
`updateSession?force=${Boolean(force)}`,
{
payload,
}
);
return result;
} catch {
throw new Error("UPDATE_SESSION_ERROR");
}
}
async function deleteSession(sessionToken: string) {
try {
await wreck.delete(`deleteSession/${sessionToken}`);
} catch {
throw new Error("DELETE_SESSION_ERROR");
}
}
return {
createUser,
getUser,
getUserByEmail,
getUserByAccount,
updateUser,
// deleteUser,
linkAccount,
// unlinkAccount,
createSession,
getSessionAndUser,
updateSession,
deleteSession,
// @ts-expect-error: Type error
} as AdapterInstance<Profile, User, Session, unknown>;
}
return getAdapter();
};

View file

@ -1,30 +0,0 @@
import { regex } from "react-admin";
export const E164Regex = /^\+[1-9]\d{1,14}$/;
/**
* Returns true if the number is a valid E164 number
*/
export const isValidE164Number = (phoneNumber: string) =>
E164Regex.test(phoneNumber);
/**
* Given a phone number approximation, will clean out whitespace and punctuation.
*/
export const sanitizeE164Number = (phoneNumber: string) => {
if (!phoneNumber) return "";
if (!phoneNumber.trim()) return "";
const sanitized = phoneNumber
.replaceAll(/\s/g, "")
.replaceAll(".", "")
.replaceAll("-", "")
.replaceAll("(", "")
.replaceAll(")", "");
if (sanitized[0] !== "+") return `+${sanitized}`;
return sanitized;
};
export const validateE164Number = regex(
E164Regex,
"Must start with a + and have no punctunation and no spaces."
);

View file

@ -1,5 +1,6 @@
import { Admin } from "./_components/Admin";
// import { Admin } from "./_components/Admin";
import { Box } from "@mui/material";
export default function Page() {
return <Admin />;
return <Box />;
}