diff --git a/.yarn/cache/@mantine-form-npm-4.0.7-bc57be438e-554398f1ae.zip b/.yarn/cache/@mantine-form-npm-4.0.7-bc57be438e-554398f1ae.zip new file mode 100644 index 0000000..f9cec62 Binary files /dev/null and b/.yarn/cache/@mantine-form-npm-4.0.7-bc57be438e-554398f1ae.zip differ diff --git a/.yarn/cache/@tailwindcss-forms-npm-0.4.0-f188d1cada-881a80b136.zip b/.yarn/cache/@tailwindcss-forms-npm-0.4.0-f188d1cada-881a80b136.zip deleted file mode 100644 index 0979307..0000000 Binary files a/.yarn/cache/@tailwindcss-forms-npm-0.4.0-f188d1cada-881a80b136.zip and /dev/null differ diff --git a/.yarn/cache/mini-svg-data-uri-npm-1.4.3-8ed8396a0e-bd1789c349.zip b/.yarn/cache/mini-svg-data-uri-npm-1.4.3-8ed8396a0e-bd1789c349.zip deleted file mode 100644 index 75af4e8..0000000 Binary files a/.yarn/cache/mini-svg-data-uri-npm-1.4.3-8ed8396a0e-bd1789c349.zip and /dev/null differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index c66e2a9..05c82a9 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/components/Form.tsx b/components/Form.tsx index 370279c..acc82eb 100644 --- a/components/Form.tsx +++ b/components/Form.tsx @@ -1,3 +1,5 @@ +import { Box } from '@mantine/core'; + interface FormProps { children: JSX.Element; onSubmit: (event: React.FormEvent) => void; @@ -5,12 +7,27 @@ interface FormProps { const Form: React.FC = ({ children, onSubmit }) => { return ( -
({ + flexGrow: 0, + minWidth: '500px', + minHeight: '500px', + display: 'block', + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[6] + : theme.colors.gray[1], + color: + theme.colorScheme === 'dark' + ? theme.colors.blue[4] + : theme.colors.blue[7], + padding: theme.spacing.xl, + borderRadius: theme.radius.lg, + position: 'relative', + })} > - {children} -
+
{children}
+ ); }; diff --git a/components/forms/Login/EmailStep.tsx b/components/forms/Login/EmailStep.tsx new file mode 100644 index 0000000..046ea60 --- /dev/null +++ b/components/forms/Login/EmailStep.tsx @@ -0,0 +1,34 @@ +import { Button, Group, TextInput } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form/lib/use-form'; +import { At } from 'tabler-icons-react'; + +export interface EmailStepValues { + email: string; +} + +type Props = { + form: UseFormReturnType; + error?: string; +}; + +function FirstStep({ form, error }: Props) { + return ( + <> + } + size="md" + required + label="Käyttäjätunnus" + placeholder="etunimi.sukunimi" + error={error} + {...form.getInputProps('email')} + /> + + + + + + ); +} + +export default FirstStep; diff --git a/components/forms/Login/FidoStep.tsx b/components/forms/Login/FidoStep.tsx new file mode 100644 index 0000000..26a188f --- /dev/null +++ b/components/forms/Login/FidoStep.tsx @@ -0,0 +1,5 @@ +type Props = {}; +function FidoStep({}: Props) { + return
FidoStep
; +} +export default FidoStep; diff --git a/components/forms/Login/OtpStep.tsx b/components/forms/Login/OtpStep.tsx new file mode 100644 index 0000000..f58ba95 --- /dev/null +++ b/components/forms/Login/OtpStep.tsx @@ -0,0 +1,5 @@ +type Props = {}; +function OtpStep({}: Props) { + return
OtpStep
; +} +export default OtpStep; diff --git a/components/forms/Login/PasswordStep.tsx b/components/forms/Login/PasswordStep.tsx new file mode 100644 index 0000000..58d4818 --- /dev/null +++ b/components/forms/Login/PasswordStep.tsx @@ -0,0 +1,44 @@ +import { Avatar, Button, Group, PasswordInput, Title } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form/lib/use-form'; + +export interface PasswordStepValues { + password: string; +} + +type Props = { + form: UseFormReturnType; + user: { + firstName: string; + photoUri: string; + } | null; + error?: string; +}; + +function SecondStep({ form, user, error }: Props) { + console.log(error); + return ( + <> + {user && ( + + + Hei, {user.firstName} + + )} + + + + + + + + ); +} + +export default SecondStep; diff --git a/components/forms/Login/Success.tsx b/components/forms/Login/Success.tsx new file mode 100644 index 0000000..4e0fd7c --- /dev/null +++ b/components/forms/Login/Success.tsx @@ -0,0 +1,5 @@ +type Props = {}; +function Success({}: Props) { + return
Success
; +} +export default Success; diff --git a/components/forms/Login/index.tsx b/components/forms/Login/index.tsx new file mode 100644 index 0000000..123795f --- /dev/null +++ b/components/forms/Login/index.tsx @@ -0,0 +1,116 @@ +import { Group } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { UseFormReturnType } from '@mantine/form/lib/use-form'; +import { useState } from 'react'; +import useAuthentication from '../../../hooks/useAuthentication'; +import Form from '../../Form'; +import ThemeToggle from '../../ThemeToggle'; +import EmailStep, { EmailStepValues } from './EmailStep'; +import FidoStep from './FidoStep'; +import OtpStep from './OtpStep'; +import PasswordStep, { PasswordStepValues } from './PasswordStep'; +import Success from './Success'; + +type LoginSteps = 'email' | 'password' | 'fido' | 'otp' | 'success'; + +type Props = {}; + +function LoginForm({}: Props) { + const [currentStep, setCurrentStep] = useState('email'); + const [error, setError] = useState(); + const [user, setUser] = useState(null); + const { startLoginProcess } = useAuthentication(); + + const formsList = [ + useForm({ + initialValues: { + email: '', + }, + }), + useForm({ + initialValues: { + password: '', + }, + }), + useForm({ + initialValues: { + password: '', + }, + }), + useForm({ + initialValues: { + password: '', + }, + }), + useForm({ + initialValues: { + password: '', + }, + }), + ]; + + const forms = { + email: formsList[0] as UseFormReturnType, + password: formsList[1] as UseFormReturnType, + fido: formsList[2] as UseFormReturnType, + otp: formsList[3] as UseFormReturnType, + success: formsList[4] as UseFormReturnType, + }; + + const steps = { + email: , + password: , + fido: , + otp: , + success: , + }; + + const handleSubmit = async (values: EmailStepValues | PasswordStepValues) => { + switch (currentStep) { + case 'email': + const { email } = values as EmailStepValues; + const res = await startLoginProcess(email); + + if (res.errors) { + console.log(res.errors); + setError((res.errors[0] as Error).toString()); + } + + if (res.user != null) { + setUser(res.user as any); + } + + setCurrentStep(res.nextScreen as LoginSteps); + } + }; + + // Make sure that it doesnt go out of bounds + if (steps[currentStep] == undefined || forms[currentStep] == undefined) { + return

You should not be seeing this

; + } + + return ( + <> +
handleSubmit(values))} + > + + + {steps[currentStep]} + + ({ + position: 'absolute', + right: theme.spacing.lg, + bottom: theme.spacing.md, + })} + > + + + +
+ + ); +} + +export default LoginForm; diff --git a/data/authentication.ts b/data/authentication.ts index b12a169..bce4916 100644 --- a/data/authentication.ts +++ b/data/authentication.ts @@ -3,41 +3,70 @@ import { Challenge, IUser } from '@rittaschool/shared'; import { GraphQLError } from 'graphql'; import { graphqlClient as client } from './baseClient'; +export const startLoginQuery = gql` + query startLogin($identifier: String!) { + startLoginProcess(email: $identifier) { + challenge { + type + id + userId + } + userFirstName + userPhotoUri + } + } +`; + export const startLogin = async ( identifier: string ): Promise<{ data?: { challenge: Challenge; + user: { + firstName: string; + photoUri: string; + }; }; - errors?: readonly GraphQLError[]; + errors?: readonly unknown[]; loading: boolean; }> => { - const res = await client.query({ - query: gql` - query startLogin($identifier: String!) { - startLoginProcess(email: $identifier) { - challenge { - type - id - userId + try { + const res = await client.query({ + query: gql` + query startLogin($identifier: String!) { + startLoginProcess(email: $identifier) { + challenge { + type + id + userId + } + userFirstName + userPhotoUri } - userFirstName - userPhotoUri } - } - `, - variables: { - identifier, - }, - }); + `, + variables: { + identifier, + }, + }); - return { - loading: res.loading, - errors: res.errors, - data: { - challenge: res.data.startLoginProcess.challenge, - }, - }; + return { + loading: res.loading, + errors: res.errors, + data: { + challenge: res.data.startLoginProcess.challenge, + user: { + firstName: res.data.startLoginProcess.userFirstName, + photoUri: res.data.startLoginProcess.userPhotoUri, + }, + }, + }; + } catch (error) { + return { + errors: [error], + loading: false, + }; + } }; export const submitChallenge = async ( @@ -65,32 +94,36 @@ export const submitChallenge = async ( challengeInput.data!.otpData = challenge.data.otpData; } - const res = await client.query({ - query: gql` - query SubmitChallenge($challenge: ChallengeInput!) { - submitChallenge(challenge: $challenge) { - user { - id - firstName - username - } - challenge { - type + try { + const res = await client.query({ + query: gql` + query SubmitChallenge($challenge: ChallengeInput!) { + submitChallenge(challenge: $challenge) { + user { + id + firstName + username + } + challenge { + type + } } } - } - `, - variables: { - challenge: challengeInput, - }, - }); + `, + variables: { + challenge: challengeInput, + }, + }); - return { - loading: res.loading, - errors: res.errors, - data: { - challenge: res.data.submitChallenge.challenge, - user: res.data.submitChallenge.user, - }, - }; + return { + loading: res.loading, + errors: res.errors, + data: { + challenge: res.data.submitChallenge.challenge, + user: res.data.submitChallenge.user, + }, + }; + } catch (error) { + console.log('auth', error); + } }; diff --git a/hooks/useAuthentication.tsx b/hooks/useAuthentication.tsx index ebe1c73..8a991c9 100644 --- a/hooks/useAuthentication.tsx +++ b/hooks/useAuthentication.tsx @@ -8,9 +8,14 @@ interface UseAuthentication { loading: boolean; user: IUser | null; challenge: Challenge | null; - startLoginProcess: ( - identifier?: string | null - ) => Promise<{ nextScreen: string }>; + startLoginProcess: (identifier?: string | null) => Promise<{ + nextScreen: string; + errors?: readonly unknown[]; + user: { + photoUri: string; + firstName: string; + } | null; + }>; submitPassword: (password: string, challenge: Challenge) => any; //TODO: make interface later fido2: { startSetup: (email: string) => Promise; @@ -21,8 +26,9 @@ interface UseAuthentication { const screens: { [key: string]: string; } = { + EMAIL_NEEDED: 'email', PASSWORD_NEEDED: 'password', - FIDO2_NEEDED: 'fido2', + FIDO2_NEEDED: 'fido', OTP_NEEDED: 'otp', }; @@ -34,7 +40,14 @@ const useAuthentication = (): UseAuthentication => { const startLoginProcess = async ( identifier?: string | null - ): Promise<{ nextScreen: string }> => { + ): Promise<{ + nextScreen: string; + errors?: readonly unknown[]; + user: { + photoUri: string; + firstName: string; + } | null; + }> => { if (!identifier) console.log('No identifier'); const { loading, errors, data } = await startLogin(identifier!); @@ -42,16 +55,24 @@ const useAuthentication = (): UseAuthentication => { setLoading(loading); if (errors || !data) { - console.log(errors); - console.log('No Data'); + return { + nextScreen: screens['EMAIL_NEEDED'], + errors: errors, + user: null, + }; } - if (data!.challenge) { - setChallenge(data!.challenge); - return { nextScreen: screens[data!.challenge!.type] }; - } else { - throw new Error('No challenge'); - } + if (!data!.challenge) throw new Error('No challenge'); + + setChallenge(data!.challenge); + + if (!data!.user) + return { + nextScreen: screens[data!.challenge!.type], + user: null, + }; + + return { nextScreen: screens[data!.challenge!.type], user: data!.user }; }; const submitPassword = async (password: string, challenge: Challenge) => { diff --git a/package.json b/package.json index 76f3ee8..510be74 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "@hexagon/base64": "^1.0.16", "@mantine/core": "^4.0.7", "@mantine/dates": "^4.0.7", + "@mantine/form": "^4.0.7", "@mantine/hooks": "^4.0.7", "@mantine/next": "^4.0.7", "@rittaschool/shared": "^0.0.27", - "@tailwindcss/forms": "^0.4.0", "autoprefixer": "^10.4.2", "bootstrap": "^5.1.3", "dayjs": "^1.10.7", diff --git a/pages/new-login.tsx b/pages/new-login.tsx new file mode 100644 index 0000000..e227e2a --- /dev/null +++ b/pages/new-login.tsx @@ -0,0 +1,102 @@ +import { + Box, + Group, + Paper, + TextInput, + Title, + Button, + Center, + Text, +} from '@mantine/core'; +import { useForm } from '@mantine/hooks'; +import { useEffect, useRef, useState } from 'react'; +import Logo from '../components/Logo'; +import useAuthentication from '../hooks/useAuthentication'; + +const Login = () => { + const form = useForm({ + initialValues: { + email: '', + }, + }); + + return ( + ({ + position: 'relative', + })} + > + ({ + display: 'block', + color: + theme.colorScheme === 'dark' + ? theme.colors.blue[4] + : theme.colors.blue[7], + padding: theme.spacing.xl, + })} + > + + + Tervetuloa käyttämään Rittaa + + Ritta on helppokäyttöinen ja monipuolinen koulujärjestelmä.
+ Tämän Ritta instanssin omistaa |insert name here|.
+
+ Ongelmia sisäänkirjautumisessa?
+ Ota yhteyttä instanssin tukeen: |insert email here|. +
+
+
+ + ({ + position: 'absolute', + top: '50%', + right: '50%', // 80px + transform: 'translate(50%)', + flexGrow: 0, + minWidth: '500px', + display: 'block', + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[6] + : theme.colors.gray[0], + color: + theme.colorScheme === 'dark' + ? theme.colors.blue[4] + : theme.colors.blue[7], + padding: theme.spacing.xl, + borderRadius: theme.radius.lg, + })} + > + + Kirjaudu Sisään + + +
console.log(values))}> + + + + + + +
+
+
+ ); +}; + +export default Login; diff --git a/pages/test.tsx b/pages/test.tsx new file mode 100644 index 0000000..0c39977 --- /dev/null +++ b/pages/test.tsx @@ -0,0 +1,17 @@ +import { Center } from '@mantine/core'; +import LoginForm from '../components/forms/Login'; +import ThemeToggle from '../components/ThemeToggle'; + +type Props = {}; +function test({}: Props) { + return ( +
({ + height: '100vh', + })} + > + +
+ ); +} +export default test; diff --git a/yarn.lock b/yarn.lock index a4bd95d..8b94658 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1643,6 +1643,15 @@ __metadata: languageName: node linkType: hard +"@mantine/form@npm:^4.0.7": + version: 4.0.7 + resolution: "@mantine/form@npm:4.0.7" + peerDependencies: + react: ">=16.8.0" + checksum: 554398f1aeb6b0a072793c04cdda3f97fdc7e1f6386465ac87f9da11abd2bb1af903c467abd0567000df5c9e7317adf2175aaab09e5abfe4b88c726f573ddb38 + languageName: node + linkType: hard + "@mantine/hooks@npm:^4.0.7": version: 4.0.7 resolution: "@mantine/hooks@npm:4.0.7" @@ -2172,17 +2181,6 @@ __metadata: languageName: node linkType: hard -"@tailwindcss/forms@npm:^0.4.0": - version: 0.4.0 - resolution: "@tailwindcss/forms@npm:0.4.0" - dependencies: - mini-svg-data-uri: ^1.2.3 - peerDependencies: - tailwindcss: ">=3.0.0 || >= 3.0.0-alpha.1" - checksum: 881a80b136f22b3da68323166ddfbcd844841f711d75ce4d64479302ae523ac7c1fe8a3bb57370216e6da69303640d70d543ecdb3ec2086f1ddf428a6c13566f - languageName: node - linkType: hard - "@tootallnate/once@npm:1": version: 1.1.2 resolution: "@tootallnate/once@npm:1.1.2" @@ -3754,11 +3752,11 @@ __metadata: "@hexagon/base64": ^1.0.16 "@mantine/core": ^4.0.7 "@mantine/dates": ^4.0.7 + "@mantine/form": ^4.0.7 "@mantine/hooks": ^4.0.7 "@mantine/next": ^4.0.7 "@rittaschool/shared": ^0.0.27 "@svgr/webpack": ^6.2.1 - "@tailwindcss/forms": ^0.4.0 "@types/node": 16.11.11 "@types/react": 17.0.37 "@types/react-grid-layout": ^1.3.1 @@ -4662,15 +4660,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"mini-svg-data-uri@npm:^1.2.3": - version: 1.4.3 - resolution: "mini-svg-data-uri@npm:1.4.3" - bin: - mini-svg-data-uri: cli.js - checksum: bd1789c34907a36ae9157897b7c7f6ad700e71c8cb6d725bb2810ce6211abcaa3717333ca17408f6f8b2a44055e571cd5b78d478688e1864513cb7f8e023ae3f - languageName: node - linkType: hard - "minimatch@npm:^3.0.4": version: 3.1.1 resolution: "minimatch@npm:3.1.1"