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/
|
||||
signald-state/*
|
||||
!./signald-state/.gitkeep
|
||||
baileys-state
|
||||
signald-state
|
||||
project.org
|
||||
|
|
@ -17,7 +17,7 @@ build-all:
|
|||
- turbo 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:
|
||||
- docker:dind
|
||||
stage: docker-build
|
||||
|
|
@ -34,7 +34,7 @@ build-all:
|
|||
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
||||
|
||||
.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:
|
||||
- docker:dind
|
||||
stage: docker-release
|
||||
|
|
@ -51,6 +51,17 @@ build-all:
|
|||
- docker tag ${DOCKER_NS}:${DOCKER_TAG} ${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:
|
||||
extends: .docker-build
|
||||
variables:
|
||||
|
|
@ -84,17 +95,6 @@ metamigo-docker-release:
|
|||
variables:
|
||||
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:
|
||||
extends: .docker-build
|
||||
variables:
|
||||
|
|
@ -200,8 +200,40 @@ zammad-docker-build:
|
|||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad
|
||||
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 --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:
|
||||
extends: .docker-release
|
||||
variables:
|
||||
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
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -14,38 +14,23 @@ Local dev with docker-compose
|
|||
|
||||
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:
|
||||
```
|
||||
npm install
|
||||
npm run docker:metamigo:dev:up # start supporting containers for metamigo
|
||||
npm run build # compile the apps
|
||||
npm run migrate # this migrates the db
|
||||
npm run dev:metamigo # this runs metamigo frontend, api, and worker
|
||||
make dev-metamigo # this starts the containers
|
||||
npm run migrate # this migrates the db
|
||||
npm run dev:metamigo # this runs metamigo frontend and api
|
||||
```
|
||||
|
||||
# TODO
|
||||
|
||||
Notes from abel regarding metamigo. these are in priority order (high priority first)
|
||||
|
||||
- [ ] Do not upgrade: postgraphile, graphql and other postgres dependencies until postgrahile supports a newer grapqhl version
|
||||
* ref: https://github.com/graphile/postgraphile/issues/1583
|
||||
- [ ] 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
|
||||
- [ ] Delete old JWT config stuff
|
||||
- [ ] Consolidate config
|
||||
- [ ] Complete react-admin upgrade.. make all the metamigo-frontend stuff work
|
||||
* https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components
|
||||
- [ ] 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
|
||||
* https://mui.com/material-ui/migration/v5-style-changes/
|
||||
* 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() {
|
||||
return <About />;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { getTemplates } from "app/_lib/opensearch";
|
||||
import { Create } from "./_components/Create";
|
||||
import { Create } from "leafcutter-common";
|
||||
|
||||
export default async function Page() {
|
||||
const templates = await getTemplates(100);
|
||||
|
||||
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() {
|
||||
return <FAQ />;
|
||||
|
|
|
|||
|
|
@ -7,16 +7,12 @@ import "@fontsource/roboto/700.css";
|
|||
import "@fontsource/playfair-display/900.css";
|
||||
// import getConfig from "next/config";
|
||||
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||
import { InternalLayout } from "app/_components/InternalLayout";
|
||||
import { headers } from 'next/headers'
|
||||
import { InternalLayout } from "../_components/InternalLayout";
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const allHeaders = headers();
|
||||
const embedded = Boolean(allHeaders.get('x-leafcutter-embedded'));
|
||||
|
||||
return <InternalLayout embedded={embedded}>{children}</InternalLayout>;
|
||||
return <InternalLayout embedded={false}>{children}</InternalLayout>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { getUserVisualizations } from "app/_lib/opensearch";
|
||||
import { Home } from "app/_components/Home";
|
||||
import { Home } from "leafcutter-common";
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
// import { Client } from "@opensearch-project/opensearch";
|
||||
import { Preview } from "./_components/Preview";
|
||||
import { Preview } from "leafcutter-common";
|
||||
// import { createVisualization } from "lib/opensearch";
|
||||
|
||||
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 { Grid, CircularProgress } from "@mui/material";
|
||||
import Iframe from "react-iframe";
|
||||
import { useAppContext } from "app/_components/AppProvider";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
|
||||
export const Setup: FC = () => {
|
||||
const {
|
||||
|
|
@ -20,6 +20,7 @@ export const Setup: FC = () => {
|
|||
<Grid
|
||||
sx={{ width: "100%", height: 700 }}
|
||||
direction="row"
|
||||
container
|
||||
justifyContent="space-around"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Setup } from './_components/Setup';
|
||||
import { Setup } from "./_components/Setup";
|
||||
|
||||
export default function Page() {
|
||||
return <Setup />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { getTrends } from "app/_lib/opensearch";
|
||||
import { Trends } from "./_components/Trends";
|
||||
import { Trends } from "leafcutter-common";
|
||||
|
||||
export default async function Page() {
|
||||
const visualizations = await getTrends(25);
|
||||
|
||||
return <Trends visualizations={visualizations} />;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import { Client } from "@opensearch-project/opensearch";
|
||||
import { VisualizationDetail } from "app/_components/VisualizationDetail";
|
||||
import { VisualizationDetail } from "leafcutter-common";
|
||||
|
||||
const getVisualization = async (visualizationID: string) => {
|
||||
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 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 visualization = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
bindTrigger,
|
||||
bindMenu,
|
||||
} from "material-ui-popup-state/hooks";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
|
||||
export const AccountButton: FC = () => {
|
||||
const t = useTranslate();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
useState,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
import { colors, typography } from "app/_styles/theme";
|
||||
import { colors, typography } from "leafcutter-common/styles/theme";
|
||||
|
||||
const basePath = process.env.GITLAB_CI
|
||||
? "/link/link-stack/apps/leafcutter"
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { FC, useState } from "react";
|
|||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { Button } from "@mui/material";
|
||||
import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
|
||||
export const HelpButton: FC = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const pathname = usePathname() ?? "";
|
||||
const [helpActive, setHelpActive] = useState(false);
|
||||
const {
|
||||
colors: { leafcutterElectricBlue },
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import CookieConsent from "react-cookie-consent";
|
|||
import { useCookies } from "react-cookie";
|
||||
import { TopNav } from "./TopNav";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { GettingStartedDialog } from "./GettingStartedDialog";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
import { GettingStartedDialog } from "leafcutter-common";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
// import { Footer } from "./Footer";
|
||||
|
||||
type LayoutProps = PropsWithChildren<{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
bindTrigger,
|
||||
bindMenu,
|
||||
} from "material-ui-popup-state/hooks";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
// import { Tooltip } from "./Tooltip";
|
||||
|
||||
export const LanguageSelect = () => {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { SessionProvider } from "next-auth/react";
|
|||
import { CssBaseline } from "@mui/material";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
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 { AppProvider } from "app/_components/AppProvider";
|
||||
import { AppProvider } from "leafcutter-common/components/AppProvider";
|
||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||
import en from "app/_locales/en.json";
|
||||
import fr from "app/_locales/fr.json";
|
||||
import en from "leafcutter-common/locales/en.json";
|
||||
import fr from "leafcutter-common/locales/fr.json";
|
||||
import "@fontsource/poppins/400.css";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
|
|
@ -21,7 +21,7 @@ import "app/_styles/global.css";
|
|||
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
||||
|
||||
LicenseInfo.setLicenseKey(
|
||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI="
|
||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
||||
);
|
||||
|
||||
const messages: any = { en, fr };
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import {
|
|||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslate } from "react-polyglot";
|
||||
import { useAppContext } from "app/_components/AppProvider";
|
||||
import { Tooltip } from "app/_components/Tooltip";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
import { Tooltip } from "leafcutter-common";
|
||||
// import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material";
|
||||
|
||||
const MenuItem = ({
|
||||
|
|
@ -101,8 +101,8 @@ interface SidebarProps {
|
|||
|
||||
export const Sidebar: FC<SidebarProps> = ({ open }) => {
|
||||
const t = useTranslate();
|
||||
const pathname = usePathname();
|
||||
const section = pathname.split("/")[1];
|
||||
const pathname = usePathname() ?? "";
|
||||
const section = pathname?.split("/")[1];
|
||||
const {
|
||||
colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue,
|
||||
} = useAppContext();
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import Image from "next/legacy/image";
|
|||
import { AppBar, Grid, Box } from "@mui/material";
|
||||
import { useTranslate } from "react-polyglot";
|
||||
import LeafcutterLogo from "images/leafcutter-logo.png";
|
||||
import { AccountButton } from "app/_components/AccountButton";
|
||||
import { HelpButton } from "app/_components/HelpButton";
|
||||
import { Tooltip } from "app/_components/Tooltip";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
import { AccountButton } from "./AccountButton";
|
||||
import { HelpButton } from "./HelpButton";
|
||||
import { Tooltip } from "leafcutter-common";
|
||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
||||
// import { LanguageSelect } from "./LanguageSelect";
|
||||
|
||||
export const TopNav: FC = () => {
|
||||
|
|
@ -43,50 +43,51 @@ export const TopNav: FC = () => {
|
|||
wrap="nowrap"
|
||||
spacing={4}
|
||||
>
|
||||
<Link href="/" passHref>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
spacing={1}
|
||||
wrap="nowrap"
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Grid item sx={{ pr: 1 }}>
|
||||
<Image src={LeafcutterLogo} alt="" width={56} height={52} />
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
spacing={1}
|
||||
wrap="nowrap"
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<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 item container direction="column" alignContent="flex-start">
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{
|
||||
...h5,
|
||||
color: leafcutterElectricBlue,
|
||||
p: 0,
|
||||
m: 0,
|
||||
pt: 1,
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
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 item>
|
||||
<Box
|
||||
sx={{
|
||||
...h6,
|
||||
m: 0,
|
||||
p: 0,
|
||||
color: cdrLinkOrange,
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
A Project of Center for Digital Resilience
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Link>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<HelpButton />
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import type { NextAuthOptions } from "next-auth";
|
||||
import Google from "next-auth/providers/google";
|
||||
import Apple from "next-auth/providers/apple";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { checkAuth } from "./opensearch";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/login",
|
||||
signOut: "/logout",
|
||||
},
|
||||
providers: [
|
||||
Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
|
||||
|
|
@ -12,6 +19,62 @@ export const authOptions: NextAuthOptions = {
|
|||
clientId: process.env.APPLE_CLIENT_ID ?? "",
|
||||
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,
|
||||
/*
|
||||
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 userMetadataIndexName = "user_metadata";
|
||||
|
||||
// const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||
|
||||
const baseURL = `https://localhost:9200`;
|
||||
const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||
|
||||
const createClient = () => new Client({
|
||||
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 getEmbedURL = (tenant: string, visualizationID: string) =>
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@ body {
|
|||
overscroll-behavior-y: 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 () => {
|
||||
const results = await getTrends(5);
|
||||
console.log({ 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 getConfig from "next/config";
|
||||
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||
import { MultiProvider } from "app/_components/MultiProvider";
|
||||
import { MultiProvider } from "./_components/MultiProvider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Leafcutter",
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ apiVersion: v2
|
|||
name: leafcutter
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.54
|
||||
appVersion: "0.1.54"
|
||||
version: 0.2.0
|
||||
appVersion: "0.2.0"
|
||||
|
|
|
|||
|
|
@ -1,57 +1,6 @@
|
|||
import { NextResponse } from "next/server";
|
||||
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);
|
||||
}
|
||||
};
|
||||
import { withAuth } from "next-auth/middleware";
|
||||
|
||||
export default withAuth(
|
||||
checkRewrites,
|
||||
{
|
||||
pages: {
|
||||
signIn: `/login`,
|
||||
|
|
@ -60,25 +9,30 @@ export default withAuth(
|
|||
authorized: ({ token, req }) => {
|
||||
const {
|
||||
url,
|
||||
headers,
|
||||
} = req;
|
||||
|
||||
// check login page
|
||||
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;
|
||||
}
|
||||
|
||||
// check session auth
|
||||
const authorizedDomains = ["redaranj.com", "digiresilience.org"];
|
||||
const userDomain = token?.email?.toLowerCase().split("@").pop() ?? "unauthorized.net";
|
||||
|
||||
if (authorizedDomains.includes(userDomain)) {
|
||||
if (token?.email) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -7,9 +7,19 @@ const ContentSecurityPolicy = `
|
|||
`;
|
||||
|
||||
module.exports = {
|
||||
publicRuntimeConfig: {
|
||||
embedded: true
|
||||
},/*
|
||||
transpilePackages: ["leafcutter-common"],
|
||||
experimental: {
|
||||
missingSuspenseWithCSRBailout: false,
|
||||
},
|
||||
rewrites: async () => ({
|
||||
fallback: [
|
||||
{
|
||||
source: "/:path*",
|
||||
destination: "/api/proxy/:path*",
|
||||
},
|
||||
],
|
||||
}),
|
||||
/*
|
||||
basePath: "/proxy/leafcutter",
|
||||
assetPrefix: "/proxy/leafcutter",
|
||||
i18n: {
|
||||
|
|
@ -17,25 +27,19 @@ module.exports = {
|
|||
defaultLocale: "en",
|
||||
},
|
||||
*/
|
||||
/* rewrites: async () => ({
|
||||
fallback: [
|
||||
{
|
||||
source: "/:path*",
|
||||
destination: "/api/proxy/:path*",
|
||||
},
|
||||
],
|
||||
}) */
|
||||
|
||||
/*
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
/*
|
||||
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
|
||||
},
|
||||
*/
|
||||
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload'
|
||||
|
|
@ -52,4 +56,5 @@ module.exports = {
|
|||
},
|
||||
]
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"login": "aws sso login --sso-session cdr",
|
||||
"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: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",
|
||||
"start": "next start",
|
||||
"export": "next export",
|
||||
|
|
@ -14,51 +14,53 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/playfair-display": "^5.0.5",
|
||||
"@fontsource/poppins": "^5.0.5",
|
||||
"@fontsource/roboto": "^5.0.5",
|
||||
"@fontsource/playfair-display": "^5.0.21",
|
||||
"@fontsource/poppins": "^5.0.12",
|
||||
"@fontsource/roboto": "^5.0.12",
|
||||
"@mui/icons-material": "^5",
|
||||
"@mui/lab": "^5.0.0-alpha.136",
|
||||
"@mui/lab": "^5.0.0-alpha.167",
|
||||
"@mui/material": "^5",
|
||||
"@mui/x-data-grid-pro": "^6.10.0",
|
||||
"@mui/x-date-pickers-pro": "^6.10.0",
|
||||
"@opensearch-project/opensearch": "^2.3.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"@mui/x-data-grid-pro": "^6.19.6",
|
||||
"@mui/x-date-pickers-pro": "^6.19.6",
|
||||
"@opensearch-project/opensearch": "^2.5.0",
|
||||
"cryptr": "^6.3.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"material-ui-popup-state": "^5.0.9",
|
||||
"next": "13.4.10",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-http-proxy-middleware": "^1.2.5",
|
||||
"nodemailer": "^6.9.3",
|
||||
"leafcutter-common": "*",
|
||||
"material-ui-popup-state": "^5.0.10",
|
||||
"next": "14.1.2",
|
||||
"next-auth": "^4.24.6",
|
||||
"next-http-proxy-middleware": "^1.2.6",
|
||||
"nodemailer": "^6.9.11",
|
||||
"react": "18.2.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-cookie-consent": "^8.0.1",
|
||||
"react-cookie": "^7.1.0",
|
||||
"react-cookie-consent": "^9.0.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-iframe": "^1.8.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-polyglot": "^0.7.2",
|
||||
"sharp": "^0.32.3",
|
||||
"swr": "^2.2.0",
|
||||
"tss-react": "^4.8.8",
|
||||
"uuid": "^9.0.0"
|
||||
"sharp": "^0.33.2",
|
||||
"swr": "^2.2.5",
|
||||
"tss-react": "^4.9.4",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.9",
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "18.2.63",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"babel-loader": "^9.1.3",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^13.4.10",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"typescript": "5.1.6"
|
||||
"eslint-config-next": "^14.1.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"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";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Box, Grid, Container, IconButton } from "@mui/material";
|
||||
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
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 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 = {
|
||||
session: any;
|
||||
|
|
@ -14,62 +29,198 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
|||
typeof window !== "undefined" && 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 = {
|
||||
borderRadius: 500,
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
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 (
|
||||
<>
|
||||
<Grid container direction="row-reverse" sx={{ p: 3 }}>
|
||||
<Grid item />
|
||||
</Grid>
|
||||
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
|
||||
<Box sx={{ backgroundColor: darkGray, height: "100vh" }}>
|
||||
<Container maxWidth="md" sx={{ p: 10 }}>
|
||||
<Grid container spacing={2} direction="column" alignItems="center">
|
||||
<Grid item>
|
||||
<Box sx={{ maxWidth: 200 }} />
|
||||
</Grid>
|
||||
<Grid item sx={{ textAlign: "center" }} />
|
||||
|
||||
<Grid item>
|
||||
{!session ? (
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
sx={{ width: 450, mt: 1 }}
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{
|
||||
width: "70px",
|
||||
height: "70px",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<Grid item sx={{ width: "100%" }}>
|
||||
<IconButton
|
||||
sx={buttonStyles}
|
||||
onClick={() =>
|
||||
signIn("google", {
|
||||
callbackUrl: `${origin}/setup`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<GoogleIcon sx={{ mr: 1 }} />
|
||||
Google
|
||||
</IconButton>
|
||||
<Image
|
||||
src={LinkLogo}
|
||||
alt="Link logo"
|
||||
width={70}
|
||||
height={70}
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
filter: "grayscale(100) brightness(100)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</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 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>
|
||||
</Container>
|
||||
) : null}
|
||||
{session ? (
|
||||
<Box component="h4">
|
||||
|
|
@ -79,6 +230,6 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
|||
</Grid>
|
||||
</Grid>
|
||||
</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 { 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";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -17,12 +17,16 @@ import {
|
|||
import {
|
||||
FeaturedPlayList as FeaturedPlayListIcon,
|
||||
Person as PersonIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Insights as InsightsIcon,
|
||||
Logout as LogoutIcon,
|
||||
Cottage as CottageIcon,
|
||||
Settings as SettingsIcon,
|
||||
ExpandCircleDown as ExpandCircleDownIcon,
|
||||
Dvr as DvrIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
LibraryBooks as LibraryBooksIcon,
|
||||
School as SchoolIcon,
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
|
@ -30,6 +34,7 @@ import Image from "next/image";
|
|||
import LinkLogo from "public/link-logo-small.png";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
||||
import { SearchBox } from "./SearchBox";
|
||||
|
||||
const openWidth = 270;
|
||||
const closedWidth = 100;
|
||||
|
|
@ -162,20 +167,31 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const username = session?.user?.name || "User";
|
||||
// @ts-ignore
|
||||
const roles = session?.user?.roles || [];
|
||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
||||
{
|
||||
document: getTicketOverviewCountsQuery,
|
||||
},
|
||||
{ refreshInterval: 10000 }
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
const findOverviewCountByID = (id: number) =>
|
||||
overviewData?.ticketOverviews?.edges?.find((overview: any) =>
|
||||
overview.node.id.endsWith(`/${id}`)
|
||||
const findOverviewByName = (name: string) =>
|
||||
overviewData?.ticketOverviews?.edges?.find(
|
||||
(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;
|
||||
const assignedCount = findOverviewCountByID(1);
|
||||
const urgentCount = findOverviewCountByID(7);
|
||||
const pendingCount = findOverviewCountByID(3);
|
||||
const unassignedCount = findOverviewCountByID(2);
|
||||
const recentCount = 0;
|
||||
const assignedID = findOverviewByName("My Assigned Tickets");
|
||||
const assignedCount = findOverviewCountByID(assignedID);
|
||||
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 = () => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
|
|
@ -221,6 +237,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
direction="column"
|
||||
justifyContent="space-between"
|
||||
wrap="nowrap"
|
||||
spacing={0}
|
||||
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
|
||||
>
|
||||
<Grid item container>
|
||||
|
|
@ -307,10 +324,30 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
</Grid>
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{ height: "0.5px", width: "100%", backgroundColor: "#666" }}
|
||||
sx={{
|
||||
height: "0.5px",
|
||||
width: "100%",
|
||||
backgroundColor: "#666",
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
component="nav"
|
||||
sx={{
|
||||
|
|
@ -359,7 +396,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
/>
|
||||
<MenuItem
|
||||
name="Tickets"
|
||||
href="/overview/assigned"
|
||||
href="/overview/recent"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
selected={
|
||||
pathname.startsWith("/overview") ||
|
||||
|
|
@ -379,12 +416,21 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
>
|
||||
<List component="div" disablePadding>
|
||||
<MenuItem
|
||||
name="Assigned"
|
||||
href="/overview/assigned"
|
||||
name="Recent"
|
||||
href="/overview/recent"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/assigned")}
|
||||
badge={assignedCount}
|
||||
selected={pathname.endsWith("/overview/recent")}
|
||||
badge={recentCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Open"
|
||||
href="/overview/open"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/open")}
|
||||
badge={openCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
@ -397,12 +443,12 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Pending"
|
||||
href="/overview/pending"
|
||||
name="Assigned"
|
||||
href="/overview/assigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/pending")}
|
||||
badge={pendingCount}
|
||||
selected={pathname.endsWith("/overview/assigned")}
|
||||
badge={assignedCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
@ -419,17 +465,33 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
<MenuItem
|
||||
name="Knowledge Base"
|
||||
href="/knowledge"
|
||||
Icon={CottageIcon}
|
||||
Icon={SchoolIcon}
|
||||
iconSize={20}
|
||||
selected={pathname.endsWith("/knowledge")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Leafcutter"
|
||||
href="/leafcutter/about"
|
||||
Icon={AnalyticsIcon}
|
||||
name="Documentation"
|
||||
href="/docs"
|
||||
Icon={LibraryBooksIcon}
|
||||
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}
|
||||
/>
|
||||
<Collapse
|
||||
|
|
@ -439,7 +501,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
onClick={undefined}
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
{/*
|
||||
<MenuItem
|
||||
name="Dashboard"
|
||||
href="/leafcutter"
|
||||
|
|
@ -447,7 +508,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
selected={pathname.endsWith("/leafcutter")}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
name="Search and Create"
|
||||
href="/leafcutter/create"
|
||||
|
|
@ -455,7 +515,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
selected={pathname.endsWith("/leafcutter/create")}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
name="Trends"
|
||||
href="/leafcutter/trends"
|
||||
|
|
@ -463,7 +522,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
selected={pathname.endsWith("/leafcutter/trends")}
|
||||
open={open}
|
||||
/>
|
||||
*/}
|
||||
<MenuItem
|
||||
name="FAQ"
|
||||
href="/leafcutter/faq"
|
||||
|
|
@ -475,7 +533,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
<MenuItem
|
||||
name="About"
|
||||
href="/leafcutter/about"
|
||||
Icon={AnalyticsIcon}
|
||||
Icon={InsightsIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/leafcutter/about")}
|
||||
open={open}
|
||||
|
|
@ -490,49 +548,57 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
selected={pathname.endsWith("/profile")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
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>
|
||||
{roles.includes("admin") && (
|
||||
<>
|
||||
<MenuItem
|
||||
name="Zammad Settings"
|
||||
name="Admin"
|
||||
href="/admin/zammad"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/zammad")}
|
||||
Icon={SettingsIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Metamigo"
|
||||
href="/admin/metamigo"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/metamigo")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Label Studio"
|
||||
href="/admin/label-studio"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/label-studio")}
|
||||
open={open}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
<Collapse
|
||||
in={pathname.startsWith("/admin/")}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
onClick={undefined}
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
<MenuItem
|
||||
name="Zammad Settings"
|
||||
href="/admin/zammad"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/zammad")}
|
||||
open={open}
|
||||
/>
|
||||
{false && roles.includes("metamigo") && (
|
||||
<MenuItem
|
||||
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
|
||||
name="Zammad Interface"
|
||||
href="/proxy/zammad"
|
||||
href="/zammad"
|
||||
Icon={DvrIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
|||
columns,
|
||||
rows,
|
||||
onRowClick,
|
||||
height = "calc(100vh - 20px)",
|
||||
height = "100%",
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
}) => {
|
||||
|
|
@ -43,6 +43,13 @@ export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
|||
border: 0,
|
||||
width: "100%",
|
||||
height,
|
||||
".MuiDataGrid-row": {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: "#1982fc33 !important",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
".MuiDataGrid-row:nth-of-type(1n)": {
|
||||
backgroundColor: "#f3f3f3",
|
||||
},
|
||||
|
|
@ -66,12 +73,14 @@ export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
|||
rows={rows}
|
||||
columns={columns}
|
||||
density="compact"
|
||||
hideFooter
|
||||
pagination
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
pageSizeOptions={[5, 10, 25]}
|
||||
paginationMode="client"
|
||||
sx={{ height }}
|
||||
rowBuffer={30}
|
||||
checkboxSelection={!!setSelectedRows}
|
||||
onRowSelectionModelChange={setSelectedRows}
|
||||
rowSelectionModel={selectedRows}
|
||||
rowHeight={46}
|
||||
scrollbarSize={0}
|
||||
disableVirtualization
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { FC, useState, useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Iframe from "react-iframe";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Box, Grid, CircularProgress } from "@mui/material";
|
||||
|
||||
type ZammadWrapperProps = {
|
||||
path: string;
|
||||
|
|
@ -15,68 +16,146 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
|||
hideSidebar = true,
|
||||
}) => {
|
||||
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 url = `/proxy/zammad${path}`;
|
||||
console.log({ url });
|
||||
const url = `/zammad${path}`;
|
||||
const id = url.replace(/[^a-zA-Z0-9]/g, "");
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Iframe
|
||||
id="zammad"
|
||||
url={url}
|
||||
width="100%"
|
||||
height="100%"
|
||||
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";
|
||||
useEffect(() => {
|
||||
const hash = window?.location?.hash;
|
||||
if (hash && hash.startsWith("#ticket/zoom/")) {
|
||||
const ticketID = hash.split("/").pop();
|
||||
router.push(`/tickets/${ticketID}`);
|
||||
}
|
||||
setHashCheckComplete(true);
|
||||
});
|
||||
|
||||
if (hideSidebar) {
|
||||
// @ts-ignore
|
||||
linkElement.contentDocument.querySelector(".sidebar").style =
|
||||
"display: none";
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!hashCheckComplete) return;
|
||||
|
||||
// @ts-ignore
|
||||
if (linkElement.contentDocument.querySelector(".overview-header")) {
|
||||
// @ts-ignore
|
||||
(
|
||||
linkElement.contentDocument.querySelector(
|
||||
".overview-header"
|
||||
) as any
|
||||
).style = "display: none";
|
||||
}
|
||||
const checkAuthenticated = async () => {
|
||||
const res = await fetch("/zammad/auth/sso", {
|
||||
method: "GET",
|
||||
redirect: "manual",
|
||||
});
|
||||
console.log({ res });
|
||||
if (res.type === "opaqueredirect") {
|
||||
setAuthenticated(true);
|
||||
} else {
|
||||
setAuthenticated(false);
|
||||
}
|
||||
};
|
||||
|
||||
setDisplay("inherit");
|
||||
checkAuthenticated();
|
||||
}, [path, hashCheckComplete]);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (session === null) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (session === null) {
|
||||
router.push("/login");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, 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%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={"https://label-studio:3000"}
|
||||
id="label-studio"
|
||||
url={"/label-studio"}
|
||||
width="100%"
|
||||
height="100%"
|
||||
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 { ZammadWrapper } from "../../../(main)/_components/ZammadWrapper";
|
||||
import { ZammadWrapper } from "app/(main)/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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() {
|
||||
redirect("/leafcutter/home");
|
||||
export default async function Page() {
|
||||
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";
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { Grid, Box } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
||||
import { StyledDataGrid } from "../../../_components/StyledDataGrid";
|
||||
import { Button } from "../../../../_components/Button";
|
||||
import { typography } from "../../../../_styles/theme";
|
||||
import { StyledDataGrid } from "app/(main)/_components/StyledDataGrid";
|
||||
import { Button } from "app/_components/Button";
|
||||
import { typography } from "app/_styles/theme";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { TicketCreateDialog } from "./TicketCreateDialog";
|
||||
|
||||
interface TicketListProps {
|
||||
title: string;
|
||||
|
|
@ -14,6 +15,7 @@ interface TicketListProps {
|
|||
}
|
||||
|
||||
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
let gridColumns: GridColDef[] = [
|
||||
{
|
||||
|
|
@ -24,12 +26,24 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
|||
{
|
||||
field: "title",
|
||||
headerName: "Title",
|
||||
flex: 1,
|
||||
flex: 5,
|
||||
},
|
||||
{
|
||||
field: "customer",
|
||||
headerName: "Sender",
|
||||
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,
|
||||
},
|
||||
{
|
||||
|
|
@ -39,47 +53,58 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
|||
flex: 1,
|
||||
},
|
||||
];
|
||||
console.log({ tickets });
|
||||
|
||||
const rowClick = ({ row }) => {
|
||||
router.push(`/tickets/${row.internalId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||
<Grid container direction="column">
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#ddd",
|
||||
px: "8px",
|
||||
pb: "16px",
|
||||
...typography.h4,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
<>
|
||||
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||
<Grid container direction="column">
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#ddd",
|
||||
px: "8px",
|
||||
pb: "16px",
|
||||
...typography.h4,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
href={""}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
text="Create"
|
||||
color="#1982FC"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button href="/tickets/create" text="Create" color="#1982FC" />
|
||||
<StyledDataGrid
|
||||
name={title}
|
||||
columns={gridColumns}
|
||||
rows={tickets}
|
||||
onRowClick={rowClick}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<StyledDataGrid
|
||||
name={title}
|
||||
columns={gridColumns}
|
||||
rows={tickets}
|
||||
onRowClick={rowClick}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
<TicketCreateDialog
|
||||
open={dialogOpen}
|
||||
closeDialog={() => setDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,32 +1,113 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { TicketList } from "./TicketList";
|
||||
import { getTicketsByOverviewQuery } from "../../../../_graphql/getTicketsByOverviewQuery";
|
||||
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
||||
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
|
||||
|
||||
type ZammadOverviewProps = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
|
||||
const [overviewID, setOverviewID] = useState(null);
|
||||
const [tickets, setTickets] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: `gid://zammad/Overview/${id}` },
|
||||
document: getTicketOverviewCountsQuery,
|
||||
},
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
|
||||
const shouldRender = !ticketError && ticketData;
|
||||
const tickets =
|
||||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldRender && <TicketList title={name} tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</>
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: overviewID, pageSize: 250 },
|
||||
},
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
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";
|
||||
|
||||
import { DisplayError } from "../../../_components/DisplayError";
|
||||
import { DisplayError } from "app/_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
|
|
|
|||
|
|
@ -21,13 +21,6 @@ export async function generateMetadata({
|
|||
};
|
||||
}
|
||||
|
||||
const overviews = {
|
||||
assigned: 1,
|
||||
unassigned: 2,
|
||||
pending: 3,
|
||||
urgent: 7,
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
overview: string;
|
||||
|
|
@ -37,5 +30,5 @@ type PageProps = {
|
|||
export default function Page({ params: { overview } }: PageProps) {
|
||||
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 { 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 = () => {
|
||||
const router = useRouter();
|
||||
|
|
@ -10,5 +11,21 @@ export const Setup: FC = () => {
|
|||
setTimeout(() => router.push("/"), 4000);
|
||||
}, [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,
|
||||
} from "@mui/material";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { updateTicketMutation } from "../../../../../_graphql/updateTicketMutation";
|
||||
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
||||
|
||||
interface ArticleCreateDialogProps {
|
||||
ticketID: string;
|
||||
open: boolean;
|
||||
closeDialog: () => void;
|
||||
kind: "reply" | "note";
|
||||
kind: string;
|
||||
recipient?: string;
|
||||
}
|
||||
|
||||
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||
|
|
@ -24,22 +25,29 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
|||
open,
|
||||
closeDialog,
|
||||
kind,
|
||||
recipient,
|
||||
}) => {
|
||||
const [body, setBody] = useState("");
|
||||
const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620";
|
||||
const color = kind === "reply" ? "white" : "black";
|
||||
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
|
||||
const color = kind === "note" ? "black" : "white";
|
||||
const { fetcher } = useSWRConfig();
|
||||
const article = {
|
||||
body,
|
||||
type: kind,
|
||||
internal: kind === "note",
|
||||
};
|
||||
|
||||
if (kind === "email") {
|
||||
article["to"] = recipient;
|
||||
}
|
||||
|
||||
const createArticle = async () => {
|
||||
await fetcher({
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
||||
input: {
|
||||
article: {
|
||||
body,
|
||||
type: kind === "note" ? "note" : "phone",
|
||||
internal: kind === "note",
|
||||
},
|
||||
article,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -51,7 +59,7 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
|||
<Dialog open={open} maxWidth="sm" fullWidth>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label={kind === "reply" ? "Write reply" : "Write internal note"}
|
||||
label={kind === "note" ? "Write internal note" : "Write reply"}
|
||||
multiline
|
||||
rows={10}
|
||||
fullWidth
|
||||
|
|
@ -92,7 +100,7 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
|||
}}
|
||||
onClick={createArticle}
|
||||
>
|
||||
{kind === "reply" ? "Send Reply" : "Save Note"}
|
||||
{kind === "note" ? "Save Note" : "Send Reply"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { getTicketQuery } from "../../../../../_graphql/getTicketQuery";
|
||||
import { getTicketArticlesQuery } from "../../../../../_graphql/getTicketArticlesQuery";
|
||||
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
|
|
@ -28,12 +28,14 @@ interface TicketDetailProps {
|
|||
}
|
||||
|
||||
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [articleKind, setArticleKind] = useState("note");
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketQuery,
|
||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||
},
|
||||
{ refreshInterval: 100000 },
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
const { data: ticketArticlesData, error: ticketArticlesError }: any = useSWR(
|
||||
{
|
||||
|
|
@ -43,12 +45,21 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
{ 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 ticketArticles = ticketArticlesData?.ticketArticles;
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
|
||||
const closeDialog = () => setDialogOpen(false);
|
||||
|
||||
const firstArticle = ticketArticles?.edges[0]?.node;
|
||||
const firstArticleKind = firstArticle?.type?.name ?? "phone";
|
||||
const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? "";
|
||||
const recipient = firstEmailSender;
|
||||
const shouldRender =
|
||||
ticketData && !ticketError && ticketArticlesData && !ticketArticlesError;
|
||||
|
||||
|
|
@ -89,11 +100,11 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
article.internal
|
||||
? "internal-note"
|
||||
: article?.sender?.name === "Agent"
|
||||
? "outgoing-message"
|
||||
: "incoming-message"
|
||||
? "outgoing-message"
|
||||
: "incoming-message"
|
||||
}
|
||||
model={{
|
||||
message: article.body.replace(/<div>*<br>*<div>/g, ""),
|
||||
message: article.bodyWithUrls,
|
||||
sentTime: article.updated_at,
|
||||
sender: article.from,
|
||||
direction:
|
||||
|
|
@ -139,7 +150,7 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
mt: 2,
|
||||
}}
|
||||
onClick={() => {
|
||||
setArticleKind("reply");
|
||||
setArticleKind(firstArticleKind);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
|
|
@ -179,6 +190,7 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
|||
open={dialogOpen}
|
||||
closeDialog={closeDialog}
|
||||
kind={articleKind}
|
||||
recipient={recipient}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ import {
|
|||
} from "@mui/material";
|
||||
import { MuiChipsInput } from "mui-chips-input";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { getTicketQuery } from "../../../../../_graphql/getTicketQuery";
|
||||
import { updateTicketMutation } from "../../../../../_graphql/updateTicketMutation";
|
||||
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
||||
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
|
||||
interface TicketEditProps {
|
||||
id: string;
|
||||
|
|
@ -25,6 +27,8 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
const [selectedOwner, setSelectedOwner] = useState("");
|
||||
const [selectedPriority, setSelectedPriority] = useState("");
|
||||
const [selectedState, setSelectedState] = useState("");
|
||||
const [pendingDate, setPendingDate] = useState(new Date());
|
||||
const [pendingVisible, setPendingVisible] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
const handleDelete = () => {
|
||||
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: states } = useSWR("/api/v1/ticket_states", 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 { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketQuery,
|
||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||
},
|
||||
{ refreshInterval: 100000 },
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
useEffect(() => {
|
||||
const ticket = ticketData?.ticket;
|
||||
|
|
@ -59,14 +68,15 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
setSelectedTags(ticket.tags);
|
||||
}
|
||||
}, [ticketData, ticketError]);
|
||||
const updateTicket = async () => {
|
||||
const input = {
|
||||
ownerId: `gid://zammad/User/${selectedOwner}`,
|
||||
priorityId: `gid://zammad/Ticket::Priority/${selectedPriority}`,
|
||||
stateId: `gid://zammad/Ticket::State/${selectedState}`,
|
||||
groupId: `gid://zammad/Group/${selectedGroup}`,
|
||||
// tags: selectedTags,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const stateName = filteredStates?.find(
|
||||
(state: any) => state.id === selectedState,
|
||||
)?.name;
|
||||
setPendingVisible(stateName?.includes("pending") ?? false);
|
||||
}, [selectedState]);
|
||||
const updateTicket = async (input: any) => {
|
||||
console.log({ input });
|
||||
const res = await fetcher({
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
|
|
@ -76,6 +86,17 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
});
|
||||
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;
|
||||
|
||||
return (
|
||||
|
|
@ -88,8 +109,11 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
defaultValue={selectedGroup}
|
||||
value={selectedGroup}
|
||||
onChange={(e: any) => {
|
||||
setSelectedGroup(e.target.value);
|
||||
updateTicket();
|
||||
const newGroup = e.target.value;
|
||||
setSelectedGroup(newGroup);
|
||||
updateTicket({
|
||||
groupId: `gid://zammad/Group/${newGroup}`,
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
|
|
@ -109,8 +133,9 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
<Select
|
||||
value={selectedOwner}
|
||||
onChange={(e: any) => {
|
||||
setSelectedOwner(e.target.value);
|
||||
updateTicket();
|
||||
const newOwner = e.target.value;
|
||||
setSelectedOwner(newOwner);
|
||||
updateTicket({ ownerId: `gid://zammad/User/${newOwner}` });
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
|
|
@ -118,20 +143,24 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{users?.map((user: any) => (
|
||||
<MenuItem key={user.id} value={user.id}>
|
||||
{agents?.map((user: any) => (
|
||||
<MenuItem key={user.id} value={`${user.id}`}>
|
||||
{user.firstname} {user.lastname}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ m: 1, mt: 0 }}>State</Box>
|
||||
<Select
|
||||
value={selectedState}
|
||||
onChange={(e: any) => {
|
||||
setSelectedState(e.target.value);
|
||||
updateTicket();
|
||||
const newState = e.target.value;
|
||||
setSelectedState(newState);
|
||||
updateTicket({
|
||||
stateId: `gid://zammad/Ticket::State/${newState}`,
|
||||
pendingTime: pendingDate.toISOString(),
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
|
|
@ -139,20 +168,45 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{states?.map((state: any) => (
|
||||
{filteredStates?.map((state: any) => (
|
||||
<MenuItem key={state.id} value={state.id}>
|
||||
{state.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</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>
|
||||
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
|
||||
<Select
|
||||
value={selectedPriority}
|
||||
onChange={(e: any) => {
|
||||
setSelectedPriority(e.target.value);
|
||||
updateTicket();
|
||||
const newPriority = e.target.value;
|
||||
setSelectedPriority(newPriority);
|
||||
updateTicket({
|
||||
priorityId: `gid://zammad/Ticket::Priority/${newPriority}`,
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
|
|
@ -175,7 +229,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
|||
value={selectedTags}
|
||||
onChange={(tags: any) => {
|
||||
setSelectedTags(tags);
|
||||
updateTicket();
|
||||
updateTags(tags);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { DisplayError } from "../../../_components/DisplayError";
|
||||
import { DisplayError } from "app/_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ interface ButtonProps {
|
|||
text: string;
|
||||
color: 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>
|
||||
<MUIButton
|
||||
variant="contained"
|
||||
disableElevation
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { FC, PropsWithChildren, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||
import { SWRConfig } from "swr";
|
||||
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 { LicenseInfo } from "@mui/x-date-pickers-pro";
|
||||
import { locales } from "leafcutter-common";
|
||||
|
||||
LicenseInfo.setLicenseKey(
|
||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI="
|
||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
||||
);
|
||||
|
||||
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
|
|
@ -21,25 +24,83 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
: null;
|
||||
const client = new GraphQLClient(`${origin}/proxy/zammad/graphql`, {
|
||||
headers: {
|
||||
const client = new GraphQLClient(`${origin}/zammad/graphql`);
|
||||
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",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const graphQLFetcher = async ({ document, variables }: any) => {
|
||||
const requestHeaders = {
|
||||
"X-CSRF-Token": csrfToken,
|
||||
};
|
||||
const { data, headers } = await client.rawRequest(
|
||||
document,
|
||||
variables,
|
||||
requestHeaders
|
||||
);
|
||||
let responseData = null;
|
||||
let responseHeaders = new Headers();
|
||||
let responseStatus = null;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
@ -47,11 +108,13 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||
<>
|
||||
<CssBaseline />
|
||||
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
||||
<SWRConfig value={{ fetcher: graphQLFetcher }}>
|
||||
<SWRConfig value={{ fetcher: multiFetcher }}>
|
||||
<SessionProvider>
|
||||
<CookiesProvider>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
{children}
|
||||
<I18n locale={locale} messages={messages[locale]}>
|
||||
{children}
|
||||
</I18n>
|
||||
</LocalizationProvider>
|
||||
</CookiesProvider>
|
||||
</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 {
|
||||
node {
|
||||
id
|
||||
body
|
||||
bodyWithUrls
|
||||
internal
|
||||
type {
|
||||
name
|
||||
|
|
@ -14,6 +14,11 @@ query getTicketArticles($ticketId: ID!) {
|
|||
sender {
|
||||
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) {
|
||||
ticket {
|
||||
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';
|
||||
|
||||
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 };
|
||||
|
|
@ -2,69 +2,48 @@ import { NextResponse } from 'next/server';
|
|||
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
|
||||
|
||||
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();
|
||||
if (request.nextUrl.pathname.startsWith('/api/v1/reports/sets')) {
|
||||
console.log(request.nextUrl.searchParams.get("sheet"));
|
||||
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.set(key, value as string);
|
||||
}
|
||||
|
||||
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 linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
|
||||
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-frontend:3000";
|
||||
const metamigoURL = process.env.METAMIGO_URL ?? "http://metamigo-api: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('/proxy/leafcutter')) {
|
||||
const headers = { 'X-Leafcutter-Embedded': "true" };
|
||||
return rewriteURL(request, linkBaseURL, leafcutterURL, headers);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/metamigo')) {
|
||||
return rewriteURL(request, linkBaseURL, metamigoURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/zammad')) {
|
||||
console.log('proxying to zammad');
|
||||
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');
|
||||
if (request.nextUrl.pathname.startsWith('/metamigo')) {
|
||||
return rewriteURL(request, `${linkBaseURL}/metamigo`, metamigoURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/label-studio')) {
|
||||
return rewriteURL(request, `${linkBaseURL}/label-studio`, labelStudioURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/zammad')) {
|
||||
return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers);
|
||||
} else if (request.nextUrl.pathname.startsWith('/auth/sso') || request.nextUrl.pathname.startsWith('/assets')) {
|
||||
return rewriteURL(request, linkBaseURL, zammadURL, headers);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/api') || request.nextUrl.pathname.startsWith('/proxy/assets')) {
|
||||
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(
|
||||
|
|
@ -77,20 +56,19 @@ export default withAuth(
|
|||
authorized: ({ token, req }) => {
|
||||
const {
|
||||
url,
|
||||
headers,
|
||||
} = req;
|
||||
|
||||
// check login page
|
||||
const noAuthPaths = ["/login", "/api/v1"];
|
||||
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;
|
||||
}
|
||||
|
||||
// check session auth
|
||||
const authorizedDomains = ["redaranj.com", "digiresilience.org"];
|
||||
const userDomain = token?.email?.toLowerCase().split("@").pop() ?? "unauthorized.net";
|
||||
|
||||
if (authorizedDomains.includes(userDomain)) {
|
||||
const roles: any = token?.roles ?? [];
|
||||
if (roles.includes("admin") || roles.includes("agent") || process.env.SETUP_MODE === "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} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
missingSuspenseWithCSRBailout: false,
|
||||
},
|
||||
modularizeImports: {
|
||||
"@mui/material": {
|
||||
transform: "@mui/material/{{member}}",
|
||||
|
|
@ -9,10 +12,13 @@ const nextConfig = {
|
|||
transform: "@mui/icons-material/{{member}}",
|
||||
},
|
||||
},
|
||||
transpilePackages: ["leafcutter-common"],
|
||||
publicRuntimeConfig: {
|
||||
linkURL: process.env.LINK_URL ?? "http://localhost:3000",
|
||||
leafcutterURL: process.env.LEAFCUTTER_URL ?? "http://localhost:3001",
|
||||
metamigoURL: process.env.METAMIGO_URL ?? "http://localhost:3002",
|
||||
leafcutterURL:
|
||||
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 ?? "",
|
||||
},
|
||||
async rewrites() {
|
||||
|
|
@ -20,7 +26,7 @@ const nextConfig = {
|
|||
fallback: [
|
||||
{
|
||||
source: "/:path*",
|
||||
destination: `/proxy/zammad/:path*`,
|
||||
destination: `/proxy/leafcutter/:path*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,49 +9,61 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"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",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/playfair-display": "^5.0.5",
|
||||
"@fontsource/poppins": "^5.0.5",
|
||||
"@fontsource/roboto": "^5.0.5",
|
||||
"@fontsource/playfair-display": "^5.0.21",
|
||||
"@fontsource/poppins": "^5.0.12",
|
||||
"@fontsource/roboto": "^5.0.12",
|
||||
"@mui/icons-material": "^5",
|
||||
"@mui/lab": "^5.0.0-alpha.136",
|
||||
"@mui/lab": "^5.0.0-alpha.167",
|
||||
"@mui/material": "^5",
|
||||
"@mui/x-data-grid-pro": "^6.10.0",
|
||||
"@mui/x-date-pickers-pro": "^6.10.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"@mui/x-data-grid-pro": "^6.19.6",
|
||||
"@mui/x-date-pickers-pro": "^6.19.6",
|
||||
"cryptr": "^6.3.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"material-ui-popup-state": "^5.0.9",
|
||||
"mui-chips-input": "^2.0.2",
|
||||
"next": "13.4.10",
|
||||
"next-auth": "^4.22.1",
|
||||
"leafcutter-common": "*",
|
||||
"material-ui-popup-state": "^5.0.10",
|
||||
"mui-chips-input": "^2.1.4",
|
||||
"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-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-iframe": "^1.8.5",
|
||||
"react-polyglot": "^0.7.2",
|
||||
"sharp": "^0.32.3",
|
||||
"swr": "^2.2.0",
|
||||
"tss-react": "^4.8.8"
|
||||
"react-qr-code": "^2.0.12",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"sharp": "^0.33.2",
|
||||
"swr": "^2.2.5",
|
||||
"tss-react": "^4.9.4",
|
||||
"twilio-client": "^1.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.9",
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "18.2.63",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"babel-loader": "^9.1.3",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^13.4.10",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"typescript": "5.1.6"
|
||||
"eslint-config-next": "^14.1.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
|
|
@ -19,10 +15,7 @@
|
|||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*",
|
||||
"../../node_modules/*"
|
||||
]
|
||||
"@/*": ["./*", "../../node_modules/*"]
|
||||
},
|
||||
"baseUrl": ".",
|
||||
"plugins": [
|
||||
|
|
@ -35,9 +28,8 @@
|
|||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
"../leafcutter/app/(login)/login/link/_components/AutoLogin.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,49 +17,49 @@
|
|||
"@graphile-contrib/pg-simplify-inflector": "^6.1.0",
|
||||
"@hapi/basic": "^7.0.2",
|
||||
"@hapi/boom": "^10.0.1",
|
||||
"@hapi/vision": "^7.0.2",
|
||||
"@hapi/vision": "^7.0.3",
|
||||
"@hapi/wreck": "^18.0.1",
|
||||
"@hapipal/schmervice": "^3.0.0",
|
||||
"@hapipal/toys": "^4.0.0",
|
||||
"blipp": "^4.0.2",
|
||||
"camelcase-keys": "^8.0.2",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"expiry-map": "^2.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"graphile-migrate": "^1.4.1",
|
||||
"graphile-worker": "^0.13.0",
|
||||
"hapi-auth-bearer-token": "^8.0.0",
|
||||
"hapi-auth-jwt2": "^10.4.0",
|
||||
"hapi-swagger": "^17.1.0",
|
||||
"joi": "^17.9.2",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"jwks-rsa": "^3.0.1",
|
||||
"hapi-auth-jwt2": "^10.5.1",
|
||||
"hapi-swagger": "^17.2.1",
|
||||
"joi": "^17.12.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"long": "^5.2.3",
|
||||
"p-memoize": "^7.1.1",
|
||||
"pg": "^8.11.1",
|
||||
"pg": "^8.11.3",
|
||||
"pg-monitor": "^2.0.0",
|
||||
"pg-promise": "^11.5.0",
|
||||
"pg-promise": "^11.5.4",
|
||||
"postgraphile": "4.12.3",
|
||||
"postgraphile-plugin-connection-filter": "^2.3.0",
|
||||
"remeda": "^1.24.0",
|
||||
"twilio": "^4.14.0",
|
||||
"typeorm": "^0.3.17",
|
||||
"@whiskeysockets/baileys": "^6.3.1"
|
||||
"remeda": "^1.46.2",
|
||||
"twilio": "^4.23.0",
|
||||
"typeorm": "^0.3.20",
|
||||
"@whiskeysockets/baileys": "^6.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/long": "^4.0.2",
|
||||
"@types/node": "*",
|
||||
"babel-preset-link": "*",
|
||||
"camelcase-keys": "^8.0.2",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"eslint-config-link": "*",
|
||||
"jest-config-link": "*",
|
||||
"nodemon": "^3.0.1",
|
||||
"nodemon": "^3.1.0",
|
||||
"pg-monitor": "^2.0.0",
|
||||
"pino-pretty": "^10.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"pino-pretty": "^10.3.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"tsconfig-link": "*",
|
||||
"typedoc": "^0.24.8",
|
||||
"typescript": "^5.1.6"
|
||||
"typedoc": "^0.25.11",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ const AppPlugin = {
|
|||
name: "App",
|
||||
async register(
|
||||
server: Hapi.Server,
|
||||
options: { config: IAppConfig }
|
||||
options: { config: IAppConfig },
|
||||
): Promise<void> {
|
||||
// declare our **run-time** plugin dependencies
|
||||
// these are runtime only deps, not registration time
|
||||
// ref: https://hapipal.com/best-practices/handling-plugin-dependencies
|
||||
server.dependency(["config", "hapi-pino"]);
|
||||
|
||||
server.validator(Joi);
|
||||
server.validator(Joi as any);
|
||||
await Plugins.register(server, options.config);
|
||||
await Services.register(server);
|
||||
await Routes.register(server);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { IAppConfig } from "@digiresilience/metamigo-config";
|
|||
import { postgraphile, HttpRequestHandler } from "postgraphile";
|
||||
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
|
||||
|
||||
export interface HapiPostgraphileOptions {}
|
||||
export interface HapiPostgraphileOptions { }
|
||||
|
||||
const PostgraphilePlugin: Hapi.Plugin<HapiPostgraphileOptions> = {
|
||||
name: "postgraphilePlugin",
|
||||
|
|
@ -29,7 +29,7 @@ const PostgraphilePlugin: Hapi.Plugin<HapiPostgraphileOptions> = {
|
|||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
} as any
|
||||
);
|
||||
|
||||
server.route({
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ export const register = async (
|
|||
},
|
||||
]);
|
||||
|
||||
await registerNextAuth(server, config);
|
||||
// await registerNextAuth(server, config);
|
||||
await registerSwagger(server);
|
||||
await registerCloudflareAccessJwt(server, config);
|
||||
await registerAuthBearer(server, config);
|
||||
//await registerCloudflareAccessJwt(server, config);
|
||||
// await registerAuthBearer(server, config);
|
||||
await registerPostgraphile(server, config);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
|||
},
|
||||
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||
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 });
|
||||
if (!voiceLine) return Boom.notFound();
|
||||
if (voiceLine.id !== voiceLineId) return Boom.badRequest();
|
||||
|
|
@ -193,7 +193,7 @@ export const TwilioRoutes = Helpers.noAuth([
|
|||
},
|
||||
},
|
||||
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
|
||||
.db()
|
||||
.voiceProviders.findById({ id: providerId });
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"@digiresilience/metamigo-db": "*",
|
||||
"@digiresilience/metamigo-api": "*",
|
||||
"@digiresilience/metamigo-worker": "*",
|
||||
"commander": "^11.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"graphile-migrate": "^1.4.1",
|
||||
"graphile-worker": "^0.13.0",
|
||||
"node-jose": "^2.2.0",
|
||||
|
|
@ -23,14 +23,14 @@
|
|||
"graphql": "15.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"pino-pretty": "^10.0.1",
|
||||
"nodemon": "^3.0.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"pino-pretty": "^10.3.1",
|
||||
"nodemon": "^3.1.0",
|
||||
"tsconfig-link": "*",
|
||||
"eslint-config-link": "*",
|
||||
"jest-config-link": "*",
|
||||
"babel-preset-link": "*",
|
||||
"typescript": "^5.1.6"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js db -- migrate",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
generateConfig,
|
||||
printConfigOptions,
|
||||
} from "@digiresilience/metamigo-common";
|
||||
import { IAppConfig, IAppConvict } from "@digiresilience/metamigo-config";
|
||||
import { loadConfigRaw } from "@digiresilience/metamigo-config";
|
||||
|
||||
export const genConf = async (): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from "commander";
|
||||
import { startWithout } from "@digiresilience/montar";
|
||||
import { migrateWrapper } from "@digiresilience/metamigo-db";
|
||||
import { loadConfig } from "@digiresilience/metamigo-config";
|
||||
import { genConf, listConfig } from "./config.js";
|
||||
import { createTokenForTesting, generateJwks } from "./jwks.js";
|
||||
import { exportGraphqlSchema } from "./metamigo-postgraphile.js";
|
||||
import "@digiresilience/metamigo-api";
|
||||
import "@digiresilience/metamigo-worker";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
export async function runServer(): Promise<void> {
|
||||
await startWithout(["worker"]);
|
||||
}
|
||||
|
||||
export async function runWorker(): Promise<void> {
|
||||
await startWithout(["server"]);
|
||||
}
|
||||
|
||||
program
|
||||
.command("config-generate")
|
||||
|
|
@ -19,6 +30,16 @@ program
|
|||
.description("Prints the entire convict config ")
|
||||
.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
|
||||
.command("db <commands...>")
|
||||
.description("Run graphile-migrate commands with your app's config loaded.")
|
||||
|
|
@ -27,6 +48,16 @@ program
|
|||
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
|
||||
.command("export-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