Loading packages/client/src/components/App.tsx +2 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ import { ChatContext } from "../contexts/ChatContext"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; import { AuthErrors } from "./AuthErrors"; const Chat = lazy(() => import("./Chat/Chat")); Loading @@ -33,6 +34,7 @@ const AppInner = () => { <DebugModal /> <SettingsSidebar /> <AuthErrors /> <ToastContainer position="top-left" /> </> Loading packages/client/src/components/AuthErrors.tsx 0 → 100644 +143 −0 Original line number Diff line number Diff line import { Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, } from "@nextui-org/react"; import { useEffect, useState } from "react"; const Params = { TYPE: "auth_type", ERROR: "auth_error", ERROR_DESC: "auth_error_desc", CAN_RETRY: "auth_retry", }; /** * Show popups that detail auth error messages * @returns */ export const AuthErrors = () => { const [params, setParams] = useState( new URLSearchParams(window.location.search) ); const onClose = () => { const url = new URL(window.location.href); url.search = ""; // window.history.replaceState({}, "", url.toString()); setParams(new URLSearchParams(window.location.search)); }; return ( <> <RPError isOpen={params.get(Params.TYPE) === "rp"} onClose={onClose} params={params} /> <OPError isOpen={params.get(Params.TYPE) === "op"} onClose={onClose} params={params} /> </> ); }; /** * This is for RP errors, which can be triggered by modifying data sent in callbacks * * These errors can typically be retried * * @param param0 * @returns */ const RPError = ({ isOpen, onClose, params, }: { isOpen: boolean; onClose: () => void; params: URLSearchParams; }) => { return ( <Modal isOpen={isOpen} onOpenChange={onClose} isDismissable={false}> <ModalContent> {(onClose) => ( <> <ModalHeader>Login Error</ModalHeader> <ModalBody> <b>Error:</b> {params.get(Params.ERROR)} <br /> <br /> <b>Error Description:</b> {params.get(Params.ERROR_DESC)} </ModalBody> <ModalFooter> <Button color="primary" href="/api/login" as={Link}> Login </Button> </ModalFooter> </> )} </ModalContent> </Modal> ); }; /** * This is for OP errors, these might not be retryable * @param param0 * @returns */ const OPError = ({ isOpen, onClose, params, }: { isOpen: boolean; onClose: () => void; params: URLSearchParams; }) => { const canRetry = params.has(Params.CAN_RETRY); const [error, setError] = useState(params.get(Params.ERROR)); const [errorDesc, setErrorDesc] = useState(params.get(Params.ERROR_DESC)); useEffect(() => { switch (params.get(Params.ERROR)) { case "invalid_grant": setErrorDesc("Invalid token, try logging in again"); break; } }, [params]); return ( <Modal isOpen={isOpen} onOpenChange={onClose} isDismissable={false}> <ModalContent> {(onClose) => ( <> <ModalHeader>Login Error</ModalHeader> <ModalBody> <b>Error:</b> {error} <br /> <br /> <b>Error Description:</b> {errorDesc} </ModalBody> <ModalFooter> {canRetry && ( <Button color="primary" href="/api/login" as={Link}> Login </Button> )} </ModalFooter> </> )} </ModalContent> </Modal> ); }; packages/server/src/api/client.ts +56 −25 Original line number Diff line number Diff line Loading @@ -4,6 +4,22 @@ import { OpenID } from "../lib/oidc"; import { TokenSet, errors as OIDC_Errors } from "openid-client"; import { Logger } from "../lib/Logger"; const ClientParams = { TYPE: "auth_type", ERROR: "auth_error", ERROR_DESC: "auth_error_desc", CAN_RETRY: "auth_retry", }; const buildQuery = (obj: { [k in keyof typeof ClientParams]?: string }) => { const params = new URLSearchParams(); for (const [k, v] of Object.entries(obj)) { const k_: keyof typeof ClientParams = k as any; params.set(ClientParams[k_], v); } return "?" + params.toString(); }; const app = Router(); app.get("/login", (req, res) => { Loading @@ -30,11 +46,14 @@ app.get("/callback", async (req, res) => { if (e instanceof OIDC_Errors.RPError) { // client error res.status(400).json({ success: false, error: e.name, error_description: e.message, }); res.redirect( "/" + buildQuery({ TYPE: "rp", ERROR: e.name, ERROR_DESC: e.message, }) ); return; } Loading @@ -47,34 +66,46 @@ app.get("/callback", async (req, res) => { Logger.error( "OpenID is improperly configured. Cannot exchange tokens, do I have valid credetials?" ); res.status(500).json({ success: false, error: "internal server error", error_description: "I'm misconfigured.", }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: "Internal Server Error", ERROR_DESC: "I'm misconfigured.", }) ); return; case "invalid_grant": res.status(400).json({ success: false, error: "invalid_grant", error_description: "retry /api/login", }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: "invalid_grant", CAN_RETRY: "true", }) ); return; } res.status(400).json({ success: false, error: e.error, error_description: e.error_description, }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: e.error, ERROR_DESC: e.error_description, }) ); return; } res.status(500).json({ success: false, error: "unknown error", error_description: "report this", }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: "unknown error", ERROR_DESC: "report this", }) ); return; } Loading Loading
packages/client/src/components/App.tsx +2 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ import { ChatContext } from "../contexts/ChatContext"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; import { AuthErrors } from "./AuthErrors"; const Chat = lazy(() => import("./Chat/Chat")); Loading @@ -33,6 +34,7 @@ const AppInner = () => { <DebugModal /> <SettingsSidebar /> <AuthErrors /> <ToastContainer position="top-left" /> </> Loading
packages/client/src/components/AuthErrors.tsx 0 → 100644 +143 −0 Original line number Diff line number Diff line import { Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, } from "@nextui-org/react"; import { useEffect, useState } from "react"; const Params = { TYPE: "auth_type", ERROR: "auth_error", ERROR_DESC: "auth_error_desc", CAN_RETRY: "auth_retry", }; /** * Show popups that detail auth error messages * @returns */ export const AuthErrors = () => { const [params, setParams] = useState( new URLSearchParams(window.location.search) ); const onClose = () => { const url = new URL(window.location.href); url.search = ""; // window.history.replaceState({}, "", url.toString()); setParams(new URLSearchParams(window.location.search)); }; return ( <> <RPError isOpen={params.get(Params.TYPE) === "rp"} onClose={onClose} params={params} /> <OPError isOpen={params.get(Params.TYPE) === "op"} onClose={onClose} params={params} /> </> ); }; /** * This is for RP errors, which can be triggered by modifying data sent in callbacks * * These errors can typically be retried * * @param param0 * @returns */ const RPError = ({ isOpen, onClose, params, }: { isOpen: boolean; onClose: () => void; params: URLSearchParams; }) => { return ( <Modal isOpen={isOpen} onOpenChange={onClose} isDismissable={false}> <ModalContent> {(onClose) => ( <> <ModalHeader>Login Error</ModalHeader> <ModalBody> <b>Error:</b> {params.get(Params.ERROR)} <br /> <br /> <b>Error Description:</b> {params.get(Params.ERROR_DESC)} </ModalBody> <ModalFooter> <Button color="primary" href="/api/login" as={Link}> Login </Button> </ModalFooter> </> )} </ModalContent> </Modal> ); }; /** * This is for OP errors, these might not be retryable * @param param0 * @returns */ const OPError = ({ isOpen, onClose, params, }: { isOpen: boolean; onClose: () => void; params: URLSearchParams; }) => { const canRetry = params.has(Params.CAN_RETRY); const [error, setError] = useState(params.get(Params.ERROR)); const [errorDesc, setErrorDesc] = useState(params.get(Params.ERROR_DESC)); useEffect(() => { switch (params.get(Params.ERROR)) { case "invalid_grant": setErrorDesc("Invalid token, try logging in again"); break; } }, [params]); return ( <Modal isOpen={isOpen} onOpenChange={onClose} isDismissable={false}> <ModalContent> {(onClose) => ( <> <ModalHeader>Login Error</ModalHeader> <ModalBody> <b>Error:</b> {error} <br /> <br /> <b>Error Description:</b> {errorDesc} </ModalBody> <ModalFooter> {canRetry && ( <Button color="primary" href="/api/login" as={Link}> Login </Button> )} </ModalFooter> </> )} </ModalContent> </Modal> ); };
packages/server/src/api/client.ts +56 −25 Original line number Diff line number Diff line Loading @@ -4,6 +4,22 @@ import { OpenID } from "../lib/oidc"; import { TokenSet, errors as OIDC_Errors } from "openid-client"; import { Logger } from "../lib/Logger"; const ClientParams = { TYPE: "auth_type", ERROR: "auth_error", ERROR_DESC: "auth_error_desc", CAN_RETRY: "auth_retry", }; const buildQuery = (obj: { [k in keyof typeof ClientParams]?: string }) => { const params = new URLSearchParams(); for (const [k, v] of Object.entries(obj)) { const k_: keyof typeof ClientParams = k as any; params.set(ClientParams[k_], v); } return "?" + params.toString(); }; const app = Router(); app.get("/login", (req, res) => { Loading @@ -30,11 +46,14 @@ app.get("/callback", async (req, res) => { if (e instanceof OIDC_Errors.RPError) { // client error res.status(400).json({ success: false, error: e.name, error_description: e.message, }); res.redirect( "/" + buildQuery({ TYPE: "rp", ERROR: e.name, ERROR_DESC: e.message, }) ); return; } Loading @@ -47,34 +66,46 @@ app.get("/callback", async (req, res) => { Logger.error( "OpenID is improperly configured. Cannot exchange tokens, do I have valid credetials?" ); res.status(500).json({ success: false, error: "internal server error", error_description: "I'm misconfigured.", }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: "Internal Server Error", ERROR_DESC: "I'm misconfigured.", }) ); return; case "invalid_grant": res.status(400).json({ success: false, error: "invalid_grant", error_description: "retry /api/login", }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: "invalid_grant", CAN_RETRY: "true", }) ); return; } res.status(400).json({ success: false, error: e.error, error_description: e.error_description, }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: e.error, ERROR_DESC: e.error_description, }) ); return; } res.status(500).json({ success: false, error: "unknown error", error_description: "report this", }); res.redirect( "/" + buildQuery({ TYPE: "op", ERROR: "unknown error", ERROR_DESC: "report this", }) ); return; } Loading