WIP 2
This commit is contained in:
parent
43bfdaa1e3
commit
fe6c9419dd
87 changed files with 16739 additions and 2526 deletions
|
|
@ -1,10 +1,5 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import "app/_styles/global.css";
|
import "app/_styles/global.css";
|
||||||
import "@fontsource/poppins/400.css";
|
|
||||||
import "@fontsource/poppins/700.css";
|
|
||||||
import "@fontsource/roboto/400.css";
|
|
||||||
import "@fontsource/roboto/700.css";
|
|
||||||
import "@fontsource/playfair-display/900.css";
|
|
||||||
// import getConfig from "next/config";
|
// import getConfig from "next/config";
|
||||||
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||||
import { InternalLayout } from "../_components/InternalLayout";
|
import { InternalLayout } from "../_components/InternalLayout";
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,6 @@ import { AppProvider } from "leafcutter-common/components/AppProvider";
|
||||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||||
import en from "leafcutter-common/locales/en.json";
|
import en from "leafcutter-common/locales/en.json";
|
||||||
import fr from "leafcutter-common/locales/fr.json";
|
import fr from "leafcutter-common/locales/fr.json";
|
||||||
import "@fontsource/poppins/400.css";
|
|
||||||
import "@fontsource/poppins/700.css";
|
|
||||||
import "@fontsource/roboto/400.css";
|
|
||||||
import "@fontsource/roboto/700.css";
|
|
||||||
import "@fontsource/playfair-display/900.css";
|
|
||||||
import "app/_styles/global.css";
|
|
||||||
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
||||||
|
|
||||||
LicenseInfo.setLicenseKey(
|
LicenseInfo.setLicenseKey(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import "app/_styles/global.css";
|
import "app/_styles/global.css";
|
||||||
import "@fontsource/poppins/400.css";
|
|
||||||
import "@fontsource/poppins/700.css";
|
|
||||||
import "@fontsource/roboto/400.css";
|
|
||||||
import "@fontsource/roboto/700.css";
|
|
||||||
import "@fontsource/playfair-display/900.css";
|
|
||||||
// import getConfig from "next/config";
|
// import getConfig from "next/config";
|
||||||
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||||
import { MultiProvider } from "./_components/MultiProvider";
|
import { MultiProvider } from "./_components/MultiProvider";
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/playfair-display": "^5.0.23",
|
|
||||||
"@fontsource/poppins": "^5.0.12",
|
|
||||||
"@fontsource/roboto": "^5.0.12",
|
|
||||||
"@mui/icons-material": "^5",
|
"@mui/icons-material": "^5",
|
||||||
"@mui/lab": "^5.0.0-alpha.168",
|
"@mui/lab": "^5.0.0-alpha.168",
|
||||||
"@mui/material": "^5",
|
"@mui/material": "^5",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
|
import { Roboto, Playfair_Display, Poppins } from "next/font/google";
|
||||||
|
|
||||||
|
const roboto = Roboto({
|
||||||
|
weight: ["400"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const playfair = Playfair_Display({
|
||||||
|
weight: ["900"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const poppins = Poppins({
|
||||||
|
weight: ["400", "700"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fonts = {
|
||||||
|
roboto,
|
||||||
|
playfair,
|
||||||
|
poppins,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const colors: any = {
|
export const colors: any = {
|
||||||
lightGray: "#ededf0",
|
lightGray: "#ededf0",
|
||||||
mediumGray: "#e3e5e5",
|
mediumGray: "#e3e5e5",
|
||||||
|
|
@ -29,33 +56,33 @@ export const colors: any = {
|
||||||
|
|
||||||
export const typography: any = {
|
export const typography: any = {
|
||||||
h1: {
|
h1: {
|
||||||
fontFamily: "Playfair, serif",
|
fontFamily: playfair.style.fontFamily,
|
||||||
fontSize: 45,
|
fontSize: 45,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
lineHeight: 1.1,
|
lineHeight: 1.1,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
},
|
},
|
||||||
h2: {
|
h2: {
|
||||||
fontFamily: "Poppins, sans-serif",
|
fontFamily: poppins.style.fontFamily,
|
||||||
fontSize: 35,
|
fontSize: 35,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
lineHeight: 1.1,
|
lineHeight: 1.1,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
},
|
},
|
||||||
h3: {
|
h3: {
|
||||||
fontFamily: "Poppins, sans-serif",
|
fontFamily: poppins.style.fontFamily,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
fontSize: 27,
|
fontSize: 27,
|
||||||
lineHeight: 1.1,
|
lineHeight: 1.1,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
},
|
},
|
||||||
h4: {
|
h4: {
|
||||||
fontFamily: "Poppins, sans-serif",
|
fontFamily: poppins.style.fontFamily,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
},
|
},
|
||||||
h5: {
|
h5: {
|
||||||
fontFamily: "Roboto, sans-serif",
|
fontFamily: roboto.style.fontFamily,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
lineHeight: "24px",
|
lineHeight: "24px",
|
||||||
|
|
@ -64,20 +91,20 @@ export const typography: any = {
|
||||||
margin: 1,
|
margin: 1,
|
||||||
},
|
},
|
||||||
h6: {
|
h6: {
|
||||||
fontFamily: "Roboto, sans-serif",
|
fontFamily: roboto.style.fontFamily,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
p: {
|
p: {
|
||||||
fontFamily: "Roboto, sans-serif",
|
fontFamily: roboto.style.fontFamily,
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
lineHeight: "26.35px",
|
lineHeight: "26.35px",
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
},
|
},
|
||||||
small: {
|
small: {
|
||||||
fontFamily: "Roboto, sans-serif",
|
fontFamily: roboto.style.fontFamily,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
lineHeight: "18px",
|
lineHeight: "18px",
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getToken } from "next-auth/jwt";
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
if (validDomains.includes(domain)) {
|
|
||||||
res.headers.set("Access-Control-Allow-Origin", origin);
|
|
||||||
res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
||||||
res.headers.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const withAuthInfo =
|
|
||||||
(handler: any) => async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const session: any = await getToken({
|
|
||||||
req,
|
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
|
||||||
});
|
|
||||||
let email = session?.email?.toLowerCase();
|
|
||||||
|
|
||||||
const requestSignature = req.query.signature;
|
|
||||||
const url = new URL(req.headers.referer as string);
|
|
||||||
const referrerSignature = url.searchParams.get("signature");
|
|
||||||
|
|
||||||
console.log({ requestSignature, referrerSignature });
|
|
||||||
const isAppPath = !!req.url?.startsWith("/app");
|
|
||||||
const isResourcePath = !!req.url?.match(/\/(api|app|bootstrap|3961|ui|translations|internal|login|node_modules)/);
|
|
||||||
|
|
||||||
if (requestSignature && isAppPath) {
|
|
||||||
console.log("Has Signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerSignature && isResourcePath) {
|
|
||||||
console.log("Has Signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
return res.status(401).json({ error: "Not authorized" });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.headers["x-proxy-user"] = email;
|
|
||||||
req.headers["x-proxy-roles"] = "leafcutter_user";
|
|
||||||
const auth = `${email}:${process.env.OPENSEARCH_USER_PASSWORD}`;
|
|
||||||
const buff = Buffer.from(auth);
|
|
||||||
const base64data = buff.toString("base64");
|
|
||||||
req.headers.Authorization = `Basic ${base64data}`;
|
|
||||||
return handler(req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxy = createProxyMiddleware({
|
|
||||||
target: process.env.OPENSEARCH_DASHBOARDS_URL,
|
|
||||||
changeOrigin: true,
|
|
||||||
xfwd: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withAuthInfo(proxy);
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
api: {
|
|
||||||
bodyParser: false,
|
|
||||||
externalResolver: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import "./_styles/global.css";
|
import "./_styles/global.css";
|
||||||
import "@fontsource/poppins/400.css";
|
|
||||||
import "@fontsource/poppins/700.css";
|
|
||||||
import "@fontsource/roboto/400.css";
|
|
||||||
import "@fontsource/roboto/700.css";
|
|
||||||
import "@fontsource/playfair-display/900.css";
|
|
||||||
import { MultiProvider } from "./_components/MultiProvider";
|
import { MultiProvider } from "./_components/MultiProvider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/playfair-display": "^5.0.23",
|
|
||||||
"@fontsource/poppins": "^5.0.12",
|
|
||||||
"@fontsource/roboto": "^5.0.12",
|
|
||||||
"@mui/icons-material": "^5",
|
"@mui/icons-material": "^5",
|
||||||
"@mui/lab": "^5.0.0-alpha.168",
|
"@mui/lab": "^5.0.0-alpha.168",
|
||||||
"@mui/material": "^5",
|
"@mui/material": "^5",
|
||||||
|
|
@ -28,22 +25,16 @@
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"leafcutter-common": "*",
|
"leafcutter-common": "*",
|
||||||
"material-ui-popup-state": "^5.0.10",
|
"material-ui-popup-state": "^5.0.10",
|
||||||
|
"metamigo-common": "*",
|
||||||
"mui-chips-input": "^2.1.4",
|
"mui-chips-input": "^2.1.4",
|
||||||
"next": "14.1.3",
|
"next": "14.1.3",
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
"ra-data-graphql": "^4.16.12",
|
|
||||||
"ra-i18n-polyglot": "^4.16.12",
|
|
||||||
"ra-input-rich-text": "^4.16.13",
|
|
||||||
"ra-language-english": "^4.16.12",
|
|
||||||
"ra-postgraphile": "^6.1.2",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-admin": "^4.16.13",
|
|
||||||
"react-cookie": "^7.1.0",
|
"react-cookie": "^7.1.0",
|
||||||
"react-digit-input": "^2.1.0",
|
"react-digit-input": "^2.1.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-iframe": "^1.8.5",
|
"react-iframe": "^1.8.5",
|
||||||
"react-polyglot": "^0.7.2",
|
"react-polyglot": "^0.7.2",
|
||||||
"react-qr-code": "^2.0.12",
|
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
|
|
|
||||||
0
apps/metamigo-frontend/app/_actions/bots.ts
Normal file
0
apps/metamigo-frontend/app/_actions/bots.ts
Normal file
31
apps/metamigo-frontend/app/_components/DeleteDialog.tsx
Normal file
31
apps/metamigo-frontend/app/_components/DeleteDialog.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Grid, Box } from "@mui/material";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { typography } from "@/app/_styles/theme";
|
||||||
|
|
||||||
|
interface DeleteDialogProps {
|
||||||
|
title: string;
|
||||||
|
entity: string;
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteDialog: FC<DeleteDialogProps> = ({ title, entity, children }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { h3 } = typography;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||||
|
<Grid container direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={h3}>{title}</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
apps/metamigo-frontend/app/_components/Detail.tsx
Normal file
30
apps/metamigo-frontend/app/_components/Detail.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Grid, Box } from "@mui/material";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { typography } from "@/app/_styles/theme";
|
||||||
|
|
||||||
|
interface DetailProps {
|
||||||
|
title: string;
|
||||||
|
entity: string;
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Detail: FC<DetailProps> = ({ title, entity, children }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { h3 } = typography;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||||
|
<Grid container direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={h3}>{title}</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
apps/metamigo-frontend/app/_components/Edit.tsx
Normal file
30
apps/metamigo-frontend/app/_components/Edit.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Grid, Box } from "@mui/material";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { typography } from "@/app/_styles/theme";
|
||||||
|
|
||||||
|
interface EditProps {
|
||||||
|
title: string;
|
||||||
|
entity: string;
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Edit: FC<EditProps> = ({ title, entity, children }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { h3 } = typography;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||||
|
<Grid container direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={h3}>{title}</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,11 +3,27 @@
|
||||||
import { FC, PropsWithChildren, useState } from "react";
|
import { FC, PropsWithChildren, useState } from "react";
|
||||||
import { Grid } from "@mui/material";
|
import { Grid } from "@mui/material";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { css, Global } from "@emotion/react";
|
||||||
|
import { fonts } from "@/app/_styles/theme";
|
||||||
|
|
||||||
|
type LayoutProps = PropsWithChildren<{
|
||||||
|
docs?: any;
|
||||||
|
}>;
|
||||||
|
|
||||||
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
|
const { roboto } = fonts;
|
||||||
|
const globalCSS = css`
|
||||||
|
* {
|
||||||
|
font-family: ${roboto.style.fontFamily};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Global styles={globalCSS} />
|
||||||
|
<CssBaseline />
|
||||||
<Grid container direction="row">
|
<Grid container direction="row">
|
||||||
<Sidebar open={open} setOpen={setOpen} />
|
<Sidebar open={open} setOpen={setOpen} />
|
||||||
<Grid
|
<Grid
|
||||||
|
|
@ -17,5 +33,6 @@ export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
{children as any}
|
{children as any}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
||||||
import { Grid, Box } from "@mui/material";
|
import { Grid, Box } from "@mui/material";
|
||||||
import { DataGridPro, GridColDef } from "@mui/x-data-grid-pro";
|
import { DataGridPro, GridColDef } from "@mui/x-data-grid-pro";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { typography } from "@/app/_styles/theme";
|
||||||
|
|
||||||
interface ListProps {
|
interface ListProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -14,6 +15,7 @@ interface ListProps {
|
||||||
|
|
||||||
export const List: FC<ListProps> = ({ title, entity, rows, columns }) => {
|
export const List: FC<ListProps> = ({ title, entity, rows, columns }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { h3 } = typography;
|
||||||
|
|
||||||
const onRowClick = (id: string) => {
|
const onRowClick = (id: string) => {
|
||||||
router.push(`/${entity}/${id}`);
|
router.push(`/${entity}/${id}`);
|
||||||
|
|
@ -23,7 +25,7 @@ export const List: FC<ListProps> = ({ title, entity, rows, columns }) => {
|
||||||
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||||
<Grid container direction="column">
|
<Grid container direction="column">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<h1>{title}</h1>
|
<Box sx={h3}>{title}</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Box
|
<Box
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ import {
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// import LinkLogo from "public/link-logo-small.png";
|
import { fonts } from "@/app/_styles/theme";
|
||||||
|
import LinkLogo from "@/public/link-logo-small.png";
|
||||||
// import { useSession, signOut } from "next-auth/react";
|
// import { useSession, signOut } from "next-auth/react";
|
||||||
|
|
||||||
const openWidth = 270;
|
const openWidth = 270;
|
||||||
|
|
@ -113,7 +114,6 @@ const MenuItem = ({
|
||||||
variant="body1"
|
variant="body1"
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: "Roboto",
|
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
border: 0,
|
border: 0,
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
|
|
@ -155,6 +155,7 @@ interface SidebarProps {
|
||||||
|
|
||||||
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const { poppins } = fonts;
|
||||||
// const { data: session } = useSession();
|
// const { data: session } = useSession();
|
||||||
// const username = session?.user?.name || "User";
|
// const username = session?.user?.name || "User";
|
||||||
|
|
||||||
|
|
@ -215,7 +216,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={"" /* LinkLogo */}
|
src={LinkLogo}
|
||||||
alt="Link logo"
|
alt="Link logo"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
|
|
@ -237,7 +238,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
mt: 1,
|
mt: 1,
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
fontFamily: "Poppins",
|
fontFamily: poppins.style.fontFamily,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Metamigo
|
Metamigo
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
const getAllBots = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOneBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const receiveMessages = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestCode = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const unverifyBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Facebook: Service = {
|
||||||
|
getAllBots,
|
||||||
|
getOneBot,
|
||||||
|
sendMessage,
|
||||||
|
receiveMessages,
|
||||||
|
registerBot,
|
||||||
|
resetBot,
|
||||||
|
requestCode,
|
||||||
|
unverifyBot,
|
||||||
|
refreshBot,
|
||||||
|
createBot,
|
||||||
|
deleteBot,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Service } from "./service";
|
||||||
|
import { Facebook } from "./facebook";
|
||||||
|
|
||||||
|
const services: Record<string, Service> = {
|
||||||
|
facebook: Facebook,
|
||||||
|
none: NextResponse.error() as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getService = (req: NextRequest): Service => {
|
||||||
|
const service = req.nextUrl.searchParams.get("service") ?? "none";
|
||||||
|
return services[service];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllBots = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.getAllBots(req);
|
||||||
|
|
||||||
|
export const getOneBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.getOneBot(req);
|
||||||
|
|
||||||
|
export const sendMessage = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.sendMessage(req);
|
||||||
|
|
||||||
|
export const receiveMessages = async (
|
||||||
|
req: NextRequest,
|
||||||
|
): Promise<NextResponse> => getService(req)?.receiveMessages(req);
|
||||||
|
|
||||||
|
export const registerBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.registerBot(req);
|
||||||
|
|
||||||
|
export const resetBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.resetBot(req);
|
||||||
|
|
||||||
|
export const requestCode = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.requestCode(req);
|
||||||
|
|
||||||
|
export const unverifyBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.unverifyBot(req);
|
||||||
|
|
||||||
|
export const refreshBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.refreshBot(req);
|
||||||
|
|
||||||
|
export const createBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.createBot(req);
|
||||||
|
|
||||||
|
export const deleteBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||||
|
getService(req)?.deleteBot(req);
|
||||||
15
apps/metamigo-frontend/app/_lib/service.ts
Normal file
15
apps/metamigo-frontend/app/_lib/service.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
getAllBots: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
getOneBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
sendMessage: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
receiveMessages: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
registerBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
resetBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
requestCode: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
unverifyBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
refreshBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
createBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
deleteBot: (req: NextRequest) => Promise<NextResponse>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
const getAllBots = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOneBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const receiveMessages = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestCode = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const unverifyBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Signal: Service = {
|
||||||
|
getAllBots,
|
||||||
|
getOneBot,
|
||||||
|
sendMessage,
|
||||||
|
receiveMessages,
|
||||||
|
registerBot,
|
||||||
|
resetBot,
|
||||||
|
requestCode,
|
||||||
|
unverifyBot,
|
||||||
|
refreshBot,
|
||||||
|
createBot,
|
||||||
|
deleteBot,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
const getAllBots = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOneBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const receiveMessages = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestCode = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const unverifyBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Voice: Service = {
|
||||||
|
getAllBots,
|
||||||
|
getOneBot,
|
||||||
|
sendMessage,
|
||||||
|
receiveMessages,
|
||||||
|
registerBot,
|
||||||
|
resetBot,
|
||||||
|
requestCode,
|
||||||
|
unverifyBot,
|
||||||
|
refreshBot,
|
||||||
|
createBot,
|
||||||
|
deleteBot,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
const getAllBots = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOneBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const receiveMessages = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestCode = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const unverifyBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBot = async (req: NextRequest) => {
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Whatsapp: Service = {
|
||||||
|
getAllBots,
|
||||||
|
getOneBot,
|
||||||
|
sendMessage,
|
||||||
|
receiveMessages,
|
||||||
|
registerBot,
|
||||||
|
resetBot,
|
||||||
|
requestCode,
|
||||||
|
unverifyBot,
|
||||||
|
refreshBot,
|
||||||
|
createBot,
|
||||||
|
deleteBot,
|
||||||
|
};
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
:root {
|
|
||||||
--max-width: 1100px;
|
|
||||||
--border-radius: 12px;
|
|
||||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
|
||||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
|
||||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
|
||||||
|
|
||||||
--foreground-rgb: 0, 0, 0;
|
|
||||||
--background-start-rgb: 214, 219, 220;
|
|
||||||
--background-end-rgb: 255, 255, 255;
|
|
||||||
|
|
||||||
--primary-glow: conic-gradient(
|
|
||||||
from 180deg at 50% 50%,
|
|
||||||
#16abff33 0deg,
|
|
||||||
#0885ff33 55deg,
|
|
||||||
#54d6ff33 120deg,
|
|
||||||
#0071ff33 160deg,
|
|
||||||
transparent 360deg
|
|
||||||
);
|
|
||||||
--secondary-glow: radial-gradient(
|
|
||||||
rgba(255, 255, 255, 1),
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
--tile-start-rgb: 239, 245, 249;
|
|
||||||
--tile-end-rgb: 228, 232, 233;
|
|
||||||
--tile-border: conic-gradient(
|
|
||||||
#00000080,
|
|
||||||
#00000040,
|
|
||||||
#00000030,
|
|
||||||
#00000020,
|
|
||||||
#00000010,
|
|
||||||
#00000010,
|
|
||||||
#00000080
|
|
||||||
);
|
|
||||||
|
|
||||||
--callout-rgb: 238, 240, 241;
|
|
||||||
--callout-border-rgb: 172, 175, 176;
|
|
||||||
--card-rgb: 180, 185, 188;
|
|
||||||
--card-border-rgb: 131, 134, 135;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--foreground-rgb: 255, 255, 255;
|
|
||||||
--background-start-rgb: 0, 0, 0;
|
|
||||||
--background-end-rgb: 0, 0, 0;
|
|
||||||
|
|
||||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
|
||||||
--secondary-glow: linear-gradient(
|
|
||||||
to bottom right,
|
|
||||||
rgba(1, 65, 255, 0),
|
|
||||||
rgba(1, 65, 255, 0),
|
|
||||||
rgba(1, 65, 255, 0.3)
|
|
||||||
);
|
|
||||||
|
|
||||||
--tile-start-rgb: 2, 13, 46;
|
|
||||||
--tile-end-rgb: 2, 5, 19;
|
|
||||||
--tile-border: conic-gradient(
|
|
||||||
#ffffff80,
|
|
||||||
#ffffff40,
|
|
||||||
#ffffff30,
|
|
||||||
#ffffff20,
|
|
||||||
#ffffff10,
|
|
||||||
#ffffff10,
|
|
||||||
#ffffff80
|
|
||||||
);
|
|
||||||
|
|
||||||
--callout-rgb: 20, 20, 20;
|
|
||||||
--callout-border-rgb: 108, 108, 108;
|
|
||||||
--card-rgb: 100, 100, 100;
|
|
||||||
--card-border-rgb: 200, 200, 200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: rgb(var(--foreground-rgb));
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
rgb(var(--background-end-rgb))
|
|
||||||
)
|
|
||||||
rgb(var(--background-start-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
apps/metamigo-frontend/app/_styles/theme.ts
Normal file
112
apps/metamigo-frontend/app/_styles/theme.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { Roboto, Playfair_Display, Poppins } from "next/font/google";
|
||||||
|
|
||||||
|
const roboto = Roboto({
|
||||||
|
weight: ["400"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const playfair = Playfair_Display({
|
||||||
|
weight: ["900"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const poppins = Poppins({
|
||||||
|
weight: ["400", "700"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fonts = {
|
||||||
|
roboto,
|
||||||
|
playfair,
|
||||||
|
poppins,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const colors: any = {
|
||||||
|
lightGray: "#ededf0",
|
||||||
|
mediumGray: "#e3e5e5",
|
||||||
|
darkGray: "#33302f",
|
||||||
|
mediumBlue: "#4285f4",
|
||||||
|
green: "#349d7b",
|
||||||
|
lavender: "#a5a6f6",
|
||||||
|
darkLavender: "#5d5fef",
|
||||||
|
pink: "#fcddec",
|
||||||
|
cdrLinkOrange: "#ff7115",
|
||||||
|
coreYellow: "#fac942",
|
||||||
|
helpYellow: "#fff4d5",
|
||||||
|
dwcDarkBlue: "#191847",
|
||||||
|
hazyMint: "#ecf7f8",
|
||||||
|
leafcutterElectricBlue: "#4d6aff",
|
||||||
|
leafcutterLightBlue: "#fafbfd",
|
||||||
|
waterbearElectricPurple: "#332c83",
|
||||||
|
waterbearLightSmokePurple: "#eff3f8",
|
||||||
|
bumpedPurple: "#212058",
|
||||||
|
mutedPurple: "#373669",
|
||||||
|
warningPink: "#ef5da8",
|
||||||
|
lightPink: "#fff0f7",
|
||||||
|
lightGreen: "#f0fff3",
|
||||||
|
lightOrange: "#fff5f0",
|
||||||
|
beige: "#f6f2f1",
|
||||||
|
almostBlack: "#33302f",
|
||||||
|
white: "#ffffff",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const typography: any = {
|
||||||
|
h1: {
|
||||||
|
fontFamily: playfair.style.fontFamily,
|
||||||
|
fontSize: 45,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontFamily: poppins.style.fontFamily,
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontFamily: poppins.style.fontFamily,
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: 27,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontFamily: poppins.style.fontFamily,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontFamily: roboto.style.fontFamily,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: "24px",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
textAlign: "center",
|
||||||
|
margin: 1,
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontFamily: roboto.style.fontFamily,
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
fontFamily: roboto.style.fontFamily,
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: "26.35px",
|
||||||
|
fontWeight: 400,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
fontFamily: roboto.style.fontFamily,
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: "18px",
|
||||||
|
fontWeight: 400,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { receiveMessages as GET } from "@/app/_lib/routing";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { registerBot as POST } from "@/app/_lib/routing";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { requestCode as POST } from "@/app/_lib/routing";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { resetBot as POST } from "@/app/_lib/routing";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { getOneBot as GET } from "@/app/_lib/routing";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { sendMessage as POST } from "@/app/_lib/routing";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { getAllBots as GET } from "@/app/_lib/routing";
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import { InternalLayout } from "./_components/InternalLayout";
|
import { InternalLayout } from "./_components/InternalLayout";
|
||||||
import "./_styles/globals.css";
|
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
LicenseInfo.setLicenseKey("7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=");
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Metamigo",
|
title: "Metamigo",
|
||||||
|
|
@ -17,7 +16,7 @@ export default function RootLayout({
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body>
|
||||||
<InternalLayout>{children}</InternalLayout>
|
<InternalLayout>{children}</InternalLayout>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Detail } from "@/app/_components/Detail";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Detail title="Signal Detail" entity="signal">
|
||||||
|
<p>Cool</p>
|
||||||
|
</Detail>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Page() {
|
||||||
|
return <h1>Signal new</h1>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Page() {
|
||||||
|
return <h1>Voice detail</h1>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Page() {
|
||||||
|
return <h1>Voice Home</h1>;
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build-xxx": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
|
|
@ -13,15 +13,13 @@
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/playfair-display": "^5.0.23",
|
|
||||||
"@fontsource/poppins": "^5.0.12",
|
|
||||||
"@fontsource/roboto": "^5.0.12",
|
|
||||||
"@mui/icons-material": "^5",
|
"@mui/icons-material": "^5",
|
||||||
"@mui/lab": "^5.0.0-alpha.168",
|
"@mui/lab": "^5.0.0-alpha.168",
|
||||||
"@mui/material": "^5",
|
"@mui/material": "^5",
|
||||||
"@mui/x-data-grid-pro": "^6.19.6",
|
"@mui/x-data-grid-pro": "^6.19.6",
|
||||||
"@mui/x-date-pickers-pro": "^6.19.7",
|
"@mui/x-date-pickers-pro": "^6.19.7",
|
||||||
"date-fns": "^3.5.0",
|
"date-fns": "^3.5.0",
|
||||||
|
"kysely": "^0.27.3",
|
||||||
"leafcutter-common": "*",
|
"leafcutter-common": "*",
|
||||||
"material-ui-popup-state": "^5.0.10",
|
"material-ui-popup-state": "^5.0.10",
|
||||||
"mui-chips-input": "^2.1.4",
|
"mui-chips-input": "^2.1.4",
|
||||||
|
|
@ -37,11 +35,11 @@
|
||||||
"tss-react": "^4.9.4"
|
"tss-react": "^4.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.1.3"
|
"eslint-config-next": "14.1.3",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
apps/metamigo-frontend/public/link-logo-small.png
Normal file
BIN
apps/metamigo-frontend/public/link-logo-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 629 B |
|
|
@ -1,7 +1,5 @@
|
||||||
import * as Worker from "graphile-worker";
|
import * as Worker from "graphile-worker";
|
||||||
import { parseCronItems } from "graphile-worker";
|
import { parseCronItems } from "graphile-worker";
|
||||||
import { defState } from "@digiresilience/montar";
|
|
||||||
import config from "@digiresilience/metamigo-config";
|
|
||||||
import { initPgp } from "./db.js";
|
import { initPgp } from "./db.js";
|
||||||
import logger from "./logger.js";
|
import logger from "./logger.js";
|
||||||
import workerUtils from "./utils.js";
|
import workerUtils from "./utils.js";
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "@digiresilience/metamigo-worker",
|
"name": "metamigo-worker",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"main": "build/main/index.js",
|
"main": "build/main/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "Abel Luck <abel@guardianproject.info>",
|
"author": "Abel Luck <abel@guardianproject.info>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@digiresilience/metamigo-common": "*",
|
|
||||||
"@digiresilience/metamigo-config": "*",
|
|
||||||
"@digiresilience/metamigo-db": "*",
|
|
||||||
"@digiresilience/montar": "*",
|
|
||||||
"graphile-worker": "^0.13.0",
|
"graphile-worker": "^0.13.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"node-fetch": "^3",
|
"node-fetch": "^3",
|
||||||
|
|
@ -41,7 +37,7 @@
|
||||||
"ext": "ts,json,js"
|
"ext": "ts,json,js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build-xxx": "tsc -p tsconfig.json",
|
||||||
"build-test": "tsc -p tsconfig.json",
|
"build-test": "tsc -p tsconfig.json",
|
||||||
"doc:html": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
|
"doc:html": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
|
||||||
"doc": "yarn run doc:html",
|
"doc": "yarn run doc:html",
|
||||||
|
|
|
||||||
0
apps/whatsapp-api/.gitkeep
Normal file
0
apps/whatsapp-api/.gitkeep
Normal file
16016
package-lock.json
generated
Normal file
16016
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -7,7 +7,7 @@
|
||||||
"dev": "dotenv -- turbo run dev --concurrency 30",
|
"dev": "dotenv -- turbo run dev --concurrency 30",
|
||||||
"build": "turbo build --concurrency 30",
|
"build": "turbo build --concurrency 30",
|
||||||
"dev:metamigo": "dotenv -- turbo run dev --concurrency 30 --filter=!link --filter=!leafcutter",
|
"dev:metamigo": "dotenv -- turbo run dev --concurrency 30 --filter=!link --filter=!leafcutter",
|
||||||
"migrate": "dotenv -- npm run migrate --workspace=@digiresilience/metamigo-cli",
|
"migrate": "dotenv -- npm run migrate --workspace=metamigo-cli",
|
||||||
"fmt": "turbo run fmt",
|
"fmt": "turbo run fmt",
|
||||||
"docker:all:up": "CURRENT_UID=$(CURRENT_UID) docker compose -f docker/compose/zammad.yml -f docker/compose/metamigo-postgresql.yml -f docker/compose/metamigo.yml -f docker/compose/opensearch.yml -f docker/compose/leafcutter.yml -f docker/compose/link.yml -f docker/compose/label-studio.yml up -d",
|
"docker:all:up": "CURRENT_UID=$(CURRENT_UID) docker compose -f docker/compose/zammad.yml -f docker/compose/metamigo-postgresql.yml -f docker/compose/metamigo.yml -f docker/compose/opensearch.yml -f docker/compose/leafcutter.yml -f docker/compose/link.yml -f docker/compose/label-studio.yml up -d",
|
||||||
"docker:all:down": "docker compose -f docker/compose/zammad.yml -f docker/compose/metamigo-postgresql.yml -f docker/compose/metamigo.yml -f docker/compose/opensearch.yml -f docker/compose/leafcutter.yml -f docker/compose/link.yml down",
|
"docker:all:down": "docker compose -f docker/compose/zammad.yml -f docker/compose/metamigo-postgresql.yml -f docker/compose/metamigo.yml -f docker/compose/opensearch.yml -f docker/compose/leafcutter.yml -f docker/compose/link.yml down",
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
"docker:label-studio:up": "docker compose -f docker/compose/label-studio.yml -f docker/compose/metamigo-postgresql.yml up -d",
|
"docker:label-studio:up": "docker compose -f docker/compose/label-studio.yml -f docker/compose/metamigo-postgresql.yml up -d",
|
||||||
"docker:label-studio:down": "docker compose -f docker/compose/label-studio.yml -f docker-compose.metamigo-postgresql.yml down",
|
"docker:label-studio:down": "docker compose -f docker/compose/label-studio.yml -f docker-compose.metamigo-postgresql.yml down",
|
||||||
"upgrade:setup": "npm i -g npm-check-updates",
|
"upgrade:setup": "npm i -g npm-check-updates",
|
||||||
"upgrade:check": "ncu && ncu -ws -x graphql -x postgraphile",
|
"upgrade:check": "ncu && ncu -ws",
|
||||||
"upgrade:all": "ncu -u && ncu -ws -u -x graphql -x postgraphile -x graphile-worker && npm i",
|
"upgrade:all": "ncu -u && ncu -ws -u && npm i",
|
||||||
"clean": "rm -f package-lock.json && rm -rf node_modules && rm -rf apps/*/node_modules && rm -rf packages/*/node_modules && rm -rf apps/*/.next && rm -rf packages/*/.turbo && rm -rf apps/*/.turbo && rm -rf docker/zammad/addons/*"
|
"clean": "rm -f package-lock.json && rm -rf node_modules && rm -rf apps/*/node_modules && rm -rf packages/*/node_modules && rm -rf apps/*/.next && rm -rf packages/*/.turbo && rm -rf apps/*/.turbo && rm -rf docker/zammad/addons/*"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|
@ -50,16 +50,6 @@
|
||||||
"packageManager": "npm",
|
"packageManager": "npm",
|
||||||
"author": "Darren Clarke",
|
"author": "Darren Clarke",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"overrides": {
|
|
||||||
"@mui/styles": {
|
|
||||||
"react": "^18.2.0"
|
|
||||||
},
|
|
||||||
"typeorm": {
|
|
||||||
"pg": "^8.11.0"
|
|
||||||
},
|
|
||||||
"graphql": "15.8.0",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv-cli": "latest",
|
"dotenv-cli": "latest",
|
||||||
"prettier": "^3.2.5"
|
"prettier": "^3.2.5"
|
||||||
|
|
|
||||||
0
packages/components-common/.gitkeep
Normal file
0
packages/components-common/.gitkeep
Normal file
|
|
@ -1,37 +1,25 @@
|
||||||
# eslint-config-amigo
|
# eslint-config
|
||||||
|
|
||||||
A shared eslint config for [CDR Tech][cdrtech].
|
A shared eslint config for [CDR Tech][cdrtech].
|
||||||
|
|
||||||
# Install
|
|
||||||
|
|
||||||
We recommend using [@digiresilience/amigo-dev][amigo-dev] to manage your dev dependencies.
|
|
||||||
|
|
||||||
[amigo-dev]: https://gitlab.com/digiresilience/link/amigo-dev
|
|
||||||
|
|
||||||
But if you want to do it manually, then:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ npm install --save-dev @digiresilience/eslint-config-amigo
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
**`.eslintrc.js`**
|
**`.eslintrc.js`**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
require('@digiresilience/eslint-config-amigo/patch/modern-module-resolution');
|
require("eslint-config-amigo/modern-module-resolution");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
// one of:
|
// one of:
|
||||||
"@digiresilience/eslint-config-amigo/profile/browser", // if targeting the browser
|
"eslint-config/profile/browser", // if targeting the browser
|
||||||
"@digiresilience/eslint-config-amigo/profile/node", // if targeting node
|
"eslint-config/profile/node", // if targeting node
|
||||||
|
|
||||||
// and optionally:
|
// and optionally:
|
||||||
"@digiresilience/eslint-config-amigo/profile/typescript", // if using typescript (node or browser)
|
"eslint-config/profile/typescript", // if using typescript (node or browser)
|
||||||
"@digiresilience/eslint-config-amigo/profile/cypress", // if using cypress
|
"eslint-config/profile/cypress", // if using cypress
|
||||||
"@digiresilience/eslint-config-amigo/profile/jest" // if using jest
|
"eslint-config/profile/jest", // if using jest
|
||||||
],
|
],
|
||||||
parserOptions: { tsconfigRootDir: __dirname }
|
parserOptions: { tsconfigRootDir: __dirname },
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -42,10 +30,10 @@ Copyright © 2020-present [Center for Digital Resilience][cdr]
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
| [![Abel Luck][abelxluck_avatar]][abelxluck_homepage]<br/>[Abel Luck][abelxluck_homepage] |
|
| [![Abel Luck][abelxluck_avatar]][abelxluck_homepage]<br/>[Abel Luck][abelxluck_homepage] |
|
||||||
|---|
|
| ---------------------------------------------------------------------------------------- |
|
||||||
|
|
||||||
[abelxluck_homepage]: https://gitlab.com/abelxluck
|
[abelxluck_homepage]: https://gitlab.com/abelxluck
|
||||||
[abelxluck_avatar]: https://secure.gravatar.com/avatar/0f605397e0ead93a68e1be26dc26481a?s=100&d=identicon
|
[abelxluck_avatar]: https://secure.gravatar.com/avatar/0f605397e0ead93a68e1be26dc26481a?s=100&d=identicon
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,6 @@
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/playfair-display": "^5.0.23",
|
|
||||||
"@fontsource/poppins": "^5.0.12",
|
|
||||||
"@fontsource/roboto": "^5.0.12",
|
|
||||||
"@mui/icons-material": "^5",
|
"@mui/icons-material": "^5",
|
||||||
"@mui/lab": "^5.0.0-alpha.168",
|
"@mui/lab": "^5.0.0-alpha.168",
|
||||||
"@mui/material": "^5",
|
"@mui/material": "^5",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +0,0 @@
|
||||||
require('eslint-config-link/patch/modern-module-resolution');
|
|
||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
"eslint-config-link/profile/node",
|
|
||||||
"eslint-config-link/profile/typescript"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
11
packages/metamigo-common/.gitignore
vendored
11
packages/metamigo-common/.gitignore
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
.idea/*
|
|
||||||
.nyc_output
|
|
||||||
build
|
|
||||||
node_modules
|
|
||||||
test
|
|
||||||
src/*/*.js
|
|
||||||
coverage
|
|
||||||
*.log
|
|
||||||
package-lock.json
|
|
||||||
.npmrc
|
|
||||||
junit.xml
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
.eslintrc.js
|
|
||||||
.editorconfig
|
|
||||||
.prettierignore
|
|
||||||
.versionrc
|
|
||||||
Makefile
|
|
||||||
.gitlab-ci.yml
|
|
||||||
coverage
|
|
||||||
jest*
|
|
||||||
tsconfig*
|
|
||||||
*.log
|
|
||||||
test*
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# package.json is formatted by package managers, so we ignore it here
|
|
||||||
package.json
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["babel-preset-link"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "@digiresilience/metamigo-common",
|
"name": "metamigo-common",
|
||||||
"version": "0.2.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "build/main/index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
|
||||||
"types": "build/main/index.d.ts",
|
|
||||||
"author": "Abel Luck <abel@guardianproject.info>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"private": false,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
"fix:lint": "eslint src --ext .ts --fix",
|
|
||||||
"fmt": "prettier \"src/**/*.ts\" --write",
|
|
||||||
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
|
|
||||||
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
|
|
||||||
"dev": "tsc-watch --build --noClear "
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"author": "",
|
||||||
"@types/figlet": "^1.5.8",
|
"license": "ISC"
|
||||||
"@types/lodash": "^4.17.0",
|
|
||||||
"@types/node": "*",
|
|
||||||
"@types/uuid": "^9.0.8",
|
|
||||||
"camelcase-keys": "^9.1.3",
|
|
||||||
"pg-monitor": "^2.0.0",
|
|
||||||
"tsc-watch": "^6.0.4",
|
|
||||||
"typedoc": "^0.25.12",
|
|
||||||
"typescript": "^5.4.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@digiresilience/hapi-nextauth": "*",
|
|
||||||
"@hapi/boom": "^10.0.1",
|
|
||||||
"@hapi/glue": "^9.0.1",
|
|
||||||
"@hapi/hapi": "^21.3.6",
|
|
||||||
"@hapi/hoek": "^11.0.4",
|
|
||||||
"@hapi/inert": "^7.1.0",
|
|
||||||
"@hapi/vision": "^7.0.3",
|
|
||||||
"@hapipal/schmervice": "^3.0.0",
|
|
||||||
"@promster/hapi": "^13.0.0",
|
|
||||||
"@promster/server": "^13.0.0",
|
|
||||||
"@promster/types": "^13.0.0",
|
|
||||||
"@types/convict": "^6.1.6",
|
|
||||||
"@types/hapi__glue": "^6.1.9",
|
|
||||||
"@types/hapi__hapi": "^20.0.13",
|
|
||||||
"@types/hapi__inert": "^5.2.10",
|
|
||||||
"@types/hapi__vision": "^5.5.8",
|
|
||||||
"@types/hapipal__schmervice": "^2.0.7",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"commander": "^12.0.0",
|
|
||||||
"convict": "^6.2.4",
|
|
||||||
"decamelcase-keys": "^1.1.1",
|
|
||||||
"figlet": "^1.7.0",
|
|
||||||
"hapi-pino": "^12.1.0",
|
|
||||||
"http-terminator": "^3.2.0",
|
|
||||||
"joi": "^17.12.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"next-auth": "^4.24.7",
|
|
||||||
"pg-promise": "^11.5.4",
|
|
||||||
"pino": "^8.19.0",
|
|
||||||
"pino-pretty": "^10.3.1",
|
|
||||||
"prom-client": "^15.x.x",
|
|
||||||
"uuid": "^9.0.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { ConvictSchema } from "./types.js";
|
|
||||||
|
|
||||||
export interface IAppMetaConfig {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
figletFont: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppMetaConfig: ConvictSchema<IAppMetaConfig> = {
|
|
||||||
version: {
|
|
||||||
doc: "The current application version",
|
|
||||||
format: String,
|
|
||||||
env: "npm_package_version",
|
|
||||||
default: undefined,
|
|
||||||
skipGenerate: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
doc: "Application name",
|
|
||||||
format: String,
|
|
||||||
env: "npm_package_name",
|
|
||||||
default: undefined,
|
|
||||||
skipGenerate: true,
|
|
||||||
},
|
|
||||||
figletFont: {
|
|
||||||
doc: "The figlet font name used to print the site name on boot",
|
|
||||||
format: String,
|
|
||||||
env: "FIGLET_FONT",
|
|
||||||
default: "Sub-Zero",
|
|
||||||
skipGenerate: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { ConvictSchema } from "./types.js";
|
|
||||||
|
|
||||||
export interface ISessionConfig {
|
|
||||||
sessionMaxAgeSeconds: number;
|
|
||||||
sessionUpdateAgeSeconds: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SessionConfig: ConvictSchema<ISessionConfig> = {
|
|
||||||
sessionMaxAgeSeconds: {
|
|
||||||
doc: "How long in seconds until an idle session expires and is no longer valid.",
|
|
||||||
format: "positiveInt",
|
|
||||||
default: 30 * 24 * 60 * 60, // 30 days
|
|
||||||
env: "SESSION_MAX_AGE_SECONDS",
|
|
||||||
},
|
|
||||||
sessionUpdateAgeSeconds: {
|
|
||||||
doc: `Throttle how frequently in seconds to write to database to extend a session.
|
|
||||||
Use it to limit write operations. Set to 0 to always update the database.
|
|
||||||
Note: This option is ignored if using JSON Web Tokens`,
|
|
||||||
format: "positiveInt",
|
|
||||||
default: 24 * 60 * 60, // 24 hours
|
|
||||||
env: "SESSION_UPDATE_AGE_SECONDS",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { ConvictSchema } from "./types.js";
|
|
||||||
|
|
||||||
export interface ICorsConfig {
|
|
||||||
allowedMethods: Array<string>;
|
|
||||||
allowedOrigins: Array<string>;
|
|
||||||
allowedHeaders: Array<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CorsConfig: ConvictSchema<ICorsConfig> = {
|
|
||||||
allowedMethods: {
|
|
||||||
doc: "The allowed CORS methods",
|
|
||||||
format: "Array",
|
|
||||||
env: "CORS_ALLOWED_METHODS",
|
|
||||||
default: ["GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
||||||
},
|
|
||||||
allowedOrigins: {
|
|
||||||
doc: "The allowed origins",
|
|
||||||
format: "Array",
|
|
||||||
env: "CORS_ALLOWED_ORIGINS",
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
allowedHeaders: {
|
|
||||||
doc: "The allowed headers",
|
|
||||||
format: "Array",
|
|
||||||
env: "CORS_ALLOWED_HEADERS",
|
|
||||||
default: [
|
|
||||||
"content-type",
|
|
||||||
"authorization",
|
|
||||||
"cf-access-authenticated-user-email",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import Joi from "joi";
|
|
||||||
import type { Format } from "convict";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const coerceString = (v: any): string => v.toString();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const validator = (s: any) => (v: any) => Joi.assert(v, s);
|
|
||||||
|
|
||||||
const url = Joi.string().uri({
|
|
||||||
scheme: ["http", "https"],
|
|
||||||
});
|
|
||||||
const ip = Joi.string().ip({ version: ["ipv4", "ipv6"], cidr: "optional" });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional configuration value formats for convict.
|
|
||||||
*
|
|
||||||
* You can use these to achieve richer validation for your configuration.
|
|
||||||
*/
|
|
||||||
export const MetamigoConvictFormats: { [index: string]: Format } = {
|
|
||||||
positiveInt: {
|
|
||||||
name: "positveInt",
|
|
||||||
coerce: (n: string): number => Number.parseInt(n, 10),
|
|
||||||
validate: validator(Joi.number().positive().integer()),
|
|
||||||
},
|
|
||||||
port: {
|
|
||||||
name: "port",
|
|
||||||
coerce: (n: string): number => Number.parseInt(n, 10),
|
|
||||||
validate: validator(Joi.number().port()),
|
|
||||||
},
|
|
||||||
ipaddress: {
|
|
||||||
name: "ipaddress",
|
|
||||||
coerce: coerceString,
|
|
||||||
validate: validator(ip),
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
name: "url",
|
|
||||||
coerce: coerceString,
|
|
||||||
validate: validator(url),
|
|
||||||
},
|
|
||||||
uri: {
|
|
||||||
name: "uri",
|
|
||||||
coerce: coerceString,
|
|
||||||
validate: validator(Joi.string().uri()),
|
|
||||||
},
|
|
||||||
optionalUri: {
|
|
||||||
name: "uri",
|
|
||||||
coerce: coerceString,
|
|
||||||
validate: validator(Joi.string().uri().allow("")),
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
name: "email",
|
|
||||||
coerce: coerceString,
|
|
||||||
validate: validator(Joi.string().email()),
|
|
||||||
},
|
|
||||||
uuid: {
|
|
||||||
name: "uuid",
|
|
||||||
coerce: coerceString,
|
|
||||||
validate: validator(Joi.string().guid()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import convict from "convict";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const visitLeaf = (acc: any, key: any, leaf: any) => {
|
|
||||||
if (leaf.skipGenerate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leaf.default === undefined) {
|
|
||||||
acc[key] = undefined;
|
|
||||||
} else {
|
|
||||||
acc[key] = leaf.default;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const visitNode = (acc: any, node: any, key = "") => {
|
|
||||||
if (node._cvtProperties) {
|
|
||||||
const keys = Object.keys(node._cvtProperties);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let subacc: any;
|
|
||||||
if (key === "") {
|
|
||||||
subacc = acc;
|
|
||||||
} else {
|
|
||||||
subacc = {};
|
|
||||||
acc[key] = subacc;
|
|
||||||
}
|
|
||||||
|
|
||||||
keys.forEach((key) => {
|
|
||||||
visitNode(subacc, node._cvtProperties[key], key);
|
|
||||||
});
|
|
||||||
// In the case that the entire sub-tree specified skipGenerate, remove the empty node
|
|
||||||
if (Object.keys(subacc).length === 0) {
|
|
||||||
delete acc[key];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
visitLeaf(acc, key, node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const generateConfig = (conf: convict.Config<any>): unknown => {
|
|
||||||
const schema = conf.getSchema();
|
|
||||||
const generated = {};
|
|
||||||
visitNode(generated, schema);
|
|
||||||
return JSON.stringify(generated, undefined, 1);
|
|
||||||
};
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import process from "node:process";
|
|
||||||
import convict, { SchemaObj } from "convict";
|
|
||||||
import { IServerConfig, ServerConfig } from "./server.js";
|
|
||||||
import { IMetricsConfig, MetricsConfig } from "./metrics-server.js";
|
|
||||||
import { IAppMetaConfig, AppMetaConfig } from "./app-meta.js";
|
|
||||||
import { ICorsConfig, CorsConfig } from "./cors.js";
|
|
||||||
import { ILoggingConfig, LoggingConfig } from "./logging.js";
|
|
||||||
import { ExtendedConvict } from "./types.js";
|
|
||||||
import { MetamigoConvictFormats } from "./formats.js";
|
|
||||||
|
|
||||||
type IEnvConfig = "production" | "development" | "test";
|
|
||||||
|
|
||||||
const EnvConfig: SchemaObj<IEnvConfig> = {
|
|
||||||
doc: "The application environment",
|
|
||||||
format: ["production", "development", "test"],
|
|
||||||
default: "development",
|
|
||||||
env: "NODE_ENV",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const configBaseSchema = {
|
|
||||||
env: EnvConfig,
|
|
||||||
server: ServerConfig,
|
|
||||||
meta: AppMetaConfig,
|
|
||||||
cors: CorsConfig,
|
|
||||||
metrics: MetricsConfig,
|
|
||||||
logging: LoggingConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* The metamigo base configuration object. Use this for easy typed access to your
|
|
||||||
* config.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
interface IMetamigoConfig {
|
|
||||||
env: IEnvConfig;
|
|
||||||
server: IServerConfig;
|
|
||||||
meta: IAppMetaConfig;
|
|
||||||
cors: ICorsConfig;
|
|
||||||
metrics: IMetricsConfig;
|
|
||||||
logging: ILoggingConfig;
|
|
||||||
isProd?: boolean;
|
|
||||||
isTest?: boolean;
|
|
||||||
isDev?: boolean;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
frontend: any;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
nextAuth: any;
|
|
||||||
}
|
|
||||||
export type IMetamigoConvict = ExtendedConvict<IMetamigoConfig>;
|
|
||||||
|
|
||||||
export type { IMetamigoConfig };
|
|
||||||
|
|
||||||
export * from "./formats.js";
|
|
||||||
export * from "./generate.js";
|
|
||||||
export * from "./print.js";
|
|
||||||
export * from "./types.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads your applications configuration from environment variables and configuration files (see METAMIGO_CONFIG).
|
|
||||||
*
|
|
||||||
* @param schema your schema definition
|
|
||||||
* @param override an optional object with config value that will override defaults but not config files and env vars (see [convict precedence docs](https://github.com/mozilla/node-convict/tree/master/packages/convict#precedence-order ))
|
|
||||||
* @returns the raw convict config object
|
|
||||||
*/
|
|
||||||
export const loadConfigurationRaw = async <T extends IMetamigoConfig>(
|
|
||||||
schema: convict.Schema<T>,
|
|
||||||
override?: Partial<T>
|
|
||||||
): Promise<ExtendedConvict<T>> => {
|
|
||||||
convict.addFormats(MetamigoConvictFormats);
|
|
||||||
const config: ExtendedConvict<T> = convict(schema);
|
|
||||||
|
|
||||||
const env = config.get("env");
|
|
||||||
|
|
||||||
config.isProd = env === "production";
|
|
||||||
config.isTest = env === "test";
|
|
||||||
config.isDev = env === "development";
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (process.env.METAMIGO_CONFIG) {
|
|
||||||
config.loadFile(process.env.METAMIGO_CONFIG);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const msg = `
|
|
||||||
|
|
||||||
|
|
||||||
🚫 Your application's configuration is invalid JSON. 🚫
|
|
||||||
|
|
||||||
${error}
|
|
||||||
|
|
||||||
`;
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (override) {
|
|
||||||
config.load(override);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
config.validate({ allowed: "strict" });
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
const msg = `
|
|
||||||
|
|
||||||
|
|
||||||
🚫 Your application's configuration is invalid. 🚫
|
|
||||||
|
|
||||||
${error.message}
|
|
||||||
|
|
||||||
`;
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set our helpers
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const configDirty = config as any;
|
|
||||||
|
|
||||||
configDirty.set("isProd", config.isProd);
|
|
||||||
configDirty.set("isTest", config.isTest);
|
|
||||||
configDirty.set("isDev", config.isDev);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads your applications configuration from environment variables and configuration files (see METAMIGO_CONFIG).
|
|
||||||
*
|
|
||||||
* @param schema your schema definition
|
|
||||||
* @param override an optional object with config value that will override defaults but not config files and env vars (see [convict precedence docs](https://github.com/mozilla/node-convict/tree/master/packages/convict#precedence-order ))
|
|
||||||
* @returns a vanilla javascript object with the config loaded values
|
|
||||||
*/
|
|
||||||
export const loadConfiguration = async <T extends IMetamigoConfig>(
|
|
||||||
schema: convict.Schema<T>,
|
|
||||||
override?: Partial<T>
|
|
||||||
): Promise<T> => {
|
|
||||||
const c = await loadConfigurationRaw(schema, override);
|
|
||||||
return c.getProperties();
|
|
||||||
};
|
|
||||||
|
|
||||||
export { type IServerConfig } from "./server.js";
|
|
||||||
export { type IMetricsConfig } from "./metrics-server.js";
|
|
||||||
export { type IAppMetaConfig } from "./app-meta.js";
|
|
||||||
export { type ICorsConfig } from "./cors.js";
|
|
||||||
export { type ILoggingConfig } from "./logging.js";
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
import { ConvictSchema } from "./types.js";
|
|
||||||
|
|
||||||
export interface ILoggingConfig {
|
|
||||||
level: string;
|
|
||||||
sql: boolean;
|
|
||||||
redact: string[];
|
|
||||||
ignorePaths: string[];
|
|
||||||
ignoreTags: string[];
|
|
||||||
requestIdHeader: string;
|
|
||||||
logRequestStart: boolean;
|
|
||||||
logRequestComplete: boolean;
|
|
||||||
logRequestPayload: boolean;
|
|
||||||
logRequestQueryParams: boolean;
|
|
||||||
prettyPrint: boolean | "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoggingConfig: ConvictSchema<ILoggingConfig> = {
|
|
||||||
level: {
|
|
||||||
doc: "The logging level",
|
|
||||||
format: ["trace", "debug", "info", "warn", "error"],
|
|
||||||
default: "info",
|
|
||||||
env: "LOG_LEVEL",
|
|
||||||
},
|
|
||||||
sql: {
|
|
||||||
doc: "Whether to log sql statements",
|
|
||||||
format: "Boolean",
|
|
||||||
default: false,
|
|
||||||
env: "LOG_SQL",
|
|
||||||
},
|
|
||||||
redact: {
|
|
||||||
doc: "Pino redaction array. These are always redacted. see https://getpino.io/#/docs/redaction",
|
|
||||||
format: "Array",
|
|
||||||
default: [
|
|
||||||
"req.remoteAddress",
|
|
||||||
"req.headers.authorization",
|
|
||||||
`req.headers["cf-access-jwt-assertion"]`,
|
|
||||||
`req.headers["cf-access-authenticated-user-email"]`,
|
|
||||||
`req.headers["cf-connecting-ip"]`,
|
|
||||||
`req.headers["cf-ipcountry"]`,
|
|
||||||
`req.headers["x-forwarded-for"]`,
|
|
||||||
"req.headers.cookie",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ignorePaths: {
|
|
||||||
doc: "Ignore http paths (exact) when logging requests",
|
|
||||||
format: "Array",
|
|
||||||
default: ["/graphql"],
|
|
||||||
},
|
|
||||||
ignoreTags: {
|
|
||||||
doc: "Ignore routes tagged with these tags when logging requests",
|
|
||||||
format: "Array",
|
|
||||||
default: ["status", "swagger", "nolog"],
|
|
||||||
},
|
|
||||||
requestIdHeader: {
|
|
||||||
doc: "The header where the request id lives",
|
|
||||||
format: String,
|
|
||||||
default: "x-request-id",
|
|
||||||
env: "REQUEST_ID_HEADER",
|
|
||||||
},
|
|
||||||
logRequestStart: {
|
|
||||||
doc: "Whether hapi-pino should add a log.info() at the beginning of Hapi requests for the given Request.",
|
|
||||||
format: "Boolean",
|
|
||||||
default: false,
|
|
||||||
env: "LOG_REQUEST_START",
|
|
||||||
},
|
|
||||||
logRequestComplete: {
|
|
||||||
doc: "Whether hapi-pino should add a log.info() at the completion of Hapi requests for the given Request.",
|
|
||||||
format: "Boolean",
|
|
||||||
default: true,
|
|
||||||
env: "LOG_REQUEST_COMPLETE",
|
|
||||||
},
|
|
||||||
logRequestPayload: {
|
|
||||||
doc: "When enabled, add the request payload as payload to the response event log.",
|
|
||||||
format: "Boolean",
|
|
||||||
default: false,
|
|
||||||
env: "LOG_REQUEST_PAYLOAD",
|
|
||||||
},
|
|
||||||
logRequestQueryParams: {
|
|
||||||
doc: "When enabled, add the request query as queryParams to the response event log.",
|
|
||||||
format: "Boolean",
|
|
||||||
default: false,
|
|
||||||
env: "LOG_REQUEST_QUERY_PARAMS",
|
|
||||||
},
|
|
||||||
prettyPrint: {
|
|
||||||
doc: "Pretty print the logs",
|
|
||||||
format: ["auto", true, false],
|
|
||||||
default: "auto",
|
|
||||||
env: "LOG_PRETTY_PRINT",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { ConvictSchema } from "./types.js";
|
|
||||||
|
|
||||||
export interface IMetricsConfig {
|
|
||||||
address: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MetricsConfig: ConvictSchema<IMetricsConfig> = {
|
|
||||||
address: {
|
|
||||||
doc: "The ip address to bind the prometheus metrics to",
|
|
||||||
format: "ipaddress",
|
|
||||||
default: "127.0.0.1",
|
|
||||||
env: "METRICS_ADDRESS",
|
|
||||||
},
|
|
||||||
port: {
|
|
||||||
doc: "The port to bind the prometheus metrics to",
|
|
||||||
format: "port",
|
|
||||||
default: 3002,
|
|
||||||
env: "METRICS_PORT",
|
|
||||||
arg: "port",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import chalk from "chalk";
|
|
||||||
import convict from "convict";
|
|
||||||
|
|
||||||
const visitLeaf = (path: any, key: any, leaf: any) => {
|
|
||||||
if (leaf.skipGenerate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = `${path}.${key}`;
|
|
||||||
if (path.length === 0) name = key;
|
|
||||||
console.log(chalk.green(name));
|
|
||||||
console.log(leaf.doc);
|
|
||||||
if (leaf.default === undefined) {
|
|
||||||
console.log(chalk.red("\t required"));
|
|
||||||
} else {
|
|
||||||
console.log(`\tdefault: ${JSON.stringify(leaf.default)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\tformat: ${leaf.format}`);
|
|
||||||
console.log(`\tenv: ${leaf.env}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const visitNode = (path: any, node: any, key = "") => {
|
|
||||||
if (node._cvtProperties) {
|
|
||||||
const keys = Object.keys(node._cvtProperties);
|
|
||||||
const subpath = key === "" ? path : `${key}`;
|
|
||||||
|
|
||||||
keys.forEach((key) => {
|
|
||||||
visitNode(subpath, node._cvtProperties[key], key);
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
} else {
|
|
||||||
visitLeaf(path, key, node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const printConfigOptions = (conf: convict.Config<any>): void => {
|
|
||||||
const schema = conf.getSchema();
|
|
||||||
visitNode("", schema);
|
|
||||||
};
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { ConvictSchema } from "./types.js";
|
|
||||||
|
|
||||||
export interface IServerConfig {
|
|
||||||
address: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ServerConfig: ConvictSchema<IServerConfig> = {
|
|
||||||
address: {
|
|
||||||
doc: "The IP address to bind the server to",
|
|
||||||
format: "ipaddress",
|
|
||||||
default: "0.0.0.0",
|
|
||||||
env: "SERVER_ADDRESS",
|
|
||||||
},
|
|
||||||
port: {
|
|
||||||
doc: "The port to bind the server to",
|
|
||||||
format: "port",
|
|
||||||
default: 3001,
|
|
||||||
env: "SERVER_PORT",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import convict from "convict";
|
|
||||||
|
|
||||||
/*
|
|
||||||
interface SSMObj {
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
interface ConvictSchemaObj<T = any> extends convict.SchemaObj<T> {
|
|
||||||
// ssm?: SSMObj;
|
|
||||||
/**
|
|
||||||
* The config item will be ignored for purposes of config file generation
|
|
||||||
*/
|
|
||||||
skipGenerate?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConvictSchema<T> = {
|
|
||||||
[P in keyof T]: convict.Schema<T[P]> | ConvictSchemaObj<T[P]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ExtendedConvict<T> extends convict.Config<T> {
|
|
||||||
isProd?: boolean;
|
|
||||||
isTest?: boolean;
|
|
||||||
isDev?: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any,max-params */
|
|
||||||
import * as Boom from "@hapi/boom";
|
|
||||||
import * as Hapi from "@hapi/hapi";
|
|
||||||
import { CrudRepository } from "../records/crud-repository.js";
|
|
||||||
import { createResponse } from "../helpers/response.js";
|
|
||||||
import {
|
|
||||||
PgRecordInfo,
|
|
||||||
UnsavedR,
|
|
||||||
SavedR,
|
|
||||||
KeyType,
|
|
||||||
} from "../records/record-info.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* A generic controller that handles exposes a [[CrudRepository]] as HTTP
|
|
||||||
* endpoints with full POST, PUT, GET, DELETE semantics.
|
|
||||||
*
|
|
||||||
* The controller yanks the instance of the crud repository out of the request at runtime.
|
|
||||||
* This assumes you're following the pattern exposed with the hapi-pg-promise plugin.
|
|
||||||
*
|
|
||||||
* @typeParam ID The type of the id column
|
|
||||||
* @typeParam T The type of the record
|
|
||||||
*/
|
|
||||||
export abstract class AbstractCrudController<
|
|
||||||
TUnsavedR,
|
|
||||||
TSavedR extends TUnsavedR & IdKeyT,
|
|
||||||
IdKeyT extends object
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* @param repoName the key at which the repository for the record can be accessed (that is, request.db[repoName])
|
|
||||||
* @param paramsIdField the placeholder used in the Hapi route for the id of the record
|
|
||||||
* @param dbDecoration the decorated function on the request to use (defaults to request.db())
|
|
||||||
*/
|
|
||||||
|
|
||||||
abstract repoName: string;
|
|
||||||
abstract paramsIdField;
|
|
||||||
abstract dbDecoration;
|
|
||||||
abstract recordType: PgRecordInfo<TUnsavedR, TSavedR, IdKeyT>;
|
|
||||||
|
|
||||||
repo(request: Hapi.Request): CrudRepository<TUnsavedR, TSavedR, IdKeyT> {
|
|
||||||
const db = request[this.dbDecoration];
|
|
||||||
if (!db)
|
|
||||||
throw Boom.badImplementation(
|
|
||||||
`CrudController for table ${this.recordType.tableName} could not find request decoration '${this.dbDecoration}'`
|
|
||||||
);
|
|
||||||
const repo = db()[this.repoName];
|
|
||||||
if (!repo)
|
|
||||||
throw Boom.badImplementation(
|
|
||||||
`CrudController for table ${this.recordType.tableName} could not find repository for '${this.dbDecoration}().${this.repoName}'`
|
|
||||||
);
|
|
||||||
return repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new record
|
|
||||||
*/
|
|
||||||
public create = async (
|
|
||||||
request: Hapi.Request,
|
|
||||||
toolkit: Hapi.ResponseToolkit
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
// would love to know how to get rid of this double cast hack
|
|
||||||
const payload: TSavedR = <TSavedR>(<any>request.payload);
|
|
||||||
const data: TSavedR = await this.repo(request).insert(payload);
|
|
||||||
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
value: data,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.badImplementation(error),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a record by ID. This method can accept partial updates.
|
|
||||||
*/
|
|
||||||
public updateById = async (
|
|
||||||
request: Hapi.Request,
|
|
||||||
toolkit: Hapi.ResponseToolkit
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const payload: Partial<TSavedR> = <any>request.payload;
|
|
||||||
const id: IdKeyT = request.params[this.paramsIdField];
|
|
||||||
const updatedRow: TSavedR = await this.repo(request).updateById(
|
|
||||||
id,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updatedRow) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.notFound(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
value: updatedRow,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.badImplementation(error),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a record given its id.
|
|
||||||
*/
|
|
||||||
public getById = async (
|
|
||||||
request: Hapi.Request,
|
|
||||||
toolkit: Hapi.ResponseToolkit
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const id: IdKeyT = request.params[this.paramsIdField];
|
|
||||||
const row: TSavedR = await this.repo(request).findById(id);
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.notFound(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
value: row,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.badImplementation(error),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all records.
|
|
||||||
*/
|
|
||||||
public getAll = async (
|
|
||||||
request: Hapi.Request,
|
|
||||||
toolkit: Hapi.ResponseToolkit
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const rows: TSavedR[] = await this.repo(request).findAll();
|
|
||||||
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
value: rows,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.badImplementation(error),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a record given its id.
|
|
||||||
*/
|
|
||||||
public deleteById = async (
|
|
||||||
request: Hapi.Request,
|
|
||||||
toolkit: Hapi.ResponseToolkit
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const id: IdKeyT = request.params[this.paramsIdField];
|
|
||||||
|
|
||||||
const count = await this.repo(request).removeById(id);
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
return createResponse(request, { boom: Boom.notFound() });
|
|
||||||
}
|
|
||||||
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
value: { id },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return toolkit.response(
|
|
||||||
createResponse(request, {
|
|
||||||
boom: Boom.badImplementation(error),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unboundCrudController<TRecordInfo extends PgRecordInfo>(
|
|
||||||
aRecordType: TRecordInfo
|
|
||||||
) {
|
|
||||||
return class CrudController extends AbstractCrudController<
|
|
||||||
UnsavedR<TRecordInfo>,
|
|
||||||
SavedR<TRecordInfo>,
|
|
||||||
KeyType<TRecordInfo>
|
|
||||||
> {
|
|
||||||
public readonly repoName: string;
|
|
||||||
public readonly paramsIdField;
|
|
||||||
public readonly dbDecoration;
|
|
||||||
public readonly recordType = aRecordType;
|
|
||||||
|
|
||||||
constructor(repoName: string, paramsIdField = "id", dbDecoration = "db") {
|
|
||||||
super();
|
|
||||||
this.repoName = repoName;
|
|
||||||
this.paramsIdField = paramsIdField;
|
|
||||||
this.dbDecoration = dbDecoration;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CrudControllerBase<Rec extends PgRecordInfo>(recordType: Rec) {
|
|
||||||
return unboundCrudController<Rec>(recordType);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const crudRoutesFor = (
|
|
||||||
name: string,
|
|
||||||
path: string,
|
|
||||||
controller: AbstractCrudController<any, any, any>,
|
|
||||||
idParam: string,
|
|
||||||
validate: Record<string, Hapi.RouteOptionsValidate>
|
|
||||||
): Hapi.ServerRoute[] => [
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `${path}`,
|
|
||||||
options: {
|
|
||||||
handler: controller.create,
|
|
||||||
validate: validate.create,
|
|
||||||
description: `Method that creates a new ${name}.`,
|
|
||||||
tags: ["api", name],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
path: `${path}/{${idParam}}`,
|
|
||||||
options: {
|
|
||||||
handler: controller.updateById,
|
|
||||||
validate: validate.updateById,
|
|
||||||
description: `Method that updates a ${name} by its id.`,
|
|
||||||
tags: ["api", name],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `${path}/{${idParam}}`,
|
|
||||||
options: {
|
|
||||||
handler: controller.getById,
|
|
||||||
validate: validate.getById,
|
|
||||||
description: `Method that gets a ${name} by its id.`,
|
|
||||||
tags: ["api", name],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `${path}`,
|
|
||||||
options: {
|
|
||||||
handler: controller.getAll,
|
|
||||||
description: `Method that gets all ${name}s.`,
|
|
||||||
tags: ["api", name],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
path: `${path}/{${idParam}}`,
|
|
||||||
options: {
|
|
||||||
handler: controller.deleteById,
|
|
||||||
validate: validate.deleteById,
|
|
||||||
description: `Method that deletes a ${name} by its id.`,
|
|
||||||
tags: ["api", name],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
/* eslint-disable unicorn/no-null,max-params */
|
|
||||||
import { createHash, randomBytes } from "node:crypto";
|
|
||||||
import omit from "lodash/omit.js";
|
|
||||||
import { IMetamigoRepositories, idKeysOf } from "../records/index.js";
|
|
||||||
import type { UnsavedAccount } from "../records/account.js";
|
|
||||||
import type { UserId, UnsavedUser, SavedUser } from "../records/user.js";
|
|
||||||
import type { UnsavedSession, SavedSession } from "../records/session.js";
|
|
||||||
import {
|
|
||||||
AdapterAccount,
|
|
||||||
AdapterSession,
|
|
||||||
AdapterUser,
|
|
||||||
} from "next-auth/adapters.js";
|
|
||||||
import { ReadableStreamDefaultController } from "stream/web";
|
|
||||||
|
|
||||||
// Sessions expire after 30 days of being idle
|
|
||||||
export const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
// Sessions updated only if session is greater than this value (0 = always)
|
|
||||||
export const defaulteSessionUpdateAge = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
const getCompoundId = (providerId: any, providerAccountId: any) =>
|
|
||||||
createHash("sha256")
|
|
||||||
.update(`${providerId}:${providerAccountId}`)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const randomToken = () => randomBytes(32).toString("hex");
|
|
||||||
|
|
||||||
export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
|
|
||||||
constructor(
|
|
||||||
private repos: TRepositories,
|
|
||||||
private readonly sessionMaxAge = defaultSessionMaxAge,
|
|
||||||
private readonly sessionUpdateAge = defaulteSessionUpdateAge
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async createUser(profile: UnsavedUser): Promise<SavedUser> {
|
|
||||||
// @ts-ignore Typescript doesn't like lodash's omit()
|
|
||||||
return this.repos.users.upsert(omit(profile, ["isActive", "id"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser(id: UserId): Promise<SavedUser | null> {
|
|
||||||
const user = await this.repos.users.findById({ id });
|
|
||||||
if (!user) return null;
|
|
||||||
// if a user has no linked accounts, then we do not return it
|
|
||||||
// see: https://github.com/nextauthjs/next-auth/issues/876
|
|
||||||
const accounts = await this.repos.accounts.findAllBy({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!accounts || accounts.length === 0) return null;
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByEmail(email: string): Promise<SavedUser | null> {
|
|
||||||
const user = await this.repos.users.findBy({ email });
|
|
||||||
if (!user) return null;
|
|
||||||
// if a user has no linked accounts, then we do not return it
|
|
||||||
// see: https://github.com/nextauthjs/next-auth/issues/876
|
|
||||||
const accounts = await this.repos.accounts.findAllBy({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!accounts || accounts.length === 0) return null;
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByAccount(
|
|
||||||
provider: string,
|
|
||||||
providerAccountId: string
|
|
||||||
): Promise<SavedUser | null> {
|
|
||||||
const account = await this.repos.accounts.findBy({
|
|
||||||
compoundId: getCompoundId(provider, providerAccountId),
|
|
||||||
});
|
|
||||||
if (!account) return null;
|
|
||||||
|
|
||||||
return this.repos.users.findById({ id: account.userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateUser(user: SavedUser): Promise<SavedUser> {
|
|
||||||
return this.repos.users.update(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async linkAccount(adapterAccount: AdapterAccount): Promise<void> {
|
|
||||||
const {
|
|
||||||
userId,
|
|
||||||
access_token: accessToken,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
provider: providerId,
|
|
||||||
providerAccountId,
|
|
||||||
expires_at: accessTokenExpires,
|
|
||||||
type: providerType,
|
|
||||||
} = adapterAccount;
|
|
||||||
const exists = await this.repos.users.existsById({ id: userId });
|
|
||||||
if (!exists) return;
|
|
||||||
const account: UnsavedAccount = {
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
compoundId: getCompoundId(providerId, providerAccountId),
|
|
||||||
providerAccountId,
|
|
||||||
providerId,
|
|
||||||
providerType,
|
|
||||||
accessTokenExpires: accessTokenExpires
|
|
||||||
? new Date(accessTokenExpires)
|
|
||||||
: new Date(),
|
|
||||||
userId,
|
|
||||||
};
|
|
||||||
await this.repos.accounts.insert(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
async unlinkAccount(
|
|
||||||
userId: string,
|
|
||||||
providerId: string,
|
|
||||||
providerAccountId: string
|
|
||||||
): Promise<void> {
|
|
||||||
await this.repos.accounts.removeBy({
|
|
||||||
userId,
|
|
||||||
compoundId: getCompoundId(providerId, providerAccountId),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createSession({
|
|
||||||
sessionToken,
|
|
||||||
userId,
|
|
||||||
}: {
|
|
||||||
sessionToken: string;
|
|
||||||
userId: string;
|
|
||||||
}): Promise<SavedSession> {
|
|
||||||
let expires;
|
|
||||||
if (this.sessionMaxAge) {
|
|
||||||
const dateExpires = new Date(Date.now() + this.sessionMaxAge);
|
|
||||||
expires = dateExpires.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const session: UnsavedSession = {
|
|
||||||
expires,
|
|
||||||
userId,
|
|
||||||
sessionToken,
|
|
||||||
//sessionToken: randomToken(),
|
|
||||||
accessToken: randomToken(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.repos.sessions.insert(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSessionAndUser(
|
|
||||||
sessionToken: string
|
|
||||||
): Promise<{ session: AdapterSession; user: any; } | null> {
|
|
||||||
const session = await this.repos.sessions.findBy({ sessionToken });
|
|
||||||
if (!session) return null;
|
|
||||||
if (session && session.expires && new Date() > session.expires) {
|
|
||||||
this.repos.sessions.remove(session);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.repos.users.findById({ id: session.userId });
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const adapterSession: AdapterSession = {
|
|
||||||
userId: session.userId,
|
|
||||||
expires: session.expires,
|
|
||||||
sessionToken: sessionToken,
|
|
||||||
};
|
|
||||||
|
|
||||||
const adapterUser: any = {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
emailVerified: user.emailVerified,
|
|
||||||
userRole: user.userRole
|
|
||||||
};
|
|
||||||
|
|
||||||
return { session: adapterSession, user: adapterUser };
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateSession(
|
|
||||||
session: SavedSession,
|
|
||||||
force?: boolean
|
|
||||||
): Promise<SavedSession | null> {
|
|
||||||
if (
|
|
||||||
this.sessionMaxAge &&
|
|
||||||
(this.sessionUpdateAge || this.sessionUpdateAge === 0) &&
|
|
||||||
session.expires
|
|
||||||
) {
|
|
||||||
// Calculate last updated date, to throttle write updates to database
|
|
||||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
|
||||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
|
||||||
//
|
|
||||||
// Default for sessionMaxAge is 30 days.
|
|
||||||
// Default for sessionUpdateAge is 1 hour.
|
|
||||||
const dateSessionIsDueToBeUpdated = new Date(
|
|
||||||
session.expires.getTime() - this.sessionMaxAge + this.sessionUpdateAge
|
|
||||||
);
|
|
||||||
|
|
||||||
// Trigger update of session expiry date and write to database, only
|
|
||||||
// if the session was last updated more than {sessionUpdateAge} ago
|
|
||||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
|
||||||
const newExpiryDate = new Date();
|
|
||||||
newExpiryDate.setTime(newExpiryDate.getTime() + this.sessionMaxAge);
|
|
||||||
session.expires = newExpiryDate;
|
|
||||||
} else if (!force) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else if (!force) {
|
|
||||||
// If session MaxAge, session UpdateAge or session.expires are
|
|
||||||
// missing then don't even try to save changes, unless force is set.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { expires } = session;
|
|
||||||
return this.repos.sessions.update({ ...session, expires });
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSession(sessionToken: string): Promise<void> {
|
|
||||||
await this.repos.sessions.removeBy({ sessionToken });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import * as PGP from "pg-promise";
|
|
||||||
import * as PGPTS from "pg-promise/typescript/pg-subset";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export type IDatabase = PGP.IDatabase<any>;
|
|
||||||
export type IMain = PGP.IMain;
|
|
||||||
export type IResult = PGPTS.IResult;
|
|
||||||
export type IInitOptions = PGP.IInitOptions;
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import * as Hapi from "@hapi/hapi";
|
|
||||||
import * as http from "node:http";
|
|
||||||
import type { HttpTerminator } from "http-terminator";
|
|
||||||
import * as Glue from "@hapi/glue";
|
|
||||||
import * as Promster from "@promster/hapi";
|
|
||||||
import figlet from "figlet";
|
|
||||||
import PinoPlugin from "hapi-pino";
|
|
||||||
import { createServer as createPrometheusServer } from "@promster/server";
|
|
||||||
import { createHttpTerminator } from "http-terminator";
|
|
||||||
|
|
||||||
import { configureLogger } from "./logger.js";
|
|
||||||
import RequestIdPlugin from "./plugins/request-id.js";
|
|
||||||
import StatusPlugin from "./plugins/status.js";
|
|
||||||
import ConfigPlugin from "./plugins/config.js";
|
|
||||||
import { IMetamigoConfig } from "./config/index.js";
|
|
||||||
|
|
||||||
export interface Server {
|
|
||||||
hapiServer: Hapi.Server;
|
|
||||||
promServer?: http.Server;
|
|
||||||
promTerminator?: HttpTerminator;
|
|
||||||
}
|
|
||||||
export const deployment = async <T extends IMetamigoConfig>(
|
|
||||||
manifest: Glue.Manifest,
|
|
||||||
config: T,
|
|
||||||
start = false
|
|
||||||
): Promise<Server> => {
|
|
||||||
const hapiServer: Hapi.Server = await Glue.compose(manifest);
|
|
||||||
|
|
||||||
await hapiServer.initialize();
|
|
||||||
|
|
||||||
if (!start) return { hapiServer };
|
|
||||||
|
|
||||||
await announce(config);
|
|
||||||
|
|
||||||
await hapiServer.start();
|
|
||||||
|
|
||||||
const { port, address } = config.metrics;
|
|
||||||
const promServer = await createPrometheusServer({
|
|
||||||
port,
|
|
||||||
hostname: address,
|
|
||||||
});
|
|
||||||
const promTerminator = createHttpTerminator({
|
|
||||||
server: promServer,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`
|
|
||||||
🚀 Server listening on http://${hapiServer.info.address}:${hapiServer.info.port}
|
|
||||||
Metrics listening on http://${address}:${port}
|
|
||||||
`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
hapiServer,
|
|
||||||
promServer,
|
|
||||||
promTerminator,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stopDeployment = async (server: Server): Promise<void> => {
|
|
||||||
await server.hapiServer.stop();
|
|
||||||
if (server.promTerminator) await server.promTerminator.terminate();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultPlugins = <T extends IMetamigoConfig>(
|
|
||||||
config: T
|
|
||||||
): string[] | Glue.PluginObject[] | Array<string | Glue.PluginObject> => {
|
|
||||||
const {
|
|
||||||
logRequestStart,
|
|
||||||
logRequestComplete,
|
|
||||||
logRequestPayload,
|
|
||||||
logRequestQueryParams,
|
|
||||||
level,
|
|
||||||
redact,
|
|
||||||
ignorePaths,
|
|
||||||
ignoreTags,
|
|
||||||
requestIdHeader,
|
|
||||||
} = config.logging;
|
|
||||||
const plugins = [
|
|
||||||
{ plugin: ConfigPlugin, options: { config } },
|
|
||||||
{
|
|
||||||
plugin: PinoPlugin,
|
|
||||||
options: {
|
|
||||||
level,
|
|
||||||
instance: configureLogger(config),
|
|
||||||
logRequestStart,
|
|
||||||
logRequestComplete,
|
|
||||||
logPayload: logRequestPayload,
|
|
||||||
logQueryParams: logRequestQueryParams,
|
|
||||||
redact: {
|
|
||||||
paths: redact,
|
|
||||||
remove: true,
|
|
||||||
},
|
|
||||||
ignorePaths,
|
|
||||||
ignoreTags,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: RequestIdPlugin,
|
|
||||||
options: {
|
|
||||||
header: requestIdHeader,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ plugin: StatusPlugin },
|
|
||||||
{ plugin: Promster.createPlugin() },
|
|
||||||
];
|
|
||||||
return plugins;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const announce = async <T extends IMetamigoConfig>(
|
|
||||||
config: T
|
|
||||||
): Promise<void> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
figlet.text(
|
|
||||||
config.meta.name,
|
|
||||||
{ font: config.meta.figletFont as any },
|
|
||||||
(err, text) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
console.log(`${text}`);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/**
|
|
||||||
* Used by Flavor to mark a type in a readable way.
|
|
||||||
*/
|
|
||||||
export interface Flavoring<FlavorT> {
|
|
||||||
_type?: FlavorT;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Create a "flavored" version of a type. TypeScript will disallow mixing
|
|
||||||
* flavors, but will allow unflavored values of that type to be passed in where
|
|
||||||
* a flavored version is expected. This is a less restrictive form of branding.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;
|
|
||||||
|
|
||||||
export type UUID = Flavor<string, "A UUID">;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const deepFreeze = (o: unknown): any => {
|
|
||||||
Object.freeze(o);
|
|
||||||
|
|
||||||
const oIsFunction = typeof o === "function";
|
|
||||||
const hasOwnProp = Object.prototype.hasOwnProperty;
|
|
||||||
|
|
||||||
Object.getOwnPropertyNames(o).forEach((prop) => {
|
|
||||||
if (
|
|
||||||
hasOwnProp.call(o, prop) &&
|
|
||||||
(oIsFunction
|
|
||||||
? prop !== "caller" && prop !== "callee" && prop !== "arguments"
|
|
||||||
: true) &&
|
|
||||||
o[prop] !== null &&
|
|
||||||
(typeof o[prop] === "object" || typeof o[prop] === "function") &&
|
|
||||||
!Object.isFrozen(o[prop])
|
|
||||||
) {
|
|
||||||
deepFreeze(o[prop]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import * as Boom from "@hapi/boom";
|
|
||||||
import * as Hapi from "@hapi/hapi";
|
|
||||||
|
|
||||||
interface IResponseMeta {
|
|
||||||
operation?: string;
|
|
||||||
method?: string;
|
|
||||||
paging?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IResponseError {
|
|
||||||
code?: string | number;
|
|
||||||
message?: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IResponse<T> {
|
|
||||||
meta: IResponseMeta;
|
|
||||||
data: T[];
|
|
||||||
errors: IResponseError[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IResponseOptions<T> {
|
|
||||||
value?: T | null | undefined;
|
|
||||||
boom?: Boom.Boom<any> | null | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createResponse<T>(
|
|
||||||
request: Hapi.Request,
|
|
||||||
{ value = undefined, boom = undefined }: IResponseOptions<T>
|
|
||||||
): IResponse<T> {
|
|
||||||
const errors: IResponseError[] = [];
|
|
||||||
const data: any = [];
|
|
||||||
|
|
||||||
if (boom) {
|
|
||||||
errors.push({
|
|
||||||
code: boom.output.payload.statusCode,
|
|
||||||
error: boom.output.payload.error,
|
|
||||||
message: boom.output.payload.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && data) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
data.push(...value);
|
|
||||||
} else {
|
|
||||||
data.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
meta: {
|
|
||||||
method: request.method.toUpperCase(),
|
|
||||||
operation: request.url.pathname,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import process from "node:process";
|
|
||||||
import * as Hapi from "@hapi/hapi";
|
|
||||||
import Joi from "joi";
|
|
||||||
import * as Hoek from "@hapi/hoek";
|
|
||||||
import * as Boom from "@hapi/boom";
|
|
||||||
|
|
||||||
export interface HapiValidationError extends Joi.ValidationError {
|
|
||||||
output: {
|
|
||||||
statusCode: number;
|
|
||||||
headers: Hapi.Utils.Dictionary<string | string[]>;
|
|
||||||
payload: {
|
|
||||||
statusCode: number;
|
|
||||||
error: string;
|
|
||||||
message?: string;
|
|
||||||
validation: {
|
|
||||||
source: string;
|
|
||||||
keys: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export function defaultValidationErrorHandler(
|
|
||||||
request: Hapi.Request,
|
|
||||||
h: Hapi.ResponseToolkit,
|
|
||||||
err?: Error
|
|
||||||
): Hapi.Lifecycle.ReturnValue {
|
|
||||||
// Newer versions of Joi don't format the key for missing params the same way. This shim
|
|
||||||
// provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class
|
|
||||||
// in JS so we have to rely on the `name` key before we can cast it.
|
|
||||||
//
|
|
||||||
// The Hapi code we're 'overwriting' can be found here:
|
|
||||||
// https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102
|
|
||||||
if (err && err.name === "ValidationError" && err.hasOwnProperty("output")) {
|
|
||||||
const validationError: HapiValidationError = err as HapiValidationError;
|
|
||||||
const validationKeys: string[] = [];
|
|
||||||
|
|
||||||
validationError.details.forEach((detail) => {
|
|
||||||
if (detail.path.length > 0) {
|
|
||||||
validationKeys.push(Hoek.escapeHtml(detail.path.join(".")));
|
|
||||||
} else {
|
|
||||||
// If no path, use the value sigil to signal the entire value had an issue.
|
|
||||||
validationKeys.push("value");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
validationError.output.payload.validation.keys = validationKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validatingFailAction = async (
|
|
||||||
request: Hapi.Request,
|
|
||||||
h: Hapi.ResponseToolkit,
|
|
||||||
err: Error
|
|
||||||
): Promise<void> => {
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
throw Boom.badRequest("Invalid request payload input");
|
|
||||||
} else {
|
|
||||||
defaultValidationErrorHandler(request, h, err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
export * from "./config/index.js";
|
|
||||||
export * from "./controllers/crud-controller.js";
|
|
||||||
export * from "./controllers/nextauth-adapter.js";
|
|
||||||
export * from "./hapi.js";
|
|
||||||
export * from "./helpers/index.js";
|
|
||||||
export * from "./helpers/response.js";
|
|
||||||
export * from "./helpers/validation-error.js";
|
|
||||||
export * from "./logger.js";
|
|
||||||
export * from "./records/index.js";
|
|
||||||
|
|
||||||
import * as pino from "pino";
|
|
||||||
|
|
||||||
declare module "@hapi/hapi" {
|
|
||||||
interface Server {
|
|
||||||
logger: pino.Logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Request {
|
|
||||||
logger: pino.Logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import pino, { LoggerOptions } from "pino";
|
|
||||||
import { IMetamigoConfig } from "./config/index.js";
|
|
||||||
|
|
||||||
export const configureLogger = <T extends IMetamigoConfig>(
|
|
||||||
config: T
|
|
||||||
): pino.Logger => {
|
|
||||||
const { level, redact } = config.logging;
|
|
||||||
const options: LoggerOptions = {
|
|
||||||
level,
|
|
||||||
transport: {
|
|
||||||
target: "pino-pretty",
|
|
||||||
},
|
|
||||||
redact: {
|
|
||||||
paths: redact,
|
|
||||||
remove: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return pino(options);
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { Server } from "@hapi/hapi";
|
|
||||||
import cloneDeep from "lodash/cloneDeep.js";
|
|
||||||
import { deepFreeze } from "../helpers/index.js";
|
|
||||||
|
|
||||||
interface ConfigOptions {
|
|
||||||
config: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
const register = async (
|
|
||||||
server: Server,
|
|
||||||
options: ConfigOptions
|
|
||||||
): Promise<void> => {
|
|
||||||
const safeConfig = deepFreeze(cloneDeep(options.config));
|
|
||||||
server.decorate("server", "config", () => safeConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConfigPlugin = {
|
|
||||||
register,
|
|
||||||
name: "config",
|
|
||||||
version: "0.0.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfigPlugin;
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { Server } from "@hapi/hapi";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
|
|
||||||
interface RequestIdOptions {
|
|
||||||
header?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const register = async (
|
|
||||||
server: Server,
|
|
||||||
options?: RequestIdOptions
|
|
||||||
): Promise<void> => {
|
|
||||||
const header = options?.header || "x-request-id";
|
|
||||||
server.ext("onPreResponse", async (request, h) => {
|
|
||||||
if (!request.response) {
|
|
||||||
return h.continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("isBoom" in request.response) {
|
|
||||||
const id = request.response.output.headers[header] || uuid();
|
|
||||||
request.response.output.headers[header] = id;
|
|
||||||
} else {
|
|
||||||
const id = request.headers[header] || uuid();
|
|
||||||
request.response.header(header, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.continue;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const RequestIdPlugin = {
|
|
||||||
register,
|
|
||||||
name: "request-id",
|
|
||||||
version: "0.0.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RequestIdPlugin;
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import { Server, RouteOptionsAccess } from "@hapi/hapi";
|
|
||||||
import { Prometheus } from "@promster/hapi";
|
|
||||||
import { Counter } from "prom-client";
|
|
||||||
|
|
||||||
interface StatusOptions {
|
|
||||||
path?: string;
|
|
||||||
auth?: RouteOptionsAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = (statusCounter: Counter) => async () => {
|
|
||||||
statusCounter.inc();
|
|
||||||
return "Incremented metamigo_status_test counter";
|
|
||||||
};
|
|
||||||
|
|
||||||
const ping = async () => "OK";
|
|
||||||
|
|
||||||
const statusRoutes = (server: Server, opt?: StatusOptions) => {
|
|
||||||
const path = opt?.path || "/status";
|
|
||||||
const statusCounter = new Prometheus.Counter({
|
|
||||||
name: "metamigo_status_test",
|
|
||||||
help: "Test counter",
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `${path}/ping`,
|
|
||||||
handler: ping,
|
|
||||||
options: {
|
|
||||||
auth: opt?.auth,
|
|
||||||
tags: ["api", "status", "ping"],
|
|
||||||
description: "Returns 200 and OK as the response.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `${path}/inc`,
|
|
||||||
handler: count(statusCounter),
|
|
||||||
options: {
|
|
||||||
auth: opt?.auth,
|
|
||||||
tags: ["api", "status", "prometheus"],
|
|
||||||
description: "Increments a test counter, for testing prometheus.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const register = async (
|
|
||||||
server: Server,
|
|
||||||
options: StatusOptions
|
|
||||||
): Promise<void> => {
|
|
||||||
server.route(statusRoutes(server, options));
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusPlugin = {
|
|
||||||
register,
|
|
||||||
name: "status",
|
|
||||||
version: "0.0.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatusPlugin;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { recordInfo } from "./record-info.js";
|
|
||||||
import { RepositoryBase } from "./base.js";
|
|
||||||
import { Flavor, UUID } from "../helpers";
|
|
||||||
import { UserId } from "./user.js";
|
|
||||||
|
|
||||||
export type AccountId = Flavor<UUID, "Account Id">;
|
|
||||||
|
|
||||||
export interface UnsavedAccount {
|
|
||||||
compoundId: string;
|
|
||||||
userId: UserId;
|
|
||||||
providerType: string;
|
|
||||||
providerId: string;
|
|
||||||
providerAccountId: string;
|
|
||||||
refreshToken: string;
|
|
||||||
accessToken: string;
|
|
||||||
accessTokenExpires: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SavedAccount extends UnsavedAccount {
|
|
||||||
id: AccountId;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AccountRecord = recordInfo<UnsavedAccount, SavedAccount>(
|
|
||||||
"app_public",
|
|
||||||
"accounts"
|
|
||||||
);
|
|
||||||
|
|
||||||
export class AccountRecordRepository extends RepositoryBase(AccountRecord) {}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { TableName } from "pg-promise";
|
|
||||||
import { IMain } from "../db/types.js";
|
|
||||||
import { CrudRepository } from "./crud-repository.js";
|
|
||||||
import { PgRecordInfo, UnsavedR, SavedR, KeyType } from "./record-info.js";
|
|
||||||
import type { IDatabase } from "pg-promise";
|
|
||||||
|
|
||||||
export type PgProtocol<T> = IDatabase<T> & T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function returns a constructor for a repository class for [[TRecordInfo]]
|
|
||||||
*
|
|
||||||
* @param aRecordType the record type runtime definition
|
|
||||||
*/
|
|
||||||
// haven't figured out a good return type for this function
|
|
||||||
|
|
||||||
export function unboundRepositoryBase<
|
|
||||||
TRecordInfo extends PgRecordInfo,
|
|
||||||
TDatabaseExtension
|
|
||||||
>(aRecordType: TRecordInfo) {
|
|
||||||
return class Repository extends CrudRepository<
|
|
||||||
UnsavedR<TRecordInfo>,
|
|
||||||
SavedR<TRecordInfo>,
|
|
||||||
KeyType<TRecordInfo>
|
|
||||||
> {
|
|
||||||
_recordType!: TRecordInfo;
|
|
||||||
static readonly recordType = aRecordType;
|
|
||||||
static readonly schemaName = aRecordType.schemaName;
|
|
||||||
static readonly tableName = aRecordType.tableName;
|
|
||||||
public readonly recordType = aRecordType;
|
|
||||||
public readonly schemaTable: TableName;
|
|
||||||
public db: PgProtocol<TDatabaseExtension>;
|
|
||||||
public pgp: IMain;
|
|
||||||
|
|
||||||
constructor(db: PgProtocol<TDatabaseExtension>) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.pgp = db.$config.pgp;
|
|
||||||
this.schemaTable = new this.pgp.helpers.TableName({
|
|
||||||
schema: aRecordType.schemaName,
|
|
||||||
table: aRecordType.tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.db = db;
|
|
||||||
if (!this.db) {
|
|
||||||
throw new Error("Missing database in repository");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RepositoryBase<
|
|
||||||
Rec extends PgRecordInfo,
|
|
||||||
TDatabaseExtension = unknown
|
|
||||||
>(recordType: Rec) {
|
|
||||||
return unboundRepositoryBase<Rec, TDatabaseExtension>(recordType);
|
|
||||||
}
|
|
||||||
|
|
@ -1,321 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { TableName } from "pg-promise";
|
|
||||||
import decamelcaseKeys from "decamelcase-keys";
|
|
||||||
import isObject from "lodash/isObject.js";
|
|
||||||
import isArray from "lodash/isArray.js";
|
|
||||||
import zipObject from "lodash/zipObject.js";
|
|
||||||
import isEmpty from "lodash/isEmpty.js";
|
|
||||||
import omit from "lodash/omit.js";
|
|
||||||
import { IDatabase, IMain, IResult } from "../db/types.js";
|
|
||||||
import { PgRecordInfo, idKeysOf } from "./record-info.js";
|
|
||||||
|
|
||||||
export interface ICrudRepository<
|
|
||||||
TUnsavedR,
|
|
||||||
TSavedR extends TUnsavedR & IdKeyT,
|
|
||||||
IdKeyT extends object
|
|
||||||
> {
|
|
||||||
findById(id: IdKeyT): Promise<TSavedR | null>;
|
|
||||||
findBy(example: Partial<TSavedR>): Promise<TSavedR | null>;
|
|
||||||
findAll(): Promise<TSavedR[]>;
|
|
||||||
findAllBy(example: Partial<TSavedR>): Promise<TSavedR[]>;
|
|
||||||
existsById(id: IdKeyT): Promise<boolean>;
|
|
||||||
countBy(example: Partial<TSavedR>): Promise<number>;
|
|
||||||
count(): Promise<number>;
|
|
||||||
insert(record: TUnsavedR): Promise<TSavedR>;
|
|
||||||
insertAll(toInsert: TUnsavedR[]): Promise<TSavedR[]>;
|
|
||||||
updateById(id: IdKeyT, attrs: Partial<TSavedR>): Promise<TSavedR>;
|
|
||||||
update(record: TSavedR): Promise<TSavedR>;
|
|
||||||
updateAll(toUpdate: TSavedR[]): Promise<TSavedR[]>;
|
|
||||||
remove(record: TSavedR): Promise<number>;
|
|
||||||
removeAll(toRemove: TSavedR[]): Promise<number>;
|
|
||||||
removeBy(example: Partial<TSavedR>): Promise<TSavedR | null>;
|
|
||||||
removeById(id: IdKeyT): Promise<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The snake cased object going into the db
|
|
||||||
type DatabaseRow = Record<string, unknown>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for generic CRUD operations on a repository for a specific type.
|
|
||||||
*
|
|
||||||
* Several assumptions are made about your environment for this generic CRUD repository to work:
|
|
||||||
*
|
|
||||||
* - the underlying column names are snake_cased (this behavior can be changed, see [[columnize]])
|
|
||||||
* - the rows have only a single primary key (composite keys are not supported)
|
|
||||||
*
|
|
||||||
* @typeParam ID The type of the id column
|
|
||||||
* @typeParam T The type of the record
|
|
||||||
*/
|
|
||||||
export abstract class CrudRepository<
|
|
||||||
TUnsavedR,
|
|
||||||
TSavedR extends TUnsavedR & IdKeyT,
|
|
||||||
IdKeyT extends object
|
|
||||||
> implements ICrudRepository<TUnsavedR, TSavedR, IdKeyT>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* the fully qualified table name
|
|
||||||
*/
|
|
||||||
abstract schemaTable: TableName;
|
|
||||||
abstract recordType: PgRecordInfo<TUnsavedR, TSavedR, IdKeyT>;
|
|
||||||
abstract db: IDatabase;
|
|
||||||
abstract pgp: IMain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the record's columns into snake_case
|
|
||||||
*
|
|
||||||
* @param record the record of type T to convert
|
|
||||||
*/
|
|
||||||
columnize(record: TSavedR | Partial<TSavedR>): DatabaseRow {
|
|
||||||
return decamelcaseKeys(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Creates a simple where clause with each key-value in `example` is
|
|
||||||
* formatted as KEY=VALUE and all kv-pairs are ANDed together.
|
|
||||||
*
|
|
||||||
* @param example key value pair of column names and values
|
|
||||||
*/
|
|
||||||
where(example: Partial<TSavedR>): string {
|
|
||||||
const snaked = this.columnize(example);
|
|
||||||
const clauses = Object.keys(snaked).reduce((acc, cur) => {
|
|
||||||
const colName = this.pgp.as.format("$1:name", cur);
|
|
||||||
return `${acc} and ${colName} = $<${cur}>`;
|
|
||||||
}, "");
|
|
||||||
const where = this.pgp.as.format(`WHERE 1=1 ${clauses}`, { ...snaked }); // Pre-format WHERE condition
|
|
||||||
return where;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a value containing the id of the record (which could be a primitive type, a composite object, or an array of values)
|
|
||||||
* into an object which can be safely passed to [[where]].
|
|
||||||
*/
|
|
||||||
idsObj(idValues: IdKeyT): IdKeyT {
|
|
||||||
if (isEmpty(idValues)) {
|
|
||||||
throw new Error(`idsObj(${this.schemaTable}): passed empty id(s)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ids = {};
|
|
||||||
const idKeys = idKeysOf(this.recordType as any);
|
|
||||||
if (isArray(idValues)) {
|
|
||||||
ids = zipObject(idKeys, idValues);
|
|
||||||
} else if (isObject(idValues)) {
|
|
||||||
ids = idValues;
|
|
||||||
} else {
|
|
||||||
if (idKeys.length !== 1) {
|
|
||||||
throw new Error(
|
|
||||||
`idsObj(${this.schemaTable}): passed record has multiple primary keys. the ids must be passed as an object or array. ${idValues}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ids[idKeys[0]] = idValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a sanity check so we don't do something like
|
|
||||||
// deleting all the data if a WHERE slips in with no ids
|
|
||||||
if (isEmpty(ids)) {
|
|
||||||
throw new Error(`idsObj(${this.schemaTable}): passed empty ids`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids as IdKeyT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all rows in the table
|
|
||||||
*/
|
|
||||||
async findAll(): Promise<TSavedR[]> {
|
|
||||||
return this.db.any("SELECT * FROM $1", [this.schemaTable]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of rows in the table
|
|
||||||
*/
|
|
||||||
async count(): Promise<number> {
|
|
||||||
return this.db.one(
|
|
||||||
"SELECT count(*) FROM $1",
|
|
||||||
[this.schemaTable],
|
|
||||||
(a: { count: string }) => Number(a.count)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of rows in the table matching the example
|
|
||||||
*/
|
|
||||||
async countBy(example: Partial<TSavedR>): Promise<number> {
|
|
||||||
return this.db.one(
|
|
||||||
"SELECT count(*) FROM $1 $2:raw ",
|
|
||||||
[this.schemaTable, this.where(example)],
|
|
||||||
(a: { count: string }) => Number(a.count)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a single row where the example are true.
|
|
||||||
* @param example key-value pairs of column names and values
|
|
||||||
*/
|
|
||||||
async findBy(example: Partial<TSavedR>): Promise<TSavedR | null> {
|
|
||||||
return this.db.oneOrNone("SELECT * FROM $1 $2:raw LIMIT 1", [
|
|
||||||
this.schemaTable,
|
|
||||||
this.where(example),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a row by ID
|
|
||||||
* @param id
|
|
||||||
*/
|
|
||||||
async findById(id: IdKeyT): Promise<TSavedR | null> {
|
|
||||||
const where = this.idsObj(id);
|
|
||||||
return this.db.oneOrNone("SELECT * FROM $1 $2:raw", [
|
|
||||||
this.schemaTable,
|
|
||||||
this.where(where),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a given row with id exists
|
|
||||||
* @param id
|
|
||||||
*/
|
|
||||||
async existsById(id: IdKeyT): Promise<boolean> {
|
|
||||||
return this.db.one(
|
|
||||||
"SELECT EXISTS(SELECT 1 FROM $1 $2:raw)",
|
|
||||||
[this.schemaTable, this.where(this.idsObj(id))],
|
|
||||||
(a: { exists: boolean }) => a.exists
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all rows where the example are true.
|
|
||||||
* @param example key-value pairs of column names and values
|
|
||||||
*/
|
|
||||||
async findAllBy(example: Partial<TSavedR>): Promise<TSavedR[]> {
|
|
||||||
return this.db.any("SELECT * FROM $1 $2:raw", [
|
|
||||||
this.schemaTable,
|
|
||||||
this.where(example),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new row
|
|
||||||
* @param record
|
|
||||||
* @return the new row
|
|
||||||
*/
|
|
||||||
async insert(record: TUnsavedR): Promise<TSavedR> {
|
|
||||||
return this.db.one("INSERT INTO $1 ($2:name) VALUES ($2:csv) RETURNING *", [
|
|
||||||
this.schemaTable,
|
|
||||||
this.columnize(record as any),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `insert` but will insert/update a batch of rows at once
|
|
||||||
*/
|
|
||||||
async insertAll(toInsert: TUnsavedR[]): Promise<TSavedR[]> {
|
|
||||||
return this.db.tx((t) => {
|
|
||||||
const insertCommands: any[] = [];
|
|
||||||
|
|
||||||
toInsert.forEach((record) => {
|
|
||||||
insertCommands.push(this.insert(record));
|
|
||||||
});
|
|
||||||
|
|
||||||
return t.batch(insertCommands);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a row by id
|
|
||||||
* @param id
|
|
||||||
* @return the number of rows affected
|
|
||||||
*/
|
|
||||||
async removeById(id: IdKeyT): Promise<number> {
|
|
||||||
return this.db.result(
|
|
||||||
"DELETE FROM $1 $2:raw",
|
|
||||||
[this.schemaTable, this.where(this.idsObj(id))],
|
|
||||||
(r: IResult) => r.rowCount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete records matching the query
|
|
||||||
* @param example key-value pairs of column names and values
|
|
||||||
*/
|
|
||||||
async removeBy(example: Partial<TSavedR>): Promise<TSavedR | null> {
|
|
||||||
if (isEmpty(example))
|
|
||||||
throw new Error(
|
|
||||||
`removeBy(${this.schemaTable}): passed empty constraint!`
|
|
||||||
);
|
|
||||||
return this.db.result("DELETE FROM $1 $2:raw", [
|
|
||||||
this.schemaTable,
|
|
||||||
this.where(example),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given row
|
|
||||||
*
|
|
||||||
* @param record to remove
|
|
||||||
* @return the number of rows affected
|
|
||||||
*/
|
|
||||||
async remove(record: TSavedR): Promise<number> {
|
|
||||||
return this.removeById(this.recordType.idOf(record));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all rows
|
|
||||||
* @param toRemove a list of rows to remove, if empty, DELETES ALL ROWS
|
|
||||||
* @return the number of rows affected
|
|
||||||
*/
|
|
||||||
async removeAll(toRemove: TSavedR[] = []): Promise<number> {
|
|
||||||
if (toRemove.length === 0) {
|
|
||||||
return this.db.result(
|
|
||||||
"DELETE FROM $1 WHERE 1=1;",
|
|
||||||
[this.schemaTable],
|
|
||||||
(r: IResult) => r.rowCount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await this.db.tx((t) => {
|
|
||||||
const delCommands: any[] = [];
|
|
||||||
|
|
||||||
toRemove.forEach((record) => {
|
|
||||||
delCommands.push(this.remove(record));
|
|
||||||
});
|
|
||||||
|
|
||||||
return t.batch(delCommands);
|
|
||||||
});
|
|
||||||
return results.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates an existing row
|
|
||||||
* @param id
|
|
||||||
* @param attrs
|
|
||||||
* @return the updated row
|
|
||||||
*/
|
|
||||||
async updateById(id: IdKeyT, attrs: Partial<TSavedR>): Promise<TSavedR> {
|
|
||||||
const idKeys = idKeysOf(this.recordType as any);
|
|
||||||
const attrsSafe = omit(attrs, idKeys);
|
|
||||||
return this.db.one(
|
|
||||||
"UPDATE $1 SET ($2:name) = ROW($2:csv) $3:raw RETURNING *",
|
|
||||||
[this.schemaTable, this.columnize(attrsSafe), this.where(this.idsObj(id))]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(record: TSavedR): Promise<TSavedR> {
|
|
||||||
return this.updateById(this.recordType.idOf(record), record);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a batch of records at once
|
|
||||||
*/
|
|
||||||
async updateAll(toUpdate: TSavedR[]): Promise<TSavedR[]> {
|
|
||||||
return this.db.tx((t) => {
|
|
||||||
const updateCommands: any[] = [];
|
|
||||||
|
|
||||||
toUpdate.forEach((record) => {
|
|
||||||
updateCommands.push(this.update(record));
|
|
||||||
});
|
|
||||||
|
|
||||||
return t.batch(updateCommands);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
export * from "./base.js";
|
|
||||||
export * from "./record-info.js";
|
|
||||||
export * from "./crud-repository.js";
|
|
||||||
export * from "./user.js";
|
|
||||||
export * from "./session.js";
|
|
||||||
export * from "./account.js";
|
|
||||||
|
|
||||||
import type { AccountRecordRepository } from "./account.js";
|
|
||||||
import type { UserRecordRepository } from "./user.js";
|
|
||||||
import type { SessionRecordRepository } from "./session.js";
|
|
||||||
|
|
||||||
export interface IMetamigoRepositories {
|
|
||||||
users: UserRecordRepository;
|
|
||||||
sessions: SessionRecordRepository;
|
|
||||||
accounts: AccountRecordRepository;
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import at from "lodash/at.js";
|
|
||||||
import pick from "lodash/pick.js";
|
|
||||||
|
|
||||||
export interface EntityType<
|
|
||||||
TUnsaved = any,
|
|
||||||
TSaved = any,
|
|
||||||
TIds extends object = any
|
|
||||||
> {
|
|
||||||
_saved: TSaved;
|
|
||||||
_unsaved: TUnsaved;
|
|
||||||
_idKeys: TIds;
|
|
||||||
idOf: (rec: TSaved) => TIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UnsavedR<T extends { _unsaved: any }> = T["_unsaved"];
|
|
||||||
export type SavedR<T extends { _saved: any }> = T["_saved"];
|
|
||||||
export type KeyType<R extends EntityType> = R["_idKeys"];
|
|
||||||
|
|
||||||
export interface PgRecordInfo<
|
|
||||||
Unsaved = any,
|
|
||||||
Saved extends Unsaved & IdType = any,
|
|
||||||
IdType extends object = any
|
|
||||||
> extends EntityType<Unsaved, Saved, IdType> {
|
|
||||||
tableName: string;
|
|
||||||
schemaName: string;
|
|
||||||
idKeys: (keyof Saved)[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the runtime key name from a recordInfo
|
|
||||||
*/
|
|
||||||
export function idKeysOf<RI extends PgRecordInfo>(
|
|
||||||
recordInfoWithIdKey: RI
|
|
||||||
): string[] {
|
|
||||||
return recordInfoWithIdKey.idKeys as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns a record type with possibly more fields than "id" into an array
|
|
||||||
*/
|
|
||||||
export function collectIdValues<RecordT extends PgRecordInfo>(
|
|
||||||
idObj: KeyType<RecordT>,
|
|
||||||
knexRecordType: RecordT
|
|
||||||
): string[] {
|
|
||||||
return at(idObj, idKeysOf(knexRecordType));
|
|
||||||
}
|
|
||||||
|
|
||||||
function castToRecordInfo(
|
|
||||||
runtimeData: Omit<PgRecordInfo, "_idKeys" | "_saved" | "_unsaved">
|
|
||||||
): PgRecordInfo {
|
|
||||||
return runtimeData as PgRecordInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Creates a record descriptor that captures the table name, primary key name,
|
|
||||||
* unsaved type, and saved type of a database record type. Assumes "id" as the
|
|
||||||
* primary key name
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function recordInfo<Unsaved, Saved extends Unsaved & { id: any }>(
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string
|
|
||||||
): PgRecordInfo<Unsaved, Saved, Pick<Saved, "id">>;
|
|
||||||
|
|
||||||
export function recordInfo<Type extends { id: string }>(
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string
|
|
||||||
): PgRecordInfo<Type, Type, Pick<Type, "id">>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Creates a record descriptor that captures the table name, primary key name,
|
|
||||||
* unsaved type, and saved type of a database record type.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function recordInfo<
|
|
||||||
Unsaved,
|
|
||||||
Saved extends Unsaved,
|
|
||||||
Id extends keyof Saved
|
|
||||||
>(
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string,
|
|
||||||
idKey: Id[]
|
|
||||||
): PgRecordInfo<Unsaved, Saved, Pick<Saved, Id>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Don't use this signature be sure to provide unsaved and saved types.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function recordInfo(
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string,
|
|
||||||
idKeys?: string[]
|
|
||||||
) {
|
|
||||||
idKeys = idKeys || ["id"];
|
|
||||||
return castToRecordInfo({
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
idKeys,
|
|
||||||
idOf: (rec) => pick(rec, idKeys as any),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Creates a record descriptor for records with composite primary keys
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function compositeRecordType<
|
|
||||||
TUnsaved,
|
|
||||||
TSaved extends TUnsaved = TUnsaved
|
|
||||||
>(
|
|
||||||
schemaName: string,
|
|
||||||
tableName: string
|
|
||||||
): {
|
|
||||||
withCompositeKeys<TKeys extends keyof TSaved>(
|
|
||||||
keys: TKeys[]
|
|
||||||
): PgRecordInfo<TUnsaved, TSaved, Pick<TSaved, TKeys>>;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
withCompositeKeys(keys) {
|
|
||||||
return castToRecordInfo({
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
idKeys: keys,
|
|
||||||
idOf: (rec) => pick(rec, keys),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { recordInfo } from "./record-info.js";
|
|
||||||
import { RepositoryBase } from "./base.js";
|
|
||||||
import { Flavor, UUID } from "../helpers";
|
|
||||||
import { UserId } from "./user.js";
|
|
||||||
|
|
||||||
export type SessionId = Flavor<UUID, "Session Id">;
|
|
||||||
|
|
||||||
export interface UnsavedSession {
|
|
||||||
userId: UserId;
|
|
||||||
expires: Date;
|
|
||||||
sessionToken: string;
|
|
||||||
accessToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SavedSession extends UnsavedSession {
|
|
||||||
id: SessionId;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SessionRecord = recordInfo<UnsavedSession, SavedSession>(
|
|
||||||
"app_private",
|
|
||||||
"sessions"
|
|
||||||
);
|
|
||||||
|
|
||||||
export class SessionRecordRepository extends RepositoryBase(SessionRecord) {}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import { recordInfo } from "./record-info.js";
|
|
||||||
import { RepositoryBase } from "./base.js";
|
|
||||||
import { Flavor, UUID } from "../helpers";
|
|
||||||
|
|
||||||
export type UserId = Flavor<UUID, "User Id">;
|
|
||||||
|
|
||||||
export interface UnsavedUser {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
emailVerified: Date;
|
|
||||||
avatar: string;
|
|
||||||
isActive: boolean;
|
|
||||||
userRole: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SavedUser extends UnsavedUser {
|
|
||||||
id: UserId;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserRecord = recordInfo<UnsavedUser, SavedUser>(
|
|
||||||
"app_public",
|
|
||||||
"users"
|
|
||||||
);
|
|
||||||
|
|
||||||
export class UserRecordRepository extends RepositoryBase(UserRecord) {
|
|
||||||
async upsert(record: UnsavedUser | SavedUser): Promise<SavedUser> {
|
|
||||||
return this.db.one(
|
|
||||||
`INSERT INTO $1 ($2:name) VALUES ($2:csv)
|
|
||||||
ON CONFLICT (email)
|
|
||||||
DO UPDATE SET
|
|
||||||
name = EXCLUDED.name,
|
|
||||||
avatar = EXCLUDED.avatar,
|
|
||||||
email_verified = EXCLUDED.email_verified
|
|
||||||
RETURNING *`,
|
|
||||||
[this.schemaTable, this.columnize(record)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"incremental": true,
|
|
||||||
"outDir": "build/main",
|
|
||||||
"rootDir": "src",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"types": ["jest", "node"],
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"composite": true,
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"],
|
|
||||||
"exclude": ["node_modules/**"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +1,14 @@
|
||||||
# tsconfig-amigo
|
# tsconfig
|
||||||
|
|
||||||
A shared tsconfig for [CDR Tech][cdrtech].
|
A shared tsconfig for [CDR Tech][cdrtech].
|
||||||
|
|
||||||
# Install
|
|
||||||
|
|
||||||
We recommend using [@digiresilience/amigo-dev][amigo-dev] to manage your dev dependencies.
|
|
||||||
|
|
||||||
[amigo-dev]: https://gitlab.com/digiresilience/link/amigo-dev
|
|
||||||
|
|
||||||
But if you want to do it manually, then:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ npm install --save-dev @digiresilience/tsconfig-amigo
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
In `tsconfig.json`
|
In `tsconfig.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"extends": "@digiresilience/tsconfig-amigo",
|
"extends": "tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"outDir": "build/main",
|
"outDir": "build/main",
|
||||||
|
|
@ -37,10 +26,10 @@ Copyright © 2020-present [Center for Digital Resilience][cdr]
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
| [![Abel Luck][abelxluck_avatar]][abelxluck_homepage]<br/>[Abel Luck][abelxluck_homepage] |
|
| [![Abel Luck][abelxluck_avatar]][abelxluck_homepage]<br/>[Abel Luck][abelxluck_homepage] |
|
||||||
|---|
|
| ---------------------------------------------------------------------------------------- |
|
||||||
|
|
||||||
[abelxluck_homepage]: https://gitlab.com/abelxluck
|
[abelxluck_homepage]: https://gitlab.com/abelxluck
|
||||||
[abelxluck_avatar]: https://secure.gravatar.com/avatar/0f605397e0ead93a68e1be26dc26481a?s=100&d=identicon
|
[abelxluck_avatar]: https://secure.gravatar.com/avatar/0f605397e0ead93a68e1be26dc26481a?s=100&d=identicon
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,5 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "echo no lint",
|
"lint": "echo no lint",
|
||||||
"test": "echo no tests"
|
"test": "echo no tests"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"@digiresilience:registry": "https://gitlab.com/api/v4/projects/21673307/packages/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue