diff --git a/apps/leafcutter/app/(main)/about/page.tsx b/apps/leafcutter/app/(main)/about/page.tsx index 2dc1b39..c92ba88 100644 --- a/apps/leafcutter/app/(main)/about/page.tsx +++ b/apps/leafcutter/app/(main)/about/page.tsx @@ -1,4 +1,4 @@ -import { About } from './_components/About'; +import { About } from "leafcutter-common"; export default function Page() { return ; diff --git a/apps/leafcutter/app/(main)/create/page.tsx b/apps/leafcutter/app/(main)/create/page.tsx index 98a1342..ece4c15 100644 --- a/apps/leafcutter/app/(main)/create/page.tsx +++ b/apps/leafcutter/app/(main)/create/page.tsx @@ -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 ; } + +export const dynamic = "force-dynamic"; diff --git a/apps/leafcutter/app/(main)/faq/page.tsx b/apps/leafcutter/app/(main)/faq/page.tsx index 93c7447..a908dfb 100644 --- a/apps/leafcutter/app/(main)/faq/page.tsx +++ b/apps/leafcutter/app/(main)/faq/page.tsx @@ -1,4 +1,4 @@ -import { FAQ } from "./_components/FAQ"; +import { FAQ } from "leafcutter-common"; export default function Page() { return ; diff --git a/apps/leafcutter/app/(main)/layout.tsx b/apps/leafcutter/app/(main)/layout.tsx index d272093..9d6b1bc 100644 --- a/apps/leafcutter/app/(main)/layout.tsx +++ b/apps/leafcutter/app/(main)/layout.tsx @@ -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 {children}; + return {children}; } diff --git a/apps/leafcutter/app/page.tsx b/apps/leafcutter/app/(main)/page.tsx similarity index 90% rename from apps/leafcutter/app/page.tsx rename to apps/leafcutter/app/(main)/page.tsx index 95940b0..41ec7df 100644 --- a/apps/leafcutter/app/page.tsx +++ b/apps/leafcutter/app/(main)/page.tsx @@ -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); diff --git a/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx b/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx index bac4dad..88991c5 100644 --- a/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx +++ b/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx @@ -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 ; + return ; } /* diff --git a/apps/leafcutter/app/(main)/setup/_components/Setup.tsx b/apps/leafcutter/app/(main)/setup/_components/Setup.tsx index 3092359..7b85439 100644 --- a/apps/leafcutter/app/(main)/setup/_components/Setup.tsx +++ b/apps/leafcutter/app/(main)/setup/_components/Setup.tsx @@ -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 "../../../_components/AppProvider"; export const Setup: FC = () => { const { @@ -20,6 +20,7 @@ export const Setup: FC = () => { ; } - diff --git a/apps/leafcutter/app/(main)/trends/page.tsx b/apps/leafcutter/app/(main)/trends/page.tsx index 9a33786..9230267 100644 --- a/apps/leafcutter/app/(main)/trends/page.tsx +++ b/apps/leafcutter/app/(main)/trends/page.tsx @@ -1,8 +1,10 @@ import { getTrends } from "app/_lib/opensearch"; -import { Trends } from "./_components/Trends"; +import { Trends } from "../../../../../packages/leafcutter-common/components/Trends"; export default async function Page() { const visualizations = await getTrends(25); return ; } + +export const dynamic = "force-dynamic"; diff --git a/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx b/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx index aa8d839..0787645 100644 --- a/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx +++ b/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx @@ -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 = { diff --git a/apps/leafcutter/app/_components/AppProvider.tsx b/apps/leafcutter/app/_components/AppProvider.tsx index e20d036..5fb065c 100644 --- a/apps/leafcutter/app/_components/AppProvider.tsx +++ b/apps/leafcutter/app/_components/AppProvider.tsx @@ -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" diff --git a/apps/leafcutter/app/_components/InternalLayout.tsx b/apps/leafcutter/app/_components/InternalLayout.tsx index 33166f2..efaa5d2 100644 --- a/apps/leafcutter/app/_components/InternalLayout.tsx +++ b/apps/leafcutter/app/_components/InternalLayout.tsx @@ -7,7 +7,7 @@ import CookieConsent from "react-cookie-consent"; import { useCookies } from "react-cookie"; import { TopNav } from "./TopNav"; import { Sidebar } from "./Sidebar"; -import { GettingStartedDialog } from "./GettingStartedDialog"; +import { GettingStartedDialog } from "leafcutter-common"; import { useAppContext } from "./AppProvider"; // import { Footer } from "./Footer"; diff --git a/apps/leafcutter/app/_components/MultiProvider.tsx b/apps/leafcutter/app/_components/MultiProvider.tsx index c6e6f04..114143a 100644 --- a/apps/leafcutter/app/_components/MultiProvider.tsx +++ b/apps/leafcutter/app/_components/MultiProvider.tsx @@ -8,10 +8,10 @@ import { CookiesProvider } from "react-cookie"; import { I18n } from "react-polyglot"; import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns"; import { LocalizationProvider } from "@mui/x-date-pickers-pro"; -import { AppProvider } from "app/_components/AppProvider"; +import { AppProvider } from "./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 }; diff --git a/apps/leafcutter/app/_components/Sidebar.tsx b/apps/leafcutter/app/_components/Sidebar.tsx index 91732b6..397473f 100644 --- a/apps/leafcutter/app/_components/Sidebar.tsx +++ b/apps/leafcutter/app/_components/Sidebar.tsx @@ -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 "./AppProvider"; +import { Tooltip } from "leafcutter-common"; // import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material"; const MenuItem = ({ diff --git a/apps/leafcutter/app/_components/TopNav.tsx b/apps/leafcutter/app/_components/TopNav.tsx index 720d2b1..71543a7 100644 --- a/apps/leafcutter/app/_components/TopNav.tsx +++ b/apps/leafcutter/app/_components/TopNav.tsx @@ -6,9 +6,9 @@ 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 { AccountButton } from "./AccountButton"; +import { HelpButton } from "./HelpButton"; +import { Tooltip } from "leafcutter-common"; import { useAppContext } from "./AppProvider"; // import { LanguageSelect } from "./LanguageSelect"; @@ -43,50 +43,51 @@ export const TopNav: FC = () => { wrap="nowrap" spacing={4} > - - - - + + + + + + + + Leafcutter + - - - - Leafcutter - - - - - A Project of Center for Digital Resilience - - + + + A Project of Center for Digital Resilience + - + + diff --git a/apps/leafcutter/app/_lib/auth.ts b/apps/leafcutter/app/_lib/auth.ts index a8c9426..24a647a 100644 --- a/apps/leafcutter/app/_lib/auth.ts +++ b/apps/leafcutter/app/_lib/auth.ts @@ -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; + } + },*/ }; + diff --git a/apps/leafcutter/app/_lib/opensearch.ts b/apps/leafcutter/app/_lib/opensearch.ts index 46730f6..230e656 100644 --- a/apps/leafcutter/app/_lib/opensearch.ts +++ b/apps/leafcutter/app/_lib/opensearch.ts @@ -23,6 +23,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) => diff --git a/apps/leafcutter/app/_styles/global.css b/apps/leafcutter/app/_styles/global.css index 4109f02..1546661 100644 --- a/apps/leafcutter/app/_styles/global.css +++ b/apps/leafcutter/app/_styles/global.css @@ -3,3 +3,7 @@ body { overscroll-behavior-y: none; text-size-adjust: none; } + +a { + text-decoration: none; +} diff --git a/apps/leafcutter/app/api/link/auth/route.ts b/apps/leafcutter/app/api/link/auth/route.ts new file mode 100644 index 0000000..37b356e --- /dev/null +++ b/apps/leafcutter/app/api/link/auth/route.ts @@ -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" }); +}; diff --git a/apps/leafcutter/app/api/proxy/[[...path]].ts b/apps/leafcutter/app/api/proxy/[[...path]].ts deleted file mode 100644 index 4956739..0000000 --- a/apps/leafcutter/app/api/proxy/[[...path]].ts +++ /dev/null @@ -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, - }, -}; diff --git a/apps/leafcutter/app/api/trends/recent/_route.ts b/apps/leafcutter/app/api/trends/recent/route.ts similarity index 74% rename from apps/leafcutter/app/api/trends/recent/_route.ts rename to apps/leafcutter/app/api/trends/recent/route.ts index 95d78a8..a635d29 100644 --- a/apps/leafcutter/app/api/trends/recent/_route.ts +++ b/apps/leafcutter/app/api/trends/recent/route.ts @@ -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'; diff --git a/apps/leafcutter/app/api/visualizations/list/route.ts b/apps/leafcutter/app/api/visualizations/list/route.ts new file mode 100644 index 0000000..a66f8f4 --- /dev/null +++ b/apps/leafcutter/app/api/visualizations/list/route.ts @@ -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); +}; + diff --git a/apps/leafcutter/app/layout.tsx b/apps/leafcutter/app/layout.tsx index 102c9b7..63ac295 100644 --- a/apps/leafcutter/app/layout.tsx +++ b/apps/leafcutter/app/layout.tsx @@ -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", diff --git a/apps/leafcutter/charts/Chart.yaml b/apps/leafcutter/charts/Chart.yaml index f3fa165..8ae83e9 100644 --- a/apps/leafcutter/charts/Chart.yaml +++ b/apps/leafcutter/charts/Chart.yaml @@ -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" diff --git a/apps/leafcutter/middleware.ts b/apps/leafcutter/middleware.ts index 1840b93..c2e5084 100644 --- a/apps/leafcutter/middleware.ts +++ b/apps/leafcutter/middleware.ts @@ -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).*)', + ], +}; diff --git a/apps/leafcutter/next.config.js b/apps/leafcutter/next.config.js index 128e197..9bd4d1d 100644 --- a/apps/leafcutter/next.config.js +++ b/apps/leafcutter/next.config.js @@ -7,9 +7,16 @@ const ContentSecurityPolicy = ` `; module.exports = { - publicRuntimeConfig: { - embedded: true - },/* + transpilePackages: ["leafcutter-common"], + rewrites: async () => ({ + fallback: [ + { + source: "/:path*", + destination: "/api/proxy/:path*", + }, + ], + }), + /* basePath: "/proxy/leafcutter", assetPrefix: "/proxy/leafcutter", i18n: { @@ -17,25 +24,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 +53,5 @@ module.exports = { }, ] } + */ }; diff --git a/apps/leafcutter/package.json b/apps/leafcutter/package.json index a3a161d..40647a0 100644 --- a/apps/leafcutter/package.json +++ b/apps/leafcutter/package.json @@ -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", @@ -21,44 +21,46 @@ "@fontsource/poppins": "^5.0.8", "@fontsource/roboto": "^5.0.8", "@mui/icons-material": "^5", - "@mui/lab": "^5.0.0-alpha.138", + "@mui/lab": "^5.0.0-alpha.140", "@mui/material": "^5", - "@mui/x-data-grid-pro": "^6.11.0", - "@mui/x-date-pickers-pro": "^6.11.0", + "@mui/x-data-grid-pro": "^6.11.1", + "@mui/x-date-pickers-pro": "^6.11.1", "@opensearch-project/opensearch": "^2.3.1", + "cryptr": "^6.2.0", "date-fns": "^2.30.0", "http-proxy-middleware": "^2.0.6", + "leafcutter-common": "*", "material-ui-popup-state": "^5.0.9", - "next": "13.4.13", - "next-auth": "^4.22.4", + "next": "13.4.7", + "next-auth": "^4.23.1", "next-http-proxy-middleware": "^1.2.5", "nodemailer": "^6.9.4", "react": "18.2.0", - "react-cookie": "^4.1.1", + "react-cookie": "^5.0.0", "react-cookie-consent": "^8.0.1", "react-dom": "18.2.0", "react-iframe": "^1.8.5", "react-markdown": "^8.0.7", "react-polyglot": "^0.7.2", - "sharp": "^0.32.4", - "swr": "^2.2.0", + "sharp": "^0.32.5", + "swr": "^2.2.1", "tss-react": "^4.8.8", "uuid": "^9.0.0" }, "devDependencies": { - "@babel/core": "^7.22.9", - "@types/node": "^20.4.8", - "@types/react": "18.2.18", + "@babel/core": "^7.22.10", + "@types/node": "^20.5.0", + "@types/react": "18.2.20", "@types/uuid": "^9.0.2", "babel-loader": "^9.1.3", - "eslint": "^8.46.0", + "eslint": "^8.47.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-next": "^13.4.13", + "eslint-config-next": "^13.4.16", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.28.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react": "^7.33.2", "typescript": "5.1.6" } } diff --git a/apps/leafcutter/pages/api/proxy/[[...path]].ts b/apps/leafcutter/pages/api/proxy/[[...path]].ts new file mode 100644 index 0000000..913b7cb --- /dev/null +++ b/apps/leafcutter/pages/api/proxy/[[...path]].ts @@ -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, + }, +}; diff --git a/apps/leafcutter/tsconfig.json b/apps/leafcutter/tsconfig.json index bb36cfa..25ba45d 100644 --- a/apps/leafcutter/tsconfig.json +++ b/apps/leafcutter/tsconfig.json @@ -24,6 +24,17 @@ } ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "../../packages/leafcutter-common/components/Create.tsx", + "../../packages/leafcutter-common/components/About.tsx", + "../../packages/leafcutter-common/components/FAQ.tsx", + "../../packages/leafcutter-common/components/Trends.tsx", + "../../packages/leafcutter-common/components/Preview.tsx", + "app/(main)/setup/_components/Setup.tsx" +, "app/api/proxy/[[...path]]" ], "exclude": ["node_modules", "babel__core"] } diff --git a/apps/link/app/(login)/login/_components/Login.tsx b/apps/link/app/(login)/login/_components/Login.tsx index bacd448..bfbe6b2 100644 --- a/apps/link/app/(login)/login/_components/Login.tsx +++ b/apps/link/app/(login)/login/_components/Login.tsx @@ -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 = ({ 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 ( - <> - - - - + + - - - - - - - {!session ? ( - + + - - - signIn("google", { - callbackUrl: `${origin}/setup`, - }) - } - > - - Google - + Link logo + + + + + CDR Link + + + + + + {!session ? ( + + + {error ? ( + + + + {`${error} error`} + + + + ) : null} + + + signIn("google", { + callbackUrl: `${origin}/setup`, + }) + } + > + + Sign in with Google + + + + + signIn("apple", { + callbackUrl: `${window.location.origin}/setup`, + }) + } + > + + Sign in with Apple + + + + + ⸺ or ⸺ + + + + setEmail(e.target.value)} + label="Email" + variant="filled" + size="small" + fullWidth + sx={{ ...fieldStyles, backgroundColor: white }} + /> + + + setPassword(e.target.value)} + label="Password" + variant="filled" + size="small" + fullWidth + sx={{ backgroundColor: white }} + type="password" + /> + + + + signIn("credentials", { + email, + password, + callbackUrl: `${origin}/setup`, + }) + } + > + + Sign in with Zammad credentials + + - {/* - - - signIn("apple", { - callbackUrl: `${window.location.origin}/setup`, - }) - } - > - - - */} - - + ) : null} {session ? ( @@ -79,6 +230,6 @@ export const Login: FC = ({ session }) => { - + ); }; diff --git a/apps/link/app/(main)/_components/ClientOnly.tsx b/apps/link/app/(main)/_components/ClientOnly.tsx new file mode 100644 index 0000000..46e79e8 --- /dev/null +++ b/apps/link/app/(main)/_components/ClientOnly.tsx @@ -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, +}); diff --git a/apps/link/app/(main)/_components/Sidebar.tsx b/apps/link/app/(main)/_components/Sidebar.tsx index 32f4e71..c762320 100644 --- a/apps/link/app/(main)/_components/Sidebar.tsx +++ b/apps/link/app/(main)/_components/Sidebar.tsx @@ -17,12 +17,14 @@ 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, } from "@mui/icons-material"; import { usePathname } from "next/navigation"; import Link from "next/link"; @@ -161,7 +163,10 @@ interface SidebarProps { export const Sidebar: FC = ({ open, setOpen }) => { const pathname = usePathname(); const { data: session } = useSession(); + console.log({ session }); const username = session?.user?.name || "User"; + // @ts-ignore + const roles = session?.user?.roles || []; const { data: overviewData, error: overviewError }: any = useSWR( { document: getTicketOverviewCountsQuery, @@ -419,17 +424,25 @@ export const Sidebar: FC = ({ open, setOpen }) => { + = ({ open, setOpen }) => { = ({ open, setOpen }) => { selected={pathname.endsWith("/profile")} open={open} /> - - - + {roles.includes("admin") && ( + <> - - - - + + + + {false && roles.includes("metamigo") && ( + + )} + {roles.includes("label_studio") && ( + + )} + + + + )} = ({ }) => { const router = useRouter(); const [display, setDisplay] = useState("none"); - const url = `/proxy/zammad${path}`; - console.log({ url }); + const url = `/zammad${path}`; return ( // @ts-ignore @@ -33,8 +32,7 @@ export const ZammadWrapper: FC = ({ linkElement.contentDocument && linkElement.contentDocument?.querySelector && linkElement.contentDocument.querySelector("#navigation") && - linkElement.contentDocument.querySelector("body") && - linkElement.contentDocument.querySelector(".sidebar") + linkElement.contentDocument.querySelector("body") ) { // @ts-ignore linkElement.contentDocument.querySelector("#navigation").style = @@ -43,7 +41,10 @@ export const ZammadWrapper: FC = ({ linkElement.contentDocument.querySelector("body").style = "font-family: Arial"; - if (hideSidebar) { + if ( + hideSidebar && + linkElement.contentDocument.querySelector(".sidebar") + ) { // @ts-ignore linkElement.contentDocument.querySelector(".sidebar").style = "display: none"; diff --git a/apps/link/app/(main)/admin/metamigo/_components/Admin.tsx b/apps/link/app/(main)/admin/metamigo/_components/Admin.tsx new file mode 100644 index 0000000..d4d1364 --- /dev/null +++ b/apps/link/app/(main)/admin/metamigo/_components/Admin.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { FC } from "react"; +import { ApolloProvider } from "@apollo/client"; +import { apolloClient } from "../_lib/apollo-client"; +import dynamic from "next/dynamic"; + +const MetamigoAdmin = dynamic(() => import("./MetamigoAdmin"), { + ssr: false, +}); + +export const Admin: FC = () => ( + + + +); diff --git a/apps/metamigo-frontend/app/_components/AdminLogin.tsx b/apps/link/app/(main)/admin/metamigo/_components/AdminLogin.tsx similarity index 100% rename from apps/metamigo-frontend/app/_components/AdminLogin.tsx rename to apps/link/app/(main)/admin/metamigo/_components/AdminLogin.tsx diff --git a/apps/metamigo-frontend/app/_components/DigitInput/DigitInput.module.css b/apps/link/app/(main)/admin/metamigo/_components/DigitInput/DigitInput.module.css similarity index 100% rename from apps/metamigo-frontend/app/_components/DigitInput/DigitInput.module.css rename to apps/link/app/(main)/admin/metamigo/_components/DigitInput/DigitInput.module.css diff --git a/apps/metamigo-frontend/app/_components/DigitInput/index.tsx b/apps/link/app/(main)/admin/metamigo/_components/DigitInput/index.tsx similarity index 100% rename from apps/metamigo-frontend/app/_components/DigitInput/index.tsx rename to apps/link/app/(main)/admin/metamigo/_components/DigitInput/index.tsx diff --git a/apps/metamigo-frontend/app/_components/MetamigoAdmin.tsx b/apps/link/app/(main)/admin/metamigo/_components/MetamigoAdmin.tsx similarity index 56% rename from apps/metamigo-frontend/app/_components/MetamigoAdmin.tsx rename to apps/link/app/(main)/admin/metamigo/_components/MetamigoAdmin.tsx index c3c1a9b..edeb205 100644 --- a/apps/metamigo-frontend/app/_components/MetamigoAdmin.tsx +++ b/apps/link/app/(main)/admin/metamigo/_components/MetamigoAdmin.tsx @@ -6,13 +6,11 @@ import { FC, useEffect, useState } from "react"; import { Admin, Resource } from "react-admin"; import { useApolloClient } from "@apollo/client"; import polyglotI18nProvider from "ra-i18n-polyglot"; -import { ThemeProvider, createTheme } from "@mui/material"; +import { ThemeProvider, createTheme } from "@mui/material/styles"; import { metamigoDataProvider } from "../_lib/dataprovider"; import { theme } from "./layout/themes"; import { Layout } from "./layout"; import englishMessages from "../_i18n/en"; -import users from "./users"; -import accounts from "./accounts"; import whatsappBots from "./whatsapp/bots"; import whatsappMessages from "./whatsapp/messages"; import whatsappAttachments from "./whatsapp/attachments"; @@ -42,29 +40,25 @@ const MetamigoAdmin: FC = () => { }, [client]); return ( dataProvider && ( - - - - - - - - - - - - - - + + + + + + + + + + ) ); }; diff --git a/apps/link/app/(main)/admin/metamigo/_components/MetamigoWrapper.tsx b/apps/link/app/(main)/admin/metamigo/_components/MetamigoWrapper.tsx deleted file mode 100644 index 93344e6..0000000 --- a/apps/link/app/(main)/admin/metamigo/_components/MetamigoWrapper.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { Grid } from "@mui/material"; -import Iframe from "react-iframe"; - -type MetamigoWrapperProps = { - path: string; -}; - -export const MetamigoWrapper: FC = ({ path }) => { - return ( - - -