Remove metamigo-add
This commit is contained in:
parent
28f7f0f47b
commit
859c11fb1c
70 changed files with 0 additions and 4044 deletions
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
export { default as AppBar } from "./AppBar";
|
|
||||||
export { Layout } from "./Layout";
|
|
||||||
export { default as Menu } from "./Menu";
|
|
||||||
|
|
@ -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)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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} />}</>;
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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",
|
|
||||||
},
|
|
||||||
}, */
|
|
||||||
});
|
|
||||||
|
|
@ -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),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
|
@ -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();
|
|
||||||
};
|
|
||||||
|
|
@ -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."
|
|
||||||
);
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import NextAuth from "next-auth";
|
|
||||||
import Google from "next-auth/providers/google";
|
|
||||||
import GitHub from "next-auth/providers/github";
|
|
||||||
import GitLab from "next-auth/providers/gitlab";
|
|
||||||
import Cognito from "next-auth/providers/cognito";
|
|
||||||
import { loadConfig, IAppConfig } from "@digiresilience/metamigo-config";
|
|
||||||
/*
|
|
||||||
import { MetamigoAdapter } from "app/_lib/nextauth-adapter";
|
|
||||||
import { CloudflareAccessProvider } from "app/_lib/cloudflare";
|
|
||||||
|
|
||||||
const nextAuthOptions = (config: IAppConfig, req: NextRequest) => {
|
|
||||||
const { nextAuth, cfaccess } = config;
|
|
||||||
const adapter = MetamigoAdapter(config);
|
|
||||||
const providers = [];
|
|
||||||
|
|
||||||
const { audience, domain } = cfaccess;
|
|
||||||
const cloudflareAccessEnabled = audience && domain;
|
|
||||||
if (cloudflareAccessEnabled)
|
|
||||||
providers.push(CloudflareAccessProvider(audience, domain, adapter, req as any));
|
|
||||||
else {
|
|
||||||
if (nextAuth.google?.id)
|
|
||||||
providers.push(
|
|
||||||
Google({
|
|
||||||
clientId: nextAuth.google.id,
|
|
||||||
clientSecret: nextAuth.google.secret,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextAuth.github?.id)
|
|
||||||
providers.push(
|
|
||||||
GitHub({
|
|
||||||
clientId: nextAuth.github.id,
|
|
||||||
clientSecret: nextAuth.github.secret,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextAuth.gitlab?.id)
|
|
||||||
providers.push(
|
|
||||||
GitLab({
|
|
||||||
clientId: nextAuth.gitlab.id,
|
|
||||||
clientSecret: nextAuth.gitlab.secret,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextAuth.cognito?.id)
|
|
||||||
providers.push(
|
|
||||||
Cognito({
|
|
||||||
clientId: nextAuth.cognito.id,
|
|
||||||
clientSecret: nextAuth.cognito.secret,
|
|
||||||
// domain: nextAuth.cognito.domain,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providers.length === 0)
|
|
||||||
throw new Error(
|
|
||||||
"No next-auth providers configured. See Metamigo configuration docs."
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
secret: nextAuth.secret,
|
|
||||||
session: {
|
|
||||||
strategy: "database",
|
|
||||||
maxAge: 8 * 60 * 60, // 8 hours
|
|
||||||
},
|
|
||||||
jwt: {
|
|
||||||
secret: nextAuth.secret,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
adapter,
|
|
||||||
callbacks: {
|
|
||||||
async session({ session, user }: any) {
|
|
||||||
session.user.id = user.id;
|
|
||||||
session.user.userRole = user.userRole;
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
const handler = async (req: NextRequest, context: any) => {
|
|
||||||
const config = await loadConfig();
|
|
||||||
const authOptions = {}; // nextAuthOptions(config, req);
|
|
||||||
// @ts-expect-error: non-existent property
|
|
||||||
return NextAuth(req, context, authOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
||||||
|
|
||||||
export const POST = createProxyMiddleware({
|
|
||||||
target:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? "http://metamigo-api:3001"
|
|
||||||
: "http://127.0.0.1:3001",
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: { "^/graphql": "/graphql" },
|
|
||||||
xfwd: true,
|
|
||||||
onProxyReq(proxyReq, req, _res) {
|
|
||||||
const auth = proxyReq.getHeader("authorization");
|
|
||||||
if (auth) {
|
|
||||||
// pass along user provided authorization header
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else extract the session token from the cookie and pass
|
|
||||||
// as bearer token to the proxy target
|
|
||||||
let token = req.cookies["__Secure-next-auth.session-token"];
|
|
||||||
if (!token) token = req.cookies["next-auth.session-token"];
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
proxyReq.setHeader("authorization", `Bearer ${token}`);
|
|
||||||
proxyReq.removeHeader("cookie");
|
|
||||||
} else {
|
|
||||||
console.error("no token found. proxied request to backend will fail.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
||||||
|
|
||||||
const handler = createProxyMiddleware({
|
|
||||||
target:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? "http://metamigo-api:3001"
|
|
||||||
: "http://localhost:3001",
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: { "^/api/v1": "/api" },
|
|
||||||
xfwd: true,
|
|
||||||
onProxyReq(proxyReq, req) {
|
|
||||||
const auth = proxyReq.getHeader("authorization");
|
|
||||||
if (auth) {
|
|
||||||
// pass along user provided authorization header
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else extract the session token from the cookie and pass
|
|
||||||
// as bearer token to the proxy target
|
|
||||||
// const token = req.cookies["next-auth.session-token"];
|
|
||||||
let token = req.cookies["__Secure-next-auth.session-token"];
|
|
||||||
if (!token) token = req.cookies["next-auth.session-token"];
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
proxyReq.setHeader("authorization", `Bearer ${token}`);
|
|
||||||
proxyReq.removeHeader("cookie");
|
|
||||||
} else {
|
|
||||||
console.error("no token found. proxied request to backend will fail.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { handler as GET, handler as POST, handler as PUT, handler as DELETE};
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue