Merge branch 'develop' into 'main'
Develop Closes #1 See merge request digiresilience/link/link-stack!7
This commit is contained in:
commit
8d42c8fdb2
402 changed files with 20153 additions and 19937 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -23,3 +23,6 @@ coverage
|
||||||
out/
|
out/
|
||||||
signald-state/*
|
signald-state/*
|
||||||
!./signald-state/.gitkeep
|
!./signald-state/.gitkeep
|
||||||
|
baileys-state
|
||||||
|
signald-state
|
||||||
|
project.org
|
||||||
|
|
@ -17,7 +17,7 @@ build-all:
|
||||||
- turbo build
|
- turbo build
|
||||||
|
|
||||||
.docker-build:
|
.docker-build:
|
||||||
image: registry.gitlab.com/guardianproject-ops/docker-alpine-git:latest
|
image: registry.gitlab.com/digiresilience/link/link-stack/buildx:${CI_COMMIT_REF_NAME}
|
||||||
services:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
stage: docker-build
|
stage: docker-build
|
||||||
|
|
@ -34,7 +34,7 @@ build-all:
|
||||||
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
||||||
|
|
||||||
.docker-release:
|
.docker-release:
|
||||||
image: registry.gitlab.com/guardianproject-ops/docker-alpine-git:latest
|
image: registry.gitlab.com/digiresilience/link/link-stack/buildx:${CI_COMMIT_REF_NAME}
|
||||||
services:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
stage: docker-release
|
stage: docker-release
|
||||||
|
|
@ -51,6 +51,17 @@ build-all:
|
||||||
- docker tag ${DOCKER_NS}:${DOCKER_TAG} ${DOCKER_NS}:${DOCKER_TAG_NEW}
|
- docker tag ${DOCKER_NS}:${DOCKER_TAG} ${DOCKER_NS}:${DOCKER_TAG_NEW}
|
||||||
- docker push ${DOCKER_NS}:${DOCKER_TAG_NEW}
|
- docker push ${DOCKER_NS}:${DOCKER_TAG_NEW}
|
||||||
|
|
||||||
|
buildx-docker-build:
|
||||||
|
extends: .docker-build
|
||||||
|
variables:
|
||||||
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/buildx
|
||||||
|
DOCKERFILE_PATH: ./docker/buildx/Dockerfile
|
||||||
|
|
||||||
|
buildx-docker-release:
|
||||||
|
extends: .docker-release
|
||||||
|
variables:
|
||||||
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/buildx
|
||||||
|
|
||||||
link-docker-build:
|
link-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
variables:
|
variables:
|
||||||
|
|
@ -84,17 +95,6 @@ metamigo-docker-release:
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo
|
||||||
|
|
||||||
metamigo-frontend-docker-build:
|
|
||||||
extends: .docker-build
|
|
||||||
variables:
|
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo-frontend
|
|
||||||
DOCKERFILE_PATH: ./apps/metamigo-frontend/Dockerfile
|
|
||||||
|
|
||||||
metamigo-frontend-docker-release:
|
|
||||||
extends: .docker-release
|
|
||||||
variables:
|
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo-frontend
|
|
||||||
|
|
||||||
elasticsearch-docker-build:
|
elasticsearch-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
variables:
|
variables:
|
||||||
|
|
@ -200,8 +200,40 @@ zammad-docker-build:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad
|
||||||
DOCKERFILE_PATH: ./docker/zammad/Dockerfile
|
DOCKERFILE_PATH: ./docker/zammad/Dockerfile
|
||||||
DOCKER_CONTEXT: ./docker/zammad
|
DOCKER_CONTEXT: ./docker/zammad
|
||||||
|
before_script:
|
||||||
|
- apk --update add nodejs npm
|
||||||
|
script:
|
||||||
|
- npm install npm@latest -g
|
||||||
|
- npm install -g turbo
|
||||||
|
- npm ci
|
||||||
|
- turbo build --force --filter zammad-addon-*
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
- DOCKER_BUILDKIT=1 docker build --build-arg EMBEDDED=true --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
|
||||||
|
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
||||||
|
|
||||||
zammad-docker-release:
|
zammad-docker-release:
|
||||||
extends: .docker-release
|
extends: .docker-release
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad
|
||||||
|
|
||||||
|
zammad-standalone-docker-build:
|
||||||
|
extends: .docker-build
|
||||||
|
variables:
|
||||||
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad-standalone
|
||||||
|
DOCKERFILE_PATH: ./docker/zammad/Dockerfile
|
||||||
|
DOCKER_CONTEXT: ./docker/zammad
|
||||||
|
before_script:
|
||||||
|
- apk --update add nodejs npm
|
||||||
|
script:
|
||||||
|
- npm install npm@latest -g
|
||||||
|
- npm install -g turbo
|
||||||
|
- npm ci
|
||||||
|
- turbo build --force --filter zammad-addon-*
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
- DOCKER_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
|
||||||
|
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
||||||
|
|
||||||
|
zammad-standalone-docker-release:
|
||||||
|
extends: .docker-release
|
||||||
|
variables:
|
||||||
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad-standalone
|
||||||
|
|
|
||||||
35
README.md
35
README.md
|
|
@ -14,38 +14,23 @@ Local dev with docker-compose
|
||||||
|
|
||||||
Or for local dev of a single app
|
Or for local dev of a single app
|
||||||
|
|
||||||
* Create `link-stack/.env` from Bitwarden `.env for root of link-stack`
|
* Create `link-stack/apps/link/.env.local` from Bitwarden `.env.local for link-stack/apps/link`
|
||||||
|
* Create `link-stack/apps/metamigo-frontend/.metamigo.local.json` from Bitwarden `.metamigo.local.json for link-stack/apps/metamigo/frontend`
|
||||||
* Build locally for development:
|
* Build locally for development:
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
npm run docker:metamigo:dev:up # start supporting containers for metamigo
|
make dev-metamigo # this starts the containers
|
||||||
npm run build # compile the apps
|
npm run migrate # this migrates the db
|
||||||
npm run migrate # this migrates the db
|
npm run dev:metamigo # this runs metamigo frontend and api
|
||||||
npm run dev:metamigo # this runs metamigo frontend, api, and worker
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
Notes from abel regarding metamigo. these are in priority order (high priority first)
|
- [ ] Delete old JWT config stuff
|
||||||
|
- [ ] Consolidate config
|
||||||
- [ ] Do not upgrade: postgraphile, graphql and other postgres dependencies until postgrahile supports a newer grapqhl version
|
- [ ] Complete react-admin upgrade.. make all the metamigo-frontend stuff work
|
||||||
* ref: https://github.com/graphile/postgraphile/issues/1583
|
* https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components
|
||||||
- [ ] Fix the proxying from metamigo-frontend to metamigo-api, this broke during the next.js `pages/api` -> `app/api/*route.js` change.
|
|
||||||
* or consider removing the proxy and having the frontend talk directly to the backend, though this may be more work.
|
|
||||||
- [ ] Upgrade metamigo-frontend react-admin components
|
|
||||||
* this is the bulk of the real outstanding work, outside of breakages that happend during dep updates between Jun 14 - Aug (of which I'm only aware of the proxying issue, see previous)
|
|
||||||
* follow react-admin upgrade guide https://marmelab.com/react-admin/Upgrade.html
|
|
||||||
* in particular: https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components
|
|
||||||
* I started this in commit 49650795dff5249c89975d3c0b1cf12836304647
|
|
||||||
* so you can follow the same pattern in future commits to fix the signal, whatsapp and twilio pages
|
|
||||||
- [ ] Get metamigo-worker working
|
- [ ] Get metamigo-worker working
|
||||||
* the package.json entry points need to be fixed to be like metamigo-api
|
|
||||||
* the worker needs a main.ts file like metamigo-api that starts the worker (without the api) `await startWithout(["server"]);`
|
|
||||||
* while you're at it, I recomnmend moving all source files into a `src` to be consistent with the other metamigo projects
|
|
||||||
- [ ] Migrate off mui/styles
|
- [ ] Migrate off mui/styles
|
||||||
* https://mui.com/material-ui/migration/v5-style-changes/
|
* https://mui.com/material-ui/migration/v5-style-changes/
|
||||||
* the codemods might help us?
|
* the codemods might help us?
|
||||||
- [ ] Delete old JWT config options stuff in `packages/metamigo-config`
|
|
||||||
* the JWT is no longer used bdad5f551c536d751be87ecb8464d16c82e32699 and 24d52eef3d26ac5ee1294b949490920765fca96f
|
|
||||||
* so all of the config related to the JWT can be removed: signingkey (and b64 one), encryption key (and b64 one), audience
|
|
||||||
- [ ] Consolidate config.. this is basically done. The idea is to not need a config js file, everything can be populated from a root level .env file. This is already done, I have been developing like this for awhile, but I notice in your .env file on bitwarden you're still using the config file.
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { About } from './_components/About';
|
import { About } from "leafcutter-common";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <About />;
|
return <About />;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { getTemplates } from "app/_lib/opensearch";
|
import { getTemplates } from "app/_lib/opensearch";
|
||||||
import { Create } from "./_components/Create";
|
import { Create } from "leafcutter-common";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const templates = await getTemplates(100);
|
const templates = await getTemplates(100);
|
||||||
|
|
||||||
return <Create templates={templates} />;
|
return <Create templates={templates} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { FAQ } from "./_components/FAQ";
|
import { FAQ } from "leafcutter-common";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <FAQ />;
|
return <FAQ />;
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,12 @@ import "@fontsource/roboto/700.css";
|
||||||
import "@fontsource/playfair-display/900.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 "app/_components/InternalLayout";
|
import { InternalLayout } from "../_components/InternalLayout";
|
||||||
import { headers } from 'next/headers'
|
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
const allHeaders = headers();
|
return <InternalLayout embedded={false}>{children}</InternalLayout>;
|
||||||
const embedded = Boolean(allHeaders.get('x-leafcutter-embedded'));
|
|
||||||
|
|
||||||
return <InternalLayout embedded={embedded}>{children}</InternalLayout>;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "app/_lib/auth";
|
import { authOptions } from "app/_lib/auth";
|
||||||
import { getUserVisualizations } from "app/_lib/opensearch";
|
import { getUserVisualizations } from "app/_lib/opensearch";
|
||||||
import { Home } from "app/_components/Home";
|
import { Home } from "leafcutter-common";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
// import { Client } from "@opensearch-project/opensearch";
|
// import { Client } from "@opensearch-project/opensearch";
|
||||||
import { Preview } from "./_components/Preview";
|
import { Preview } from "leafcutter-common";
|
||||||
// import { createVisualization } from "lib/opensearch";
|
// import { createVisualization } from "lib/opensearch";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <Preview visualization={undefined} visualizationType={""} data={[]}/>;
|
return <Preview visualization={undefined} visualizationType={""} data={[]} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useLayoutEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Grid, CircularProgress } from "@mui/material";
|
import { Grid, CircularProgress } from "@mui/material";
|
||||||
import Iframe from "react-iframe";
|
import Iframe from "react-iframe";
|
||||||
import { useAppContext } from "app/_components/AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
|
|
||||||
export const Setup: FC = () => {
|
export const Setup: FC = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -20,6 +20,7 @@ export const Setup: FC = () => {
|
||||||
<Grid
|
<Grid
|
||||||
sx={{ width: "100%", height: 700 }}
|
sx={{ width: "100%", height: 700 }}
|
||||||
direction="row"
|
direction="row"
|
||||||
|
container
|
||||||
justifyContent="space-around"
|
justifyContent="space-around"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
alignContent="center"
|
alignContent="center"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Setup } from './_components/Setup';
|
import { Setup } from "./_components/Setup";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <Setup />;
|
return <Setup />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { getTrends } from "app/_lib/opensearch";
|
import { getTrends } from "app/_lib/opensearch";
|
||||||
import { Trends } from "./_components/Trends";
|
import { Trends } from "leafcutter-common";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const visualizations = await getTrends(25);
|
const visualizations = await getTrends(25);
|
||||||
|
|
||||||
return <Trends visualizations={visualizations} />;
|
return <Trends visualizations={visualizations} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
import { Client } from "@opensearch-project/opensearch";
|
import { Client } from "@opensearch-project/opensearch";
|
||||||
import { VisualizationDetail } from "app/_components/VisualizationDetail";
|
import { VisualizationDetail } from "leafcutter-common";
|
||||||
|
|
||||||
const getVisualization = async (visualizationID: string) => {
|
const getVisualization = async (visualizationID: string) => {
|
||||||
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||||
|
|
@ -18,7 +18,7 @@ const getVisualization = async (visualizationID: string) => {
|
||||||
const response = rawResponse.body;
|
const response = rawResponse.body;
|
||||||
|
|
||||||
const hits = response.hits.hits.filter(
|
const hits = response.hits.hits.filter(
|
||||||
(hit: any) => hit._id.split(":")[1] === visualizationID[0]
|
(hit: any) => hit._id.split(":")[1] === visualizationID[0],
|
||||||
);
|
);
|
||||||
const hit = hits[0];
|
const hit = hits[0];
|
||||||
const visualization = {
|
const visualization = {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
bindTrigger,
|
bindTrigger,
|
||||||
bindMenu,
|
bindMenu,
|
||||||
} from "material-ui-popup-state/hooks";
|
} from "material-ui-popup-state/hooks";
|
||||||
import { useAppContext } from "./AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
|
|
||||||
export const AccountButton: FC = () => {
|
export const AccountButton: FC = () => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { colors, typography } from "app/_styles/theme";
|
import { colors, typography } from "leafcutter-common/styles/theme";
|
||||||
|
|
||||||
const basePath = process.env.GITLAB_CI
|
const basePath = process.env.GITLAB_CI
|
||||||
? "/link/link-stack/apps/leafcutter"
|
? "/link/link-stack/apps/leafcutter"
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { FC, useState } from "react";
|
||||||
import { useRouter, usePathname } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material";
|
import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material";
|
||||||
import { useAppContext } from "./AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
|
|
||||||
export const HelpButton: FC = () => {
|
export const HelpButton: FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname() ?? "";
|
||||||
const [helpActive, setHelpActive] = useState(false);
|
const [helpActive, setHelpActive] = useState(false);
|
||||||
const {
|
const {
|
||||||
colors: { leafcutterElectricBlue },
|
colors: { leafcutterElectricBlue },
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import CookieConsent from "react-cookie-consent";
|
||||||
import { useCookies } from "react-cookie";
|
import { useCookies } from "react-cookie";
|
||||||
import { TopNav } from "./TopNav";
|
import { TopNav } from "./TopNav";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
import { GettingStartedDialog } from "./GettingStartedDialog";
|
import { GettingStartedDialog } from "leafcutter-common";
|
||||||
import { useAppContext } from "./AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
// import { Footer } from "./Footer";
|
// import { Footer } from "./Footer";
|
||||||
|
|
||||||
type LayoutProps = PropsWithChildren<{
|
type LayoutProps = PropsWithChildren<{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
bindTrigger,
|
bindTrigger,
|
||||||
bindMenu,
|
bindMenu,
|
||||||
} from "material-ui-popup-state/hooks";
|
} from "material-ui-popup-state/hooks";
|
||||||
import { useAppContext } from "./AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
// import { Tooltip } from "./Tooltip";
|
// import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
export const LanguageSelect = () => {
|
export const LanguageSelect = () => {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import { SessionProvider } from "next-auth/react";
|
||||||
import { CssBaseline } from "@mui/material";
|
import { CssBaseline } from "@mui/material";
|
||||||
import { CookiesProvider } from "react-cookie";
|
import { CookiesProvider } from "react-cookie";
|
||||||
import { I18n } from "react-polyglot";
|
import { I18n } from "react-polyglot";
|
||||||
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
|
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3";
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
||||||
import { AppProvider } from "app/_components/AppProvider";
|
import { AppProvider } from "leafcutter-common/components/AppProvider";
|
||||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||||
import en from "app/_locales/en.json";
|
import en from "leafcutter-common/locales/en.json";
|
||||||
import fr from "app/_locales/fr.json";
|
import fr from "leafcutter-common/locales/fr.json";
|
||||||
import "@fontsource/poppins/400.css";
|
import "@fontsource/poppins/400.css";
|
||||||
import "@fontsource/poppins/700.css";
|
import "@fontsource/poppins/700.css";
|
||||||
import "@fontsource/roboto/400.css";
|
import "@fontsource/roboto/400.css";
|
||||||
|
|
@ -21,7 +21,7 @@ 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(
|
||||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI="
|
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
||||||
);
|
);
|
||||||
|
|
||||||
const messages: any = { en, fr };
|
const messages: any = { en, fr };
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useTranslate } from "react-polyglot";
|
import { useTranslate } from "react-polyglot";
|
||||||
import { useAppContext } from "app/_components/AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
import { Tooltip } from "app/_components/Tooltip";
|
import { Tooltip } from "leafcutter-common";
|
||||||
// import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material";
|
// import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material";
|
||||||
|
|
||||||
const MenuItem = ({
|
const MenuItem = ({
|
||||||
|
|
@ -101,8 +101,8 @@ interface SidebarProps {
|
||||||
|
|
||||||
export const Sidebar: FC<SidebarProps> = ({ open }) => {
|
export const Sidebar: FC<SidebarProps> = ({ open }) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname() ?? "";
|
||||||
const section = pathname.split("/")[1];
|
const section = pathname?.split("/")[1];
|
||||||
const {
|
const {
|
||||||
colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue,
|
colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue,
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import Image from "next/legacy/image";
|
||||||
import { AppBar, Grid, Box } from "@mui/material";
|
import { AppBar, Grid, Box } from "@mui/material";
|
||||||
import { useTranslate } from "react-polyglot";
|
import { useTranslate } from "react-polyglot";
|
||||||
import LeafcutterLogo from "images/leafcutter-logo.png";
|
import LeafcutterLogo from "images/leafcutter-logo.png";
|
||||||
import { AccountButton } from "app/_components/AccountButton";
|
import { AccountButton } from "./AccountButton";
|
||||||
import { HelpButton } from "app/_components/HelpButton";
|
import { HelpButton } from "./HelpButton";
|
||||||
import { Tooltip } from "app/_components/Tooltip";
|
import { Tooltip } from "leafcutter-common";
|
||||||
import { useAppContext } from "./AppProvider";
|
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||||
// import { LanguageSelect } from "./LanguageSelect";
|
// import { LanguageSelect } from "./LanguageSelect";
|
||||||
|
|
||||||
export const TopNav: FC = () => {
|
export const TopNav: FC = () => {
|
||||||
|
|
@ -43,50 +43,51 @@ export const TopNav: FC = () => {
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
spacing={4}
|
spacing={4}
|
||||||
>
|
>
|
||||||
<Link href="/" passHref>
|
<Grid
|
||||||
<Grid
|
item
|
||||||
item
|
container
|
||||||
container
|
direction="row"
|
||||||
direction="row"
|
justifyContent="flex-start"
|
||||||
justifyContent="flex-start"
|
alignItems="center"
|
||||||
spacing={1}
|
alignContent="center"
|
||||||
wrap="nowrap"
|
spacing={1}
|
||||||
sx={{ cursor: "pointer" }}
|
wrap="nowrap"
|
||||||
>
|
sx={{ cursor: "pointer" }}
|
||||||
<Grid item sx={{ pr: 1 }}>
|
>
|
||||||
<Image src={LeafcutterLogo} alt="" width={56} height={52} />
|
<Grid item sx={{ pr: 1 }}>
|
||||||
|
<Image src={LeafcutterLogo} alt="" width={56} height={52} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item container direction="column" alignContent="flex-start">
|
||||||
|
<Grid item sx={{ mt: -1 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
...h5,
|
||||||
|
color: leafcutterElectricBlue,
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
pt: 1,
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Leafcutter
|
||||||
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item container direction="column" alignContent="flex-start">
|
<Grid item>
|
||||||
<Grid item>
|
<Box
|
||||||
<Box
|
sx={{
|
||||||
sx={{
|
...h6,
|
||||||
...h5,
|
m: 0,
|
||||||
color: leafcutterElectricBlue,
|
p: 0,
|
||||||
p: 0,
|
color: cdrLinkOrange,
|
||||||
m: 0,
|
textAlign: "left",
|
||||||
pt: 1,
|
}}
|
||||||
textAlign: "left",
|
>
|
||||||
}}
|
A Project of Center for Digital Resilience
|
||||||
>
|
</Box>
|
||||||
Leafcutter
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
...h6,
|
|
||||||
m: 0,
|
|
||||||
p: 0,
|
|
||||||
color: cdrLinkOrange,
|
|
||||||
textAlign: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
A Project of Center for Digital Resilience
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Link>
|
</Grid>
|
||||||
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<HelpButton />
|
<HelpButton />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import type { NextAuthOptions } from "next-auth";
|
import type { NextAuthOptions } from "next-auth";
|
||||||
import Google from "next-auth/providers/google";
|
import Google from "next-auth/providers/google";
|
||||||
import Apple from "next-auth/providers/apple";
|
import Apple from "next-auth/providers/apple";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import { checkAuth } from "./opensearch";
|
||||||
|
|
||||||
export const authOptions: NextAuthOptions = {
|
export const authOptions: NextAuthOptions = {
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
error: "/login",
|
||||||
|
signOut: "/logout",
|
||||||
|
},
|
||||||
providers: [
|
providers: [
|
||||||
Google({
|
Google({
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
|
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
|
||||||
|
|
@ -12,6 +19,62 @@ export const authOptions: NextAuthOptions = {
|
||||||
clientId: process.env.APPLE_CLIENT_ID ?? "",
|
clientId: process.env.APPLE_CLIENT_ID ?? "",
|
||||||
clientSecret: process.env.APPLE_CLIENT_SECRET ?? "",
|
clientSecret: process.env.APPLE_CLIENT_SECRET ?? "",
|
||||||
}),
|
}),
|
||||||
|
Credentials({
|
||||||
|
name: "Link",
|
||||||
|
credentials: {
|
||||||
|
authToken: { label: "AuthToken", type: "text", },
|
||||||
|
},
|
||||||
|
async authorize(credentials, req) {
|
||||||
|
const { headers } = req;
|
||||||
|
console.log({ headers });
|
||||||
|
const leafcutterUser = headers?.["x-leafcutter-user"];
|
||||||
|
const authToken = credentials?.authToken;
|
||||||
|
|
||||||
|
if (!leafcutterUser || leafcutterUser.trim() === "") {
|
||||||
|
console.log("no leafcutter user");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ authToken });
|
||||||
|
return null;
|
||||||
|
/*
|
||||||
|
try {
|
||||||
|
// add role check
|
||||||
|
await checkAuth(username, password);
|
||||||
|
const user = {
|
||||||
|
id: leafcutterUser,
|
||||||
|
email: leafcutterUser
|
||||||
|
};
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (e) {
|
||||||
|
console.log({ e });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
],
|
],
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
/*
|
||||||
|
callbacks: {
|
||||||
|
signIn: async ({ user, account, profile }) => {
|
||||||
|
const roles: any = [];
|
||||||
|
return roles.includes("admin") || roles.includes("agent");
|
||||||
|
},
|
||||||
|
session: async ({ session, user, token }) => {
|
||||||
|
// @ts-ignore
|
||||||
|
session.user.roles = token.roles;
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
jwt: async ({ token, user, account, profile, trigger }) => {
|
||||||
|
if (user) {
|
||||||
|
token.roles = [];
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
},*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ const globalIndex = ".kibana_1";
|
||||||
const dataIndexName = "sample_tagged_tickets";
|
const dataIndexName = "sample_tagged_tickets";
|
||||||
const userMetadataIndexName = "user_metadata";
|
const userMetadataIndexName = "user_metadata";
|
||||||
|
|
||||||
// const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||||
|
|
||||||
const baseURL = `https://localhost:9200`;
|
|
||||||
|
|
||||||
const createClient = () => new Client({
|
const createClient = () => new Client({
|
||||||
node: baseURL,
|
node: baseURL,
|
||||||
|
|
@ -23,6 +21,24 @@ const createClient = () => new Client({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createUserClient = (username: string, password: string) => new Client({
|
||||||
|
node: baseURL,
|
||||||
|
auth: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const checkAuth = async (username: string, password: string) => {
|
||||||
|
const client = createUserClient(username, password);
|
||||||
|
const res = await client.ping();
|
||||||
|
|
||||||
|
return res.statusCode === 200;
|
||||||
|
};
|
||||||
|
|
||||||
const getDocumentID = (doc: any) => doc._id.split(":")[1];
|
const getDocumentID = (doc: any) => doc._id.split(":")[1];
|
||||||
|
|
||||||
const getEmbedURL = (tenant: string, visualizationID: string) =>
|
const getEmbedURL = (tenant: string, visualizationID: string) =>
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,7 @@ body {
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
15
apps/leafcutter/app/api/link/auth/route.ts
Normal file
15
apps/leafcutter/app/api/link/auth/route.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const GET = async (req: NextRequest) => {
|
||||||
|
const validDomains = "localhost";
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async (req: NextRequest) => {
|
||||||
|
const validDomains = "localhost";
|
||||||
|
console.log({ req });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
};
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getToken } from "next-auth/jwt";
|
|
||||||
|
|
||||||
const withAuthInfo =
|
|
||||||
(handler: any) => async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const session: any = await getToken({
|
|
||||||
req,
|
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return res.redirect("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
req.headers["x-proxy-user"] = session.email.toLowerCase();
|
|
||||||
req.headers["x-proxy-roles"] = "leafcutter_user";
|
|
||||||
const auth = `${session.email.toLowerCase()}:${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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { getTrends } from "app/_lib/opensearch";
|
||||||
|
|
||||||
export const GET = async () => {
|
export const GET = async () => {
|
||||||
const results = await getTrends(5);
|
const results = await getTrends(5);
|
||||||
|
console.log({ results });
|
||||||
|
|
||||||
NextResponse.json(results);
|
NextResponse.json(results);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
13
apps/leafcutter/app/api/visualizations/list/route.ts
Normal file
13
apps/leafcutter/app/api/visualizations/list/route.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "app/_lib/auth";
|
||||||
|
import { getUserVisualizations } from "app/_lib/opensearch";
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const { user: { email } }: any = session;
|
||||||
|
const visualizations = await getUserVisualizations(email, 20);
|
||||||
|
|
||||||
|
return NextResponse.json(visualizations);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ import "@fontsource/roboto/700.css";
|
||||||
import "@fontsource/playfair-display/900.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 "app/_components/MultiProvider";
|
import { MultiProvider } from "./_components/MultiProvider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Leafcutter",
|
title: "Leafcutter",
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@ apiVersion: v2
|
||||||
name: leafcutter
|
name: leafcutter
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for Kubernetes
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.54
|
version: 0.2.0
|
||||||
appVersion: "0.1.54"
|
appVersion: "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,6 @@
|
||||||
import { NextResponse } from "next/server";
|
import { withAuth } from "next-auth/middleware";
|
||||||
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
|
|
||||||
import getConfig from "next/config";
|
|
||||||
|
|
||||||
const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => {
|
|
||||||
if (request.nextUrl.protocol.startsWith('ws')) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.nextUrl.pathname.includes('/_next/static/development/')) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
|
|
||||||
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
|
||||||
|
|
||||||
const requestHeaders = new Headers(request.headers);
|
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
|
||||||
// @ts-ignore
|
|
||||||
requestHeaders.set(key, value);
|
|
||||||
}
|
|
||||||
requestHeaders.delete('connection');
|
|
||||||
|
|
||||||
// console.log({ finalHeaders: requestHeaders });
|
|
||||||
|
|
||||||
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkRewrites = async (request: NextRequestWithAuth) => {
|
|
||||||
console.log({ currentURL: request.nextUrl.href });
|
|
||||||
|
|
||||||
const leafcutterBaseURL = process.env.LEAFCUTTER_URL ?? "http://localhost:3000";
|
|
||||||
const opensearchDashboardsURL = process.env.OPENSEARCH_URL ?? "http://localhost:5602";
|
|
||||||
|
|
||||||
if (request.nextUrl.pathname.startsWith('/proxy/opensearch')) {
|
|
||||||
console.log('proxying to zammad');
|
|
||||||
const { token } = request.nextauth;
|
|
||||||
const auth = `${token?.email?.toLowerCase()}:${process.env.OPENSEARCH_USER_PASSWORD}`;
|
|
||||||
const buff = Buffer.from(auth);
|
|
||||||
const base64data = buff.toString("base64");
|
|
||||||
const headers = {
|
|
||||||
'X-Proxy-User': token?.email?.toLowerCase(),
|
|
||||||
"X-Proxy-Roles": "leafcutter_user",
|
|
||||||
"Authorization": `Basic ${base64data}`
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log({ headers });
|
|
||||||
|
|
||||||
return rewriteURL(request, `${leafcutterBaseURL}/proxy/opensearch`, opensearchDashboardsURL, headers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withAuth(
|
export default withAuth(
|
||||||
checkRewrites,
|
|
||||||
{
|
{
|
||||||
pages: {
|
pages: {
|
||||||
signIn: `/login`,
|
signIn: `/login`,
|
||||||
|
|
@ -60,25 +9,30 @@ export default withAuth(
|
||||||
authorized: ({ token, req }) => {
|
authorized: ({ token, req }) => {
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
headers,
|
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
// check login page
|
|
||||||
const parsedURL = new URL(url);
|
const parsedURL = new URL(url);
|
||||||
if (parsedURL.pathname.startsWith('/login')) {
|
|
||||||
|
console.log({ url });
|
||||||
|
console.log({ pathname: parsedURL.pathname });
|
||||||
|
console.log({ allowed: parsedURL.pathname.startsWith("/app") });
|
||||||
|
const allowed = parsedURL.pathname.startsWith('/login') || parsedURL.pathname.startsWith('/api' || parsedURL.pathname.startsWith("/app"));
|
||||||
|
if (allowed) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check session auth
|
if (token?.email) {
|
||||||
const authorizedDomains = ["redaranj.com", "digiresilience.org"];
|
|
||||||
const userDomain = token?.email?.toLowerCase().split("@").pop() ?? "unauthorized.net";
|
|
||||||
|
|
||||||
if (authorizedDomains.includes(userDomain)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/((?!api|app|bootstrap|3961|ui|translations|internal|login|node_modules|_next/static|_next/image|favicon.ico).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
||||||
1
apps/leafcutter/next-env.d.ts
vendored
1
apps/leafcutter/next-env.d.ts
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,19 @@ const ContentSecurityPolicy = `
|
||||||
`;
|
`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
publicRuntimeConfig: {
|
transpilePackages: ["leafcutter-common"],
|
||||||
embedded: true
|
experimental: {
|
||||||
},/*
|
missingSuspenseWithCSRBailout: false,
|
||||||
|
},
|
||||||
|
rewrites: async () => ({
|
||||||
|
fallback: [
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
destination: "/api/proxy/:path*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
/*
|
||||||
basePath: "/proxy/leafcutter",
|
basePath: "/proxy/leafcutter",
|
||||||
assetPrefix: "/proxy/leafcutter",
|
assetPrefix: "/proxy/leafcutter",
|
||||||
i18n: {
|
i18n: {
|
||||||
|
|
@ -17,25 +27,19 @@ module.exports = {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
/* rewrites: async () => ({
|
|
||||||
fallback: [
|
/*
|
||||||
{
|
|
||||||
source: "/:path*",
|
|
||||||
destination: "/api/proxy/:path*",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}) */
|
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: '/:path*',
|
source: '/:path*',
|
||||||
headers: [
|
headers: [
|
||||||
/*
|
|
||||||
{
|
{
|
||||||
key: 'Content-Security-Policy',
|
key: 'Content-Security-Policy',
|
||||||
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
|
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
{
|
{
|
||||||
key: 'Strict-Transport-Security',
|
key: 'Strict-Transport-Security',
|
||||||
value: 'max-age=63072000; includeSubDomains; preload'
|
value: 'max-age=63072000; includeSubDomains; preload'
|
||||||
|
|
@ -52,4 +56,5 @@ module.exports = {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"login": "aws sso login --sso-session cdr",
|
"login": "aws sso login --sso-session cdr",
|
||||||
"kubeconfig": "aws eks update-kubeconfig --name cdr-leafcutter-dashboard-cluster --profile cdr-leafcutter-dashboard-production",
|
"kubeconfig": "aws eks update-kubeconfig --name cdr-leafcutter-dashboard-cluster --profile cdr-leafcutter-dashboard-production",
|
||||||
"fwd:opensearch": "kubectl port-forward opensearch-cluster-master-0 9200:9200 --namespace leafcutter",
|
"fwd:opensearch": "kubectl port-forward opensearch-cluster-master-0 9200:9200 --namespace leafcutter",
|
||||||
"fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-vgmtf 5602:5601 --namespace leafcutter",
|
"fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-mx4qq 5602:5601 --namespace leafcutter",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"export": "next export",
|
"export": "next export",
|
||||||
|
|
@ -14,51 +14,53 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
"@emotion/react": "^11.11.1",
|
"@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.5",
|
"@fontsource/playfair-display": "^5.0.21",
|
||||||
"@fontsource/poppins": "^5.0.5",
|
"@fontsource/poppins": "^5.0.12",
|
||||||
"@fontsource/roboto": "^5.0.5",
|
"@fontsource/roboto": "^5.0.12",
|
||||||
"@mui/icons-material": "^5",
|
"@mui/icons-material": "^5",
|
||||||
"@mui/lab": "^5.0.0-alpha.136",
|
"@mui/lab": "^5.0.0-alpha.167",
|
||||||
"@mui/material": "^5",
|
"@mui/material": "^5",
|
||||||
"@mui/x-data-grid-pro": "^6.10.0",
|
"@mui/x-data-grid-pro": "^6.19.6",
|
||||||
"@mui/x-date-pickers-pro": "^6.10.0",
|
"@mui/x-date-pickers-pro": "^6.19.6",
|
||||||
"@opensearch-project/opensearch": "^2.3.0",
|
"@opensearch-project/opensearch": "^2.5.0",
|
||||||
"date-fns": "^2.30.0",
|
"cryptr": "^6.3.0",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"material-ui-popup-state": "^5.0.9",
|
"leafcutter-common": "*",
|
||||||
"next": "13.4.10",
|
"material-ui-popup-state": "^5.0.10",
|
||||||
"next-auth": "^4.22.1",
|
"next": "14.1.2",
|
||||||
"next-http-proxy-middleware": "^1.2.5",
|
"next-auth": "^4.24.6",
|
||||||
"nodemailer": "^6.9.3",
|
"next-http-proxy-middleware": "^1.2.6",
|
||||||
|
"nodemailer": "^6.9.11",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^7.1.0",
|
||||||
"react-cookie-consent": "^8.0.1",
|
"react-cookie-consent": "^9.0.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-iframe": "^1.8.5",
|
"react-iframe": "^1.8.5",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^9.0.1",
|
||||||
"react-polyglot": "^0.7.2",
|
"react-polyglot": "^0.7.2",
|
||||||
"sharp": "^0.32.3",
|
"sharp": "^0.33.2",
|
||||||
"swr": "^2.2.0",
|
"swr": "^2.2.5",
|
||||||
"tss-react": "^4.8.8",
|
"tss-react": "^4.9.4",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.9",
|
"@babel/core": "^7.24.0",
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.11.24",
|
||||||
"@types/react": "18.2.15",
|
"@types/react": "18.2.63",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.8",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.1.3",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-next": "^13.4.10",
|
"eslint-config-next": "^14.1.2",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
apps/leafcutter/pages/api/proxy/[[...path]].ts
Normal file
66
apps/leafcutter/pages/api/proxy/[[...path]].ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
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,9 +1,24 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
import { Box, Grid, Container, IconButton } from "@mui/material";
|
import {
|
||||||
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
|
Box,
|
||||||
|
Grid,
|
||||||
|
Container,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
Apple as AppleIcon,
|
||||||
|
Google as GoogleIcon,
|
||||||
|
Key as KeyIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import LinkLogo from "public/link-logo-small.png";
|
||||||
|
import { colors } from "app/_styles/theme";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
type LoginProps = {
|
type LoginProps = {
|
||||||
session: any;
|
session: any;
|
||||||
|
|
@ -14,62 +29,198 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
typeof window !== "undefined" && window.location.origin
|
typeof window !== "undefined" && window.location.origin
|
||||||
? window.location.origin
|
? window.location.origin
|
||||||
: "";
|
: "";
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const params = useSearchParams();
|
||||||
|
const error = params.get("error");
|
||||||
|
const { darkGray, cdrLinkOrange, white } = colors;
|
||||||
const buttonStyles = {
|
const buttonStyles = {
|
||||||
borderRadius: 500,
|
borderRadius: 500,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
|
backgroundColor: white,
|
||||||
|
"&:hover": {
|
||||||
|
color: white,
|
||||||
|
backgroundColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fieldStyles = {
|
||||||
|
"& label.Mui-focused": {
|
||||||
|
color: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
"& .MuiInput-underline:after": {
|
||||||
|
borderBottomColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
"& .MuiFilledInput-underline:after": {
|
||||||
|
borderBottomColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box sx={{ backgroundColor: darkGray, height: "100vh" }}>
|
||||||
<Grid container direction="row-reverse" sx={{ p: 3 }}>
|
<Container maxWidth="md" sx={{ p: 10 }}>
|
||||||
<Grid item />
|
|
||||||
</Grid>
|
|
||||||
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
|
|
||||||
<Grid container spacing={2} direction="column" alignItems="center">
|
<Grid container spacing={2} direction="column" alignItems="center">
|
||||||
<Grid item>
|
<Grid
|
||||||
<Box sx={{ maxWidth: 200 }} />
|
item
|
||||||
</Grid>
|
container
|
||||||
<Grid item sx={{ textAlign: "center" }} />
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
<Grid item>
|
alignItems="center"
|
||||||
{!session ? (
|
>
|
||||||
<Grid
|
<Grid item>
|
||||||
container
|
<Box
|
||||||
spacing={3}
|
sx={{
|
||||||
direction="column"
|
width: "70px",
|
||||||
alignItems="center"
|
height: "70px",
|
||||||
sx={{ width: 450, mt: 1 }}
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Grid item sx={{ width: "100%" }}>
|
<Image
|
||||||
<IconButton
|
src={LinkLogo}
|
||||||
sx={buttonStyles}
|
alt="Link logo"
|
||||||
onClick={() =>
|
width={70}
|
||||||
signIn("google", {
|
height={70}
|
||||||
callbackUrl: `${origin}/setup`,
|
style={{
|
||||||
})
|
objectFit: "cover",
|
||||||
}
|
filter: "grayscale(100) brightness(100)",
|
||||||
>
|
}}
|
||||||
<GoogleIcon sx={{ mr: 1 }} />
|
/>
|
||||||
Google
|
</Box>
|
||||||
</IconButton>
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontSize: 36,
|
||||||
|
color: "white",
|
||||||
|
fontWeight: 700,
|
||||||
|
mt: 1,
|
||||||
|
ml: 0.5,
|
||||||
|
fontFamily: "Poppins",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
CDR Link
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
{!session ? (
|
||||||
|
<Container
|
||||||
|
maxWidth="xs"
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
mt: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<Box sx={{ backgroundColor: "red", p: 3 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
fontSize: 18,
|
||||||
|
color: "white",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${error} error`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
) : null}
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<IconButton
|
||||||
|
sx={buttonStyles}
|
||||||
|
onClick={() =>
|
||||||
|
signIn("google", {
|
||||||
|
callbackUrl: `${origin}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<GoogleIcon sx={{ mr: 1 }} />
|
||||||
|
Sign in with Google
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Sign in with Apple"
|
||||||
|
sx={buttonStyles}
|
||||||
|
onClick={() =>
|
||||||
|
signIn("apple", {
|
||||||
|
callbackUrl: `${window.location.origin}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AppleIcon sx={{ mr: 1 }} />
|
||||||
|
Sign in with Apple
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
fontSize: 18,
|
||||||
|
color: white,
|
||||||
|
textAlign: "center",
|
||||||
|
mt: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⸺ or ⸺
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<TextField
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
label="Email"
|
||||||
|
variant="filled"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
sx={{ ...fieldStyles, backgroundColor: white }}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sx={{ ...fieldStyles, width: "100%" }}>
|
||||||
|
<TextField
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
label="Password"
|
||||||
|
variant="filled"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
sx={{ backgroundColor: white }}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<IconButton
|
||||||
|
sx={buttonStyles}
|
||||||
|
onClick={() =>
|
||||||
|
signIn("credentials", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
callbackUrl: `${origin}/setup`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<KeyIcon sx={{ mr: 1 }} />
|
||||||
|
Sign in with Zammad credentials
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/*
|
</Container>
|
||||||
<Grid item sx={{ width: "100%" }}>
|
|
||||||
<IconButton
|
|
||||||
sx={buttonStyles}
|
|
||||||
onClick={() =>
|
|
||||||
signIn("apple", {
|
|
||||||
callbackUrl: `${window.location.origin}/setup`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AppleIcon sx={{ mr: 1 }} />
|
|
||||||
</IconButton>
|
|
||||||
</Grid>*/}
|
|
||||||
<Grid item sx={{ mt: 2 }} />
|
|
||||||
</Grid>
|
|
||||||
) : null}
|
) : null}
|
||||||
{session ? (
|
{session ? (
|
||||||
<Box component="h4">
|
<Box component="h4">
|
||||||
|
|
@ -79,6 +230,6 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
14
apps/link/app/(main)/_components/ClientOnly.tsx
Normal file
14
apps/link/app/(main)/_components/ClientOnly.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
type ClientOnlyProps = { children: JSX.Element };
|
||||||
|
const ClientOnly = (props: ClientOnlyProps) => {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dynamic(() => Promise.resolve(ClientOnly), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { ZammadWrapper } from "./ZammadWrapper";
|
import { ZammadWrapper } from "./ZammadWrapper";
|
||||||
|
|
||||||
export const Home: FC = () => <ZammadWrapper path="/#dashboard" hideSidebar />;
|
export const Home: FC = () => <ZammadWrapper path="#dashboard" hideSidebar />;
|
||||||
|
|
|
||||||
150
apps/link/app/(main)/_components/SearchBox.tsx
Normal file
150
apps/link/app/(main)/_components/SearchBox.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { FC, useState, useEffect } from "react";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { Grid, Box, TextField, Autocomplete } from "@mui/material";
|
||||||
|
import { searchQuery } from "@/app/_graphql/searchQuery";
|
||||||
|
import { colors } from "@/app/_styles/theme";
|
||||||
|
|
||||||
|
type SearchResultProps = {
|
||||||
|
props: any;
|
||||||
|
option: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchInput = (params: any) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder="Search"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 10,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 10,
|
||||||
|
py: 0,
|
||||||
|
legend: {
|
||||||
|
marginLeft: "30px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiAutocomplete-inputRoot": {
|
||||||
|
paddingLeft: "20px !important",
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
"& .MuiInputLabel-outlined": {
|
||||||
|
paddingLeft: "20px",
|
||||||
|
},
|
||||||
|
"& .MuiInputLabel-shrink": {
|
||||||
|
marginLeft: "20px",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
paddingRight: 0,
|
||||||
|
background: "white",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
|
||||||
|
console.log({ option });
|
||||||
|
|
||||||
|
const { lightGrey, mediumGray, black, white } = colors;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1.25,
|
||||||
|
":hover": {
|
||||||
|
background: `${lightGrey}`,
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
color: `${black} !important`,
|
||||||
|
},
|
||||||
|
borderBottom: `1px solid ${mediumGray}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container direction="column" spacing={0.1}>
|
||||||
|
<Grid item container direction="row" justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 0,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.title}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={{ width: "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
color: "#999",
|
||||||
|
fontSize: 13,
|
||||||
|
wrap: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.note}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchBox: FC = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedValue, setSelectedValue] = useState(null);
|
||||||
|
const [searchTerms, setSearchTerms] = useState("");
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const { data, error }: any = useSWR({
|
||||||
|
document: searchQuery,
|
||||||
|
variables: {
|
||||||
|
search: searchTerms ?? "",
|
||||||
|
limit: 50,
|
||||||
|
},
|
||||||
|
refreshInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
forcePopupIcon={false}
|
||||||
|
openOnFocus
|
||||||
|
blurOnSelect
|
||||||
|
value={selectedValue}
|
||||||
|
onBlur={() => setOpen(false)}
|
||||||
|
inputValue={searchTerms}
|
||||||
|
onChange={(_event, option, reason) => {
|
||||||
|
if (!option) return;
|
||||||
|
const url = `/tickets/${option.internalId}`;
|
||||||
|
setSelectedValue("");
|
||||||
|
router.push(url);
|
||||||
|
}}
|
||||||
|
onInputChange={(_event, value) => {
|
||||||
|
setSearchTerms(value);
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
onOpen={() => setOpen(true)}
|
||||||
|
noOptionsText="No results"
|
||||||
|
options={data?.search ?? []}
|
||||||
|
getOptionLabel={(option: any) => {
|
||||||
|
if (option) {
|
||||||
|
return option.title;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderOption={(props, option: any) => (
|
||||||
|
<SearchResult props={props} key={option.id} option={option} />
|
||||||
|
)}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
renderInput={(params: any) => <SearchInput {...params} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -17,12 +17,16 @@ import {
|
||||||
import {
|
import {
|
||||||
FeaturedPlayList as FeaturedPlayListIcon,
|
FeaturedPlayList as FeaturedPlayListIcon,
|
||||||
Person as PersonIcon,
|
Person as PersonIcon,
|
||||||
Analytics as AnalyticsIcon,
|
Insights as InsightsIcon,
|
||||||
Logout as LogoutIcon,
|
Logout as LogoutIcon,
|
||||||
Cottage as CottageIcon,
|
Cottage as CottageIcon,
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
ExpandCircleDown as ExpandCircleDownIcon,
|
ExpandCircleDown as ExpandCircleDownIcon,
|
||||||
Dvr as DvrIcon,
|
Dvr as DvrIcon,
|
||||||
|
Assessment as AssessmentIcon,
|
||||||
|
LibraryBooks as LibraryBooksIcon,
|
||||||
|
School as SchoolIcon,
|
||||||
|
Search as SearchIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -30,6 +34,7 @@ import Image from "next/image";
|
||||||
import LinkLogo from "public/link-logo-small.png";
|
import LinkLogo from "public/link-logo-small.png";
|
||||||
import { useSession, signOut } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
||||||
|
import { SearchBox } from "./SearchBox";
|
||||||
|
|
||||||
const openWidth = 270;
|
const openWidth = 270;
|
||||||
const closedWidth = 100;
|
const closedWidth = 100;
|
||||||
|
|
@ -162,20 +167,31 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const username = session?.user?.name || "User";
|
const username = session?.user?.name || "User";
|
||||||
|
// @ts-ignore
|
||||||
|
const roles = session?.user?.roles || [];
|
||||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
const { data: overviewData, error: overviewError }: any = useSWR(
|
||||||
{
|
{
|
||||||
document: getTicketOverviewCountsQuery,
|
document: getTicketOverviewCountsQuery,
|
||||||
},
|
},
|
||||||
{ refreshInterval: 10000 }
|
{ refreshInterval: 10000 },
|
||||||
);
|
);
|
||||||
const findOverviewCountByID = (id: number) =>
|
const findOverviewByName = (name: string) =>
|
||||||
overviewData?.ticketOverviews?.edges?.find((overview: any) =>
|
overviewData?.ticketOverviews?.edges?.find(
|
||||||
overview.node.id.endsWith(`/${id}`)
|
(overview: any) => overview.node.name === name,
|
||||||
|
)?.node?.id;
|
||||||
|
const findOverviewCountByID = (id: string) =>
|
||||||
|
overviewData?.ticketOverviews?.edges?.find(
|
||||||
|
(overview: any) => overview.node.id === id,
|
||||||
)?.node?.ticketCount ?? 0;
|
)?.node?.ticketCount ?? 0;
|
||||||
const assignedCount = findOverviewCountByID(1);
|
const recentCount = 0;
|
||||||
const urgentCount = findOverviewCountByID(7);
|
const assignedID = findOverviewByName("My Assigned Tickets");
|
||||||
const pendingCount = findOverviewCountByID(3);
|
const assignedCount = findOverviewCountByID(assignedID);
|
||||||
const unassignedCount = findOverviewCountByID(2);
|
const openID = findOverviewByName("Open Tickets");
|
||||||
|
const openCount = findOverviewCountByID(openID);
|
||||||
|
const urgentID = findOverviewByName("Escalated Tickets");
|
||||||
|
const urgentCount = findOverviewCountByID(urgentID);
|
||||||
|
const unassignedID = findOverviewByName("Unassigned & Open Tickets");
|
||||||
|
const unassignedCount = findOverviewCountByID(unassignedID);
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
signOut({ callbackUrl: "/login" });
|
signOut({ callbackUrl: "/login" });
|
||||||
|
|
@ -221,6 +237,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
direction="column"
|
direction="column"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
|
spacing={0}
|
||||||
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
|
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
|
||||||
>
|
>
|
||||||
<Grid item container>
|
<Grid item container>
|
||||||
|
|
@ -307,10 +324,30 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Box
|
<Box
|
||||||
sx={{ height: "0.5px", width: "100%", backgroundColor: "#666" }}
|
sx={{
|
||||||
|
height: "0.5px",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#666",
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item container direction="column" sx={{ mt: "6px" }} flexGrow={1}>
|
<Grid item>
|
||||||
|
<SearchBox />
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
sx={{
|
||||||
|
mt: "6px",
|
||||||
|
overflow: "scroll",
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
msOverflowStyle: "none",
|
||||||
|
"&::-webkit-scrollbar": { display: "none" },
|
||||||
|
}}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
<List
|
<List
|
||||||
component="nav"
|
component="nav"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -359,7 +396,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Tickets"
|
name="Tickets"
|
||||||
href="/overview/assigned"
|
href="/overview/recent"
|
||||||
Icon={FeaturedPlayListIcon}
|
Icon={FeaturedPlayListIcon}
|
||||||
selected={
|
selected={
|
||||||
pathname.startsWith("/overview") ||
|
pathname.startsWith("/overview") ||
|
||||||
|
|
@ -379,12 +416,21 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
>
|
>
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Assigned"
|
name="Recent"
|
||||||
href="/overview/assigned"
|
href="/overview/recent"
|
||||||
Icon={FeaturedPlayListIcon}
|
Icon={FeaturedPlayListIcon}
|
||||||
iconSize={0}
|
iconSize={0}
|
||||||
selected={pathname.endsWith("/overview/assigned")}
|
selected={pathname.endsWith("/overview/recent")}
|
||||||
badge={assignedCount}
|
badge={recentCount}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Open"
|
||||||
|
href="/overview/open"
|
||||||
|
Icon={FeaturedPlayListIcon}
|
||||||
|
iconSize={0}
|
||||||
|
selected={pathname.endsWith("/overview/open")}
|
||||||
|
badge={openCount}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
@ -397,12 +443,12 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Pending"
|
name="Assigned"
|
||||||
href="/overview/pending"
|
href="/overview/assigned"
|
||||||
Icon={FeaturedPlayListIcon}
|
Icon={FeaturedPlayListIcon}
|
||||||
iconSize={0}
|
iconSize={0}
|
||||||
selected={pathname.endsWith("/overview/pending")}
|
selected={pathname.endsWith("/overview/assigned")}
|
||||||
badge={pendingCount}
|
badge={assignedCount}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
@ -419,17 +465,33 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Knowledge Base"
|
name="Knowledge Base"
|
||||||
href="/knowledge"
|
href="/knowledge"
|
||||||
Icon={CottageIcon}
|
Icon={SchoolIcon}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
selected={pathname.endsWith("/knowledge")}
|
selected={pathname.endsWith("/knowledge")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Leafcutter"
|
name="Documentation"
|
||||||
href="/leafcutter/about"
|
href="/docs"
|
||||||
Icon={AnalyticsIcon}
|
Icon={LibraryBooksIcon}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
selected={pathname.endsWith("/leafcutter")}
|
selected={pathname.endsWith("/docs")}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Reporting"
|
||||||
|
href="/reporting"
|
||||||
|
Icon={AssessmentIcon}
|
||||||
|
iconSize={20}
|
||||||
|
selected={pathname.endsWith("/reporting")}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Leafcutter"
|
||||||
|
href="/leafcutter"
|
||||||
|
Icon={InsightsIcon}
|
||||||
|
iconSize={20}
|
||||||
|
selected={false}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<Collapse
|
<Collapse
|
||||||
|
|
@ -439,7 +501,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
onClick={undefined}
|
onClick={undefined}
|
||||||
>
|
>
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
{/*
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Dashboard"
|
name="Dashboard"
|
||||||
href="/leafcutter"
|
href="/leafcutter"
|
||||||
|
|
@ -447,7 +508,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={pathname.endsWith("/leafcutter")}
|
selected={pathname.endsWith("/leafcutter")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Search and Create"
|
name="Search and Create"
|
||||||
href="/leafcutter/create"
|
href="/leafcutter/create"
|
||||||
|
|
@ -455,7 +515,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={pathname.endsWith("/leafcutter/create")}
|
selected={pathname.endsWith("/leafcutter/create")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Trends"
|
name="Trends"
|
||||||
href="/leafcutter/trends"
|
href="/leafcutter/trends"
|
||||||
|
|
@ -463,7 +522,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={pathname.endsWith("/leafcutter/trends")}
|
selected={pathname.endsWith("/leafcutter/trends")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
*/}
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="FAQ"
|
name="FAQ"
|
||||||
href="/leafcutter/faq"
|
href="/leafcutter/faq"
|
||||||
|
|
@ -475,7 +533,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="About"
|
name="About"
|
||||||
href="/leafcutter/about"
|
href="/leafcutter/about"
|
||||||
Icon={AnalyticsIcon}
|
Icon={InsightsIcon}
|
||||||
iconSize={0}
|
iconSize={0}
|
||||||
selected={pathname.endsWith("/leafcutter/about")}
|
selected={pathname.endsWith("/leafcutter/about")}
|
||||||
open={open}
|
open={open}
|
||||||
|
|
@ -490,49 +548,57 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={pathname.endsWith("/profile")}
|
selected={pathname.endsWith("/profile")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
{roles.includes("admin") && (
|
||||||
name="Admin"
|
<>
|
||||||
href="/admin/zammad"
|
|
||||||
Icon={SettingsIcon}
|
|
||||||
iconSize={20}
|
|
||||||
open={open}
|
|
||||||
/>
|
|
||||||
<Collapse
|
|
||||||
in={pathname.startsWith("/admin/")}
|
|
||||||
timeout="auto"
|
|
||||||
unmountOnExit
|
|
||||||
onClick={undefined}
|
|
||||||
>
|
|
||||||
<List component="div" disablePadding>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Zammad Settings"
|
name="Admin"
|
||||||
href="/admin/zammad"
|
href="/admin/zammad"
|
||||||
Icon={FeaturedPlayListIcon}
|
Icon={SettingsIcon}
|
||||||
iconSize={0}
|
iconSize={20}
|
||||||
selected={pathname.endsWith("/admin/zammad")}
|
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<Collapse
|
||||||
name="Metamigo"
|
in={pathname.startsWith("/admin/")}
|
||||||
href="/admin/metamigo"
|
timeout="auto"
|
||||||
Icon={FeaturedPlayListIcon}
|
unmountOnExit
|
||||||
iconSize={0}
|
onClick={undefined}
|
||||||
selected={pathname.endsWith("/admin/metamigo")}
|
>
|
||||||
open={open}
|
<List component="div" disablePadding>
|
||||||
/>
|
<MenuItem
|
||||||
<MenuItem
|
name="Zammad Settings"
|
||||||
name="Label Studio"
|
href="/admin/zammad"
|
||||||
href="/admin/label-studio"
|
Icon={FeaturedPlayListIcon}
|
||||||
Icon={FeaturedPlayListIcon}
|
iconSize={0}
|
||||||
iconSize={0}
|
selected={pathname.endsWith("/admin/zammad")}
|
||||||
selected={pathname.endsWith("/admin/label-studio")}
|
open={open}
|
||||||
open={open}
|
/>
|
||||||
/>
|
{false && roles.includes("metamigo") && (
|
||||||
</List>
|
<MenuItem
|
||||||
</Collapse>
|
name="Metamigo"
|
||||||
|
href="/admin/metamigo"
|
||||||
|
Icon={FeaturedPlayListIcon}
|
||||||
|
iconSize={0}
|
||||||
|
selected={pathname.endsWith("/admin/metamigo")}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{roles.includes("label_studio") && (
|
||||||
|
<MenuItem
|
||||||
|
name="Label Studio"
|
||||||
|
href="/admin/label-studio"
|
||||||
|
Icon={FeaturedPlayListIcon}
|
||||||
|
iconSize={0}
|
||||||
|
selected={pathname.endsWith("/admin/label-studio")}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Zammad Interface"
|
name="Zammad Interface"
|
||||||
href="/proxy/zammad"
|
href="/zammad"
|
||||||
Icon={DvrIcon}
|
Icon={DvrIcon}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
open={open}
|
open={open}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
||||||
columns,
|
columns,
|
||||||
rows,
|
rows,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
height = "calc(100vh - 20px)",
|
height = "100%",
|
||||||
selectedRows,
|
selectedRows,
|
||||||
setSelectedRows,
|
setSelectedRows,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -43,6 +43,13 @@ export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
||||||
border: 0,
|
border: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height,
|
height,
|
||||||
|
".MuiDataGrid-row": {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#1982fc33 !important",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
".MuiDataGrid-row:nth-of-type(1n)": {
|
".MuiDataGrid-row:nth-of-type(1n)": {
|
||||||
backgroundColor: "#f3f3f3",
|
backgroundColor: "#f3f3f3",
|
||||||
},
|
},
|
||||||
|
|
@ -66,12 +73,14 @@ export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
density="compact"
|
density="compact"
|
||||||
hideFooter
|
pagination
|
||||||
|
initialState={{
|
||||||
|
pagination: { paginationModel: { pageSize: 25 } },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
paginationMode="client"
|
||||||
sx={{ height }}
|
sx={{ height }}
|
||||||
rowBuffer={30}
|
rowBuffer={30}
|
||||||
checkboxSelection={!!setSelectedRows}
|
|
||||||
onRowSelectionModelChange={setSelectedRows}
|
|
||||||
rowSelectionModel={selectedRows}
|
|
||||||
rowHeight={46}
|
rowHeight={46}
|
||||||
scrollbarSize={0}
|
scrollbarSize={0}
|
||||||
disableVirtualization
|
disableVirtualization
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState, useEffect, useRef } from "react";
|
||||||
import getConfig from "next/config";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Iframe from "react-iframe";
|
import Iframe from "react-iframe";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { Box, Grid, CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
type ZammadWrapperProps = {
|
type ZammadWrapperProps = {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -15,68 +16,146 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||||
hideSidebar = true,
|
hideSidebar = true,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession({ required: true });
|
||||||
|
const timeoutRef = useRef(null);
|
||||||
|
const [hashCheckComplete, setHashCheckComplete] = useState(false);
|
||||||
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
const [display, setDisplay] = useState("none");
|
const [display, setDisplay] = useState("none");
|
||||||
const url = `/proxy/zammad${path}`;
|
const url = `/zammad${path}`;
|
||||||
console.log({ url });
|
const id = url.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
// @ts-ignore
|
const hash = window?.location?.hash;
|
||||||
<Iframe
|
if (hash && hash.startsWith("#ticket/zoom/")) {
|
||||||
id="zammad"
|
const ticketID = hash.split("/").pop();
|
||||||
url={url}
|
router.push(`/tickets/${ticketID}`);
|
||||||
width="100%"
|
}
|
||||||
height="100%"
|
setHashCheckComplete(true);
|
||||||
frameBorder={0}
|
});
|
||||||
styles={{ display }}
|
|
||||||
onLoad={() => {
|
|
||||||
const linkElement = document.querySelector("iframe");
|
|
||||||
if (
|
|
||||||
linkElement.contentDocument &&
|
|
||||||
linkElement.contentDocument?.querySelector &&
|
|
||||||
linkElement.contentDocument.querySelector("#navigation") &&
|
|
||||||
linkElement.contentDocument.querySelector("body") &&
|
|
||||||
linkElement.contentDocument.querySelector(".sidebar")
|
|
||||||
) {
|
|
||||||
// @ts-ignore
|
|
||||||
linkElement.contentDocument.querySelector("#navigation").style =
|
|
||||||
"display: none";
|
|
||||||
// @ts-ignore
|
|
||||||
linkElement.contentDocument.querySelector("body").style =
|
|
||||||
"font-family: Arial";
|
|
||||||
|
|
||||||
if (hideSidebar) {
|
useEffect(() => {
|
||||||
// @ts-ignore
|
if (!hashCheckComplete) return;
|
||||||
linkElement.contentDocument.querySelector(".sidebar").style =
|
|
||||||
"display: none";
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
const checkAuthenticated = async () => {
|
||||||
if (linkElement.contentDocument.querySelector(".overview-header")) {
|
const res = await fetch("/zammad/auth/sso", {
|
||||||
// @ts-ignore
|
method: "GET",
|
||||||
(
|
redirect: "manual",
|
||||||
linkElement.contentDocument.querySelector(
|
});
|
||||||
".overview-header"
|
console.log({ res });
|
||||||
) as any
|
if (res.type === "opaqueredirect") {
|
||||||
).style = "display: none";
|
setAuthenticated(true);
|
||||||
}
|
} else {
|
||||||
|
setAuthenticated(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setDisplay("inherit");
|
checkAuthenticated();
|
||||||
|
}, [path, hashCheckComplete]);
|
||||||
|
|
||||||
if (linkElement.contentWindow) {
|
useEffect(() => {
|
||||||
linkElement.contentWindow.addEventListener("hashchange", () => {
|
if (session === null) {
|
||||||
const hash = linkElement.contentWindow?.location?.hash ?? "";
|
timeoutRef.current = setTimeout(() => {
|
||||||
if (hash.startsWith("#ticket/zoom/")) {
|
if (session === null) {
|
||||||
setDisplay("none");
|
router.push("/login");
|
||||||
const ticketID = hash.split("/").pop();
|
|
||||||
router.push(`/tickets/${ticketID}`);
|
|
||||||
setTimeout(() => {
|
|
||||||
setDisplay("inherit");
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}, 3000);
|
||||||
/>
|
}
|
||||||
);
|
|
||||||
|
if (session !== null) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutRef.current);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
if (!session || !authenticated) {
|
||||||
|
console.log("Not authenticated");
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "100%" }}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
sx={{ height: 500 }}
|
||||||
|
justifyContent="center"
|
||||||
|
alignContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<CircularProgress size={80} color="success" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session && authenticated) {
|
||||||
|
console.log("Session and authenticated");
|
||||||
|
return (
|
||||||
|
<Iframe
|
||||||
|
id={id}
|
||||||
|
url={url}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
frameBorder={0}
|
||||||
|
styles={{ display }}
|
||||||
|
onLoad={() => {
|
||||||
|
const linkElement = document.querySelector(
|
||||||
|
`#${id}`,
|
||||||
|
) as HTMLIFrameElement;
|
||||||
|
|
||||||
|
console.log({ path });
|
||||||
|
console.log({ id });
|
||||||
|
console.log({ linkElement });
|
||||||
|
if (
|
||||||
|
linkElement.contentDocument &&
|
||||||
|
linkElement.contentDocument?.querySelector &&
|
||||||
|
linkElement.contentDocument.querySelector("#navigation") &&
|
||||||
|
linkElement.contentDocument.querySelector("body")
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
linkElement.contentDocument.querySelector("#navigation").style =
|
||||||
|
"display: none";
|
||||||
|
// @ts-ignore
|
||||||
|
linkElement.contentDocument.querySelector("body").style =
|
||||||
|
"font-family: Arial";
|
||||||
|
|
||||||
|
if (
|
||||||
|
hideSidebar &&
|
||||||
|
linkElement.contentDocument.querySelector(".sidebar")
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
linkElement.contentDocument.querySelector(".sidebar").style =
|
||||||
|
"display: none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (linkElement.contentDocument.querySelector(".overview-header")) {
|
||||||
|
// @ts-ignore
|
||||||
|
(
|
||||||
|
linkElement.contentDocument.querySelector(
|
||||||
|
".overview-header",
|
||||||
|
) as any
|
||||||
|
).style = "display: none";
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplay("inherit");
|
||||||
|
|
||||||
|
if (linkElement.contentWindow) {
|
||||||
|
linkElement.contentWindow.addEventListener("hashchange", () => {
|
||||||
|
const hash = linkElement.contentWindow?.location?.hash ?? "";
|
||||||
|
if (hash.startsWith("#ticket/zoom/")) {
|
||||||
|
setDisplay("none");
|
||||||
|
const ticketID = hash.split("/").pop();
|
||||||
|
router.push(`/tickets/${ticketID}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisplay("inherit");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ export const LabelStudioWrapper: FC = () => (
|
||||||
>
|
>
|
||||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||||
<Iframe
|
<Iframe
|
||||||
id="link"
|
id="label-studio"
|
||||||
url={"https://label-studio:3000"}
|
url={"/label-studio"}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
frameBorder={0}
|
frameBorder={0}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import getConfig from "next/config";
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import Iframe from "react-iframe";
|
|
||||||
|
|
||||||
type MetamigoWrapperProps = {
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MetamigoWrapper: FC<MetamigoWrapperProps> = ({ path }) => {
|
|
||||||
const {
|
|
||||||
publicRuntimeConfig: { linkURL },
|
|
||||||
} = getConfig();
|
|
||||||
const fullMetamigoURL = `${linkURL}/metamigo/${path}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
spacing={0}
|
|
||||||
sx={{ height: "100%", width: "100%" }}
|
|
||||||
direction="column"
|
|
||||||
>
|
|
||||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
|
||||||
<Iframe
|
|
||||||
id="link"
|
|
||||||
url={fullMetamigoURL}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
frameBorder={0}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { Metadata } from "next";
|
|
||||||
import { MetamigoWrapper } from "./_components/MetamigoWrapper";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Metamigo",
|
|
||||||
};
|
|
||||||
|
|
||||||
type PageProps = {
|
|
||||||
params: {
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Page({ params: { path } }: PageProps) {
|
|
||||||
return <MetamigoWrapper path={path} />;
|
|
||||||
}
|
|
||||||
6
apps/link/app/(main)/admin/metamigo/page.tsx
Normal file
6
apps/link/app/(main)/admin/metamigo/page.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// import { Admin } from "./_components/Admin";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <Box />;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { ZammadWrapper } from "../../../(main)/_components/ZammadWrapper";
|
import { ZammadWrapper } from "app/(main)/_components/ZammadWrapper";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Zammad",
|
title: "Zammad",
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import NextAuth from "next-auth";
|
|
||||||
import Google from "next-auth/providers/google";
|
|
||||||
// import Apple from "next-auth/providers/apple";
|
|
||||||
|
|
||||||
const handler = NextAuth({
|
|
||||||
providers: [
|
|
||||||
Google({
|
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
}),
|
|
||||||
/*
|
|
||||||
Apple({
|
|
||||||
clientId: process.env.APPLE_CLIENT_ID,
|
|
||||||
clientSecret: process.env.APPLE_CLIENT_SECRET
|
|
||||||
}),
|
|
||||||
*/
|
|
||||||
],
|
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
|
||||||
24
apps/link/app/(main)/docs/_components/DocsWrapper.tsx
Normal file
24
apps/link/app/(main)/docs/_components/DocsWrapper.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import Iframe from "react-iframe";
|
||||||
|
|
||||||
|
export const DocsWrapper: FC = () => (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
sx={{ height: "100%", width: "100%" }}
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
|
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||||
|
<Iframe
|
||||||
|
id="docs"
|
||||||
|
url={"https://digiresilience.org/docs/link/about/"}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
frameBorder={0}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
10
apps/link/app/(main)/docs/page.tsx
Normal file
10
apps/link/app/(main)/docs/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { DocsWrapper } from "./_components/DocsWrapper";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Documentation",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <DocsWrapper />;
|
||||||
|
}
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import getConfig from "next/config";
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import Iframe from "react-iframe";
|
|
||||||
|
|
||||||
type LeafcutterWrapperProps = {
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LeafcutterWrapper: FC<LeafcutterWrapperProps> = ({ path }) => {
|
|
||||||
const {
|
|
||||||
publicRuntimeConfig: { linkURL },
|
|
||||||
} = getConfig();
|
|
||||||
const fullLeafcutterURL = `${linkURL}/proxy/leafcutter/${path}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
spacing={0}
|
|
||||||
sx={{ height: "100%", width: "100%" }}
|
|
||||||
direction="column"
|
|
||||||
>
|
|
||||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
|
||||||
<Iframe
|
|
||||||
id="leafcutter"
|
|
||||||
url={fullLeafcutterURL}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
frameBorder={0}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { LeafcutterWrapper } from "./_components/LeafcutterWrapper";
|
|
||||||
|
|
||||||
type PageProps = {
|
|
||||||
params: {
|
|
||||||
view: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Page({ params: { view } }: PageProps) {
|
|
||||||
<LeafcutterWrapper path={view} />;
|
|
||||||
}
|
|
||||||
30
apps/link/app/(main)/leafcutter/_components/Home.tsx
Normal file
30
apps/link/app/(main)/leafcutter/_components/Home.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, /* useEffect,*/ useState } from "react";
|
||||||
|
import { Home as HomeInternal } from "leafcutter-common";
|
||||||
|
// import { fetchLeafcutter } from "@/app/_lib/utils";
|
||||||
|
import ClientOnly from "@/app/(main)/_components/ClientOnly";
|
||||||
|
|
||||||
|
export const Home: FC = () => {
|
||||||
|
const [visualizations, setVisualizations] = useState([]);
|
||||||
|
/*
|
||||||
|
useEffect(() => {
|
||||||
|
const getVisualizations = async () => {
|
||||||
|
const visualizations = await fetchLeafcutter(
|
||||||
|
"/api/visualizations/list",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
if (visualizations) {
|
||||||
|
setVisualizations(visualizations);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getVisualizations();
|
||||||
|
}, []);
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<ClientOnly>
|
||||||
|
<HomeInternal visualizations={visualizations} />
|
||||||
|
</ClientOnly>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { FC, PropsWithChildren } from "react";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
export const LeafcutterWrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
return <Box sx={{ p: 3 }}>{children}</Box>;
|
||||||
|
};
|
||||||
10
apps/link/app/(main)/leafcutter/about/page.tsx
Normal file
10
apps/link/app/(main)/leafcutter/about/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { About } from "leafcutter-common";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<About />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
apps/link/app/(main)/leafcutter/create/page.tsx
Normal file
13
apps/link/app/(main)/leafcutter/create/page.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// import { getTemplates } from "app/_lib/opensearch";
|
||||||
|
import { Create } from "leafcutter-common";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const templates = []; // await getTemplates(100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Create templates={templates} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
apps/link/app/(main)/leafcutter/faq/page.tsx
Normal file
10
apps/link/app/(main)/leafcutter/faq/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { FAQ } from "leafcutter-common";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<FAQ />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
apps/link/app/(main)/leafcutter/login/page.tsx
Normal file
10
apps/link/app/(main)/leafcutter/login/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { FAQ } from "leafcutter-common";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<FAQ />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { redirect } from "next/navigation";
|
import { Home } from "./_components/Home";
|
||||||
|
import { LeafcutterWrapper } from "./_components/LeafcutterWrapper";
|
||||||
|
|
||||||
export default function Page() {
|
export default async function Page() {
|
||||||
redirect("/leafcutter/home");
|
return (
|
||||||
|
<LeafcutterWrapper>
|
||||||
|
<Home />
|
||||||
|
</LeafcutterWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
apps/link/app/(main)/leafcutter/trends/page.tsx
Normal file
12
apps/link/app/(main)/leafcutter/trends/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { Trends } from "leafcutter-common";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Trends visualizations={[]} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
9
apps/link/app/(main)/logout/page.tsx
Normal file
9
apps/link/app/(main)/logout/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { signOut } from "next-auth/react";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
signOut({ callbackUrl: "/login" });
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
} from "@mui/material";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import { createTicketMutation } from "app/_graphql/createTicketMutation";
|
||||||
|
|
||||||
|
interface TicketCreateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
closeDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
|
||||||
|
open,
|
||||||
|
closeDialog,
|
||||||
|
}) => {
|
||||||
|
const [kind, setKind] = useState("note");
|
||||||
|
const [customerID, setCustomerID] = useState("");
|
||||||
|
const [groupID, setGroupID] = useState("");
|
||||||
|
const [ownerID, setOwnerID] = useState("");
|
||||||
|
const [priorityID, setPriorityID] = useState("");
|
||||||
|
const [stateID, setStateID] = useState("");
|
||||||
|
const [tags, setTags] = useState([]);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [body, setBody] = useState("");
|
||||||
|
const backgroundColor = "#1982FC";
|
||||||
|
const color = "white";
|
||||||
|
const { fetcher } = useSWRConfig();
|
||||||
|
const ticket = {
|
||||||
|
customerId: customerID,
|
||||||
|
groupId: groupID,
|
||||||
|
ownerId: ownerID,
|
||||||
|
priorityId: priorityID,
|
||||||
|
stateId: stateID,
|
||||||
|
tags,
|
||||||
|
title,
|
||||||
|
article: {
|
||||||
|
body,
|
||||||
|
type: kind,
|
||||||
|
internal: kind === "note",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: users, error: usersError }: any = useSWR({
|
||||||
|
url: "/api/v1/users",
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ users, usersError });
|
||||||
|
|
||||||
|
const customers =
|
||||||
|
users?.filter((user: any) => user.role_ids.includes(3)) ?? [];
|
||||||
|
const formattedCustomers = customers.map((customer: any) => ({
|
||||||
|
label: customer.login,
|
||||||
|
id: `${customer.id}`,
|
||||||
|
}));
|
||||||
|
const createTicket = async () => {
|
||||||
|
await fetcher({
|
||||||
|
document: createTicketMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
ticket,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
closeDialog();
|
||||||
|
setBody("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} maxWidth="md" fullWidth>
|
||||||
|
<DialogContent>
|
||||||
|
<Grid container direction="column" spacing={2}>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
label={"Title"}
|
||||||
|
fullWidth
|
||||||
|
value={title}
|
||||||
|
onChange={(e: any) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Autocomplete
|
||||||
|
disablePortal
|
||||||
|
options={formattedCustomers}
|
||||||
|
value={customerID}
|
||||||
|
sx={{ width: 300 }}
|
||||||
|
onChange={(e: any) => setCustomerID(e.target.value.id)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Customer" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
label={"Details"}
|
||||||
|
multiline
|
||||||
|
rows={10}
|
||||||
|
fullWidth
|
||||||
|
value={body}
|
||||||
|
onChange={(e: any) => setBody(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
color: "#666",
|
||||||
|
fontFamily: "Poppins, sans-serif",
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: 2,
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setBody("");
|
||||||
|
closeDialog();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
backgroundColor,
|
||||||
|
color,
|
||||||
|
fontFamily: "Poppins, sans-serif",
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: 2,
|
||||||
|
textTransform: "none",
|
||||||
|
px: 3,
|
||||||
|
}}
|
||||||
|
onClick={createTicket}
|
||||||
|
>
|
||||||
|
Create Ticket
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
import { Grid, Box } from "@mui/material";
|
import { Grid, Box } from "@mui/material";
|
||||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
import { GridColDef } from "@mui/x-data-grid-pro";
|
||||||
import { StyledDataGrid } from "../../../_components/StyledDataGrid";
|
import { StyledDataGrid } from "app/(main)/_components/StyledDataGrid";
|
||||||
import { Button } from "../../../../_components/Button";
|
import { Button } from "app/_components/Button";
|
||||||
import { typography } from "../../../../_styles/theme";
|
import { typography } from "app/_styles/theme";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { TicketCreateDialog } from "./TicketCreateDialog";
|
||||||
|
|
||||||
interface TicketListProps {
|
interface TicketListProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -14,6 +15,7 @@ interface TicketListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
let gridColumns: GridColDef[] = [
|
let gridColumns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -24,12 +26,24 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
||||||
{
|
{
|
||||||
field: "title",
|
field: "title",
|
||||||
headerName: "Title",
|
headerName: "Title",
|
||||||
flex: 1,
|
flex: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "customer",
|
field: "customer",
|
||||||
headerName: "Sender",
|
headerName: "Sender",
|
||||||
valueGetter: (params) => params.row?.customer?.fullname,
|
valueGetter: (params) => params.row?.customer?.fullname,
|
||||||
|
flex: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "createdAt",
|
||||||
|
headerName: "Created At",
|
||||||
|
valueGetter: (params) => new Date(params.row?.createdAt).toLocaleString(),
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "updatedAt",
|
||||||
|
headerName: "Updated At",
|
||||||
|
valueGetter: (params) => new Date(params.row?.updatedAt).toLocaleString(),
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -39,47 +53,58 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
console.log({ tickets });
|
|
||||||
const rowClick = ({ row }) => {
|
const rowClick = ({ row }) => {
|
||||||
router.push(`/tickets/${row.internalId}`);
|
router.push(`/tickets/${row.internalId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
<>
|
||||||
<Grid container direction="column">
|
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||||
<Grid
|
<Grid container direction="column">
|
||||||
item
|
<Grid
|
||||||
container
|
item
|
||||||
direction="row"
|
container
|
||||||
justifyContent="space-between"
|
direction="row"
|
||||||
alignItems="center"
|
justifyContent="space-between"
|
||||||
>
|
alignItems="center"
|
||||||
<Grid item>
|
>
|
||||||
<Box
|
<Grid item>
|
||||||
sx={{
|
<Box
|
||||||
backgroundColor: "#ddd",
|
sx={{
|
||||||
px: "8px",
|
backgroundColor: "#ddd",
|
||||||
pb: "16px",
|
px: "8px",
|
||||||
...typography.h4,
|
pb: "16px",
|
||||||
fontSize: 24,
|
...typography.h4,
|
||||||
}}
|
fontSize: 24,
|
||||||
>
|
}}
|
||||||
{title}
|
>
|
||||||
</Box>
|
{title}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
href={""}
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
|
text="Create"
|
||||||
|
color="#1982FC"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button href="/tickets/create" text="Create" color="#1982FC" />
|
<StyledDataGrid
|
||||||
|
name={title}
|
||||||
|
columns={gridColumns}
|
||||||
|
rows={tickets}
|
||||||
|
onRowClick={rowClick}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
</Box>
|
||||||
<StyledDataGrid
|
<TicketCreateDialog
|
||||||
name={title}
|
open={dialogOpen}
|
||||||
columns={gridColumns}
|
closeDialog={() => setDialogOpen(false)}
|
||||||
rows={tickets}
|
/>
|
||||||
onRowClick={rowClick}
|
</>
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,113 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { TicketList } from "./TicketList";
|
import { TicketList } from "./TicketList";
|
||||||
import { getTicketsByOverviewQuery } from "../../../../_graphql/getTicketsByOverviewQuery";
|
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
||||||
|
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
|
||||||
|
|
||||||
type ZammadOverviewProps = {
|
type ZammadOverviewProps = {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
|
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
|
||||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
const [overviewID, setOverviewID] = useState(null);
|
||||||
|
const [tickets, setTickets] = useState([]);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const { data: overviewData, error: overviewError }: any = useSWR(
|
||||||
{
|
{
|
||||||
document: getTicketsByOverviewQuery,
|
document: getTicketOverviewCountsQuery,
|
||||||
variables: { overviewId: `gid://zammad/Overview/${id}` },
|
|
||||||
},
|
},
|
||||||
{ refreshInterval: 10000 },
|
{ refreshInterval: 10000 },
|
||||||
);
|
);
|
||||||
|
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||||
const shouldRender = !ticketError && ticketData;
|
{
|
||||||
const tickets =
|
document: getTicketsByOverviewQuery,
|
||||||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
variables: { overviewId: overviewID, pageSize: 250 },
|
||||||
|
},
|
||||||
return (
|
{ refreshInterval: 10000 },
|
||||||
<>
|
|
||||||
{shouldRender && <TicketList title={name} tickets={tickets} />}
|
|
||||||
{ticketError && <div>{ticketError.toString()}</div>}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
const overviewLookup = {
|
||||||
|
Assigned: "My Assigned Tickets",
|
||||||
|
Open: "Open Tickets",
|
||||||
|
Urgent: "Escalated Tickets",
|
||||||
|
Unassigned: "Unassigned & Open Tickets",
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOverviewByName = (name: string) => {
|
||||||
|
const fullName = overviewLookup[name];
|
||||||
|
return overviewData?.ticketOverviews?.edges?.find(
|
||||||
|
(overview: any) => overview.node.name === fullName,
|
||||||
|
)?.node?.id;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (overviewData) {
|
||||||
|
setOverviewID(findOverviewByName(name));
|
||||||
|
}
|
||||||
|
}, [overviewData, name]);
|
||||||
|
console.log({
|
||||||
|
name,
|
||||||
|
overviewID,
|
||||||
|
overviewData,
|
||||||
|
overviewError,
|
||||||
|
ticketData,
|
||||||
|
ticketError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||||
|
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
|
||||||
|
|
||||||
|
const sortTickets = (tickets: any) => {
|
||||||
|
return tickets.sort((a: any, b: any) => {
|
||||||
|
if (a.internalId < b.internalId) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.internalId > b.internalId) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (name != "Recent") {
|
||||||
|
const edges = ticketData?.ticketsByOverview?.edges;
|
||||||
|
if (edges) {
|
||||||
|
const nodes = edges.map((edge: any) => edge.node);
|
||||||
|
console.log({ nodes });
|
||||||
|
setError(null);
|
||||||
|
setTickets(sortTickets(nodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ticketError) {
|
||||||
|
setError(ticketError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ticketData, ticketError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRecentTickets = async () => {
|
||||||
|
if (name === "Recent" && recent) {
|
||||||
|
let allTickets = [];
|
||||||
|
for (const rec of recent) {
|
||||||
|
const res = await fetch(`/api/v1/tickets/${rec.o_id}`);
|
||||||
|
const tkt = await res.json();
|
||||||
|
allTickets.push({
|
||||||
|
...tkt,
|
||||||
|
internalId: tkt.id,
|
||||||
|
createdAt: tkt.created_at,
|
||||||
|
updatedAt: tkt.updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTickets(sortTickets(allTickets));
|
||||||
|
console.log({ allTickets });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchRecentTickets();
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
const shouldRender = tickets && !error;
|
||||||
|
console.log({ shouldRender, tickets, error });
|
||||||
|
return shouldRender && <TicketList title={name} tickets={tickets} />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DisplayError } from "../../../_components/DisplayError";
|
import { DisplayError } from "app/_components/DisplayError";
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
error: Error;
|
error: Error;
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,6 @@ export async function generateMetadata({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const overviews = {
|
|
||||||
assigned: 1,
|
|
||||||
unassigned: 2,
|
|
||||||
pending: 3,
|
|
||||||
urgent: 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: {
|
params: {
|
||||||
overview: string;
|
overview: string;
|
||||||
|
|
@ -37,5 +30,5 @@ type PageProps = {
|
||||||
export default function Page({ params: { overview } }: PageProps) {
|
export default function Page({ params: { overview } }: PageProps) {
|
||||||
const section = getSection(overview);
|
const section = getSection(overview);
|
||||||
|
|
||||||
return <ZammadOverview name={section} id={overviews[overview]} />;
|
return <ZammadOverview name={section} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
apps/link/app/(main)/reporting/page.tsx
Normal file
11
apps/link/app/(main)/reporting/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { ZammadWrapper } from "../../(main)/_components/ZammadWrapper";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Reporting",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <ZammadWrapper path="#report" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import { FC, useLayoutEffect } from "react";
|
import { FC, useLayoutEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ZammadWrapper } from "../../../(main)/_components/ZammadWrapper";
|
import { CircularProgress, Box, Grid } from "@mui/material";
|
||||||
|
import { ZammadWrapper } from "app/(main)/_components/ZammadWrapper";
|
||||||
|
|
||||||
export const Setup: FC = () => {
|
export const Setup: FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -10,5 +11,21 @@ export const Setup: FC = () => {
|
||||||
setTimeout(() => router.push("/"), 4000);
|
setTimeout(() => router.push("/"), 4000);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return <ZammadWrapper path="/auth/sso" hideSidebar={false} />;
|
return (
|
||||||
|
<Box sx={{ width: "100%" }}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
sx={{ height: 500 }}
|
||||||
|
justifyContent="center"
|
||||||
|
alignContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<CircularProgress size={80} color="success" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<ZammadWrapper path="/auth/sso" hideSidebar={false} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { updateTicketMutation } from "../../../../../_graphql/updateTicketMutation";
|
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
||||||
|
|
||||||
interface ArticleCreateDialogProps {
|
interface ArticleCreateDialogProps {
|
||||||
ticketID: string;
|
ticketID: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
kind: "reply" | "note";
|
kind: string;
|
||||||
|
recipient?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||||
|
|
@ -24,22 +25,29 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
kind,
|
kind,
|
||||||
|
recipient,
|
||||||
}) => {
|
}) => {
|
||||||
const [body, setBody] = useState("");
|
const [body, setBody] = useState("");
|
||||||
const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620";
|
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
|
||||||
const color = kind === "reply" ? "white" : "black";
|
const color = kind === "note" ? "black" : "white";
|
||||||
const { fetcher } = useSWRConfig();
|
const { fetcher } = useSWRConfig();
|
||||||
|
const article = {
|
||||||
|
body,
|
||||||
|
type: kind,
|
||||||
|
internal: kind === "note",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kind === "email") {
|
||||||
|
article["to"] = recipient;
|
||||||
|
}
|
||||||
|
|
||||||
const createArticle = async () => {
|
const createArticle = async () => {
|
||||||
await fetcher({
|
await fetcher({
|
||||||
document: updateTicketMutation,
|
document: updateTicketMutation,
|
||||||
variables: {
|
variables: {
|
||||||
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
||||||
input: {
|
input: {
|
||||||
article: {
|
article,
|
||||||
body,
|
|
||||||
type: kind === "note" ? "note" : "phone",
|
|
||||||
internal: kind === "note",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -51,7 +59,7 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||||
<Dialog open={open} maxWidth="sm" fullWidth>
|
<Dialog open={open} maxWidth="sm" fullWidth>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
label={kind === "reply" ? "Write reply" : "Write internal note"}
|
label={kind === "note" ? "Write internal note" : "Write reply"}
|
||||||
multiline
|
multiline
|
||||||
rows={10}
|
rows={10}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
@ -92,7 +100,7 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||||
}}
|
}}
|
||||||
onClick={createArticle}
|
onClick={createArticle}
|
||||||
>
|
>
|
||||||
{kind === "reply" ? "Send Reply" : "Save Note"}
|
{kind === "note" ? "Save Note" : "Send Reply"}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { getTicketQuery } from "../../../../../_graphql/getTicketQuery";
|
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||||
import { getTicketArticlesQuery } from "../../../../../_graphql/getTicketArticlesQuery";
|
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
|
||||||
import {
|
import {
|
||||||
Grid,
|
Grid,
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -28,12 +28,14 @@ interface TicketDetailProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [articleKind, setArticleKind] = useState("note");
|
||||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||||
{
|
{
|
||||||
document: getTicketQuery,
|
document: getTicketQuery,
|
||||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||||
},
|
},
|
||||||
{ refreshInterval: 100000 },
|
{ refreshInterval: 10000 },
|
||||||
);
|
);
|
||||||
const { data: ticketArticlesData, error: ticketArticlesError }: any = useSWR(
|
const { data: ticketArticlesData, error: ticketArticlesError }: any = useSWR(
|
||||||
{
|
{
|
||||||
|
|
@ -43,12 +45,21 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||||
{ refreshInterval: 2000 },
|
{ refreshInterval: 2000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: recentViewData, error: recentViewError }: any = useSWR({
|
||||||
|
url: "/api/v1/recent_view",
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ object: "Ticket", o_id: id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => setDialogOpen(false);
|
||||||
|
console.log({ recentViewData, recentViewError });
|
||||||
|
|
||||||
const ticket = ticketData?.ticket;
|
const ticket = ticketData?.ticket;
|
||||||
const ticketArticles = ticketArticlesData?.ticketArticles;
|
const ticketArticles = ticketArticlesData?.ticketArticles;
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const firstArticle = ticketArticles?.edges[0]?.node;
|
||||||
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
|
const firstArticleKind = firstArticle?.type?.name ?? "phone";
|
||||||
const closeDialog = () => setDialogOpen(false);
|
const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? "";
|
||||||
|
const recipient = firstEmailSender;
|
||||||
const shouldRender =
|
const shouldRender =
|
||||||
ticketData && !ticketError && ticketArticlesData && !ticketArticlesError;
|
ticketData && !ticketError && ticketArticlesData && !ticketArticlesError;
|
||||||
|
|
||||||
|
|
@ -89,11 +100,11 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||||
article.internal
|
article.internal
|
||||||
? "internal-note"
|
? "internal-note"
|
||||||
: article?.sender?.name === "Agent"
|
: article?.sender?.name === "Agent"
|
||||||
? "outgoing-message"
|
? "outgoing-message"
|
||||||
: "incoming-message"
|
: "incoming-message"
|
||||||
}
|
}
|
||||||
model={{
|
model={{
|
||||||
message: article.body.replace(/<div>*<br>*<div>/g, ""),
|
message: article.bodyWithUrls,
|
||||||
sentTime: article.updated_at,
|
sentTime: article.updated_at,
|
||||||
sender: article.from,
|
sender: article.from,
|
||||||
direction:
|
direction:
|
||||||
|
|
@ -139,7 +150,7 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||||
mt: 2,
|
mt: 2,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setArticleKind("reply");
|
setArticleKind(firstArticleKind);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -179,6 +190,7 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
closeDialog={closeDialog}
|
closeDialog={closeDialog}
|
||||||
kind={articleKind}
|
kind={articleKind}
|
||||||
|
recipient={recipient}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MuiChipsInput } from "mui-chips-input";
|
import { MuiChipsInput } from "mui-chips-input";
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
import { getTicketQuery } from "../../../../../_graphql/getTicketQuery";
|
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||||
import { updateTicketMutation } from "../../../../../_graphql/updateTicketMutation";
|
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
||||||
|
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
|
||||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||||
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
|
||||||
interface TicketEditProps {
|
interface TicketEditProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -25,6 +27,8 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
const [selectedOwner, setSelectedOwner] = useState("");
|
const [selectedOwner, setSelectedOwner] = useState("");
|
||||||
const [selectedPriority, setSelectedPriority] = useState("");
|
const [selectedPriority, setSelectedPriority] = useState("");
|
||||||
const [selectedState, setSelectedState] = useState("");
|
const [selectedState, setSelectedState] = useState("");
|
||||||
|
const [pendingDate, setPendingDate] = useState(new Date());
|
||||||
|
const [pendingVisible, setPendingVisible] = useState(false);
|
||||||
const [selectedTags, setSelectedTags] = useState([]);
|
const [selectedTags, setSelectedTags] = useState([]);
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
console.info("You clicked the delete icon.");
|
console.info("You clicked the delete icon.");
|
||||||
|
|
@ -35,15 +39,20 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
const { data: users } = useSWR("/api/v1/users", restFetcher);
|
const { data: users } = useSWR("/api/v1/users", restFetcher);
|
||||||
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
|
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
|
||||||
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
|
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
|
||||||
const { data: tags } = useSWR("/api/v1/tags", restFetcher);
|
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
|
||||||
|
|
||||||
|
// const { data: tags } = useSWR("/api/v1/tags", restFetcher);
|
||||||
|
const filteredStates = states?.filter(
|
||||||
|
(state: any) => !["new", "merged", "removed"].includes(state.name),
|
||||||
|
);
|
||||||
|
const agents = users?.filter((user: any) => user.role_ids.includes(2)) ?? [];
|
||||||
const { fetcher } = useSWRConfig();
|
const { fetcher } = useSWRConfig();
|
||||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||||
{
|
{
|
||||||
document: getTicketQuery,
|
document: getTicketQuery,
|
||||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||||
},
|
},
|
||||||
{ refreshInterval: 100000 },
|
{ refreshInterval: 10000 },
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ticket = ticketData?.ticket;
|
const ticket = ticketData?.ticket;
|
||||||
|
|
@ -59,14 +68,15 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
setSelectedTags(ticket.tags);
|
setSelectedTags(ticket.tags);
|
||||||
}
|
}
|
||||||
}, [ticketData, ticketError]);
|
}, [ticketData, ticketError]);
|
||||||
const updateTicket = async () => {
|
|
||||||
const input = {
|
useEffect(() => {
|
||||||
ownerId: `gid://zammad/User/${selectedOwner}`,
|
const stateName = filteredStates?.find(
|
||||||
priorityId: `gid://zammad/Ticket::Priority/${selectedPriority}`,
|
(state: any) => state.id === selectedState,
|
||||||
stateId: `gid://zammad/Ticket::State/${selectedState}`,
|
)?.name;
|
||||||
groupId: `gid://zammad/Group/${selectedGroup}`,
|
setPendingVisible(stateName?.includes("pending") ?? false);
|
||||||
// tags: selectedTags,
|
}, [selectedState]);
|
||||||
};
|
const updateTicket = async (input: any) => {
|
||||||
|
console.log({ input });
|
||||||
const res = await fetcher({
|
const res = await fetcher({
|
||||||
document: updateTicketMutation,
|
document: updateTicketMutation,
|
||||||
variables: {
|
variables: {
|
||||||
|
|
@ -76,6 +86,17 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
});
|
});
|
||||||
console.log({ res });
|
console.log({ res });
|
||||||
};
|
};
|
||||||
|
const updateTags = async (tags: string[]) => {
|
||||||
|
console.log({ tags });
|
||||||
|
const res = await fetcher({
|
||||||
|
document: updateTagsMutation,
|
||||||
|
variables: {
|
||||||
|
objectId: `gid://zammad/Ticket/${id}`,
|
||||||
|
tags,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log({ res });
|
||||||
|
};
|
||||||
const shouldRender = ticketData && !ticketError;
|
const shouldRender = ticketData && !ticketError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -88,8 +109,11 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
defaultValue={selectedGroup}
|
defaultValue={selectedGroup}
|
||||||
value={selectedGroup}
|
value={selectedGroup}
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setSelectedGroup(e.target.value);
|
const newGroup = e.target.value;
|
||||||
updateTicket();
|
setSelectedGroup(newGroup);
|
||||||
|
updateTicket({
|
||||||
|
groupId: `gid://zammad/Group/${newGroup}`,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -109,8 +133,9 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
<Select
|
<Select
|
||||||
value={selectedOwner}
|
value={selectedOwner}
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setSelectedOwner(e.target.value);
|
const newOwner = e.target.value;
|
||||||
updateTicket();
|
setSelectedOwner(newOwner);
|
||||||
|
updateTicket({ ownerId: `gid://zammad/User/${newOwner}` });
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -118,20 +143,24 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{users?.map((user: any) => (
|
{agents?.map((user: any) => (
|
||||||
<MenuItem key={user.id} value={user.id}>
|
<MenuItem key={user.id} value={`${user.id}`}>
|
||||||
{user.firstname} {user.lastname}
|
{user.firstname} {user.lastname}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<Box sx={{ m: 1, mt: 0 }}>State</Box>
|
<Box sx={{ m: 1, mt: 0 }}>State</Box>
|
||||||
<Select
|
<Select
|
||||||
value={selectedState}
|
value={selectedState}
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setSelectedState(e.target.value);
|
const newState = e.target.value;
|
||||||
updateTicket();
|
setSelectedState(newState);
|
||||||
|
updateTicket({
|
||||||
|
stateId: `gid://zammad/Ticket::State/${newState}`,
|
||||||
|
pendingTime: pendingDate.toISOString(),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -139,20 +168,45 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{states?.map((state: any) => (
|
{filteredStates?.map((state: any) => (
|
||||||
<MenuItem key={state.id} value={state.id}>
|
<MenuItem key={state.id} value={state.id}>
|
||||||
{state.name}
|
{state.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
sx={{ display: pendingVisible ? "inherit" : "none" }}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
label="Pending Date"
|
||||||
|
value={pendingDate}
|
||||||
|
onChange={(newValue: any) => {
|
||||||
|
console.log(newValue);
|
||||||
|
setPendingDate(newValue);
|
||||||
|
updateTicket({
|
||||||
|
pendingTime: newValue.toISOString(),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
slotProps={{ textField: { size: "small" } }}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
|
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
|
||||||
<Select
|
<Select
|
||||||
value={selectedPriority}
|
value={selectedPriority}
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setSelectedPriority(e.target.value);
|
const newPriority = e.target.value;
|
||||||
updateTicket();
|
setSelectedPriority(newPriority);
|
||||||
|
updateTicket({
|
||||||
|
priorityId: `gid://zammad/Ticket::Priority/${newPriority}`,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -175,7 +229,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
value={selectedTags}
|
value={selectedTags}
|
||||||
onChange={(tags: any) => {
|
onChange={(tags: any) => {
|
||||||
setSelectedTags(tags);
|
setSelectedTags(tags);
|
||||||
updateTicket();
|
updateTags(tags);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DisplayError } from "../../../_components/DisplayError";
|
import { DisplayError } from "app/_components/DisplayError";
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
error: Error;
|
error: Error;
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,15 @@ interface ButtonProps {
|
||||||
text: string;
|
text: string;
|
||||||
color: string;
|
color: string;
|
||||||
href: string;
|
href: string;
|
||||||
|
onClick: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button: FC<ButtonProps> = ({ text, color, href }) => (
|
export const Button: FC<ButtonProps> = ({ text, color, href, onClick }) => (
|
||||||
<Link href={href} passHref>
|
<Link href={href} passHref>
|
||||||
<MUIButton
|
<MUIButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disableElevation
|
disableElevation
|
||||||
|
onClick={onClick}
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: "Poppins, sans-serif",
|
fontFamily: "Poppins, sans-serif",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, PropsWithChildren, useState } from "react";
|
import { FC, PropsWithChildren, useState } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
import { CssBaseline } from "@mui/material";
|
import { CssBaseline } from "@mui/material";
|
||||||
import { CookiesProvider } from "react-cookie";
|
import { CookiesProvider } from "react-cookie";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
import { GraphQLClient } from "graphql-request";
|
import { GraphQLClient } from "graphql-request";
|
||||||
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
|
import { I18n } from "react-polyglot";
|
||||||
|
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3";
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
||||||
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
||||||
|
import { locales } from "leafcutter-common";
|
||||||
|
|
||||||
LicenseInfo.setLicenseKey(
|
LicenseInfo.setLicenseKey(
|
||||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI="
|
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
|
@ -21,25 +24,83 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
typeof window !== "undefined" && window.location.origin
|
typeof window !== "undefined" && window.location.origin
|
||||||
? window.location.origin
|
? window.location.origin
|
||||||
: null;
|
: null;
|
||||||
const client = new GraphQLClient(`${origin}/proxy/zammad/graphql`, {
|
const client = new GraphQLClient(`${origin}/zammad/graphql`);
|
||||||
headers: {
|
const messages: any = { en: locales.en, fr: locales.fr };
|
||||||
|
const locale = "en";
|
||||||
|
const fetchAndCheckAuth = async ({
|
||||||
|
document,
|
||||||
|
variables,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
}: any) => {
|
||||||
|
const requestHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
|
||||||
});
|
|
||||||
const graphQLFetcher = async ({ document, variables }: any) => {
|
|
||||||
const requestHeaders = {
|
|
||||||
"X-CSRF-Token": csrfToken,
|
"X-CSRF-Token": csrfToken,
|
||||||
};
|
};
|
||||||
const { data, headers } = await client.rawRequest(
|
let responseData = null;
|
||||||
document,
|
let responseHeaders = new Headers();
|
||||||
variables,
|
let responseStatus = null;
|
||||||
requestHeaders
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = headers.get("CSRF-Token");
|
if (document) {
|
||||||
|
const { data, headers, status } = await client.rawRequest(
|
||||||
|
document,
|
||||||
|
variables,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
|
responseData = data;
|
||||||
|
responseHeaders = headers;
|
||||||
|
responseStatus = status;
|
||||||
|
} else {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: requestHeaders,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
responseData = await res.json();
|
||||||
|
responseHeaders = res.headers;
|
||||||
|
responseStatus = res.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseStatus !== 200) {
|
||||||
|
const res = await fetch("/zammad/auth/sso", {
|
||||||
|
method: "GET",
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ checkAuth: res });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = responseHeaders.get("CSRF-Token");
|
||||||
setCsrfToken(token);
|
setCsrfToken(token);
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiFetcher = async ({
|
||||||
|
document,
|
||||||
|
variables,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
}: any) => {
|
||||||
|
let checks = 0;
|
||||||
|
let data = null;
|
||||||
|
|
||||||
|
while (!data && checks < 2) {
|
||||||
|
data = await fetchAndCheckAuth({
|
||||||
|
document,
|
||||||
|
variables,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
checks++;
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,11 +108,13 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
<>
|
<>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
||||||
<SWRConfig value={{ fetcher: graphQLFetcher }}>
|
<SWRConfig value={{ fetcher: multiFetcher }}>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
{children}
|
<I18n locale={locale} messages={messages[locale]}>
|
||||||
|
{children}
|
||||||
|
</I18n>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</CookiesProvider>
|
</CookiesProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
|
|
|
||||||
13
apps/link/app/_graphql/createTicketMutation.ts
Normal file
13
apps/link/app/_graphql/createTicketMutation.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { gql } from "graphql-request";
|
||||||
|
|
||||||
|
export const createTicketMutation = gql`
|
||||||
|
mutation CreateTicket($ticketId: ID!, $input: TicketCreateInput!) {
|
||||||
|
ticketCreate(input: $input) {
|
||||||
|
ticket {
|
||||||
|
id
|
||||||
|
priority {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
@ -6,7 +6,7 @@ query getTicketArticles($ticketId: ID!) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
body
|
bodyWithUrls
|
||||||
internal
|
internal
|
||||||
type {
|
type {
|
||||||
name
|
name
|
||||||
|
|
@ -14,6 +14,11 @@ query getTicketArticles($ticketId: ID!) {
|
||||||
sender {
|
sender {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
from {
|
||||||
|
parsed {
|
||||||
|
emailAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
apps/link/app/_graphql/searchQuery.ts
Normal file
20
apps/link/app/_graphql/searchQuery.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { gql } from 'graphql-request';
|
||||||
|
|
||||||
|
export const searchQuery = gql`
|
||||||
|
query search($search: String!, $limit: Int = 10, $onlyIn: EnumSearchableModels = Ticket) {
|
||||||
|
search(search: $search, limit: $limit, onlyIn: $onlyIn) {
|
||||||
|
... on Ticket {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
internalId
|
||||||
|
title
|
||||||
|
state {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
stateColorCode
|
||||||
|
note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
12
apps/link/app/_graphql/updateTagsMutation.ts
Normal file
12
apps/link/app/_graphql/updateTagsMutation.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { gql } from "graphql-request";
|
||||||
|
|
||||||
|
export const updateTagsMutation = gql`
|
||||||
|
mutation UpdateTags($objectId: ID!, $tags: [String!]!) {
|
||||||
|
tagAssignmentUpdate(objectId: $objectId, tags: $tags) {
|
||||||
|
success
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
@ -5,6 +5,9 @@ mutation UpdateTicket($ticketId: ID!, $input: TicketUpdateInput!) {
|
||||||
ticketUpdate(ticketId: $ticketId, input: $input) {
|
ticketUpdate(ticketId: $ticketId, input: $input) {
|
||||||
ticket {
|
ticket {
|
||||||
id
|
id
|
||||||
|
priority {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
|
||||||
41
apps/link/app/_lib/utils.ts
Normal file
41
apps/link/app/_lib/utils.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
export const fetchLeafcutter = async (url: string, options: any) => {
|
||||||
|
/*
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'X-Opensearch-Username': process.env.OPENSEARCH_USER!,
|
||||||
|
'X-Opensearch-Password': process.env.OPENSEARCH_PASSWORD!,
|
||||||
|
'X-Leafcutter-User': token.email.toLowerCase()
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
const fetchData = async (url: string, options: any) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
const json = await res.json();
|
||||||
|
return json;
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await fetchData(url, options);
|
||||||
|
console.log({ data });
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
const csrfURL = `${process.env.NEXT_PUBLIC_LEAFCUTTER_URL}/api/auth/csrf`;
|
||||||
|
const csrfData = await fetchData(csrfURL, {});
|
||||||
|
console.log({ csrfData });
|
||||||
|
const authURL = `${process.env.NEXT_PUBLIC_LEAFCUTTER_URL}/api/auth/callback/credentials`;
|
||||||
|
const authData = await fetchData(authURL, { method: "POST" });
|
||||||
|
console.log({ authData });
|
||||||
|
if (!authData) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return await fetchData(url, options);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
128
apps/link/app/api/auth/[...nextauth]/route.ts
Normal file
128
apps/link/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
import Google from "next-auth/providers/google";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import Apple from "next-auth/providers/apple";
|
||||||
|
|
||||||
|
const headers = { Authorization: `Token ${process.env.ZAMMAD_API_TOKEN}` };
|
||||||
|
|
||||||
|
const fetchRoles = async () => {
|
||||||
|
const url = `${process.env.ZAMMAD_URL}/api/v1/roles`;
|
||||||
|
const res = await fetch(url, { headers });
|
||||||
|
const roles = await res.json();
|
||||||
|
console.log({ roles });
|
||||||
|
const formattedRoles = roles.reduce((acc: any, role: any) => {
|
||||||
|
acc[role.id] = role.name;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return formattedRoles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUser = async (email: string) => {
|
||||||
|
console.log({ email });
|
||||||
|
const url = `${process.env.ZAMMAD_URL}/api/v1/users/search?query=login:${email}&limit=1`;
|
||||||
|
console.log({ url });
|
||||||
|
const res = await fetch(url, { headers });
|
||||||
|
console.log({ res });
|
||||||
|
const users = await res.json();
|
||||||
|
console.log({ users });
|
||||||
|
const user = users?.[0];
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserRoles = async (email: string) => {
|
||||||
|
try {
|
||||||
|
const user = await fetchUser(email);
|
||||||
|
console.log({ user });
|
||||||
|
const allRoles = await fetchRoles();
|
||||||
|
console.log({ allRoles });
|
||||||
|
const roles = user.role_ids.map((roleID: number) => {
|
||||||
|
const role = allRoles[roleID];
|
||||||
|
return role ? role.toLowerCase().replace(" ", "_") : null;
|
||||||
|
});
|
||||||
|
return roles.filter((role: string) => role !== null);
|
||||||
|
} catch (e) {
|
||||||
|
console.log({ e });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = async (email: string, password: string) => {
|
||||||
|
const url = `${process.env.ZAMMAD_URL}/api/v1/users/me`;
|
||||||
|
console.log({ url });
|
||||||
|
const authorization =
|
||||||
|
"Basic " + Buffer.from(email + ":" + password).toString("base64");
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
authorization,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const user = await res.json();
|
||||||
|
console.log({ user });
|
||||||
|
|
||||||
|
if (user && !user.error && user.id) {
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = NextAuth({
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
error: "/login",
|
||||||
|
signOut: "/logout",
|
||||||
|
},
|
||||||
|
providers: [
|
||||||
|
Google({
|
||||||
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
}),
|
||||||
|
Apple({
|
||||||
|
clientId: process.env.APPLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.APPLE_CLIENT_SECRET,
|
||||||
|
}),
|
||||||
|
Credentials({
|
||||||
|
name: "Zammad",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "text" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
async authorize(credentials, req) {
|
||||||
|
const user = await login(credentials.email, credentials.password);
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
|
||||||
|
callbacks: {
|
||||||
|
signIn: async ({ user, account, profile }) => {
|
||||||
|
const roles = (await getUserRoles(user.email)) ?? [];
|
||||||
|
return (
|
||||||
|
roles.includes("admin") ||
|
||||||
|
roles.includes("agent") ||
|
||||||
|
process.env.SETUP_MODE === "true"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
session: async ({ session, user, token }) => {
|
||||||
|
// @ts-ignore
|
||||||
|
session.user.roles = token.roles ?? [];
|
||||||
|
// @ts-ignore
|
||||||
|
session.user.leafcutter = token.leafcutter; // remove
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
jwt: async ({ token, user, account, profile, trigger }) => {
|
||||||
|
if (user) {
|
||||||
|
token.roles = (await getUserRoles(user.email)) ?? [];
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
const handler = (req: NextRequest) => {
|
const handler = (req: NextRequest) => {
|
||||||
NextResponse.redirect('/proxy/zammad/api/v1' + req.url.substring('/api/v1'.length));
|
NextResponse.redirect('/zammad/api/v1' + req.url.substring('/api/v1'.length));
|
||||||
};
|
};
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
|
|
@ -2,69 +2,48 @@ import { NextResponse } from 'next/server';
|
||||||
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
|
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
|
||||||
|
|
||||||
const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => {
|
const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => {
|
||||||
if (request.nextUrl.protocol.startsWith('ws')) {
|
if (request.nextUrl.pathname.startsWith('/api/v1/reports/sets')) {
|
||||||
return NextResponse.next();
|
console.log(request.nextUrl.searchParams.get("sheet"));
|
||||||
}
|
NextResponse.next();
|
||||||
|
|
||||||
if (request.nextUrl.pathname.includes('/_next/static/development/')) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
|
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
|
||||||
|
|
||||||
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
||||||
|
|
||||||
const requestHeaders = new Headers(request.headers);
|
const requestHeaders = new Headers(request.headers);
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
// @ts-ignore
|
requestHeaders.set(key, value as string);
|
||||||
requestHeaders.set(key, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestHeaders.delete('connection');
|
requestHeaders.delete('connection');
|
||||||
|
|
||||||
// console.log({ finalHeaders: requestHeaders });
|
|
||||||
|
|
||||||
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
|
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkRewrites = async (request: NextRequestWithAuth) => {
|
const checkRewrites = async (request: NextRequestWithAuth) => {
|
||||||
console.log({ currentURL: request.nextUrl.href });
|
|
||||||
|
|
||||||
const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
|
const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
|
||||||
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
|
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
|
||||||
const leafcutterURL = process.env.LEAFCUTTER_URL ?? "https://lc.digiresilience.org";
|
const metamigoURL = process.env.METAMIGO_URL ?? "http://metamigo-api:3000";
|
||||||
const metamigoURL = process.env.METAMIGO_URL ?? "http://metamigo-frontend:3000";
|
const labelStudioURL = process.env.LABEL_STUDIO_URL ?? "http://label-studio:8080";
|
||||||
|
const { token } = request.nextauth;
|
||||||
|
const headers = { 'X-Forwarded-User': token?.email?.toLowerCase() };
|
||||||
|
|
||||||
console.log({ linkBaseURL, zammadURL, leafcutterURL, metamigoURL });
|
if (request.nextUrl.pathname.startsWith('/metamigo')) {
|
||||||
|
return rewriteURL(request, `${linkBaseURL}/metamigo`, metamigoURL);
|
||||||
if (request.nextUrl.pathname.startsWith('/proxy/leafcutter')) {
|
} else if (request.nextUrl.pathname.startsWith('/label-studio')) {
|
||||||
const headers = { 'X-Leafcutter-Embedded': "true" };
|
return rewriteURL(request, `${linkBaseURL}/label-studio`, labelStudioURL);
|
||||||
return rewriteURL(request, linkBaseURL, leafcutterURL, headers);
|
} else if (request.nextUrl.pathname.startsWith('/zammad')) {
|
||||||
} else if (request.nextUrl.pathname.startsWith('/proxy/metamigo')) {
|
return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers);
|
||||||
return rewriteURL(request, linkBaseURL, metamigoURL);
|
} else if (request.nextUrl.pathname.startsWith('/auth/sso') || request.nextUrl.pathname.startsWith('/assets')) {
|
||||||
} else if (request.nextUrl.pathname.startsWith('/proxy/zammad')) {
|
return rewriteURL(request, linkBaseURL, zammadURL, headers);
|
||||||
console.log('proxying to zammad');
|
} else if (request.nextUrl.pathname.startsWith('/proxy/api') || request.nextUrl.pathname.startsWith('/proxy/assets')) {
|
||||||
const { token } = request.nextauth;
|
|
||||||
|
|
||||||
// console.log({ nextauth: request.nextauth });
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'X-Forwarded-User': token.email.toLowerCase(),
|
|
||||||
host: 'link-stack-dev.digiresilience.org'
|
|
||||||
};
|
|
||||||
|
|
||||||
// console.log({ headers });
|
|
||||||
|
|
||||||
return rewriteURL(request, `${linkBaseURL}/proxy/zammad`, zammadURL, headers);
|
|
||||||
} else if (request.nextUrl.pathname.startsWith('/assets') || request.nextUrl.pathname.startsWith('/api/v1')) {
|
|
||||||
console.log('asset');
|
|
||||||
return rewriteURL(request, linkBaseURL, zammadURL);
|
|
||||||
} else if (request.nextUrl.pathname.startsWith('/proxy/assets')) {
|
|
||||||
console.log('proxy asset');
|
|
||||||
return rewriteURL(request, `${linkBaseURL}/proxy`, zammadURL);
|
|
||||||
} else if (request.nextUrl.pathname.startsWith('/proxy/api')) {
|
|
||||||
console.log('proxy api');
|
|
||||||
return rewriteURL(request, `${linkBaseURL}/proxy`, zammadURL);
|
return rewriteURL(request, `${linkBaseURL}/proxy`, zammadURL);
|
||||||
|
} else if (request.nextUrl.pathname.startsWith('/api/v1') || request.nextUrl.pathname.startsWith('/auth/sso') || request.nextUrl.pathname.startsWith('/mobile')) {
|
||||||
|
return rewriteURL(request, linkBaseURL, zammadURL, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withAuth(
|
export default withAuth(
|
||||||
|
|
@ -77,20 +56,19 @@ export default withAuth(
|
||||||
authorized: ({ token, req }) => {
|
authorized: ({ token, req }) => {
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
headers,
|
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
// check login page
|
const noAuthPaths = ["/login", "/api/v1"];
|
||||||
const parsedURL = new URL(url);
|
const parsedURL = new URL(url);
|
||||||
if (parsedURL.pathname.startsWith('/login')) {
|
const path = parsedURL.pathname;
|
||||||
|
|
||||||
|
if (noAuthPaths.some((p: string) => path.startsWith(p))) {
|
||||||
|
console.log({ p: parsedURL.pathname, auth: "no" });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check session auth
|
const roles: any = token?.roles ?? [];
|
||||||
const authorizedDomains = ["redaranj.com", "digiresilience.org"];
|
if (roles.includes("admin") || roles.includes("agent") || process.env.SETUP_MODE === "true") {
|
||||||
const userDomain = token?.email?.toLowerCase().split("@").pop() ?? "unauthorized.net";
|
|
||||||
|
|
||||||
if (authorizedDomains.includes(userDomain)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,3 +77,10 @@ export default withAuth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/((?!ws|wss|_next/static|_next/image|favicon.ico).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
experimental: {
|
||||||
|
missingSuspenseWithCSRBailout: false,
|
||||||
|
},
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
"@mui/material": {
|
"@mui/material": {
|
||||||
transform: "@mui/material/{{member}}",
|
transform: "@mui/material/{{member}}",
|
||||||
|
|
@ -9,10 +12,13 @@ const nextConfig = {
|
||||||
transform: "@mui/icons-material/{{member}}",
|
transform: "@mui/icons-material/{{member}}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
transpilePackages: ["leafcutter-common"],
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
linkURL: process.env.LINK_URL ?? "http://localhost:3000",
|
linkURL: process.env.LINK_URL ?? "http://localhost:3000",
|
||||||
leafcutterURL: process.env.LEAFCUTTER_URL ?? "http://localhost:3001",
|
leafcutterURL:
|
||||||
metamigoURL: process.env.METAMIGO_URL ?? "http://localhost:3002",
|
process.env.LEAFCUTTER_URL ?? "https://lc.digiresilience.org",
|
||||||
|
metamigoURL: process.env.METAMIGO_URL ?? "http://localhost:8002",
|
||||||
|
labelStudioURL: process.env.LABEL_STUDIO_URL ?? "http://localhost:8006",
|
||||||
muiLicenseKey: process.env.MUI_LICENSE_KEY ?? "",
|
muiLicenseKey: process.env.MUI_LICENSE_KEY ?? "",
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
|
|
@ -20,7 +26,7 @@ const nextConfig = {
|
||||||
fallback: [
|
fallback: [
|
||||||
{
|
{
|
||||||
source: "/:path*",
|
source: "/:path*",
|
||||||
destination: `/proxy/zammad/:path*`,
|
destination: `/proxy/leafcutter/:path*`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,49 +9,61 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chatscope/chat-ui-kit-react": "^1.10.1",
|
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
||||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
"@emotion/react": "^11.11.1",
|
"@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.5",
|
"@fontsource/playfair-display": "^5.0.21",
|
||||||
"@fontsource/poppins": "^5.0.5",
|
"@fontsource/poppins": "^5.0.12",
|
||||||
"@fontsource/roboto": "^5.0.5",
|
"@fontsource/roboto": "^5.0.12",
|
||||||
"@mui/icons-material": "^5",
|
"@mui/icons-material": "^5",
|
||||||
"@mui/lab": "^5.0.0-alpha.136",
|
"@mui/lab": "^5.0.0-alpha.167",
|
||||||
"@mui/material": "^5",
|
"@mui/material": "^5",
|
||||||
"@mui/x-data-grid-pro": "^6.10.0",
|
"@mui/x-data-grid-pro": "^6.19.6",
|
||||||
"@mui/x-date-pickers-pro": "^6.10.0",
|
"@mui/x-date-pickers-pro": "^6.19.6",
|
||||||
"date-fns": "^2.30.0",
|
"cryptr": "^6.3.0",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"material-ui-popup-state": "^5.0.9",
|
"leafcutter-common": "*",
|
||||||
"mui-chips-input": "^2.0.2",
|
"material-ui-popup-state": "^5.0.10",
|
||||||
"next": "13.4.10",
|
"mui-chips-input": "^2.1.4",
|
||||||
"next-auth": "^4.22.1",
|
"next": "14.1.2",
|
||||||
|
"next-auth": "^4.24.6",
|
||||||
|
"ra-data-graphql": "^4.16.12",
|
||||||
|
"ra-i18n-polyglot": "^4.16.12",
|
||||||
|
"ra-input-rich-text": "^4.16.12",
|
||||||
|
"ra-language-english": "^4.16.12",
|
||||||
|
"ra-postgraphile": "^6.1.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-admin": "^4.16.12",
|
||||||
|
"react-cookie": "^7.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",
|
||||||
"sharp": "^0.32.3",
|
"react-qr-code": "^2.0.12",
|
||||||
"swr": "^2.2.0",
|
"react-timer-hook": "^3.0.7",
|
||||||
"tss-react": "^4.8.8"
|
"sharp": "^0.33.2",
|
||||||
|
"swr": "^2.2.5",
|
||||||
|
"tss-react": "^4.9.4",
|
||||||
|
"twilio-client": "^1.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.9",
|
"@babel/core": "^7.24.0",
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.11.24",
|
||||||
"@types/react": "18.2.15",
|
"@types/react": "18.2.63",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.8",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.1.3",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-next": "^13.4.10",
|
"eslint-config-next": "^14.1.2",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
|
@ -19,10 +15,7 @@
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*", "../../node_modules/*"]
|
||||||
"./*",
|
|
||||||
"../../node_modules/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|
@ -35,9 +28,8 @@
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
"../leafcutter/app/(login)/login/link/_components/AutoLogin.tsx"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,49 +17,49 @@
|
||||||
"@graphile-contrib/pg-simplify-inflector": "^6.1.0",
|
"@graphile-contrib/pg-simplify-inflector": "^6.1.0",
|
||||||
"@hapi/basic": "^7.0.2",
|
"@hapi/basic": "^7.0.2",
|
||||||
"@hapi/boom": "^10.0.1",
|
"@hapi/boom": "^10.0.1",
|
||||||
"@hapi/vision": "^7.0.2",
|
"@hapi/vision": "^7.0.3",
|
||||||
"@hapi/wreck": "^18.0.1",
|
"@hapi/wreck": "^18.0.1",
|
||||||
"@hapipal/schmervice": "^3.0.0",
|
"@hapipal/schmervice": "^3.0.0",
|
||||||
"@hapipal/toys": "^4.0.0",
|
"@hapipal/toys": "^4.0.0",
|
||||||
"blipp": "^4.0.2",
|
"blipp": "^4.0.2",
|
||||||
"camelcase-keys": "^8.0.2",
|
"camelcase-keys": "^9.1.3",
|
||||||
"expiry-map": "^2.0.0",
|
"expiry-map": "^2.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"graphile-migrate": "^1.4.1",
|
"graphile-migrate": "^1.4.1",
|
||||||
"graphile-worker": "^0.13.0",
|
"graphile-worker": "^0.13.0",
|
||||||
"hapi-auth-bearer-token": "^8.0.0",
|
"hapi-auth-bearer-token": "^8.0.0",
|
||||||
"hapi-auth-jwt2": "^10.4.0",
|
"hapi-auth-jwt2": "^10.5.1",
|
||||||
"hapi-swagger": "^17.1.0",
|
"hapi-swagger": "^17.2.1",
|
||||||
"joi": "^17.9.2",
|
"joi": "^17.12.2",
|
||||||
"jsonwebtoken": "^9.0.1",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwks-rsa": "^3.0.1",
|
"jwks-rsa": "^3.1.0",
|
||||||
"long": "^5.2.3",
|
"long": "^5.2.3",
|
||||||
"p-memoize": "^7.1.1",
|
"p-memoize": "^7.1.1",
|
||||||
"pg": "^8.11.1",
|
"pg": "^8.11.3",
|
||||||
"pg-monitor": "^2.0.0",
|
"pg-monitor": "^2.0.0",
|
||||||
"pg-promise": "^11.5.0",
|
"pg-promise": "^11.5.4",
|
||||||
"postgraphile": "4.12.3",
|
"postgraphile": "4.12.3",
|
||||||
"postgraphile-plugin-connection-filter": "^2.3.0",
|
"postgraphile-plugin-connection-filter": "^2.3.0",
|
||||||
"remeda": "^1.24.0",
|
"remeda": "^1.46.2",
|
||||||
"twilio": "^4.14.0",
|
"twilio": "^4.23.0",
|
||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.20",
|
||||||
"@whiskeysockets/baileys": "^6.3.1"
|
"@whiskeysockets/baileys": "^6.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/long": "^4.0.2",
|
"@types/long": "^4.0.2",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"babel-preset-link": "*",
|
"babel-preset-link": "*",
|
||||||
"camelcase-keys": "^8.0.2",
|
"camelcase-keys": "^9.1.3",
|
||||||
"eslint-config-link": "*",
|
"eslint-config-link": "*",
|
||||||
"jest-config-link": "*",
|
"jest-config-link": "*",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.1.0",
|
||||||
"pg-monitor": "^2.0.0",
|
"pg-monitor": "^2.0.0",
|
||||||
"pino-pretty": "^10.0.1",
|
"pino-pretty": "^10.3.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-watch": "^6.0.4",
|
"tsc-watch": "^6.0.4",
|
||||||
"tsconfig-link": "*",
|
"tsconfig-link": "*",
|
||||||
"typedoc": "^0.24.8",
|
"typedoc": "^0.25.11",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ const AppPlugin = {
|
||||||
name: "App",
|
name: "App",
|
||||||
async register(
|
async register(
|
||||||
server: Hapi.Server,
|
server: Hapi.Server,
|
||||||
options: { config: IAppConfig }
|
options: { config: IAppConfig },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// declare our **run-time** plugin dependencies
|
// declare our **run-time** plugin dependencies
|
||||||
// these are runtime only deps, not registration time
|
// these are runtime only deps, not registration time
|
||||||
// ref: https://hapipal.com/best-practices/handling-plugin-dependencies
|
// ref: https://hapipal.com/best-practices/handling-plugin-dependencies
|
||||||
server.dependency(["config", "hapi-pino"]);
|
server.dependency(["config", "hapi-pino"]);
|
||||||
|
|
||||||
server.validator(Joi);
|
server.validator(Joi as any);
|
||||||
await Plugins.register(server, options.config);
|
await Plugins.register(server, options.config);
|
||||||
await Services.register(server);
|
await Services.register(server);
|
||||||
await Routes.register(server);
|
await Routes.register(server);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { IAppConfig } from "@digiresilience/metamigo-config";
|
||||||
import { postgraphile, HttpRequestHandler } from "postgraphile";
|
import { postgraphile, HttpRequestHandler } from "postgraphile";
|
||||||
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
|
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
|
||||||
|
|
||||||
export interface HapiPostgraphileOptions {}
|
export interface HapiPostgraphileOptions { }
|
||||||
|
|
||||||
const PostgraphilePlugin: Hapi.Plugin<HapiPostgraphileOptions> = {
|
const PostgraphilePlugin: Hapi.Plugin<HapiPostgraphileOptions> = {
|
||||||
name: "postgraphilePlugin",
|
name: "postgraphilePlugin",
|
||||||
|
|
@ -29,7 +29,7 @@ const PostgraphilePlugin: Hapi.Plugin<HapiPostgraphileOptions> = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
} as any
|
||||||
);
|
);
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ export const register = async (
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await registerNextAuth(server, config);
|
// await registerNextAuth(server, config);
|
||||||
await registerSwagger(server);
|
await registerSwagger(server);
|
||||||
await registerCloudflareAccessJwt(server, config);
|
//await registerCloudflareAccessJwt(server, config);
|
||||||
await registerAuthBearer(server, config);
|
// await registerAuthBearer(server, config);
|
||||||
await registerPostgraphile(server, config);
|
await registerPostgraphile(server, config);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
},
|
},
|
||||||
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
const { voiceLineId } = request.params;
|
const { voiceLineId } = request.params;
|
||||||
const { To } = request.payload as { To: string };
|
const { To } = request.payload as { To: string; };
|
||||||
const voiceLine = await request.db().voiceLines.findBy({ number: To });
|
const voiceLine = await request.db().voiceLines.findBy({ number: To });
|
||||||
if (!voiceLine) return Boom.notFound();
|
if (!voiceLine) return Boom.notFound();
|
||||||
if (voiceLine.id !== voiceLineId) return Boom.badRequest();
|
if (voiceLine.id !== voiceLineId) return Boom.badRequest();
|
||||||
|
|
@ -193,7 +193,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
|
||||||
const { providerId } = request.params as { providerId: string };
|
const { providerId } = request.params as { providerId: string; };
|
||||||
const provider: SavedVoiceProvider = await request
|
const provider: SavedVoiceProvider = await request
|
||||||
.db()
|
.db()
|
||||||
.voiceProviders.findById({ id: providerId });
|
.voiceProviders.findById({ id: providerId });
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"@digiresilience/metamigo-db": "*",
|
"@digiresilience/metamigo-db": "*",
|
||||||
"@digiresilience/metamigo-api": "*",
|
"@digiresilience/metamigo-api": "*",
|
||||||
"@digiresilience/metamigo-worker": "*",
|
"@digiresilience/metamigo-worker": "*",
|
||||||
"commander": "^11.0.0",
|
"commander": "^12.0.0",
|
||||||
"graphile-migrate": "^1.4.1",
|
"graphile-migrate": "^1.4.1",
|
||||||
"graphile-worker": "^0.13.0",
|
"graphile-worker": "^0.13.0",
|
||||||
"node-jose": "^2.2.0",
|
"node-jose": "^2.2.0",
|
||||||
|
|
@ -23,14 +23,14 @@
|
||||||
"graphql": "15.8.0"
|
"graphql": "15.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.3",
|
"@types/jest": "^29.5.12",
|
||||||
"pino-pretty": "^10.0.1",
|
"pino-pretty": "^10.3.1",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.1.0",
|
||||||
"tsconfig-link": "*",
|
"tsconfig-link": "*",
|
||||||
"eslint-config-link": "*",
|
"eslint-config-link": "*",
|
||||||
"jest-config-link": "*",
|
"jest-config-link": "*",
|
||||||
"babel-preset-link": "*",
|
"babel-preset-link": "*",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"migrate": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js db -- migrate",
|
"migrate": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js db -- migrate",
|
||||||
|
|
@ -40,4 +40,4 @@
|
||||||
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
|
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
|
||||||
"test": "echo no tests"
|
"test": "echo no tests"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
generateConfig,
|
generateConfig,
|
||||||
printConfigOptions,
|
printConfigOptions,
|
||||||
} from "@digiresilience/metamigo-common";
|
} from "@digiresilience/metamigo-common";
|
||||||
|
import { IAppConfig, IAppConvict } from "@digiresilience/metamigo-config";
|
||||||
import { loadConfigRaw } from "@digiresilience/metamigo-config";
|
import { loadConfigRaw } from "@digiresilience/metamigo-config";
|
||||||
|
|
||||||
export const genConf = async (): Promise<void> => {
|
export const genConf = async (): Promise<void> => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
import { startWithout } from "@digiresilience/montar";
|
||||||
import { migrateWrapper } from "@digiresilience/metamigo-db";
|
import { migrateWrapper } from "@digiresilience/metamigo-db";
|
||||||
import { loadConfig } from "@digiresilience/metamigo-config";
|
import { loadConfig } from "@digiresilience/metamigo-config";
|
||||||
import { genConf, listConfig } from "./config.js";
|
import { genConf, listConfig } from "./config.js";
|
||||||
|
import { createTokenForTesting, generateJwks } from "./jwks.js";
|
||||||
import { exportGraphqlSchema } from "./metamigo-postgraphile.js";
|
import { exportGraphqlSchema } from "./metamigo-postgraphile.js";
|
||||||
|
import "@digiresilience/metamigo-api";
|
||||||
|
import "@digiresilience/metamigo-worker";
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
export async function runServer(): Promise<void> {
|
||||||
|
await startWithout(["worker"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runWorker(): Promise<void> {
|
||||||
|
await startWithout(["server"]);
|
||||||
|
}
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("config-generate")
|
.command("config-generate")
|
||||||
|
|
@ -19,6 +30,16 @@ program
|
||||||
.description("Prints the entire convict config ")
|
.description("Prints the entire convict config ")
|
||||||
.action(listConfig);
|
.action(listConfig);
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("api")
|
||||||
|
.description("Run the application api server")
|
||||||
|
.action(runServer);
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("worker")
|
||||||
|
.description("Run the worker to process jobs")
|
||||||
|
.action(runWorker);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("db <commands...>")
|
.command("db <commands...>")
|
||||||
.description("Run graphile-migrate commands with your app's config loaded.")
|
.description("Run graphile-migrate commands with your app's config loaded.")
|
||||||
|
|
@ -27,6 +48,16 @@ program
|
||||||
return migrateWrapper(args, config);
|
return migrateWrapper(args, config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("gen-jwks")
|
||||||
|
.description("Generate the JWKS")
|
||||||
|
.action(generateJwks);
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("gen-testing-jwt")
|
||||||
|
.description("Generate a JWT for the test suite")
|
||||||
|
.action(createTokenForTesting);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("export-graphql-schema")
|
.command("export-graphql-schema")
|
||||||
.description("Export the graphql schema")
|
.description("Export the graphql schema")
|
||||||
|
|
|
||||||
67
apps/metamigo-cli/src/jwks.ts
Normal file
67
apps/metamigo-cli/src/jwks.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import jose from "node-jose";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
const generateKeystore = async () => {
|
||||||
|
const keystore = jose.JWK.createKeyStore();
|
||||||
|
await keystore.generate("oct", 256, {
|
||||||
|
alg: "A256GCM",
|
||||||
|
use: "enc",
|
||||||
|
});
|
||||||
|
await keystore.generate("oct", 256, {
|
||||||
|
alg: "HS512",
|
||||||
|
use: "sig",
|
||||||
|
});
|
||||||
|
return keystore;
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeString = (input) =>
|
||||||
|
Buffer.from(JSON.stringify(input)).toString("base64");
|
||||||
|
|
||||||
|
const stringify = (v) => JSON.stringify(v, undefined, 2);
|
||||||
|
|
||||||
|
const _generateJwks = async () => {
|
||||||
|
const keystore = await generateKeystore();
|
||||||
|
const encryption = keystore.all({ use: "enc" })[0].toJSON(true);
|
||||||
|
const signing = keystore.all({ use: "sig" })[0].toJSON(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextAuth: {
|
||||||
|
signingKeyB64: safeString(signing),
|
||||||
|
encryptionKeyB64: safeString(encryption),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateJwks = async (): Promise<void> => {
|
||||||
|
console.log(stringify(await _generateJwks()));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTokenForTesting = async (): Promise<void> => {
|
||||||
|
const keys = await _generateJwks();
|
||||||
|
const signingKey = Buffer.from(
|
||||||
|
JSON.parse(
|
||||||
|
Buffer.from(keys.nextAuth.signingKeyB64, "base64").toString("utf-8")
|
||||||
|
).k,
|
||||||
|
"base64"
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
iss: "Test Env",
|
||||||
|
iat: 1606893960,
|
||||||
|
aud: "metamigo",
|
||||||
|
sub: "abel@guardianproject.info",
|
||||||
|
name: "Abel Luck",
|
||||||
|
email: "abel@guardianproject.info",
|
||||||
|
userRole: "admin",
|
||||||
|
},
|
||||||
|
signingKey,
|
||||||
|
{ expiresIn: "100y", algorithm: "HS512" }
|
||||||
|
);
|
||||||
|
console.log("CONFIG");
|
||||||
|
console.log(stringify(keys));
|
||||||
|
console.log();
|
||||||
|
console.log("TOKEN");
|
||||||
|
console.log(token);
|
||||||
|
console.log();
|
||||||
|
};
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
.git
|
|
||||||
.idea
|
|
||||||
**/node_modules
|
|
||||||
!/node_modules
|
|
||||||
**/build
|
|
||||||
**/dist
|
|
||||||
**/tmp
|
|
||||||
**/.env*
|
|
||||||
**/coverage
|
|
||||||
**/.next
|
|
||||||
**/amigo.*.json
|
|
||||||
**/cypress/videos
|
|
||||||
**/cypress/screenshots
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
node_modules
|
|
||||||
**/dist
|
|
||||||
/data/schema.graphql
|
|
||||||
/data/schema.sql
|
|
||||||
/graphql/index.*
|
|
||||||
/client/.next
|
|
||||||
.next
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
require("eslint-config-link/patch/modern-module-resolution");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
"eslint-config-link/profile/node",
|
|
||||||
"eslint-config-link/profile/typescript",
|
|
||||||
"eslint-config-link/profile/jest",
|
|
||||||
"next",
|
|
||||||
],
|
|
||||||
parserOptions: { tsconfigRootDir: __dirname },
|
|
||||||
};
|
|
||||||
3
apps/metamigo-frontend/.eslintrc.json
Normal file
3
apps/metamigo-frontend/.eslintrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue