From 402550a0afd31e67e246519428676e4254fb8176 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Wed, 18 Oct 2023 17:27:10 -0500 Subject: [PATCH 01/19] Redesigned Sign in --- src/Tools/_framework/NewToolRoot.jsx | 1 - src/Tools/_framework/Paths/SignIn.jsx | 198 ++++++++++++++++++++++++++ src/index.jsx | 25 +++- 3 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 src/Tools/_framework/Paths/SignIn.jsx diff --git a/src/Tools/_framework/NewToolRoot.jsx b/src/Tools/_framework/NewToolRoot.jsx index 810884e23c..165832fc1e 100644 --- a/src/Tools/_framework/NewToolRoot.jsx +++ b/src/Tools/_framework/NewToolRoot.jsx @@ -107,7 +107,6 @@ export default function ToolRoot() { import("./ToolPanels/PublicActivityViewer"), ), CourseCards: lazy(() => import("./ToolPanels/CourseCards")), - SignIn: lazy(() => import("./ToolPanels/SignIn")), SignOut: lazy(() => import("./ToolPanels/SignOut")), NavigationPanel: lazy(() => import("./ToolPanels/NavigationPanel")), Dashboard: lazy(() => import("./ToolPanels/Dashboard")), diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx new file mode 100644 index 0000000000..2ca877e882 --- /dev/null +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -0,0 +1,198 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Center, + Checkbox, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Heading, + Image, + Input, + Stack, + Text, +} from "@chakra-ui/react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useLoaderData } from "react-router"; +import { useFetcher } from "react-router-dom"; + +export async function loader({ request, params }) { + // const url = new URL(request.url); + // const from = url.searchParams.get("from"); + try { + return { success: true }; + } catch (e) { + return { success: false, message: e.response.data.message }; + } +} + +export async function action({ params, request }) { + const formData = await request.formData(); + let formObj = Object.fromEntries(formData); + console.log({ formObj }); + + try { + if (formObj._action == "submit email") { + let { data } = axios.get("/api/sendSignInEmail.php", { + params: { email: params.emailAddress }, + }); + return { + _action: formObj._action, + success: true, + }; + } + + return { success: true }; + } catch (e) { + return { success: false, message: e.response.data.message }; + } +} + +function AskForEmailCard() { + const [emailAddress, setEmailAddress] = useState(""); + const [emailError, setEmailError] = useState(null); + const [isChecked, setIsChecked] = useState(false); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const fetcher = useFetcher(); + + return ( + + + Doenet Logo + + + + + Doenet Logo + + Sign In via Email + + + Email address + { + let nextValue = e.target.value; + //Clear error if email is now good + if (emailError != null && emailRegex.test(nextValue)) { + setEmailError(null); + } + setEmailAddress(nextValue); + }} + /> + {emailError} + + + setIsChecked(e.target.checked)} + > + Stay Signed In + + + + + + + + + + + ); +} + +export function SignIn() { + const { success } = useLoaderData(); + + const [stage, setStage] = useState("Init"); + console.log(success); + // const fetcher = useFetcher(); + // const setActivityByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); //TODO: remove after recoil is gone + // const setPageByDoenetId = useSetRecoilState(itemByDoenetId(pageId)); //TODO: remove after recoil is gone + + // let location = useLocation(); + + // const navigate = useNavigate(); + + // const [recoilPageToolView, setRecoilPageToolView] = + // useRecoilState(pageToolViewAtom); + + // let navigateTo = useRef(""); + + // if (navigateTo.current != "") { + // const newHref = navigateTo.current; + // navigateTo.current = ""; + // location.href = newHref; + // navigate(newHref); + // } + + //Optimistic UI + // let effectiveLabel = activityData.pageLabel; + // if (activityData.isSinglePage) { + // effectiveLabel = activityData.label; + // if (fetcher.data?._action == "update label") { + // effectiveLabel = fetcher.data.label; + // } + // } else { + // if (fetcher.data?._action == "update page label") { + // effectiveLabel = fetcher.data.pageLabel; + // } + // } + + return ( + <> + + + + + + + ); +} diff --git a/src/index.jsx b/src/index.jsx index 94174762bf..64fc496755 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -5,8 +5,11 @@ import { redirect, RouterProvider, } from "react-router-dom"; +import { ChakraProvider, extendTheme } from "@chakra-ui/react"; + import { RecoilRoot } from "recoil"; import { createRoot } from "react-dom/client"; +import ErrorPage from "./Tools/_framework/Paths/ErrorPage"; import ToolRoot from "./Tools/_framework/NewToolRoot"; import { MathJaxContext } from "better-react-mathjax"; @@ -46,12 +49,10 @@ import { action as portfolioActivityViewerAction, PortfolioActivityViewer, } from "./Tools/_framework/Paths/PortfolioActivityViewer"; -import { ChakraProvider, extendTheme } from "@chakra-ui/react"; import { action as editorSupportPanelAction, loader as editorSupportPanelLoader, } from "./Tools/_framework/Panels/NewSupportPanel"; -import ErrorPage from "./Tools/_framework/Paths/ErrorPage"; import "@fontsource/jost"; import { @@ -80,6 +81,11 @@ import { CourseLinkPageViewer, loader as courseLinkPageViewerLoader, } from "./Tools/_framework/Paths/CourseLinkPageViewer"; +import { + SignIn, + loader as signInLoader, + action as signInAction, +} from "./Tools/_framework/Paths/SignIn"; { /* */ @@ -319,6 +325,21 @@ const router = createBrowserRouter([ }, ], }, + { + path: "signin", + loader: signInLoader, + action: signInAction, + errorElement: ( + + + + ), + element: ( + + + + ), + }, { path: "public", loader: editorSupportPanelLoader, From 85e6ce596c97a099f69c7fed7a0d526356deba02 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Wed, 18 Oct 2023 23:17:14 -0500 Subject: [PATCH 02/19] start entering code --- src/Tools/_framework/Paths/SignIn.jsx | 122 +++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 10 deletions(-) diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index 2ca877e882..2678d18d9f 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -11,12 +11,16 @@ import { FormControl, FormErrorMessage, FormLabel, + HStack, Heading, Image, Input, + PinInput, + PinInputField, Stack, Text, } from "@chakra-ui/react"; +import axios from "axios"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useLoaderData } from "react-router"; import { useFetcher } from "react-router-dom"; @@ -38,11 +42,12 @@ export async function action({ params, request }) { try { if (formObj._action == "submit email") { - let { data } = axios.get("/api/sendSignInEmail.php", { - params: { email: params.emailAddress }, + let { data } = await axios.get("/api/sendSignInEmail.php", { + params: { email: formObj.emailAddress }, }); return { _action: formObj._action, + deviceName: data.deviceName, success: true, }; } @@ -53,12 +58,11 @@ export async function action({ params, request }) { } } -function AskForEmailCard() { +function AskForEmailCard({ fetcher }) { const [emailAddress, setEmailAddress] = useState(""); const [emailError, setEmailError] = useState(null); const [isChecked, setIsChecked] = useState(false); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const fetcher = useFetcher(); return ( + + Doenet Logo + + + + + Doenet Logo + + Check your email for the code. + + + Code (9 digit code): + + setCode(code)}> + + + + + + + + + + + + {codeError} + + + + + + + + + + + ); +} + export function SignIn() { const { success } = useLoaderData(); + const fetcher = useFetcher(); + + let card = ; + + // console.log("fetcher", fetcher); + if (fetcher.state === "idle" && fetcher.data?._action === "submit email") { + card =
test
; + } + + card = ; - const [stage, setStage] = useState("Init"); - console.log(success); - // const fetcher = useFetcher(); // const setActivityByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); //TODO: remove after recoil is gone // const setPageByDoenetId = useSetRecoilState(itemByDoenetId(pageId)); //TODO: remove after recoil is gone @@ -189,9 +293,7 @@ export function SignIn() { return ( <> - - - + {card} ); From 817c7c702a0ed04742e30ca89f050ce669ff83f5 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Thu, 19 Oct 2023 16:48:13 -0500 Subject: [PATCH 03/19] email message and start of JWT as axios --- src/Tools/_framework/Paths/SignIn.jsx | 98 ++++++++++++++++++++------- src/index.jsx | 9 +++ 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index 2678d18d9f..4c15cf02b8 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -22,7 +22,7 @@ import { } from "@chakra-ui/react"; import axios from "axios"; import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useLoaderData } from "react-router"; +import { redirect, useLoaderData } from "react-router"; import { useFetcher } from "react-router-dom"; export async function loader({ request, params }) { @@ -38,16 +38,48 @@ export async function loader({ request, params }) { export async function action({ params, request }) { const formData = await request.formData(); let formObj = Object.fromEntries(formData); - console.log({ formObj }); try { if (formObj._action == "submit email") { let { data } = await axios.get("/api/sendSignInEmail.php", { - params: { email: formObj.emailAddress }, + params: { emailaddress: formObj.emailAddress }, }); return { _action: formObj._action, deviceName: data.deviceName, + emailAddress: formObj.emailAddress, + staySignedIn: formObj.staySignedIn, + success: true, + }; + } else if (formObj._action == "submit code") { + let { data } = await axios.get("/api/checkCredentials.php", { + params: { + emailaddress: formObj.emailAddress, + nineCode: formObj.code, + deviceName: formObj.deviceName, + }, + }); + + // if (data.hasFullName == 1) { + //Only should get here with success + //Store cookies! + const { data: jwtdata } = await axios.get( + `/api/jwt.php?emailaddress=${encodeURIComponent( + formObj.emailAddress, + )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ + formObj.deviceName + }&newAccount=${data.existed}&stay=${ + formObj.staySignedIn == "true" ? "1" : "0" + }`, + { withCredentials: true }, + ); + + console.log("jwtdata", jwtdata); + // } + return { + _action: formObj._action, + isNewAccount: data.existed, + hasFullName: data.hasFullName, success: true, }; } @@ -152,11 +184,10 @@ function AskForEmailCard({ fetcher }) { ); } -function EnterCodeCard({ fetcher }) { +function EnterCodeCard({ fetcher, emailAddress, deviceName, staySignedIn }) { const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(null); - console.log("code", code); - console.log("codeError", codeError); + return ( Check your email for the code. - Code (9 digit code): + Sign-in code (9 digit code): setCode(code)}> @@ -219,21 +250,17 @@ function EnterCodeCard({ fetcher }) { setCodeError("Please enter all nine digits."); } else { setCodeError(null); + fetcher.submit( + { + _action: "submit code", + emailAddress, + deviceName, + staySignedIn, + code, + }, + { method: "post" }, + ); } - //else if (!emailRegex.test(emailAddress)) { - // setEmailError("Invalid email format"); - // } else { - // setEmailError(null); - // //Email is correct - // fetcher.submit( - // { - // _action: "submit email", - // emailAddress, - // staySignedIn: isChecked, - // }, - // { method: "post" }, - // ); - // } }} > Submit Code @@ -248,15 +275,38 @@ function EnterCodeCard({ fetcher }) { export function SignIn() { const { success } = useLoaderData(); const fetcher = useFetcher(); + console.log("fetcher", fetcher); + + let emailAddress = useRef(null); + let deviceName = useRef(null); + let staySignedIn = useRef(null); let card = ; - // console.log("fetcher", fetcher); if (fetcher.state === "idle" && fetcher.data?._action === "submit email") { - card =
test
; + emailAddress.current = fetcher.data.emailAddress; + staySignedIn.current = fetcher.data.staySignedIn; + deviceName.current = fetcher.data.deviceName; + + card = ( + + ); + } else if ( + fetcher.state === "idle" && + fetcher.data?._action === "submit code" + ) { + // if (fetcher.data?.hasFullName == 1) { + // } + // console.log("fetcher", fetcher); + card = Full name; } - card = ; + // card = ; // const setActivityByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); //TODO: remove after recoil is gone // const setPageByDoenetId = useSetRecoilState(itemByDoenetId(pageId)); //TODO: remove after recoil is gone diff --git a/src/index.jsx b/src/index.jsx index 64fc496755..54be7bcbd5 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -441,6 +441,15 @@ const router = createBrowserRouter([ ), }, + // { + // path: "/api/", + // element:
Loading...
, + // errorElement: ( + // + // + // + // ), + // }, { path: "*", From a929c0c60c1434765043d5ba71c22dd9a7fbfaf9 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Thu, 19 Oct 2023 22:51:49 -0500 Subject: [PATCH 04/19] started all cards --- public/api/jwt.php | 135 ++++++++++----------- public/api/sendSignInEmail.php | 2 +- public/api/signInEmail.html | 9 +- src/Tools/_framework/Paths/SignIn.jsx | 166 ++++++++++++++++++++++---- 4 files changed, 212 insertions(+), 100 deletions(-) diff --git a/public/api/jwt.php b/public/api/jwt.php index 7114779b3d..0289a3ab29 100644 --- a/public/api/jwt.php +++ b/public/api/jwt.php @@ -1,10 +1,4 @@ 10) { - echo 'Code expired'; -} else { + throw new Exception("Code expired."); +} $sql = "SELECT signInCode AS nineCode, userId FROM user_device WHERE email='$emailaddress' AND deviceName='$deviceName'"; @@ -37,74 +33,75 @@ $row = $result->fetch_assoc(); $userId = $row['userId']; if ($row['nineCode'] != $nineCode) { - echo 'Invalid Code'; - } else { - //Valid code and not expired - http_response_code(200); + throw new Exception("Invalid Code."); + } + //Valid code and not expired + http_response_code(200); - $expirationTime = 0; - if ($stay == 1) { - $expirationTime = 2147483647; - } + $expirationTime = 0; + if ($stay == 1) { + $expirationTime = 2147483647; + } - $payload = [ - // "email" => $emailaddress, - 'userId' => $userId, - 'deviceName' => $deviceName, - // "expires" => $expirationTime - ]; - $jwt = JWT::encode($payload, $key); + $payload = [ + // "email" => $emailaddress, + 'userId' => $userId, + 'deviceName' => $deviceName, + // "expires" => $expirationTime + ]; + $jwt = JWT::encode($payload, $key); - $sql = "UPDATE user_device - SET signedIn = '1' - WHERE userId='$userId' AND deviceName='$deviceName'"; - $result = $conn->query($sql); + $sql = "UPDATE user_device + SET signedIn = '1' + WHERE userId='$userId' AND deviceName='$deviceName'"; + $result = $conn->query($sql); - $value = $jwt; + $value = $jwt; - $path = '/'; - //$domain = $ini_array['dbhost']; - $domain = $_SERVER["SERVER_NAME"]; - if ($domain == 'apache'){$domain = 'localhost';} - $isSecure = true; - if ($domain == 'apache') { - $domain = 'localhost'; - } - if ($domain == 'localhost') { - $isSecure = false; - } - $isHttpOnly = true; - setcookie( - 'JWT', - $value, - $expirationTime, - $path, - $domain, - $isSecure, - $isHttpOnly - ); - setcookie( - 'JWT_JS', - 1, - $expirationTime, - $path, - $domain, - $isSecure, - false - ); - header('Location: /signin'); //needs to store profile into localstorage + $path = '/'; + //$domain = $ini_array['dbhost']; + $domain = $_SERVER["SERVER_NAME"]; + if ($domain == 'apache'){$domain = 'localhost';} + $isSecure = true; + if ($domain == 'apache') { + $domain = 'localhost'; + } + if ($domain == 'localhost') { + $isSecure = false; + } + $isHttpOnly = true; + setcookie( + 'JWT', + $value, + $expirationTime, + $path, + $domain, + $isSecure, + $isHttpOnly + ); + setcookie( + 'JWT_JS', + 1, + $expirationTime, + $path, + $domain, + $isSecure, + false + ); - // setcookie("JWT", $value, array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>$isHttpOnly, "samesite"=>"strict")); - // setcookie("JWT_JS", 1, array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>false, "samesite"=>"strict")); - // if ($newAccount == 1){ - // // header("Location: /accountsettings"); - // header("Location: /library"); - // }else{ - // // header("Location: /dashboard"); - // header("Location: /course"); - // } - } +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); + +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); } $conn->close(); +?> diff --git a/public/api/sendSignInEmail.php b/public/api/sendSignInEmail.php index 3e8ccba30e..fe88911f98 100644 --- a/public/api/sendSignInEmail.php +++ b/public/api/sendSignInEmail.php @@ -64,7 +64,7 @@ // Generate and modify email content $htmlContent = file_get_contents("signInEmail.html"); -$htmlContent = str_replace(array("deviceName", "signInCode"), array($deviceName, $signInCode), $htmlContent); +$htmlContent = str_replace(array("signInCode"), array($signInCode), $htmlContent); $from = 'noreply@doenet.org'; $fromName = 'Doenet Accounts'; diff --git a/public/api/signInEmail.html b/public/api/signInEmail.html index 04d0631196..c6e7eed83c 100644 --- a/public/api/signInEmail.html +++ b/public/api/signInEmail.html @@ -136,14 +136,11 @@ align="left" >

-
Welcome to Doenet, you've - requested a sign-in code for - deviceName.
+
Welcome to Doenet!

- Access code: - signInCode + ```signInCode```

diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index 4c15cf02b8..901038b434 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -17,6 +17,7 @@ import { Input, PinInput, PinInputField, + Spinner, Stack, Text, } from "@chakra-ui/react"; @@ -60,22 +61,22 @@ export async function action({ params, request }) { }, }); - // if (data.hasFullName == 1) { - //Only should get here with success - //Store cookies! - const { data: jwtdata } = await axios.get( - `/api/jwt.php?emailaddress=${encodeURIComponent( - formObj.emailAddress, - )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ - formObj.deviceName - }&newAccount=${data.existed}&stay=${ - formObj.staySignedIn == "true" ? "1" : "0" - }`, - { withCredentials: true }, - ); - - console.log("jwtdata", jwtdata); - // } + if (data.hasFullName == 1) { + //Only should get here with success + //Store cookies! + const { data: jwtdata } = await axios.get( + `/api/jwt.php?emailaddress=${encodeURIComponent( + formObj.emailAddress, + )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ + formObj.deviceName + }&newAccount=${data.existed}&stay=${ + formObj.staySignedIn == "true" ? "1" : "0" + }`, + { withCredentials: true }, + ); + + // console.log("jwtdata", jwtdata); + } return { _action: formObj._action, isNewAccount: data.existed, @@ -94,6 +95,7 @@ function AskForEmailCard({ fetcher }) { const [emailAddress, setEmailAddress] = useState(""); const [emailError, setEmailError] = useState(null); const [isChecked, setIsChecked] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return ( @@ -155,6 +157,8 @@ function AskForEmailCard({ fetcher }) { + + + +
+ ); +} + export function SignIn() { const { success } = useLoaderData(); const fetcher = useFetcher(); @@ -280,15 +393,16 @@ export function SignIn() { let emailAddress = useRef(null); let deviceName = useRef(null); let staySignedIn = useRef(null); - - let card = ; + //card is a ref because we need the card to stay + // and not have to track every possible state + let card = useRef(); if (fetcher.state === "idle" && fetcher.data?._action === "submit email") { emailAddress.current = fetcher.data.emailAddress; staySignedIn.current = fetcher.data.staySignedIn; deviceName.current = fetcher.data.deviceName; - card = ( + card.current = ( Full name; + card.current = ( + + ); } // card = ; @@ -343,7 +461,7 @@ export function SignIn() { return ( <> - {card} + {card.current} ); From 7a75b6adfa8fcd070389c3c986a54ffccda7491b Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Fri, 20 Oct 2023 08:58:29 -0500 Subject: [PATCH 05/19] process improvements --- public/api/jwt.php | 6 ++- src/Tools/_framework/Paths/SignIn.jsx | 78 ++++++++++++++++----------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/public/api/jwt.php b/public/api/jwt.php index 0289a3ab29..5731d3ee13 100644 --- a/public/api/jwt.php +++ b/public/api/jwt.php @@ -89,6 +89,11 @@ false ); + $response_arr = [ + 'success' => true, + ]; + + http_response_code(200); } catch (Exception $e) { $response_arr = [ @@ -103,5 +108,4 @@ $conn->close(); } -$conn->close(); ?> diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index 901038b434..52c510e42c 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -53,6 +53,7 @@ export async function action({ params, request }) { success: true, }; } else if (formObj._action == "submit code") { + //TODO: need check credentials to give back the portfolio course id let { data } = await axios.get("/api/checkCredentials.php", { params: { emailaddress: formObj.emailAddress, @@ -61,21 +62,24 @@ export async function action({ params, request }) { }, }); + //Attempt to store cookies! + const { data: jwtdata } = await axios.get( + `/api/jwt.php?emailaddress=${encodeURIComponent( + formObj.emailAddress, + )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ + formObj.deviceName + }&newAccount=${data.existed}&stay=${ + formObj.staySignedIn == "true" ? "1" : "0" + }`, + { withCredentials: true }, + ); + + console.log("jwtdata", jwtdata); + if (data.hasFullName == 1) { - //Only should get here with success - //Store cookies! - const { data: jwtdata } = await axios.get( - `/api/jwt.php?emailaddress=${encodeURIComponent( - formObj.emailAddress, - )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ - formObj.deviceName - }&newAccount=${data.existed}&stay=${ - formObj.staySignedIn == "true" ? "1" : "0" - }`, - { withCredentials: true }, - ); - - // console.log("jwtdata", jwtdata); + //Redirect if we have their full name + //and there wasn't an error with sign in + // TODO: Redirect to portfolio } return { _action: formObj._action, @@ -83,6 +87,18 @@ export async function action({ params, request }) { hasFullName: data.hasFullName, success: true, }; + } else if (formObj._action == "submit name") { + let { data } = await axios.get("/api/saveUsersName.php", { + params: { + firstName: formObj.firstName, + lastName: formObj.lastName, + email: formObj.emailAddress, + }, + }); + console.log("data", data); + return true; + + // TODO: Redirect to portfolio } return { success: true }; @@ -320,11 +336,11 @@ function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { First Name: { - if (firstName != "") { + onChange={(e) => { + if (e.target.value != "") { setFirstNameError(null); } - setFirstName(firstName); + setFirstName(e.target.value); }} /> {firstNameError} @@ -332,11 +348,11 @@ function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { Last Name: { - if (lastName != "") { + onChange={(e) => { + if (e.target.value != "") { setLastNameError(null); } - setLastName(lastName); + setLastName(e.target.value); }} /> {lastNameError} @@ -361,18 +377,16 @@ function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { setFirstNameError(null); setLastNameError(null); setIsDisabled(true); - console.log("firstName", firstName); - console.log("lastName", lastName); - // fetcher.submit( - // { - // _action: "submit code", - // emailAddress, - // deviceName, - // staySignedIn, - // code, - // }, - // { method: "post" }, - // ); + + fetcher.submit( + { + _action: "submit name", + firstName, + lastName, + emailAddress, + }, + { method: "post" }, + ); } }} > From 6d8ee9e4c61db5e19e0277462e21972494e4f0da Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Sat, 21 Oct 2023 08:43:45 -0500 Subject: [PATCH 06/19] last in the series --- src/Tools/_framework/Paths/SignIn.jsx | 171 +++++++++++++++++--------- 1 file changed, 116 insertions(+), 55 deletions(-) diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index 52c510e42c..aef4975b8c 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -38,7 +38,7 @@ export async function loader({ request, params }) { export async function action({ params, request }) { const formData = await request.formData(); - let formObj = Object.fromEntries(formData); + const formObj = Object.fromEntries(formData); try { if (formObj._action == "submit email") { @@ -103,7 +103,11 @@ export async function action({ params, request }) { return { success: true }; } catch (e) { - return { success: false, message: e.response.data.message }; + return { + success: false, + message: e.response.data.message, + _action: formObj._action, + }; } } @@ -209,6 +213,20 @@ function EnterCodeCard({ fetcher, emailAddress, deviceName, staySignedIn }) { const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(null); const [isDisabled, setIsDisabled] = useState(false); + const [isExpired, setIsExpired] = useState(false); + + console.log("EnterCodeCard fetcher", fetcher); + //Handle code entry errors + if (fetcher.data?.success == false) { + //Guard against an infinite loop + if (codeError !== fetcher.data.message) { + setCodeError(fetcher.data.message); + setIsDisabled(false); + if (fetcher.data.message == "Code expired.") { + setIsExpired(true); + } + } + } return ( Sign-in code (9 digit code): - setCode(code)}> + setCode(code)}> @@ -260,36 +278,64 @@ function EnterCodeCard({ fetcher, emailAddress, deviceName, staySignedIn }) { - + }} + > + Send New Code + + ) : ( + + )} @@ -400,42 +446,57 @@ function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { } export function SignIn() { - const { success } = useLoaderData(); + // const { success } = useLoaderData(); const fetcher = useFetcher(); - console.log("fetcher", fetcher); + let formObj = {}; + if (fetcher.formData !== undefined) { + formObj = Object.fromEntries(fetcher.formData); + } + console.log("fetcher.state", fetcher.state); + console.log("fetcher.data", fetcher.data); + console.log("formObj", formObj); + console.log("---------------------\n"); let emailAddress = useRef(null); let deviceName = useRef(null); let staySignedIn = useRef(null); //card is a ref because we need the card to stay // and not have to track every possible state - let card = useRef(); - if (fetcher.state === "idle" && fetcher.data?._action === "submit email") { - emailAddress.current = fetcher.data.emailAddress; - staySignedIn.current = fetcher.data.staySignedIn; - deviceName.current = fetcher.data.deviceName; - - card.current = ( - - ); - } else if ( - fetcher.state === "idle" && - fetcher.data?._action === "submit code" - ) { - card.current = ( - - ); + //Enter Email + let card = useRef(); + if (fetcher.state === "idle") { + if ( + (fetcher.data?._action === "submit email" && fetcher.data?.success) || + (fetcher.data?._action === "submit code" && !fetcher.data?.success) + ) { + //Enter Code + emailAddress.current = fetcher.data.emailAddress; + staySignedIn.current = fetcher.data.staySignedIn; + deviceName.current = fetcher.data.deviceName; + + card.current = ( + + ); + } else if ( + (fetcher.data?._action === "submit code" && fetcher.data?.success) || + (fetcher.data?._action === "submit name" && !fetcher.data?.success) + ) { + //Enter Name + card.current = ( + + ); + } } // card = ; From 7c15def26356341af298db0675b6aef6d5a20c65 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Sat, 21 Oct 2023 22:56:32 -0500 Subject: [PATCH 07/19] Progress on three path design --- public/api/checkCredentials.php | 81 +-- public/api/sendSignInEmail.php | 31 +- src/Tools/_framework/Paths/SignIn.jsx | 592 ++++------------------ src/Tools/_framework/Paths/SignInCode.jsx | 238 +++++++++ src/Tools/_framework/Paths/SignInName.jsx | 533 +++++++++++++++++++ src/index.jsx | 42 +- 6 files changed, 984 insertions(+), 533 deletions(-) create mode 100644 src/Tools/_framework/Paths/SignInCode.jsx create mode 100644 src/Tools/_framework/Paths/SignInName.jsx diff --git a/public/api/checkCredentials.php b/public/api/checkCredentials.php index 4efc6852aa..e6181dea9a 100644 --- a/public/api/checkCredentials.php +++ b/public/api/checkCredentials.php @@ -7,11 +7,23 @@ include "db_connection.php"; - $emailaddress = mysqli_real_escape_string($conn,$_REQUEST["emailaddress"]); $nineCode = mysqli_real_escape_string($conn,$_REQUEST["nineCode"]); $deviceName = mysqli_real_escape_string($conn,$_REQUEST["deviceName"]); +if(!isset($_REQUEST["emailaddress"])){ + throw new Exception("Internal Error: missing emailaddress"); +} +if(!isset($_REQUEST["nineCode"])){ + throw new Exception("Internal Error: missing nineCode"); +} +if(!isset($_REQUEST["deviceName"])){ + throw new Exception("Internal Error: missing deviceName"); +} + +$response_arr; +try { + //Check if expired $sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes FROM user_device @@ -20,19 +32,15 @@ $result = $conn->query($sql); $row = $result->fetch_assoc(); -//Assume success and it already exists - - -$success = 1; -$existed = 1; -$hasFullName = 0; +//Assume it already exists +$existed = true; +$hasFullName = false; $reason = ""; //Check if it took longer than 10 minutes to enter the code if ($row['minutes'] > 10){ - $success = 0; - $reason = "Code expired"; -}else{ + throw new Exception("Code expired"); +} $sql = "SELECT signInCode AS nineCode FROM user_device @@ -41,10 +49,8 @@ $row = $result->fetch_assoc(); if ($row["nineCode"] != $nineCode){ - $success = 0; - $reason = "Invalid Code"; - - }else{ + throw new Exception("Invalid Code"); + } //Valid code and not expired //Update signedIn on user_device table @@ -54,7 +60,6 @@ $result = $conn->query($sql); //Test if it's a new account - $sql = "SELECT firstName,lastName, screenName FROM user WHERE email='$emailaddress' @@ -63,13 +68,13 @@ $row = $result->fetch_assoc(); if ($row["firstName"] != "" && $row["lastName"] != ""){ - $hasFullName = 1; + $hasFullName = true; } //Only new accounts won't have a screen name if ($row["screenName"] === null){ // New Account! - $existed = 0; + $existed = false; // Make a new profile // Random screen name @@ -84,27 +89,37 @@ // Store screen name and profile picture $sql = "UPDATE user SET screenName='$screen_name',profilePicture='$profile_pic' WHERE email='$emailaddress' "; $result = $conn->query($sql); - } - - - - } - - -} + } + $sql = "SELECT c.courseId + FROM course AS c + LEFT JOIN user AS u + ON u.userId = c.portfolioCourseForUserId + WHERE u.email = '$emailaddress'"; + $result = $conn->query($sql); + $row = $result->fetch_assoc(); + $portfolioCourseId = "_"; + if ($result->num_rows > 0) { + $portfolioCourseId = $row['courseId']; + } $response_arr = array( - "success" => $success, + "success" => true, "existed" => $existed, "hasFullName" => $hasFullName, - "reason" => $reason, + "portfolioCourseId" => $portfolioCourseId, ); -http_response_code(200); - -// make it json format -echo json_encode($response_arr); - -$conn->close(); +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); + +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} diff --git a/public/api/sendSignInEmail.php b/public/api/sendSignInEmail.php index fe88911f98..c3e616c013 100644 --- a/public/api/sendSignInEmail.php +++ b/public/api/sendSignInEmail.php @@ -13,6 +13,8 @@ //Nine digit random number $signInCode = rand(100000000,999999999); +$response_arr; +try { $sql = "SELECT email, userId FROM user @@ -76,18 +78,29 @@ $headers .= 'From: '.$fromName.'<'.$from.'>' . "\r\n"; //SEND EMAIL WITH CODE HERE -mail($emailaddress,$subject,$htmlContent, $headers); +$mailSuccess = mail($emailaddress,$subject,$htmlContent, $headers); -$response_arr = array( - "success" => 1, - "deviceName" => $deviceName, - ); +if (!$mailSuccess && $mode != 'development'){ + throw new Exception("Sending Email Failed."); +} -// set response code - 200 OK -http_response_code(200); +$response_arr = [ + 'success' => true, + "deviceName" => $deviceName, + ]; -echo json_encode($response_arr); + http_response_code(200); +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); -$conn->close(); +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index aef4975b8c..5d897ef721 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -5,38 +5,23 @@ import { Card, CardBody, CardFooter, - Center, Checkbox, Flex, FormControl, FormErrorMessage, FormLabel, - HStack, Heading, Image, Input, - PinInput, - PinInputField, Spinner, Stack, - Text, } from "@chakra-ui/react"; import axios from "axios"; -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { redirect, useLoaderData } from "react-router"; +import React, { useState } from "react"; +import { redirect } from "react-router"; import { useFetcher } from "react-router-dom"; -export async function loader({ request, params }) { - // const url = new URL(request.url); - // const from = url.searchParams.get("from"); - try { - return { success: true }; - } catch (e) { - return { success: false, message: e.response.data.message }; - } -} - -export async function action({ params, request }) { +export async function action({ request }) { const formData = await request.formData(); const formObj = Object.fromEntries(formData); @@ -45,63 +30,18 @@ export async function action({ params, request }) { let { data } = await axios.get("/api/sendSignInEmail.php", { params: { emailaddress: formObj.emailAddress }, }); - return { - _action: formObj._action, - deviceName: data.deviceName, - emailAddress: formObj.emailAddress, - staySignedIn: formObj.staySignedIn, - success: true, - }; - } else if (formObj._action == "submit code") { - //TODO: need check credentials to give back the portfolio course id - let { data } = await axios.get("/api/checkCredentials.php", { - params: { - emailaddress: formObj.emailAddress, - nineCode: formObj.code, - deviceName: formObj.deviceName, - }, - }); - - //Attempt to store cookies! - const { data: jwtdata } = await axios.get( - `/api/jwt.php?emailaddress=${encodeURIComponent( - formObj.emailAddress, - )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ - formObj.deviceName - }&newAccount=${data.existed}&stay=${ - formObj.staySignedIn == "true" ? "1" : "0" - }`, - { withCredentials: true }, + return redirect( + `/signinCode?email=${formObj.emailAddress}&device=${data.deviceName}&stay=${formObj.staySignedIn}`, ); - console.log("jwtdata", jwtdata); - - if (data.hasFullName == 1) { - //Redirect if we have their full name - //and there wasn't an error with sign in - // TODO: Redirect to portfolio - } - return { - _action: formObj._action, - isNewAccount: data.existed, - hasFullName: data.hasFullName, - success: true, - }; - } else if (formObj._action == "submit name") { - let { data } = await axios.get("/api/saveUsersName.php", { - params: { - firstName: formObj.firstName, - lastName: formObj.lastName, - email: formObj.emailAddress, - }, - }); - console.log("data", data); - return true; - - // TODO: Redirect to portfolio + // return { + // _action: formObj._action, + // deviceName: data.deviceName, + // emailAddress: formObj.emailAddress, + // staySignedIn: formObj.staySignedIn, + // success: true, + // }; } - - return { success: true }; } catch (e) { return { success: false, @@ -111,432 +51,106 @@ export async function action({ params, request }) { } } -function AskForEmailCard({ fetcher }) { +export function SignIn() { + const fetcher = useFetcher(); const [emailAddress, setEmailAddress] = useState(""); const [emailError, setEmailError] = useState(null); const [isChecked, setIsChecked] = useState(false); const [isDisabled, setIsDisabled] = useState(false); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return ( - - - Doenet Logo - - - - - Doenet Logo - - Sign In via Email - - - Email address - { - let nextValue = e.target.value; - //Clear error if email is now good - if (emailError != null && emailRegex.test(nextValue)) { - setEmailError(null); - } - setEmailAddress(nextValue); - }} - /> - {emailError} - - - setIsChecked(e.target.checked)} - > - Stay Signed In - - - - - - - - - - - ); -} - -function EnterCodeCard({ fetcher, emailAddress, deviceName, staySignedIn }) { - const [code, setCode] = useState(""); - const [codeError, setCodeError] = useState(null); - const [isDisabled, setIsDisabled] = useState(false); - const [isExpired, setIsExpired] = useState(false); - - console.log("EnterCodeCard fetcher", fetcher); - //Handle code entry errors - if (fetcher.data?.success == false) { - //Guard against an infinite loop - if (codeError !== fetcher.data.message) { - setCodeError(fetcher.data.message); - setIsDisabled(false); - if (fetcher.data.message == "Code expired.") { - setIsExpired(true); - } - } - } - - return ( - - - Doenet Logo - - - - - Doenet Logo - - Check your email for the code. - - - Sign-in code (9 digit code): - - setCode(code)}> - - - - - - - - - - - - {codeError} - - - - - - {isExpired ? ( - - ) : ( - - )} - - - - - ); -} - -function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { - const [firstName, setFirstName] = useState(""); - const [firstNameError, setFirstNameError] = useState(null); - const [lastName, setLastName] = useState(""); - const [lastNameError, setLastNameError] = useState(null); - const [isDisabled, setIsDisabled] = useState(false); - - return ( - - - Doenet Logo - - - - - Doenet Logo - - Please Enter Your Name. - - - First Name: - { - if (e.target.value != "") { - setFirstNameError(null); - } - setFirstName(e.target.value); - }} - /> - {firstNameError} - - - Last Name: - { - if (e.target.value != "") { - setLastNameError(null); - } - setLastName(e.target.value); - }} - /> - {lastNameError} - - - - - - - - - - - ); -} - -export function SignIn() { - // const { success } = useLoaderData(); - const fetcher = useFetcher(); - let formObj = {}; - if (fetcher.formData !== undefined) { - formObj = Object.fromEntries(fetcher.formData); - } - console.log("fetcher.state", fetcher.state); - console.log("fetcher.data", fetcher.data); - console.log("formObj", formObj); - console.log("---------------------\n"); - - let emailAddress = useRef(null); - let deviceName = useRef(null); - let staySignedIn = useRef(null); - //card is a ref because we need the card to stay - // and not have to track every possible state - - //Enter Email - let card = useRef(); - if (fetcher.state === "idle") { - if ( - (fetcher.data?._action === "submit email" && fetcher.data?.success) || - (fetcher.data?._action === "submit code" && !fetcher.data?.success) - ) { - //Enter Code - emailAddress.current = fetcher.data.emailAddress; - staySignedIn.current = fetcher.data.staySignedIn; - deviceName.current = fetcher.data.deviceName; - - card.current = ( - - ); - } else if ( - (fetcher.data?._action === "submit code" && fetcher.data?.success) || - (fetcher.data?._action === "submit name" && !fetcher.data?.success) - ) { - //Enter Name - card.current = ( - - ); - } - } - - // card = ; - - // const setActivityByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); //TODO: remove after recoil is gone - // const setPageByDoenetId = useSetRecoilState(itemByDoenetId(pageId)); //TODO: remove after recoil is gone - - // let location = useLocation(); - - // const navigate = useNavigate(); - - // const [recoilPageToolView, setRecoilPageToolView] = - // useRecoilState(pageToolViewAtom); - - // let navigateTo = useRef(""); - - // if (navigateTo.current != "") { - // const newHref = navigateTo.current; - // navigateTo.current = ""; - // location.href = newHref; - // navigate(newHref); - // } - - //Optimistic UI - // let effectiveLabel = activityData.pageLabel; - // if (activityData.isSinglePage) { - // effectiveLabel = activityData.label; - // if (fetcher.data?._action == "update label") { - // effectiveLabel = fetcher.data.label; - // } - // } else { - // if (fetcher.data?._action == "update page label") { - // effectiveLabel = fetcher.data.pageLabel; - // } - // } - return ( <> - {card.current} + + + + Doenet Logo + + + + + Doenet Logo + + Sign In via Email + + + Email address + { + let nextValue = e.target.value; + //Clear error if email is now good + if (emailError != null && emailRegex.test(nextValue)) { + setEmailError(null); + } + setEmailAddress(nextValue); + }} + /> + {emailError} + + + setIsChecked(e.target.checked)} + > + Stay Signed In + + + + + + + + + + + ); diff --git a/src/Tools/_framework/Paths/SignInCode.jsx b/src/Tools/_framework/Paths/SignInCode.jsx new file mode 100644 index 0000000000..b3cdeae43f --- /dev/null +++ b/src/Tools/_framework/Paths/SignInCode.jsx @@ -0,0 +1,238 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Center, + Checkbox, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Heading, + Image, + Input, + PinInput, + PinInputField, + Spinner, + Stack, + Text, +} from "@chakra-ui/react"; +import axios from "axios"; +import React, { useState } from "react"; +import { redirect, useLoaderData } from "react-router"; +import { useFetcher } from "react-router-dom"; + +export async function loader({ request }) { + //Search Parameters to useLoaderData + const url = new URL(request.url); + const emailAddress = url.searchParams.get("email"); + const deviceName = url.searchParams.get("device"); + const staySignedIn = url.searchParams.get("stay"); + return { emailAddress, deviceName, staySignedIn }; +} + +export async function action({ params, request }) { + const formData = await request.formData(); + const formObj = Object.fromEntries(formData); + + try { + if (formObj._action == "send new code") { + let { data } = await axios.get("/api/sendSignInEmail.php", { + params: { emailaddress: formObj.emailAddress }, + }); + return { + success: true, + _action: formObj._action, + }; + } else if (formObj._action == "submit code") { + //TODO: need check credentials to give back the portfolio course id + let { data } = await axios.get("/api/checkCredentials.php", { + params: { + emailaddress: formObj.emailAddress, + nineCode: formObj.code, + deviceName: formObj.deviceName, + }, + }); + console.log("submit code data", data); + + //Attempt to store cookies! + const { data: jwtdata } = await axios.get( + `/api/jwt.php?emailaddress=${encodeURIComponent( + formObj.emailAddress, + )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ + formObj.deviceName + }&newAccount=${data.existed}&stay=${ + formObj.staySignedIn == "true" ? "1" : "0" + }`, + { withCredentials: true }, + ); + + console.log("jwtdata", jwtdata); + + //Redirect to portfolio + //or ask for name + if (data.hasFullName) { + //Redirect to portfolio + return redirect(`/portfolio/${data.portfolioCourseId}`); + } else { + //Redirect to askname + return redirect(`/signinName`); + } + } + } catch (e) { + return { + success: false, + message: e.response.data.message, + _action: formObj._action, + }; + } +} + +export function SignInCode() { + const { emailAddress, deviceName, staySignedIn } = useLoaderData(); + + const fetcher = useFetcher(); + let formObj = {}; + if (fetcher.formData !== undefined) { + formObj = Object.fromEntries(fetcher.formData); + } + console.log("fetcher.state", fetcher.state); + console.log("fetcher.data", fetcher.data); + console.log("formObj", formObj); + console.log("---------------------\n"); + + const [code, setCode] = useState(""); + const [codeError, setCodeError] = useState(null); + const [isDisabled, setIsDisabled] = useState(false); + const [isExpired, setIsExpired] = useState(false); + + //Handle code entry errors + if (fetcher.data?.success == false) { + //Guard against an infinite loop + if (codeError !== fetcher.data.message) { + setCodeError(fetcher.data.message); + setIsDisabled(false); + if (fetcher.data.message == "Code expired.") { + setIsExpired(true); + } + } + } + + return ( + <> + + + + + Doenet Logo + + + + + Doenet Logo + + Check your email for the code. + + + Sign-in code (9 digit code): + + setCode(code)}> + + + + + + + + + + + + {codeError} + + + + + + {isExpired ? ( + + ) : ( + + )} + + + + + + + + ); +} diff --git a/src/Tools/_framework/Paths/SignInName.jsx b/src/Tools/_framework/Paths/SignInName.jsx new file mode 100644 index 0000000000..b863fc5732 --- /dev/null +++ b/src/Tools/_framework/Paths/SignInName.jsx @@ -0,0 +1,533 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Center, + Checkbox, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Heading, + Image, + Input, + PinInput, + PinInputField, + Spinner, + Stack, + Text, +} from "@chakra-ui/react"; +import axios from "axios"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { redirect, useLoaderData } from "react-router"; +import { useFetcher } from "react-router-dom"; + +export async function action({ params, request }) { + const formData = await request.formData(); + const formObj = Object.fromEntries(formData); + + try { + if (formObj._action == "submit email") { + let { data } = await axios.get("/api/sendSignInEmail.php", { + params: { emailaddress: formObj.emailAddress }, + }); + return { + _action: formObj._action, + deviceName: data.deviceName, + emailAddress: formObj.emailAddress, + staySignedIn: formObj.staySignedIn, + success: true, + }; + } else if (formObj._action == "submit code") { + //TODO: need check credentials to give back the portfolio course id + let { data } = await axios.get("/api/checkCredentials.php", { + params: { + emailaddress: formObj.emailAddress, + nineCode: formObj.code, + deviceName: formObj.deviceName, + }, + }); + + //Attempt to store cookies! + const { data: jwtdata } = await axios.get( + `/api/jwt.php?emailaddress=${encodeURIComponent( + formObj.emailAddress, + )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ + formObj.deviceName + }&newAccount=${data.existed}&stay=${ + formObj.staySignedIn == "true" ? "1" : "0" + }`, + { withCredentials: true }, + ); + + console.log("jwtdata", jwtdata); + + if (data.hasFullName == 1) { + //Redirect if we have their full name + //and there wasn't an error with sign in + // TODO: Redirect to portfolio + } + return { + _action: formObj._action, + isNewAccount: data.existed, + hasFullName: data.hasFullName, + success: true, + }; + } else if (formObj._action == "submit name") { + let { data } = await axios.get("/api/saveUsersName.php", { + params: { + firstName: formObj.firstName, + lastName: formObj.lastName, + email: formObj.emailAddress, + }, + }); + console.log("data", data); + return true; + + // TODO: Redirect to portfolio + } + + return { success: true }; + } catch (e) { + return { + success: false, + message: e.response.data.message, + _action: formObj._action, + }; + } +} + +function AskForEmailCard({ fetcher }) { + const [emailAddress, setEmailAddress] = useState(""); + const [emailError, setEmailError] = useState(null); + const [isChecked, setIsChecked] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + return ( + + + Doenet Logo + + + + + Doenet Logo + + Sign In via Email + + + Email address + { + let nextValue = e.target.value; + //Clear error if email is now good + if (emailError != null && emailRegex.test(nextValue)) { + setEmailError(null); + } + setEmailAddress(nextValue); + }} + /> + {emailError} + + + setIsChecked(e.target.checked)} + > + Stay Signed In + + + + + + + + + + + ); +} + +function EnterCodeCard({ fetcher, emailAddress, deviceName, staySignedIn }) { + const [code, setCode] = useState(""); + const [codeError, setCodeError] = useState(null); + const [isDisabled, setIsDisabled] = useState(false); + const [isExpired, setIsExpired] = useState(false); + + console.log("EnterCodeCard fetcher", fetcher); + //Handle code entry errors + if (fetcher.data?.success == false) { + //Guard against an infinite loop + if (codeError !== fetcher.data.message) { + setCodeError(fetcher.data.message); + setIsDisabled(false); + if (fetcher.data.message == "Code expired.") { + setIsExpired(true); + } + } + } + + return ( + + + Doenet Logo + + + + + Doenet Logo + + Check your email for the code. + + + Sign-in code (9 digit code): + + setCode(code)}> + + + + + + + + + + + + {codeError} + + + + + + {isExpired ? ( + + ) : ( + + )} + + + + + ); +} + +function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { + const [firstName, setFirstName] = useState(""); + const [firstNameError, setFirstNameError] = useState(null); + const [lastName, setLastName] = useState(""); + const [lastNameError, setLastNameError] = useState(null); + const [isDisabled, setIsDisabled] = useState(false); + + return ( + + + Doenet Logo + + + + + Doenet Logo + + Please Enter Your Name. + + + First Name: + { + if (e.target.value != "") { + setFirstNameError(null); + } + setFirstName(e.target.value); + }} + /> + {firstNameError} + + + Last Name: + { + if (e.target.value != "") { + setLastNameError(null); + } + setLastName(e.target.value); + }} + /> + {lastNameError} + + + + + + + + + + + ); +} + +export function SignInName() { + // const { success } = useLoaderData(); + const fetcher = useFetcher(); + let formObj = {}; + if (fetcher.formData !== undefined) { + formObj = Object.fromEntries(fetcher.formData); + } + console.log("fetcher.state", fetcher.state); + console.log("fetcher.data", fetcher.data); + console.log("formObj", formObj); + console.log("---------------------\n"); + + let emailAddress = useRef(null); + let deviceName = useRef(null); + let staySignedIn = useRef(null); + //card is a ref because we need the card to stay + // and not have to track every possible state + + //Enter Email + let card = useRef(); + if (fetcher.state === "idle") { + if ( + (fetcher.data?._action === "submit email" && fetcher.data?.success) || + (fetcher.data?._action === "submit code" && !fetcher.data?.success) + ) { + //Enter Code + emailAddress.current = fetcher.data.emailAddress; + staySignedIn.current = fetcher.data.staySignedIn; + deviceName.current = fetcher.data.deviceName; + + card.current = ( + + ); + } else if ( + (fetcher.data?._action === "submit code" && fetcher.data?.success) || + (fetcher.data?._action === "submit name" && !fetcher.data?.success) + ) { + //Enter Name + card.current = ( + + ); + } + } + + // card = ; + + // const setActivityByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); //TODO: remove after recoil is gone + // const setPageByDoenetId = useSetRecoilState(itemByDoenetId(pageId)); //TODO: remove after recoil is gone + + // let location = useLocation(); + + // const navigate = useNavigate(); + + // const [recoilPageToolView, setRecoilPageToolView] = + // useRecoilState(pageToolViewAtom); + + // let navigateTo = useRef(""); + + // if (navigateTo.current != "") { + // const newHref = navigateTo.current; + // navigateTo.current = ""; + // location.href = newHref; + // navigate(newHref); + // } + + //Optimistic UI + // let effectiveLabel = activityData.pageLabel; + // if (activityData.isSinglePage) { + // effectiveLabel = activityData.label; + // if (fetcher.data?._action == "update label") { + // effectiveLabel = fetcher.data.label; + // } + // } else { + // if (fetcher.data?._action == "update page label") { + // effectiveLabel = fetcher.data.pageLabel; + // } + // } + + return ( + <> + + {card.current} + + + ); +} diff --git a/src/index.jsx b/src/index.jsx index 54be7bcbd5..cd4b71e17e 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -83,9 +83,18 @@ import { } from "./Tools/_framework/Paths/CourseLinkPageViewer"; import { SignIn, - loader as signInLoader, action as signInAction, } from "./Tools/_framework/Paths/SignIn"; +import { + SignInCode, + loader as signInCodeLoader, + action as signInCodeAction, +} from "./Tools/_framework/Paths/SignInCode"; +import { + SignInName, + action as signInNameAction, + loader as signInNameLoader, +} from "./Tools/_framework/Paths/SignInName"; { /* */ @@ -327,7 +336,6 @@ const router = createBrowserRouter([ }, { path: "signin", - loader: signInLoader, action: signInAction, errorElement: ( @@ -340,6 +348,36 @@ const router = createBrowserRouter([ ), }, + { + path: "signinCode", + loader: signInCodeLoader, + action: signInCodeAction, + errorElement: ( + + + + ), + element: ( + + + + ), + }, + { + path: "signinName", + loader: signInNameLoader, + action: signInNameAction, + errorElement: ( + + + + ), + element: ( + + + + ), + }, { path: "public", loader: editorSupportPanelLoader, From 4fa1e9b2106abf51449519f8c056e15b9cb8d74a Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Sun, 22 Oct 2023 15:41:54 -0500 Subject: [PATCH 08/19] basics of three path method --- public/api/baseModel.php | 26 +- public/api/sendSignInEmail.php | 40 +- src/Tools/_framework/Paths/SignInCode.jsx | 63 +-- src/Tools/_framework/Paths/SignInName.jsx | 582 ++++------------------ src/index.jsx | 4 - 5 files changed, 169 insertions(+), 546 deletions(-) diff --git a/public/api/baseModel.php b/public/api/baseModel.php index 12235e448a..e74c27cf96 100644 --- a/public/api/baseModel.php +++ b/public/api/baseModel.php @@ -28,11 +28,14 @@ public static function runQuery($conn, $query) { public static function queryFetchAssoc($conn, $query) { $result = Base_Model::runQuery($conn, $query); if ($result->num_rows > 0) { - $rows = []; + $data = []; while($row = $result->fetch_assoc()){ - $rows[] = $row; + $data['rows'] = $row; + foreach($row as $key => $value){ + $data[$key][] = $value; + } } - return $rows; + return $data; } else { return []; } @@ -45,18 +48,23 @@ public static function queryFetchAssoc($conn, $query) { * * If more than one row is returned, throws an exception. */ - public static function queryExpectingOneRow($conn, $query) { - $rows = Base_Model::queryFetchAssoc($conn, $query); - - if (count($rows) == 1) { - return $rows[0]; - } else if (count($rows) == 0) { + public static function queryOneRowOrError($conn, $query) { + $data = Base_Model::queryFetchAssoc($conn, $query); + + if (count($data['rows']) == 1) { + return $data; + } else if (count($data['rows']) == 0) { return null; } else { + throw new Exception("Unexpected error, only expected one row from this query."); + error_log("Unexpected error, only expected one row from this query." . + "\n " . $conn->error . + "\n" . $query); } } + /** * Validate that a list of keys are present in a given associative array. * diff --git a/public/api/sendSignInEmail.php b/public/api/sendSignInEmail.php index c3e616c013..081e4044bc 100644 --- a/public/api/sendSignInEmail.php +++ b/public/api/sendSignInEmail.php @@ -6,39 +6,41 @@ header('Content-Type: application/json'); include "db_connection.php"; +include "baseModel.php"; $emailaddress = mysqli_real_escape_string($conn,$_REQUEST["emailaddress"]); $deviceNames = include "deviceNames.php"; -//Nine digit random number -$signInCode = rand(100000000,999999999); $response_arr; try { + Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress"]); + + //Nine digit random number + $signInCode = rand(100000000,999999999); -$sql = "SELECT email, userId -FROM user -WHERE email='$emailaddress'"; + $sql = "SELECT email, userId + FROM user + WHERE email='$emailaddress'"; -$result = $conn->query($sql); + $userEmailArray = Base_Model::queryFetchAssoc($conn, $sql); -if ($result->num_rows > 0){ - //Already have an email with this account - $row = $result->fetch_assoc(); - $user_id = $row['userId']; - //unique deviceName - //Remove device names which are already in use +if (count($userEmailArray) > 0){ + //We have an email with this account + + $user_id = $userEmailArray['userId'][0]; + //In order to maintain unique deviceNames + //remove device names which are already in use $sql = " SELECT deviceName FROM user_device WHERE userId='$user_id' + AND signedIn=1 "; - $result = $conn->query($sql); - $used_deviceNames = array(); - while($row = $result->fetch_assoc()){ - array_push($used_deviceNames,$row['deviceName']); - } + $devicesArray = Base_Model::queryFetchAssoc($conn, $sql); + $used_deviceNames = $devicesArray['deviceName'] != null ? $devicesArray['deviceName'] : []; + $deviceNames = array_values(array_diff($deviceNames,$used_deviceNames)); if (count($deviceNames) < 1){ //Ran out of device names @@ -54,14 +56,14 @@ //New email address $user_id = include "randomId.php"; $sql = "INSERT INTO user (userId,email) VALUE ('$user_id','$emailaddress')"; - $result = $conn->query($sql); + Base_Model::runQuery($conn,$sql); //Define device name $randomNumber = rand(0,(count($deviceNames) - 1)); $deviceName = $deviceNames[$randomNumber]; } $sql = "INSERT INTO user_device (userId,email,signInCode,timestampOfSignInCode, deviceName) VALUE ('$user_id','$emailaddress','$signInCode',NOW(),'$deviceName')"; - $result = $conn->query($sql); +Base_Model::runQuery($conn,$sql); // Generate and modify email content diff --git a/src/Tools/_framework/Paths/SignInCode.jsx b/src/Tools/_framework/Paths/SignInCode.jsx index b3cdeae43f..a82779b44e 100644 --- a/src/Tools/_framework/Paths/SignInCode.jsx +++ b/src/Tools/_framework/Paths/SignInCode.jsx @@ -5,8 +5,6 @@ import { Card, CardBody, CardFooter, - Center, - Checkbox, Flex, FormControl, FormErrorMessage, @@ -14,35 +12,28 @@ import { HStack, Heading, Image, - Input, PinInput, PinInputField, Spinner, Stack, - Text, } from "@chakra-ui/react"; import axios from "axios"; import React, { useState } from "react"; import { redirect, useLoaderData } from "react-router"; import { useFetcher } from "react-router-dom"; -export async function loader({ request }) { - //Search Parameters to useLoaderData +export async function action({ request }) { + const formData = await request.formData(); + const formObj = Object.fromEntries(formData); const url = new URL(request.url); const emailAddress = url.searchParams.get("email"); const deviceName = url.searchParams.get("device"); const staySignedIn = url.searchParams.get("stay"); - return { emailAddress, deviceName, staySignedIn }; -} - -export async function action({ params, request }) { - const formData = await request.formData(); - const formObj = Object.fromEntries(formData); try { if (formObj._action == "send new code") { let { data } = await axios.get("/api/sendSignInEmail.php", { - params: { emailaddress: formObj.emailAddress }, + params: { emailaddress: emailAddress }, }); return { success: true, @@ -52,27 +43,24 @@ export async function action({ params, request }) { //TODO: need check credentials to give back the portfolio course id let { data } = await axios.get("/api/checkCredentials.php", { params: { - emailaddress: formObj.emailAddress, + emailaddress: emailAddress, nineCode: formObj.code, - deviceName: formObj.deviceName, + deviceName: deviceName, }, }); - console.log("submit code data", data); //Attempt to store cookies! const { data: jwtdata } = await axios.get( `/api/jwt.php?emailaddress=${encodeURIComponent( - formObj.emailAddress, - )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ - formObj.deviceName - }&newAccount=${data.existed}&stay=${ - formObj.staySignedIn == "true" ? "1" : "0" + emailAddress, + )}&nineCode=${encodeURIComponent( + formObj.code, + )}&deviceName=${deviceName}&newAccount=${data.existed}&stay=${ + staySignedIn == "true" ? "1" : "0" }`, { withCredentials: true }, ); - console.log("jwtdata", jwtdata); - //Redirect to portfolio //or ask for name if (data.hasFullName) { @@ -80,7 +68,11 @@ export async function action({ params, request }) { return redirect(`/portfolio/${data.portfolioCourseId}`); } else { //Redirect to askname - return redirect(`/signinName`); + return redirect( + `/signinName?email=${encodeURIComponent( + emailAddress, + )}&portfolioId=${encodeURIComponent(data.portfolioCourseId)}`, + ); } } } catch (e) { @@ -93,17 +85,15 @@ export async function action({ params, request }) { } export function SignInCode() { - const { emailAddress, deviceName, staySignedIn } = useLoaderData(); - const fetcher = useFetcher(); - let formObj = {}; - if (fetcher.formData !== undefined) { - formObj = Object.fromEntries(fetcher.formData); - } - console.log("fetcher.state", fetcher.state); - console.log("fetcher.data", fetcher.data); - console.log("formObj", formObj); - console.log("---------------------\n"); + // let formObj = {}; + // if (fetcher.formData !== undefined) { + // formObj = Object.fromEntries(fetcher.formData); + // } + // console.log("fetcher.state", fetcher.state); + // console.log("fetcher.data", fetcher.data); + // console.log("formObj", formObj); + // console.log("---------------------\n"); const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(null); @@ -186,8 +176,6 @@ export function SignInCode() { fetcher.submit( { _action: "send new code", - emailAddress: "char0042@umn.edu", - staySignedIn: true, }, { method: "post" }, ); @@ -214,9 +202,6 @@ export function SignInCode() { fetcher.submit( { _action: "submit code", - emailAddress, - deviceName, - staySignedIn, code, }, { method: "post" }, diff --git a/src/Tools/_framework/Paths/SignInName.jsx b/src/Tools/_framework/Paths/SignInName.jsx index b863fc5732..f406e337b1 100644 --- a/src/Tools/_framework/Paths/SignInName.jsx +++ b/src/Tools/_framework/Paths/SignInName.jsx @@ -5,90 +5,40 @@ import { Card, CardBody, CardFooter, - Center, - Checkbox, Flex, FormControl, FormErrorMessage, FormLabel, - HStack, Heading, Image, Input, - PinInput, - PinInputField, Spinner, Stack, - Text, } from "@chakra-ui/react"; import axios from "axios"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useState } from "react"; import { redirect, useLoaderData } from "react-router"; import { useFetcher } from "react-router-dom"; -export async function action({ params, request }) { +export async function action({ request }) { const formData = await request.formData(); const formObj = Object.fromEntries(formData); + const url = new URL(request.url); + const portfolioId = url.searchParams.get("portfolioId"); + const emailAddress = url.searchParams.get("email"); try { - if (formObj._action == "submit email") { - let { data } = await axios.get("/api/sendSignInEmail.php", { - params: { emailaddress: formObj.emailAddress }, - }); - return { - _action: formObj._action, - deviceName: data.deviceName, - emailAddress: formObj.emailAddress, - staySignedIn: formObj.staySignedIn, - success: true, - }; - } else if (formObj._action == "submit code") { - //TODO: need check credentials to give back the portfolio course id - let { data } = await axios.get("/api/checkCredentials.php", { - params: { - emailaddress: formObj.emailAddress, - nineCode: formObj.code, - deviceName: formObj.deviceName, - }, - }); - - //Attempt to store cookies! - const { data: jwtdata } = await axios.get( - `/api/jwt.php?emailaddress=${encodeURIComponent( - formObj.emailAddress, - )}&nineCode=${encodeURIComponent(formObj.code)}&deviceName=${ - formObj.deviceName - }&newAccount=${data.existed}&stay=${ - formObj.staySignedIn == "true" ? "1" : "0" - }`, - { withCredentials: true }, - ); - - console.log("jwtdata", jwtdata); - - if (data.hasFullName == 1) { - //Redirect if we have their full name - //and there wasn't an error with sign in - // TODO: Redirect to portfolio - } - return { - _action: formObj._action, - isNewAccount: data.existed, - hasFullName: data.hasFullName, - success: true, - }; - } else if (formObj._action == "submit name") { + if (formObj._action == "submit name") { let { data } = await axios.get("/api/saveUsersName.php", { params: { firstName: formObj.firstName, lastName: formObj.lastName, - email: formObj.emailAddress, + email: emailAddress, }, }); - console.log("data", data); - return true; - // TODO: Redirect to portfolio + //Redirect to portfolio + return redirect(`/portfolio/${portfolioId}`); } return { success: true }; @@ -101,432 +51,114 @@ export async function action({ params, request }) { } } -function AskForEmailCard({ fetcher }) { - const [emailAddress, setEmailAddress] = useState(""); - const [emailError, setEmailError] = useState(null); - const [isChecked, setIsChecked] = useState(false); - const [isDisabled, setIsDisabled] = useState(false); - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - return ( - - - Doenet Logo - - - - - Doenet Logo - - Sign In via Email - - - Email address - { - let nextValue = e.target.value; - //Clear error if email is now good - if (emailError != null && emailRegex.test(nextValue)) { - setEmailError(null); - } - setEmailAddress(nextValue); - }} - /> - {emailError} - - - setIsChecked(e.target.checked)} - > - Stay Signed In - - - - - - - - - - - ); -} - -function EnterCodeCard({ fetcher, emailAddress, deviceName, staySignedIn }) { - const [code, setCode] = useState(""); - const [codeError, setCodeError] = useState(null); - const [isDisabled, setIsDisabled] = useState(false); - const [isExpired, setIsExpired] = useState(false); - - console.log("EnterCodeCard fetcher", fetcher); - //Handle code entry errors - if (fetcher.data?.success == false) { - //Guard against an infinite loop - if (codeError !== fetcher.data.message) { - setCodeError(fetcher.data.message); - setIsDisabled(false); - if (fetcher.data.message == "Code expired.") { - setIsExpired(true); - } - } - } - - return ( - - - Doenet Logo - - - - - Doenet Logo - - Check your email for the code. - - - Sign-in code (9 digit code): - - setCode(code)}> - - - - - - - - - - - - {codeError} - - - - - - {isExpired ? ( - - ) : ( - - )} - - - - - ); -} +export function SignInName() { + const fetcher = useFetcher(); + // let formObj = {}; + // if (fetcher.formData !== undefined) { + // formObj = Object.fromEntries(fetcher.formData); + // } -function AskForNameCard({ fetcher, emailAddress, deviceName, staySignedIn }) { const [firstName, setFirstName] = useState(""); const [firstNameError, setFirstNameError] = useState(null); const [lastName, setLastName] = useState(""); const [lastNameError, setLastNameError] = useState(null); const [isDisabled, setIsDisabled] = useState(false); - return ( - - - Doenet Logo - - - - - Doenet Logo - - Please Enter Your Name. - - - First Name: - { - if (e.target.value != "") { - setFirstNameError(null); - } - setFirstName(e.target.value); - }} - /> - {firstNameError} - - - Last Name: - { - if (e.target.value != "") { - setLastNameError(null); - } - setLastName(e.target.value); - }} - /> - {lastNameError} - - - - - - - - - - - ); -} - -export function SignInName() { - // const { success } = useLoaderData(); - const fetcher = useFetcher(); - let formObj = {}; - if (fetcher.formData !== undefined) { - formObj = Object.fromEntries(fetcher.formData); - } - console.log("fetcher.state", fetcher.state); - console.log("fetcher.data", fetcher.data); - console.log("formObj", formObj); - console.log("---------------------\n"); - - let emailAddress = useRef(null); - let deviceName = useRef(null); - let staySignedIn = useRef(null); - //card is a ref because we need the card to stay - // and not have to track every possible state - - //Enter Email - let card = useRef(); - if (fetcher.state === "idle") { - if ( - (fetcher.data?._action === "submit email" && fetcher.data?.success) || - (fetcher.data?._action === "submit code" && !fetcher.data?.success) - ) { - //Enter Code - emailAddress.current = fetcher.data.emailAddress; - staySignedIn.current = fetcher.data.staySignedIn; - deviceName.current = fetcher.data.deviceName; - - card.current = ( - - ); - } else if ( - (fetcher.data?._action === "submit code" && fetcher.data?.success) || - (fetcher.data?._action === "submit name" && !fetcher.data?.success) - ) { - //Enter Name - card.current = ( - - ); - } - } - - // card = ; - - // const setActivityByDoenetId = useSetRecoilState(itemByDoenetId(doenetId)); //TODO: remove after recoil is gone - // const setPageByDoenetId = useSetRecoilState(itemByDoenetId(pageId)); //TODO: remove after recoil is gone - - // let location = useLocation(); - - // const navigate = useNavigate(); - - // const [recoilPageToolView, setRecoilPageToolView] = - // useRecoilState(pageToolViewAtom); - - // let navigateTo = useRef(""); - - // if (navigateTo.current != "") { - // const newHref = navigateTo.current; - // navigateTo.current = ""; - // location.href = newHref; - // navigate(newHref); - // } - - //Optimistic UI - // let effectiveLabel = activityData.pageLabel; - // if (activityData.isSinglePage) { - // effectiveLabel = activityData.label; - // if (fetcher.data?._action == "update label") { - // effectiveLabel = fetcher.data.label; - // } - // } else { - // if (fetcher.data?._action == "update page label") { - // effectiveLabel = fetcher.data.pageLabel; - // } - // } - return ( <> - {card.current} + + + + Doenet Logo + + + + + Doenet Logo + + Please Enter Your Name. + + + First Name: + { + if (e.target.value != "") { + setFirstNameError(null); + } + setFirstName(e.target.value); + }} + /> + {firstNameError} + + + Last Name: + { + if (e.target.value != "") { + setLastNameError(null); + } + setLastName(e.target.value); + }} + /> + {lastNameError} + + + + + + + + + + + ); diff --git a/src/index.jsx b/src/index.jsx index cd4b71e17e..ba45b22000 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -87,13 +87,11 @@ import { } from "./Tools/_framework/Paths/SignIn"; import { SignInCode, - loader as signInCodeLoader, action as signInCodeAction, } from "./Tools/_framework/Paths/SignInCode"; import { SignInName, action as signInNameAction, - loader as signInNameLoader, } from "./Tools/_framework/Paths/SignInName"; { @@ -350,7 +348,6 @@ const router = createBrowserRouter([ }, { path: "signinCode", - loader: signInCodeLoader, action: signInCodeAction, errorElement: ( @@ -365,7 +362,6 @@ const router = createBrowserRouter([ }, { path: "signinName", - loader: signInNameLoader, action: signInNameAction, errorElement: ( From 3a06d31cfd53418896c4536289c3c64e9953629a Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Sun, 22 Oct 2023 16:14:16 -0500 Subject: [PATCH 09/19] handle code expired --- public/api/checkCredentials.php | 18 +++---- public/api/jwt.php | 2 + public/api/saveUsersName.php | 60 +++++++++++++---------- src/Tools/_framework/Paths/SignInCode.jsx | 12 +---- 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/public/api/checkCredentials.php b/public/api/checkCredentials.php index e6181dea9a..9743624a69 100644 --- a/public/api/checkCredentials.php +++ b/public/api/checkCredentials.php @@ -6,23 +6,15 @@ header('Content-Type: application/json'); include "db_connection.php"; +include "baseModel.php"; $emailaddress = mysqli_real_escape_string($conn,$_REQUEST["emailaddress"]); $nineCode = mysqli_real_escape_string($conn,$_REQUEST["nineCode"]); $deviceName = mysqli_real_escape_string($conn,$_REQUEST["deviceName"]); -if(!isset($_REQUEST["emailaddress"])){ - throw new Exception("Internal Error: missing emailaddress"); -} -if(!isset($_REQUEST["nineCode"])){ - throw new Exception("Internal Error: missing nineCode"); -} -if(!isset($_REQUEST["deviceName"])){ - throw new Exception("Internal Error: missing deviceName"); -} - $response_arr; try { + Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress","nineCode","deviceName"]); //Check if expired $sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes @@ -36,6 +28,7 @@ $existed = true; $hasFullName = false; $reason = ""; +throw new Exception("Code expired"); //Delete me //Check if it took longer than 10 minutes to enter the code if ($row['minutes'] > 10){ @@ -110,6 +103,9 @@ "portfolioCourseId" => $portfolioCourseId, ); +http_response_code(200); + + } catch (Exception $e) { $response_arr = [ 'success' => false, @@ -122,4 +118,4 @@ echo json_encode($response_arr); $conn->close(); } - +?> diff --git a/public/api/jwt.php b/public/api/jwt.php index 5731d3ee13..8110f24052 100644 --- a/public/api/jwt.php +++ b/public/api/jwt.php @@ -1,5 +1,6 @@ query($sql); - -$response_arr = array( - 'success' => $success, - 'message' => $message, -); - -// set response code - 200 OK -http_response_code(200); - -// make it json format -echo json_encode($response_arr); - -$conn->close(); -?> +$response_arr; +try { + Base_Model::checkForRequiredInputs($_REQUEST,["email","firstName","lastName"]); + + $sql = " + UPDATE user + SET firstName='$firstName', + lastName='$lastName' + WHERE email='$email' + "; + $conn->query($sql); + + $response_arr = array( + 'success' => true, + 'message' => $message, + ); + + http_response_code(200); + +} catch (Exception $e) { + $response_arr = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + http_response_code(400); + +} finally { + // make it json format + echo json_encode($response_arr); + $conn->close(); +} +?> \ No newline at end of file diff --git a/src/Tools/_framework/Paths/SignInCode.jsx b/src/Tools/_framework/Paths/SignInCode.jsx index a82779b44e..39218e2ec4 100644 --- a/src/Tools/_framework/Paths/SignInCode.jsx +++ b/src/Tools/_framework/Paths/SignInCode.jsx @@ -86,14 +86,6 @@ export async function action({ request }) { export function SignInCode() { const fetcher = useFetcher(); - // let formObj = {}; - // if (fetcher.formData !== undefined) { - // formObj = Object.fromEntries(fetcher.formData); - // } - // console.log("fetcher.state", fetcher.state); - // console.log("fetcher.data", fetcher.data); - // console.log("formObj", formObj); - // console.log("---------------------\n"); const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(null); @@ -101,12 +93,12 @@ export function SignInCode() { const [isExpired, setIsExpired] = useState(false); //Handle code entry errors - if (fetcher.data?.success == false) { + if (fetcher.data?.success === false && fetcher.state === "idle") { //Guard against an infinite loop if (codeError !== fetcher.data.message) { setCodeError(fetcher.data.message); setIsDisabled(false); - if (fetcher.data.message == "Code expired.") { + if (fetcher.data.message == "Code expired") { setIsExpired(true); } } From a7bf41029af225f20d163d9e66d58218895e3311 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Mon, 23 Oct 2023 15:59:17 -0500 Subject: [PATCH 10/19] fixed bugs --- public/api/checkCredentials.php | 49 +++++---- public/api/jwt.php | 31 +++--- public/api/sendSignInEmail.php | 125 ++++++++++++---------- src/Tools/_framework/Paths/SignInCode.jsx | 7 +- 4 files changed, 122 insertions(+), 90 deletions(-) diff --git a/public/api/checkCredentials.php b/public/api/checkCredentials.php index 9743624a69..7c5b3665a9 100644 --- a/public/api/checkCredentials.php +++ b/public/api/checkCredentials.php @@ -16,28 +16,37 @@ try { Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress","nineCode","deviceName"]); -//Check if expired -$sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes -FROM user_device -WHERE email='$emailaddress' AND deviceName='$deviceName'"; - -$result = $conn->query($sql); -$row = $result->fetch_assoc(); - -//Assume it already exists -$existed = true; -$hasFullName = false; -$reason = ""; -throw new Exception("Code expired"); //Delete me - -//Check if it took longer than 10 minutes to enter the code -if ($row['minutes'] > 10){ - throw new Exception("Code expired"); -} + //Check if expired + $sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes + FROM user_device + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1 + "; + + $result = $conn->query($sql); + $row = $result->fetch_assoc(); + + //Assume it already exists + $existed = true; + $hasFullName = false; + $reason = ""; + + // throw new Exception("Code expired"); //DELETE ME!!! + + + //Check if it took longer than 10 minutes to enter the code + if ($row['minutes'] > 10){ + throw new Exception("Code expired"); + } + //Only the most recent one $sql = "SELECT signInCode AS nineCode FROM user_device - WHERE email='$emailaddress' AND deviceName='$deviceName'"; + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1 + "; $result = $conn->query($sql); $row = $result->fetch_assoc(); @@ -91,7 +100,7 @@ WHERE u.email = '$emailaddress'"; $result = $conn->query($sql); $row = $result->fetch_assoc(); - $portfolioCourseId = "_"; + $portfolioCourseId = "not_created"; if ($result->num_rows > 0) { $portfolioCourseId = $row['courseId']; } diff --git a/public/api/jwt.php b/public/api/jwt.php index 8110f24052..9e3eb4f169 100644 --- a/public/api/jwt.php +++ b/public/api/jwt.php @@ -16,21 +16,26 @@ $response_arr; try { Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress","nineCode","deviceName","newAccount","stay"]); -//Check if expired -$sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes -FROM user_device -WHERE email='$emailaddress' AND deviceName='$deviceName'"; + //Check if expired + $sql = "SELECT TIMESTAMPDIFF(MINUTE, timestampOfSignInCode, NOW()) AS minutes + FROM user_device + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1 + "; -$result = $conn->query($sql); -$row = $result->fetch_assoc(); + $result = $conn->query($sql); + $row = $result->fetch_assoc(); -//Check if it took longer than 10 minutes to enter the code -if ($row['minutes'] > 10) { - throw new Exception("Code expired."); -} - $sql = "SELECT signInCode AS nineCode, userId - FROM user_device - WHERE email='$emailaddress' AND deviceName='$deviceName'"; + //Check if it took longer than 10 minutes to enter the code + if ($row['minutes'] > 10) { + throw new Exception("Code expired."); + } + $sql = "SELECT signInCode AS nineCode,userId + FROM user_device + WHERE email='$emailaddress' AND deviceName='$deviceName' + ORDER BY timestampOfSignInCode DESC + LIMIT 1"; $result = $conn->query($sql); $row = $result->fetch_assoc(); $userId = $row['userId']; diff --git a/public/api/sendSignInEmail.php b/public/api/sendSignInEmail.php index 081e4044bc..d2e903d3e1 100644 --- a/public/api/sendSignInEmail.php +++ b/public/api/sendSignInEmail.php @@ -9,82 +9,99 @@ include "baseModel.php"; $emailaddress = mysqli_real_escape_string($conn,$_REQUEST["emailaddress"]); +$deviceName = mysqli_real_escape_string($conn,$_REQUEST["deviceName"]); -$deviceNames = include "deviceNames.php"; $response_arr; try { Base_Model::checkForRequiredInputs($_REQUEST,["emailaddress"]); - - //Nine digit random number + + //Create a nine digit random number $signInCode = rand(100000000,999999999); + //Do we have an account with this email? $sql = "SELECT email, userId FROM user WHERE email='$emailaddress'"; $userEmailArray = Base_Model::queryFetchAssoc($conn, $sql); - -if (count($userEmailArray) > 0){ - //We have an email with this account - - $user_id = $userEmailArray['userId'][0]; - //In order to maintain unique deviceNames - //remove device names which are already in use - $sql = " - SELECT deviceName - FROM user_device - WHERE userId='$user_id' - AND signedIn=1 - "; - - $devicesArray = Base_Model::queryFetchAssoc($conn, $sql); - $used_deviceNames = $devicesArray['deviceName'] != null ? $devicesArray['deviceName'] : []; - - $deviceNames = array_values(array_diff($deviceNames,$used_deviceNames)); - if (count($deviceNames) < 1){ - //Ran out of device names - $deviceName = include 'randomId.php'; + if (count($userEmailArray) < 1){ + //We need an account created + $user_id = include "randomId.php"; + $sql = "INSERT INTO user (userId,email) VALUE ('$user_id','$emailaddress')"; + Base_Model::runQuery($conn,$sql); }else{ - //Pick from what is left - $randomNumber = rand(0,(count($deviceNames) - 1)); - $deviceName = $deviceNames[$randomNumber]; + $user_id = $userEmailArray['userId'][0]; } + if (array_key_exists("deviceName",$_REQUEST)){ + //Already have a device name + //Just update the signInCode and timestampOfSignInCode + //of the latest entry of that device name + $sql = "UPDATE user_device + SET signInCode = '$signInCode', timestampOfSignInCode = NOW() + WHERE (userId, email, deviceName, timestampOfSignInCode) = ( + SELECT userId, email, deviceName, MAX(timestampOfSignInCode) + FROM ( + SELECT * FROM user_device + ) AS temp + WHERE userId = '$user_id' AND email = '$emailaddress' AND deviceName = '$deviceName' + ) + "; + Base_Model::runQuery($conn,$sql); -}else{ - //New email address - $user_id = include "randomId.php"; - $sql = "INSERT INTO user (userId,email) VALUE ('$user_id','$emailaddress')"; - Base_Model::runQuery($conn,$sql); - //Define device name - $randomNumber = rand(0,(count($deviceNames) - 1)); - $deviceName = $deviceNames[$randomNumber]; -} -$sql = "INSERT INTO user_device (userId,email,signInCode,timestampOfSignInCode, deviceName) - VALUE ('$user_id','$emailaddress','$signInCode',NOW(),'$deviceName')"; -Base_Model::runQuery($conn,$sql); + }else{ + //Select a device name + $deviceNames = include "deviceNames.php"; + //In order to maintain unique deviceNames + //remove device names which are already in use + $sql = " + SELECT deviceName + FROM user_device + WHERE userId='$user_id' + AND signedIn=1 + "; + + $devicesArray = Base_Model::queryFetchAssoc($conn, $sql); + $used_deviceNames = $devicesArray['deviceName'] != null ? $devicesArray['deviceName'] : []; + + $deviceNames = array_values(array_diff($deviceNames,$used_deviceNames)); + if (count($deviceNames) < 1){ + //Ran out of device names + $deviceName = include 'randomId.php'; + }else{ + //Pick from what is left + $randomNumber = rand(0,(count($deviceNames) - 1)); + $deviceName = $deviceNames[$randomNumber]; + } + + //Insert the device with the code so a user with the right code can sign in + $sql = "INSERT INTO user_device (userId,email,signInCode,timestampOfSignInCode, deviceName) + VALUE ('$user_id','$emailaddress','$signInCode',NOW(),'$deviceName')"; + Base_Model::runQuery($conn,$sql); + } + -// Generate and modify email content -$htmlContent = file_get_contents("signInEmail.html"); -$htmlContent = str_replace(array("signInCode"), array($signInCode), $htmlContent); + // Generate and modify email content + $htmlContent = file_get_contents("signInEmail.html"); + $htmlContent = str_replace(array("signInCode"), array($signInCode), $htmlContent); -$from = 'noreply@doenet.org'; -$fromName = 'Doenet Accounts'; -$subject = 'Sign-In Request'; + $from = 'noreply@doenet.org'; + $fromName = 'Doenet Accounts'; + $subject = 'Sign-In Request'; -// Set content-type header for sending HTML email -$headers = "MIME-Version: 1.0" . "\r\n"; -$headers .= "Content-type:text/html;charset=UTF-8" . "\r\n"; -$headers .= 'From: '.$fromName.'<'.$from.'>' . "\r\n"; + // Set content-type header for sending HTML email + $headers = "MIME-Version: 1.0" . "\r\n"; + $headers .= "Content-type:text/html;charset=UTF-8" . "\r\n"; + $headers .= 'From: '.$fromName.'<'.$from.'>' . "\r\n"; -//SEND EMAIL WITH CODE HERE -$mailSuccess = mail($emailaddress,$subject,$htmlContent, $headers); + //SEND EMAIL WITH CODE HERE + $mailSuccess = mail($emailaddress,$subject,$htmlContent, $headers); -if (!$mailSuccess && $mode != 'development'){ - throw new Exception("Sending Email Failed."); -} + if (!$mailSuccess && $mode != 'development'){ + throw new Exception("Sending Email Failed."); + } $response_arr = [ 'success' => true, diff --git a/src/Tools/_framework/Paths/SignInCode.jsx b/src/Tools/_framework/Paths/SignInCode.jsx index 39218e2ec4..a0c5828e54 100644 --- a/src/Tools/_framework/Paths/SignInCode.jsx +++ b/src/Tools/_framework/Paths/SignInCode.jsx @@ -19,7 +19,7 @@ import { } from "@chakra-ui/react"; import axios from "axios"; import React, { useState } from "react"; -import { redirect, useLoaderData } from "react-router"; +import { redirect } from "react-router"; import { useFetcher } from "react-router-dom"; export async function action({ request }) { @@ -33,8 +33,9 @@ export async function action({ request }) { try { if (formObj._action == "send new code") { let { data } = await axios.get("/api/sendSignInEmail.php", { - params: { emailaddress: emailAddress }, + params: { emailaddress: emailAddress, deviceName }, }); + return { success: true, _action: formObj._action, @@ -98,7 +99,7 @@ export function SignInCode() { if (codeError !== fetcher.data.message) { setCodeError(fetcher.data.message); setIsDisabled(false); - if (fetcher.data.message == "Code expired") { + if (fetcher.data.message == "Code expired.") { setIsExpired(true); } } From 8ee72847cfdf6277615a6a0401422d1596409648 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Mon, 23 Oct 2023 21:51:05 -0500 Subject: [PATCH 11/19] Base_Model conversion --- public/api/checkCredentials.php | 15 ++++++--------- public/api/jwt.php | 8 +++----- public/api/saveUsersName.php | 3 +-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/public/api/checkCredentials.php b/public/api/checkCredentials.php index 7c5b3665a9..15213e03ad 100644 --- a/public/api/checkCredentials.php +++ b/public/api/checkCredentials.php @@ -24,8 +24,7 @@ LIMIT 1 "; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); //Assume it already exists $existed = true; @@ -47,8 +46,7 @@ ORDER BY timestampOfSignInCode DESC LIMIT 1 "; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); if ($row["nineCode"] != $nineCode){ throw new Exception("Invalid Code"); @@ -59,15 +57,14 @@ $sql = "UPDATE user_device SET signedIn='1' WHERE email='$emailaddress' AND deviceName='$deviceName'"; - $result = $conn->query($sql); + Base_Model::runQuery($conn,$sql); //Test if it's a new account $sql = "SELECT firstName,lastName, screenName FROM user WHERE email='$emailaddress' "; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); if ($row["firstName"] != "" && $row["lastName"] != ""){ $hasFullName = true; @@ -90,7 +87,7 @@ $profile_pic = $profile_pics[$randomNumber]; // Store screen name and profile picture $sql = "UPDATE user SET screenName='$screen_name',profilePicture='$profile_pic' WHERE email='$emailaddress' "; - $result = $conn->query($sql); + Base_Model::runQuery($conn,$sql); } $sql = "SELECT c.courseId @@ -98,7 +95,7 @@ LEFT JOIN user AS u ON u.userId = c.portfolioCourseForUserId WHERE u.email = '$emailaddress'"; - $result = $conn->query($sql); + $result = Base_Model::runQuery($conn,$sql); $row = $result->fetch_assoc(); $portfolioCourseId = "not_created"; if ($result->num_rows > 0) { diff --git a/public/api/jwt.php b/public/api/jwt.php index 9e3eb4f169..9f7e63f1e0 100644 --- a/public/api/jwt.php +++ b/public/api/jwt.php @@ -24,8 +24,7 @@ LIMIT 1 "; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); //Check if it took longer than 10 minutes to enter the code if ($row['minutes'] > 10) { @@ -36,8 +35,7 @@ WHERE email='$emailaddress' AND deviceName='$deviceName' ORDER BY timestampOfSignInCode DESC LIMIT 1"; - $result = $conn->query($sql); - $row = $result->fetch_assoc(); + $row = Base_Model::runQuery($conn,$sql)->fetch_assoc(); $userId = $row['userId']; if ($row['nineCode'] != $nineCode) { throw new Exception("Invalid Code."); @@ -61,7 +59,7 @@ $sql = "UPDATE user_device SET signedIn = '1' WHERE userId='$userId' AND deviceName='$deviceName'"; - $result = $conn->query($sql); + Base_Model::runQuery($conn,$sql); $value = $jwt; diff --git a/public/api/saveUsersName.php b/public/api/saveUsersName.php index 0708c6d203..0f1fbbabe8 100644 --- a/public/api/saveUsersName.php +++ b/public/api/saveUsersName.php @@ -21,11 +21,10 @@ lastName='$lastName' WHERE email='$email' "; - $conn->query($sql); + Base_Model::runQuery($conn,$sql); $response_arr = array( 'success' => true, - 'message' => $message, ); http_response_code(200); From a8047f68d3d539551512ffc88beaa3f650ec2615 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Tue, 24 Oct 2023 14:48:03 -0500 Subject: [PATCH 12/19] Fixed sign out --- public/api/signOut.php | 80 ++++++++++-------- src/Tools/_framework/Paths/SignOut.jsx | 111 +++++++++++++++++++++++++ src/_utils/applicationUtils.js | 2 +- src/index.jsx | 18 ++++ 4 files changed, 175 insertions(+), 36 deletions(-) create mode 100644 src/Tools/_framework/Paths/SignOut.jsx diff --git a/public/api/signOut.php b/public/api/signOut.php index 7b9221be39..ffe7f0b1bf 100644 --- a/public/api/signOut.php +++ b/public/api/signOut.php @@ -1,45 +1,55 @@ query($sql); //TODO: upgrade the script response - -// set response code - 200 OK -http_response_code(200); - -$path = '/'; -// $domain = $ini_array['dbhost']; -$domain = $_SERVER["SERVER_NAME"]; -if ($domain == 'apache'){$domain = 'localhost';} +var_dump($cookies); -$isSecure = true; -if ($domain=="localhost"){ - $isSecure = false; -} -$isHttpOnly = true; -$expirationTime = -3600; - -setcookie("JWT", "", $expirationTime, $path, $domain, $isSecure, $isHttpOnly); -setcookie("JWT_JS", "", $expirationTime, $path, $domain, $isSecure, false); -setcookie("EJWT", "", $expirationTime, $path, $domain, $isSecure, $isHttpOnly); -setcookie("EJWT_JS", "", $expirationTime, $path, $domain, $isSecure, false); -// setcookie("JWT", "", array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>$isHttpOnly, "samesite"=>"strict")); -// setcookie("JWT_JS", "", array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>false, "samesite"=>"strict")); -// setcookie("TrackingConsent", "", array("expires"=>$expirationTime, "path"=>$path, "domain"=>$domain, "secure"=>$isSecure, "httponly"=>false, "samesite"=>"strict")); -// make it json format -// echo json_encode($response_arr); - -$conn->close(); +?> diff --git a/src/Tools/_framework/Paths/SignOut.jsx b/src/Tools/_framework/Paths/SignOut.jsx new file mode 100644 index 0000000000..7d29927443 --- /dev/null +++ b/src/Tools/_framework/Paths/SignOut.jsx @@ -0,0 +1,111 @@ +import { + AbsoluteCenter, + Box, + Button, + Card, + CardBody, + CardFooter, + Flex, + Heading, + Image, + ListItem, + Stack, + Text, + UnorderedList, +} from "@chakra-ui/react"; +import React from "react"; +import { useLoaderData, useNavigate } from "react-router"; +import { + checkIfUserClearedOut, + clearUsersInformationFromTheBrowser, +} from "../../../_utils/applicationUtils"; + +export async function loader() { + await clearUsersInformationFromTheBrowser(); + const isSignedOutObj = await checkIfUserClearedOut(); + return { isSignedOutObj }; +} + +//TODO: inform if not signed out +export function SignOut() { + const { isSignedOutObj } = useLoaderData(); + const navigate = useNavigate(); + + return ( + <> + + + + + Doenet Logo + + + + + Doenet Logo + + {isSignedOutObj.cookieRemoved && + isSignedOutObj.userInformationIsCompletelyRemoved ? ( + <> + + You are Signed Out! + + + + + + + + ) : ( + <> + + You are NOT Signed Out! + + + + Hit refresh to try again. + + + + Errors + + {isSignedOutObj.messageArray.map((msg, i) => { + return ( + + {msg} + + ); + })} + + + )} + + + + + + + ); +} diff --git a/src/_utils/applicationUtils.js b/src/_utils/applicationUtils.js index a4eebc6818..722a9a69dd 100644 --- a/src/_utils/applicationUtils.js +++ b/src/_utils/applicationUtils.js @@ -3,7 +3,7 @@ import { clear as idb_clear, keys as idb_keys } from "idb-keyval"; export async function clearUsersInformationFromTheBrowser() { localStorage.clear(); //Clear out the profile of the last exam taker - await axios.get("/api/signOut.php"); + await axios.get("/api/signOut.php", { withCredentials: true }); //Clear all cookies await idb_clear(); return true; } diff --git a/src/index.jsx b/src/index.jsx index ba45b22000..5f1efe82fc 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -93,6 +93,10 @@ import { SignInName, action as signInNameAction, } from "./Tools/_framework/Paths/SignInName"; +import { + SignOut, + loader as signOutLoader, +} from "./Tools/_framework/Paths/SignOut"; { /* */ @@ -374,6 +378,20 @@ const router = createBrowserRouter([ ), }, + { + path: "signout", + loader: signOutLoader, + errorElement: ( + + + + ), + element: ( + + + + ), + }, { path: "public", loader: editorSupportPanelLoader, From d48b05927b7c97a60023ff8311484f9c967be254 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Tue, 24 Oct 2023 15:16:33 -0500 Subject: [PATCH 13/19] fix backticks --- public/api/signInEmail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/api/signInEmail.html b/public/api/signInEmail.html index c6e7eed83c..27c70874a7 100644 --- a/public/api/signInEmail.html +++ b/public/api/signInEmail.html @@ -140,7 +140,7 @@

Sign-in code:
- ```signInCode```signInCode

From 28152a3143daf3431c02592e8212bbe42b890c22 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Tue, 24 Oct 2023 23:07:30 -0500 Subject: [PATCH 14/19] tests for signin --- cypress/e2e/AsStudent/signIn.cy.js | 88 +++-- package-lock.json | 372 ++++++++++++++++------ package.json | 2 +- src/Tools/_framework/Paths/SignIn.jsx | 2 + src/Tools/_framework/Paths/SignInCode.jsx | 30 +- src/Tools/_framework/Paths/SignInName.jsx | 11 +- src/Tools/_framework/Paths/SignOut.jsx | 1 + src/Tools/_framework/Paths/SiteHeader.jsx | 8 +- 8 files changed, 377 insertions(+), 137 deletions(-) diff --git a/cypress/e2e/AsStudent/signIn.cy.js b/cypress/e2e/AsStudent/signIn.cy.js index 65f3f19852..cbc6ff9a24 100644 --- a/cypress/e2e/AsStudent/signIn.cy.js +++ b/cypress/e2e/AsStudent/signIn.cy.js @@ -1,24 +1,7 @@ describe("Student Sign-In Test", function () { const userId = "cyuserId"; - // const studentUserId = "cyStudentUserId"; const courseId = "courseid1"; - // const doenetId = "activity1id"; - // const pageDoenetId = "_page1id"; - before(() => { - cy.signin({ userId }); - cy.clearAllOfAUsersCoursesAndItems({ userId }); - // cy.clearAllOfAUsersCoursesAndItems({ userId: studentUserId }); - cy.createCourse({ userId, courseId }); - }); - beforeEach(() => { - cy.signin({ userId }); - cy.clearIndexedDB(); - cy.clearAllOfAUsersActivities({ userId }); - // cy.clearAllOfAUsersActivities({ userId: studentUserId }); - // cy.createActivity({ courseId, doenetId, parentDoenetId:courseId, pageDoenetId }); - cy.visit(`/course?tool=people&courseId=${courseId}`); - }); Cypress.on("uncaught:exception", (err, runnable) => { // Returning false here prevents Cypress from failing the test @@ -26,6 +9,12 @@ describe("Student Sign-In Test", function () { }); it("Student can sign in after being added to a course", () => { + cy.createCourse({ userId, courseId }); + cy.signin({ userId }); + cy.clearIndexedDB(); + cy.clearAllOfAUsersActivities({ userId }); + cy.visit(`/course?tool=people&courseId=${courseId}`); + const emailAddress = "scoobydoo@doenet.org"; cy.get('[data-test="First"]').type("Scooby"); cy.get('[data-test="Last"]').type("Doo"); @@ -42,8 +31,17 @@ describe("Student Sign-In Test", function () { `SELECT signInCode FROM user_device ORDER BY id DESC LIMIT 1`, ).then((result) => { const code = result[0].signInCode; - cy.get('[data-test="signinCodeInput"]').type(code); - cy.get('[data-test="signInButton"]').click(); + // cy.get('[data-test="signinCodeInput"]').type(code); + cy.get('[data-test="code-input-0"]').type(code.charAt(0)); + cy.get('[data-test="code-input-1"]').type(code.charAt(1)); + cy.get('[data-test="code-input-2"]').type(code.charAt(2)); + cy.get('[data-test="code-input-3"]').type(code.charAt(3)); + cy.get('[data-test="code-input-4"]').type(code.charAt(4)); + cy.get('[data-test="code-input-5"]').type(code.charAt(5)); + cy.get('[data-test="code-input-6"]').type(code.charAt(6)); + cy.get('[data-test="code-input-7"]').type(code.charAt(7)); + cy.get('[data-test="code-input-8"]').type(code.charAt(8)); + cy.get('[data-test="submitCodeButton"]').click(); cy.get('[data-test="My Courses"]').click(); cy.get('[data-test="Course Label"]').should( "have.text", @@ -53,4 +51,56 @@ describe("Student Sign-In Test", function () { cy.document().should("contain.text", "Welcome"); }); }); + + it("Signed out to in to out with all entry errors", () => { + const emailAddress = "scrapydoo@doenet.org"; + const firstName = "Scrapy"; + const lastName = "Doo"; + //Delete entry so we will need to enter the name + cy.task( + "queryDb", + `DELETE FROM user WHERE email='${emailAddress}'`, + ).then(() => { + cy.visit(`/`); + cy.get('[data-test="Nav to signin"]').click(); + cy.get('[data-test="email input"]').type(emailAddress); + cy.get('[data-test="sendEmailButton"]').click(); + cy.wait(500); //Wait for it to be stored in db + cy.task( + "queryDb", + `SELECT signInCode FROM user_device ORDER BY id DESC LIMIT 1`, + ).then((result) => { + const code = result[0].signInCode; + //Try no code + cy.get('[data-test="submitCodeButton"]').click(); + cy.get('[data-test="code-err"]').should('contain', "Please enter the nine digits sent to your email."); + //Try only one number + cy.get('[data-test="code-input-0"]').type(code.charAt(0)); + cy.get('[data-test="submitCodeButton"]').click(); + cy.get('[data-test="code-err"]').should('contain', "Please enter all nine digits."); + + cy.get('[data-test="code-input-0"]').type(code.charAt(0)); + cy.get('[data-test="code-input-1"]').type(code.charAt(1)); + cy.get('[data-test="code-input-2"]').type(code.charAt(2)); + cy.get('[data-test="code-input-3"]').type(code.charAt(3)); + cy.get('[data-test="code-input-4"]').type(code.charAt(4)); + cy.get('[data-test="code-input-5"]').type(code.charAt(5)); + cy.get('[data-test="code-input-6"]').type(code.charAt(6)); + cy.get('[data-test="code-input-7"]').type(code.charAt(7)); + cy.get('[data-test="code-input-8"]').type(code.charAt(8)); + cy.get('[data-test="submitCodeButton"]').click(); + //Try no names + cy.get('[data-test="submitName"]').click(); + cy.get('[data-test="firstNameError"]').should('contain', 'Please enter your first name.') + cy.get('[data-test="lastNameError"]').should('contain', 'Please enter your last name.') + + cy.get('[data-test="firstNameInput"]').type(firstName); + cy.get('[data-test="lastNameInput"]').type(lastName); + cy.get('[data-test="submitName"]').click(); + cy.get('[data-test="AvatarMenuButton"]').click(); + cy.get('[data-test="AvatarMenuSignOut"]').click(); + cy.get('[data-test="homepage button"]').click(); + }); + }); + }); }); diff --git a/package-lock.json b/package-lock.json index 3c3e504443..3f4f3a4c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "crypto-js": "^3.3.0", "cssesc": "^3.0.0", "csv-parse": "^5.3.6", + "cypress": "^13.3.3", "esm-seedrandom": "^3.0.5", "framer-motion": "^10.12.10", "handsontable": "^12.1.2", @@ -109,7 +110,7 @@ }, "optionalDependencies": { "@esbuild/linux-arm64": "^0.17.19", - "cypress": "^12.12.0", + "cypress": "^13.3.3", "cypress-file-upload": "^5.0.8", "cypress-parallel": "^0.13.0", "cypress-plugin-tab": "^1.0.5", @@ -1973,8 +1974,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.10", - "license": "Apache-2.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "optional": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -1990,9 +1992,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -2002,7 +2004,8 @@ }, "node_modules/@cypress/request/node_modules/form-data": { "version": "2.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "optional": true, "dependencies": { "asynckit": "^0.4.0", @@ -2014,11 +2017,18 @@ } }, "node_modules/@cypress/request/node_modules/qs": { - "version": "6.5.3", - "license": "BSD-3-Clause", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", "optional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/@cypress/xvfb": { @@ -3443,8 +3453,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.11.4", - "license": "MIT" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -4213,7 +4224,8 @@ }, "node_modules/asn1": { "version": "0.2.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "optional": true, "dependencies": { "safer-buffer": "~2.1.0" @@ -4221,7 +4233,8 @@ }, "node_modules/assert-plus": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "optional": true, "engines": { "node": ">=0.8" @@ -4291,15 +4304,17 @@ }, "node_modules/aws-sign2": { "version": "0.7.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "optional": true, "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.11.0", - "license": "MIT", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "optional": true }, "node_modules/axe-core": { @@ -4734,7 +4749,8 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "optional": true, "dependencies": { "tweetnacl": "^0.14.3" @@ -4942,7 +4958,7 @@ }, "node_modules/call-bind": { "version": "1.0.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -4997,7 +5013,8 @@ }, "node_modules/caseless": { "version": "0.12.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "optional": true }, "node_modules/chai": { @@ -5508,6 +5525,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "optional": true + }, "node_modules/cors": { "version": "2.8.5", "license": "MIT", @@ -5603,15 +5626,15 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.12.0.tgz", - "integrity": "sha512-UU5wFQ7SMVCR/hyKok/KmzG6fpZgBHHfrXcHzDmPHWrT+UUetxFzQgt7cxCszlwfozckzwkd22dxMwl/vNkWRw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.3.tgz", + "integrity": "sha512-mbdkojHhKB1xbrj7CrKWHi22uFx9P9vQFiR0sYDZZoK99OMp9/ZYN55TO5pjbXmV7xvCJ4JwBoADXjOJK8aCJw==", "hasInstallScript": true, "optional": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -5644,9 +5667,10 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -5656,7 +5680,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || >=18.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, "node_modules/cypress-file-upload": { @@ -6112,11 +6136,6 @@ "license": "MIT", "optional": true }, - "node_modules/cypress/node_modules/@types/node": { - "version": "14.18.32", - "license": "MIT", - "optional": true - }, "node_modules/cypress/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -6205,8 +6224,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.3.8", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "optional": true, "dependencies": { "lru-cache": "^6.0.0" @@ -6239,7 +6259,8 @@ }, "node_modules/dashdash": { "version": "1.14.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -6479,7 +6500,8 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "optional": true, "dependencies": { "jsbn": "~0.1.0", @@ -7772,7 +7794,8 @@ }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "optional": true }, "node_modules/extend-shallow": { @@ -7865,10 +7888,11 @@ }, "node_modules/extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true }, "node_modules/fast-deep-equal": { @@ -8059,7 +8083,8 @@ }, "node_modules/forever-agent": { "version": "0.6.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "optional": true, "engines": { "node": "*" @@ -8232,7 +8257,7 @@ }, "node_modules/get-intrinsic": { "version": "1.1.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -8298,7 +8323,8 @@ }, "node_modules/getpass": { "version": "0.1.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -8461,7 +8487,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8627,7 +8653,8 @@ }, "node_modules/http-signature": { "version": "1.3.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -9178,7 +9205,8 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "optional": true }, "node_modules/is-unicode-supported": { @@ -9249,7 +9277,8 @@ }, "node_modules/isstream": { "version": "0.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "optional": true }, "node_modules/istanbul-lib-coverage": { @@ -9366,7 +9395,8 @@ }, "node_modules/jsbn": { "version": "0.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "optional": true }, "node_modules/jsesc": { @@ -9385,7 +9415,8 @@ }, "node_modules/json-schema": { "version": "0.4.0", - "license": "(AFL-2.1 OR BSD-3-Clause)", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "optional": true }, "node_modules/json-schema-traverse": { @@ -9407,7 +9438,8 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "optional": true }, "node_modules/json5": { @@ -9433,10 +9465,11 @@ }, "node_modules/jsprim": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true, "dependencies": { "assert-plus": "1.0.0", @@ -10613,7 +10646,7 @@ }, "node_modules/object-inspect": { "version": "1.12.2", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10955,7 +10988,8 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "optional": true }, "node_modules/picocolors": { @@ -11210,6 +11244,15 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/progress": { "version": "2.0.3", "dev": true, @@ -11238,7 +11281,8 @@ }, "node_modules/psl": { "version": "1.9.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "optional": true }, "node_modules/pump": { @@ -11294,6 +11338,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "optional": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "devOptional": true, @@ -11872,7 +11922,7 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/resize-observer-polyfill": { @@ -12322,7 +12372,7 @@ }, "node_modules/side-channel": { "version": "1.0.4", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -12720,8 +12770,9 @@ } }, "node_modules/sshpk": { - "version": "1.17.0", - "license": "MIT", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "dependencies": { "asn1": "~0.2.3", @@ -13226,15 +13277,27 @@ } }, "node_modules/tough-cookie": { - "version": "2.5.0", - "license": "BSD-3-Clause", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "optional": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "optional": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/tr46": { @@ -13285,7 +13348,8 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "optional": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -13296,7 +13360,8 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "license": "Unlicense", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "optional": true }, "node_modules/type-check": { @@ -13532,6 +13597,16 @@ "version": "0.1.0", "license": "MIT" }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "optional": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use": { "version": "3.1.1", "license": "MIT", @@ -13600,7 +13675,8 @@ }, "node_modules/uuid": { "version": "8.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -13636,10 +13712,11 @@ }, "node_modules/verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -13647,11 +13724,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "license": "MIT", - "optional": true - }, "node_modules/vite": { "version": "4.2.1", "dev": true, @@ -15568,7 +15640,9 @@ "optional": true }, "@cypress/request": { - "version": "2.88.10", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "optional": true, "requires": { "aws-sign2": "~0.7.0", @@ -15584,15 +15658,17 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, "dependencies": { "form-data": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "optional": true, "requires": { "asynckit": "^0.4.0", @@ -15601,8 +15677,13 @@ } }, "qs": { - "version": "6.5.3", - "optional": true + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "optional": true, + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -16532,7 +16613,9 @@ "dev": true }, "@types/node": { - "version": "18.11.4" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "@types/normalize-package-data": { "version": "2.4.1" @@ -17058,6 +17141,8 @@ }, "asn1": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "optional": true, "requires": { "safer-buffer": "~2.1.0" @@ -17065,6 +17150,8 @@ }, "assert-plus": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "optional": true }, "assertion-error": { @@ -17101,10 +17188,14 @@ }, "aws-sign2": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "optional": true }, "aws4": { - "version": "1.11.0", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "optional": true }, "axe-core": { @@ -17395,6 +17486,8 @@ }, "bcrypt-pbkdf": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "optional": true, "requires": { "tweetnacl": "^0.14.3" @@ -17524,7 +17617,7 @@ }, "call-bind": { "version": "1.0.2", - "dev": true, + "devOptional": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -17548,6 +17641,8 @@ }, "caseless": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "optional": true }, "chai": { @@ -17880,6 +17975,12 @@ "version": "3.26.0", "dev": true }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "optional": true + }, "cors": { "version": "2.8.5", "requires": { @@ -17947,14 +18048,14 @@ "version": "5.3.6" }, "cypress": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.12.0.tgz", - "integrity": "sha512-UU5wFQ7SMVCR/hyKok/KmzG6fpZgBHHfrXcHzDmPHWrT+UUetxFzQgt7cxCszlwfozckzwkd22dxMwl/vNkWRw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.3.tgz", + "integrity": "sha512-mbdkojHhKB1xbrj7CrKWHi22uFx9P9vQFiR0sYDZZoK99OMp9/ZYN55TO5pjbXmV7xvCJ4JwBoADXjOJK8aCJw==", "optional": true, "requires": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -17987,19 +18088,16 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "dependencies": { - "@types/node": { - "version": "14.18.32", - "optional": true - }, "ansi-styles": { "version": "4.3.0", "optional": true, @@ -18048,7 +18146,9 @@ "optional": true }, "semver": { - "version": "7.3.8", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "optional": true, "requires": { "lru-cache": "^6.0.0" @@ -18405,6 +18505,8 @@ }, "dashdash": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "optional": true, "requires": { "assert-plus": "^1.0.0" @@ -18550,6 +18652,8 @@ }, "ecc-jsbn": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "optional": true, "requires": { "jsbn": "~0.1.0", @@ -19337,6 +19441,8 @@ }, "extend": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "optional": true }, "extend-shallow": { @@ -19397,6 +19503,8 @@ }, "extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "optional": true }, "fast-deep-equal": { @@ -19519,6 +19627,8 @@ }, "forever-agent": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "optional": true }, "form-data": { @@ -19625,7 +19735,7 @@ }, "get-intrinsic": { "version": "1.1.3", - "dev": true, + "devOptional": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -19664,6 +19774,8 @@ }, "getpass": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "optional": true, "requires": { "assert-plus": "^1.0.0" @@ -19768,7 +19880,7 @@ }, "has-symbols": { "version": "1.0.3", - "dev": true + "devOptional": true }, "has-tostringtag": { "version": "1.0.0", @@ -19880,6 +19992,8 @@ }, "http-signature": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "optional": true, "requires": { "assert-plus": "^1.0.0", @@ -20191,6 +20305,8 @@ }, "is-typedarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "optional": true }, "is-unicode-supported": { @@ -20229,6 +20345,8 @@ }, "isstream": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "optional": true }, "istanbul-lib-coverage": { @@ -20309,6 +20427,8 @@ }, "jsbn": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "optional": true }, "jsesc": { @@ -20319,6 +20439,8 @@ }, "json-schema": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "optional": true }, "json-schema-traverse": { @@ -20334,6 +20456,8 @@ }, "json-stringify-safe": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "optional": true }, "json5": { @@ -20349,6 +20473,8 @@ }, "jsprim": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "optional": true, "requires": { "assert-plus": "1.0.0", @@ -21169,7 +21295,7 @@ }, "object-inspect": { "version": "1.12.2", - "dev": true + "devOptional": true }, "object-keys": { "version": "1.1.1", @@ -21372,6 +21498,8 @@ }, "performance-now": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "optional": true }, "picocolors": { @@ -21529,6 +21657,12 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "optional": true + }, "progress": { "version": "2.0.3", "dev": true @@ -21552,6 +21686,8 @@ }, "psl": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "optional": true }, "pump": { @@ -21591,6 +21727,12 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "optional": true + }, "queue-microtask": { "version": "1.2.3", "devOptional": true @@ -21942,7 +22084,7 @@ }, "requires-port": { "version": "1.0.0", - "dev": true + "devOptional": true }, "resize-observer-polyfill": { "version": "1.5.1" @@ -22231,7 +22373,7 @@ }, "side-channel": { "version": "1.0.4", - "dev": true, + "devOptional": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -22506,7 +22648,9 @@ "dev": true }, "sshpk": { - "version": "1.17.0", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "requires": { "asn1": "~0.2.3", @@ -22849,11 +22993,23 @@ "dev": true }, "tough-cookie": { - "version": "2.5.0", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "optional": true, "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "optional": true + } } }, "tr46": { @@ -22893,6 +23049,8 @@ }, "tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "optional": true, "requires": { "safe-buffer": "^5.0.1" @@ -22900,6 +23058,8 @@ }, "tweetnacl": { "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "optional": true }, "type-check": { @@ -23037,6 +23197,16 @@ "urix": { "version": "0.1.0" }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "optional": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "use": { "version": "3.1.1" }, @@ -23069,6 +23239,8 @@ }, "uuid": { "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true }, "v8-to-istanbul": { @@ -23092,17 +23264,13 @@ }, "verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "optional": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "optional": true - } } }, "vite": { diff --git a/package.json b/package.json index 1bd4fd65d0..2ed0964a8b 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ }, "optionalDependencies": { "@esbuild/linux-arm64": "^0.17.19", - "cypress": "^12.12.0", + "cypress": "^13.3.3", "cypress-file-upload": "^5.0.8", "cypress-parallel": "^0.13.0", "cypress-plugin-tab": "^1.0.5", diff --git a/src/Tools/_framework/Paths/SignIn.jsx b/src/Tools/_framework/Paths/SignIn.jsx index 5d897ef721..aa77e8d65d 100644 --- a/src/Tools/_framework/Paths/SignIn.jsx +++ b/src/Tools/_framework/Paths/SignIn.jsx @@ -97,6 +97,7 @@ export function SignIn() { size="md" type="email" value={emailAddress} + data-test="email input" onChange={(e) => { let nextValue = e.target.value; //Clear error if email is now good @@ -124,6 +125,7 @@ export function SignIn() { isDisabled={isDisabled} rightIcon={isDisabled ? : undefined} colorScheme="blue" + data-test="sendEmailButton" onClick={() => { if (emailAddress == "") { setEmailError("Please enter your email address"); diff --git a/src/Tools/_framework/Paths/SignInCode.jsx b/src/Tools/_framework/Paths/SignInCode.jsx index a0c5828e54..7c1d4602dd 100644 --- a/src/Tools/_framework/Paths/SignInCode.jsx +++ b/src/Tools/_framework/Paths/SignInCode.jsx @@ -140,19 +140,25 @@ export function SignInCode() { Sign-in code (9 digit code): - setCode(code)}> - - - - - - - - - + setCode(code)} + > + + + + + + + + + - {codeError} + + {codeError} + @@ -162,6 +168,7 @@ export function SignInCode() { diff --git a/src/Tools/_framework/Paths/SiteHeader.jsx b/src/Tools/_framework/Paths/SiteHeader.jsx index e5764f2fc9..f2788fb318 100644 --- a/src/Tools/_framework/Paths/SiteHeader.jsx +++ b/src/Tools/_framework/Paths/SiteHeader.jsx @@ -189,7 +189,7 @@ export function SiteHeader(props) { {signedIn ? (
- + @@ -225,7 +225,11 @@ export function SiteHeader(props) { */} - + Sign Out From baa2f196b9c01b5480b3838ea2e76d4783ca498b Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Tue, 24 Oct 2023 23:11:55 -0500 Subject: [PATCH 15/19] cleaned tool root --- src/Tools/_framework/NewToolRoot.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tools/_framework/NewToolRoot.jsx b/src/Tools/_framework/NewToolRoot.jsx index 165832fc1e..47fb1ae45f 100644 --- a/src/Tools/_framework/NewToolRoot.jsx +++ b/src/Tools/_framework/NewToolRoot.jsx @@ -107,7 +107,6 @@ export default function ToolRoot() { import("./ToolPanels/PublicActivityViewer"), ), CourseCards: lazy(() => import("./ToolPanels/CourseCards")), - SignOut: lazy(() => import("./ToolPanels/SignOut")), NavigationPanel: lazy(() => import("./ToolPanels/NavigationPanel")), Dashboard: lazy(() => import("./ToolPanels/Dashboard")), Gradebook: lazy(() => import("./ToolPanels/Gradebook")), From 7589a9b98a9a4eb94fb21247a797fad276ab82e6 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Wed, 25 Oct 2023 09:42:19 -0500 Subject: [PATCH 16/19] Removed commented code --- src/index.jsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index 5f1efe82fc..e04e00c117 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -493,16 +493,6 @@ const router = createBrowserRouter([ ), }, - // { - // path: "/api/", - // element:
Loading...
, - // errorElement: ( - // - // - // - // ), - // }, - { path: "*", element: ( From 834f893bc7eda4f95d07bde4fcd6c3b97bc3b60f Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Wed, 25 Oct 2023 14:47:30 -0500 Subject: [PATCH 17/19] Chakra darkmode signout bug fixed --- src/_utils/applicationUtils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_utils/applicationUtils.js b/src/_utils/applicationUtils.js index 722a9a69dd..865d4a41b9 100644 --- a/src/_utils/applicationUtils.js +++ b/src/_utils/applicationUtils.js @@ -20,6 +20,11 @@ export async function checkIfUserClearedOut() { //Check for local storage //TODO: find something is stored in localStorage and test if this clears it let localStorageRemoved = localStorage.length == 0; + //Chakra UI will put darkmode back so check that + if (localStorage.length === 1 && localStorage.key(0) === 'chakra-ui-color-mode') { + localStorageRemoved = true; + } + if (!localStorageRemoved) { messageArray.push("local storage not removed"); } From 1b54f33557673986897d85eec734f5decb46fbd9 Mon Sep 17 00:00:00 2001 From: Kevin Charles Date: Wed, 25 Oct 2023 15:23:23 -0500 Subject: [PATCH 18/19] Fixed measurement of signed out --- public/api/getQuickCheckSignedIn.php | 14 +++++++++----- public/api/signOut.php | 1 - src/_utils/applicationUtils.js | 10 ++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/public/api/getQuickCheckSignedIn.php b/public/api/getQuickCheckSignedIn.php index 56afe122f0..3fac4436e1 100644 --- a/public/api/getQuickCheckSignedIn.php +++ b/public/api/getQuickCheckSignedIn.php @@ -5,14 +5,18 @@ header('Access-Control-Allow-Credentials: true'); header('Content-Type: application/json'); -//ONLY TESTING IF THE SECURE SIGNED IN (JWT) COOKIE EXISTS -$signedIn = false; - +$secureCookieExists = false; if ($_COOKIE["JWT"] != NULL){ - $signedIn = true; + $secureCookieExists = true; +} +$unsecureCookieExists = false; +if ($_COOKIE["JWT_JS"] != NULL){ + $unsecureCookieExists = true; } -$response_arr = ['signedIn' => $signedIn]; +$response_arr = ['secureCookieExists' => $secureCookieExists, +'unsecureCookieExists' => $unsecureCookieExists, +]; // set response code - 200 OK http_response_code(200); diff --git a/public/api/signOut.php b/public/api/signOut.php index ffe7f0b1bf..687314ea69 100644 --- a/public/api/signOut.php +++ b/public/api/signOut.php @@ -17,7 +17,6 @@ $parts = explode('=', $cookie); $name = trim($parts[0]); // Set the cookie to expire one hour ago - // $success = setcookie($name, '', time()-3600); $success = setcookie($name, '', time()-3600, '/', $domain); //Stop the script if deleting a cookie fails if (!$success){ diff --git a/src/_utils/applicationUtils.js b/src/_utils/applicationUtils.js index 865d4a41b9..46939c7565 100644 --- a/src/_utils/applicationUtils.js +++ b/src/_utils/applicationUtils.js @@ -32,15 +32,13 @@ export async function checkIfUserClearedOut() { //Check for cookie //Ask the server without hitting the database const { data } = await axios.get("/api/getQuickCheckSignedIn.php"); - const secureCookieRemoved = !data?.signedIn; - const vanillaCookies = document.cookie.split(";"); - const vanillaCookieRemoved = - vanillaCookies.length === 1 && vanillaCookies[0] === ""; + const secureCookieRemoved = !data?.secureCookieExists; + const unsecureCookieRemoved = !data?.unsecureCookieExists; - let cookieRemoved = vanillaCookieRemoved && secureCookieRemoved; + let cookieRemoved = unsecureCookieRemoved && secureCookieRemoved; - if (!vanillaCookieRemoved) { + if (!unsecureCookieRemoved) { messageArray.push("cookie not removed"); } From eb1e0b6d8bfbf3016f89e1adb5a7f7b20ea5b42c Mon Sep 17 00:00:00 2001 From: Jason Altekruse Date: Tue, 23 Apr 2024 09:02:49 -0500 Subject: [PATCH 19/19] Revert changes to add an unnecessary extra loop over every cell in SQL result sets --- public/api/baseModel.php | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/public/api/baseModel.php b/public/api/baseModel.php index 7dcefb46b0..d65e5c4f26 100644 --- a/public/api/baseModel.php +++ b/public/api/baseModel.php @@ -30,14 +30,11 @@ public static function runQuery($conn, $query) { public static function queryFetchAssoc($conn, $query) { $result = Base_Model::runQuery($conn, $query); if ($result->num_rows > 0) { - $data = []; + $rows = []; while($row = $result->fetch_assoc()){ - $data['rows'] = $row; - foreach($row as $key => $value){ - $data[$key][] = $value; - } + $rows[] = $row; } - return $data; + return $rows; } else { return []; } @@ -51,22 +48,20 @@ public static function queryFetchAssoc($conn, $query) { * If more than one row is returned, throws an exception. */ public static function queryOneRowOrError($conn, $query) { - $data = Base_Model::queryFetchAssoc($conn, $query); - - if (count($data['rows']) == 1) { - return $data; - } else if (count($data['rows']) == 0) { + $rows = Base_Model::queryFetchAssoc($conn, $query); + + if (count($rows) == 1) { + return $rows[0]; + } else if (count($rows) == 0) { return null; } else { - throw new Exception("Unexpected error, only expected one row from this query."); error_log("Unexpected error, only expected one row from this query." . - "\n " . $conn->error . - "\n" . $query); + "\n" . $query); + } } - /** * Validate that a list of keys are present in a given associative array. *