diff --git a/components/CheckLogin.tsx b/components/CheckLogin.tsx index e38bd565..6a275cfa 100644 --- a/components/CheckLogin.tsx +++ b/components/CheckLogin.tsx @@ -56,17 +56,12 @@ export default function CheckLogin({ // skip query if user is already defined skip: user ? true : false, onCompleted: (data) => { - setLoading(false); setUser(data?.me); // could be undefined! + setLoading(false); }, }); useEffect(() => { - let mounted = true; - // if we skip above query, because user is already loaded - if (user && mounted) setLoading(false); - return () => { - mounted = false; - }; + if (user) setLoading(false); }, [user]); if (loading) { diff --git a/components/Page.tsx b/components/Page.tsx index 701c5f42..26e6c381 100644 --- a/components/Page.tsx +++ b/components/Page.tsx @@ -2,7 +2,7 @@ import { Flex, Box, Heading, Text, Link as A } from "rebass"; import Head from "next/head"; import { useUser } from "state/user"; import { LoginForm } from "pages/user/login"; -import { ReactNode } from "react"; +import React, { ReactNode } from "react"; import CheckLogin from "./CheckLogin"; import Link from "next/link"; import { FlexProps } from "rebass"; @@ -10,6 +10,7 @@ import { Role } from "graphql/types"; import { Footer } from "components/Footer"; import { TopBar } from "./TopBar"; import IconClose from "../public/images/icon_close.svg"; +import { Spinner } from "theme-ui"; export const Page: React.FC<{ children?: React.ReactNode; @@ -170,6 +171,10 @@ export const Container: React.FC = (props) => { ); }; +export const Loading: React.FC = () => ( + +); + export const ErrorPage: React.FC = (props) => ( Oh je, es ist ein Fehler aufgetreten diff --git a/components/Schools.tsx b/components/Schools.tsx index 12a7a3a4..9dff09d1 100644 --- a/components/Schools.tsx +++ b/components/Schools.tsx @@ -8,6 +8,7 @@ import { ShowField } from "./Users"; import { cantonNames } from "../util/cantons"; import { useState, ReactElement } from "react"; import { School } from "@prisma/client"; +import { Loading } from "components/Page"; import { useSchoolsWithMembersQuery, useSetSchoolMutation, @@ -49,7 +50,7 @@ export const Schools: React.FC = () => { return Error loading data: {schoolsQuery.error.message}; } if (schoolsQuery.loading) { - return Loading data; + return ; } return ( <> @@ -138,7 +139,7 @@ export const SelectSchool: React.FC = () => { ); } if (!schools) { - return

Loading...

; + return ; } const options = schools?.reduce( diff --git a/graphql/resolvers/users.ts b/graphql/resolvers/users.ts index 8ff430db..c0c953a9 100644 --- a/graphql/resolvers/users.ts +++ b/graphql/resolvers/users.ts @@ -288,7 +288,7 @@ export async function sendVerificationEmail( process.env.NODE_ENV !== "production" ? process.env.NODE_ENV : "" }`; const token = await createVerificationToken(db, email); - const url = `${process.env.BASE_URL}user/login?t=${token}&p=${purpose}`; + const url = `${process.env.BASE_URL}user/verify?t=${token}&p=${purpose}`; const subjects: Record = { verification: "voty: Bitte Email bestätigen", reset: "voty: Passwort zurücksetzen?", diff --git a/pages/user/login.tsx b/pages/user/login.tsx index 6023eee7..b527b8e2 100644 --- a/pages/user/login.tsx +++ b/pages/user/login.tsx @@ -1,26 +1,19 @@ import { useRouter } from "next/router"; import { AppPage } from "components/Page"; -import { gql, useMutation, useApolloClient } from "@apollo/client"; -import { useState, useEffect, ReactElement } from "react"; -import { Text, Box, Button, Heading, Flex } from "rebass"; -import { Grid } from "theme-ui"; -import { Label, Input } from "@rebass/forms"; +import { gql, useMutation } from "@apollo/client"; +import { useState, ReactElement } from "react"; +import { Text, Button, Heading, Flex } from "rebass"; import { QForm, ErrorBox } from "components/Form"; import CheckLogin from "components/CheckLogin"; import { usePageEvent, trackEvent } from "util/stats"; -import { - useSetAccessToken, - useUser, - useSetUser, - SessionUser, -} from "../../state/user"; +import { useSetAccessToken, useUser, useSetUser } from "../../state/user"; import { useQueryParam } from "util/hooks"; import { Role, useLoginMutation, - useCheckVerificationMutation, useEmailVerificationMutation, } from "graphql/types"; +import VerifyPage from "./verify"; export const LOGIN = gql` mutation login($email: String!, $password: String!) { @@ -81,12 +74,9 @@ export default function Login(): ReactElement { } // purpose: verification, reset, login + // this needs to stay in for a while, so that old links continue to work if (token && purpose) { - return ( - void router.push("/")}> - - - ); + return ; } else { return ( void router.push("/")}> @@ -184,7 +174,7 @@ export function LoginForm(): ReactElement { ); } -function VerificationForm({ email }: { email: string }): ReactElement { +export function VerificationForm({ email }: { email: string }): ReactElement { usePageEvent({ category: "Login", action: "NotVerified" }); const [mailSent, setMailSent] = useState(false); const [error, setError] = useState(""); @@ -221,22 +211,17 @@ function VerificationForm({ email }: { email: string }): ReactElement { ); } -function getStartpage(role?: string) { - let page = ""; +export function getStartpage(role?: string): string { switch (role) { case Role.Teacher: - page = "/teacher"; - break; + return "/teacher"; case Role.Student: - page = "/student"; - break; + return "/student"; case Role.Admin: - page = "/admin"; - break; + return "/admin"; default: - page = "/"; + return "/"; } - return page; } function AfterLogin() { @@ -259,79 +244,6 @@ function AfterLogin() { } } -function CheckToken({ token, purpose }: { token: string; purpose: string }) { - const setUser = useSetUser(); - const setAccessToken = useSetAccessToken(); - const [error, setError] = useState(""); - const [tempUser, setTempUser] = useState(); - const router = useRouter(); - const client = useApolloClient(); - const [doVerification] = useCheckVerificationMutation({ - onCompleted: (data) => { - if (data.checkVerification && data.checkVerification.token) { - setTempUser(data.checkVerification.user); - setAccessToken(data.checkVerification.token); - } - }, - onError(error) { - setError("Dieser Email-Link ist leider nicht mehr gültig!"); - console.error(error.message); - }, - }); - - useEffect(() => { - // first logout current user, as doVerification will auto-login user based on token - void client.clearStore(); - setAccessToken(""); - setUser(undefined); - void doVerification({ variables: { token } }); - }, []); - - const isTeacher = tempUser?.role === Role.Teacher; - // token verification succeded, we have a session & user - if (tempUser !== undefined) { - // login -> go straight back - if (purpose === "login") { - setUser(tempUser); - } - if (purpose === "verification") { - trackEvent({ category: "Login", action: "EmailVerified" }); - return ( - - - Super, Deine Email-Adresse ist nun bestätigt.{" "} - {isTeacher - ? "Dein Konto für Lehrpersonen ist nun eröffnet und Du bist bereits angemeldet." - : ""} - - - - ); - } - if (purpose === "reset") { - return ; - } - } - - if (error) { - return ( - <> - Fehler - {error} - - - ); - } - - return Überprüfen; -} - function RequestReset({ onCancel }: { email: string; onCancel: () => void }) { const [mailSent, setMailSent] = useState(false); const [error, setError] = useState(""); @@ -392,79 +304,3 @@ function RequestReset({ onCancel }: { email: string; onCancel: () => void }) { ); } - -function PasswordResetForm() { - usePageEvent({ category: "Login", action: "PasswordRequest" }); - const user = useUser(); - const setUser = useSetUser(); - const setAccessToken = useSetAccessToken(); - const [password, setPassword] = useState(""); - const [password2, setPassword2] = useState(""); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(false); - const router = useRouter(); - - const [doChangePassword] = useMutation(CHANGE_PASSWORD, { - onCompleted({ changePassword }) { - if (changePassword && changePassword.token) { - setUser(changePassword.user); - setAccessToken(changePassword.token); - } - setSuccess(true); - }, - onError() { - setError("Es ist ein Fehler aufgetreten."); - }, - }); - - async function checkPasswords(pw1: string, pw2: string) { - if (pw1 !== pw2) { - setError("Die beiden Passwörter stimmen nicht überein…"); - } - return doChangePassword({ variables: { password } }); - } - if (success) { - return ( - <> - Passwort geändert - Super, das hat geklappt. - - - ); - } - return ( - <> - Passwort ändern - - - ) => - setPassword(event.currentTarget.value) - } - /> - - ) => - setPassword2(event.currentTarget.value) - } - /> - - - - - ); -} diff --git a/pages/user/verify.tsx b/pages/user/verify.tsx new file mode 100644 index 00000000..7bb99059 --- /dev/null +++ b/pages/user/verify.tsx @@ -0,0 +1,189 @@ +import { useRouter } from "next/router"; +import { useMutation, useApolloClient } from "@apollo/client"; +import { useState, useEffect } from "react"; +import { Text, Box, Button, Heading } from "rebass"; +import { Grid } from "theme-ui"; +import { Label, Input } from "@rebass/forms"; +import { AppPage, Loading } from "components/Page"; +import { ErrorBox } from "components/Form"; +import { usePageEvent, trackEvent } from "util/stats"; +import { useQueryParam } from "util/hooks"; +import { + useSetAccessToken, + useUser, + useSetUser, + SessionUser, +} from "../../state/user"; +import { Role, useCheckVerificationMutation } from "graphql/types"; +import { getStartpage, CHANGE_PASSWORD } from "./login"; + +export default function VerifyPage(): React.ReactElement { + const token = useQueryParam("t"); + const purpose = useQueryParam("p"); + const router = useRouter(); + + if (!token || !purpose) { + return ( + + + + ); + } + + return ( + void router.push("/")}> + + + ); +} + +const CheckToken: React.FC<{ + token: string; + purpose: string; +}> = ({ token, purpose }) => { + const setUser = useSetUser(); + const setAccessToken = useSetAccessToken(); + const [error, setError] = useState(""); + const [tempUser, setTempUser] = useState(); + const router = useRouter(); + const client = useApolloClient(); + const [doVerification] = useCheckVerificationMutation({ + onCompleted: (data) => { + if (data.checkVerification && data.checkVerification.token) { + setTempUser(data.checkVerification.user); + setAccessToken(data.checkVerification.token); + } + }, + onError(error) { + setError("Dieser Email-Link ist leider nicht mehr gültig!"); + console.error(error.message); + }, + }); + + useEffect(() => { + // first logout current user, as doVerification will auto-login user based on token + void client.clearStore(); + setAccessToken(""); + setUser(undefined); + void doVerification({ variables: { token } }); + }, []); + + const isTeacher = tempUser?.role === Role.Teacher; + // token verification succeded, we have a session & user + if (tempUser !== undefined) { + // login -> go straight back + if (purpose === "login") { + setUser(tempUser); + } + if (purpose === "verification") { + trackEvent({ category: "Login", action: "EmailVerified" }); + return ( + + + Super, Deine Email-Adresse ist nun bestätigt.{" "} + {isTeacher + ? "Dein Konto für Lehrpersonen ist nun eröffnet und Du bist bereits angemeldet." + : ""} + + + + ); + } + if (purpose === "reset") { + return ; + } + } + + if (error) { + return ( + <> + Fehler + {error} + + + ); + } + + return Überprüfen; +}; +function PasswordResetForm() { + usePageEvent({ category: "Login", action: "PasswordRequest" }); + const user = useUser(); + const setUser = useSetUser(); + const setAccessToken = useSetAccessToken(); + const [password, setPassword] = useState(""); + const [password2, setPassword2] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + const router = useRouter(); + + const [doChangePassword] = useMutation(CHANGE_PASSWORD, { + onCompleted({ changePassword }) { + if (changePassword && changePassword.token) { + setUser(changePassword.user); + setAccessToken(changePassword.token); + } + setSuccess(true); + }, + onError() { + setError("Es ist ein Fehler aufgetreten."); + }, + }); + + async function checkPasswords(pw1: string, pw2: string) { + if (pw1 !== pw2) { + return setError("Die beiden Passwörter stimmen nicht überein…"); + } + return doChangePassword({ variables: { password } }); + } + if (success) { + return ( + <> + Passwort geändert + Super, das hat geklappt. + + + ); + } + return ( + <> + Passwort ändern + + + ) => + setPassword(event.currentTarget.value) + } + /> + + ) => + setPassword2(event.currentTarget.value) + } + /> + + + + + ); +}