finish authorization flow

This commit is contained in:
João Geonizeli
2022-07-09 19:13:01 -03:00
parent 95bd6ad376
commit 2f4607143d
10 changed files with 240 additions and 30 deletions

View File

@@ -7,6 +7,7 @@
"@emotion/styled": "^11.9.3",
"@fontsource/roboto": "^4.5.7",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.89",
"@mui/material": "^5.8.7",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
@@ -17,6 +18,7 @@
"@types/react-dom": "^18.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.33.1",
"react-scripts": "5.0.1",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"

View File

@@ -9,6 +9,8 @@ import { AuthProvider } from "./providers/AuthProvider";
import "./index.css";
import { LoginDialog } from "./components/LoginDialog";
import { UnautorizedBlock } from "./components/UnautorizedBlock";
import { NewAccount } from "./pages/NewAccount/NewAccount";
function App() {
return (
@@ -16,6 +18,10 @@ function App() {
<Topbar />
<LoginDialog />
<Container>
<UnautorizedBlock
protectedContent={<Home />}
unauthenticatedContent={<NewAccount />}
/>
<Home />
</Container>
</AuthProvider>

View File

@@ -7,11 +7,11 @@ import {
TextField,
} from "@mui/material";
import { useState } from "react";
import { useAuth } from "../providers/AuthProvider";
import { useAuth } from "../hooks/useAuth";
export const LoginDialog = () => {
const { isLoginDialogOpen, setIsLoginDialogOpen, login } = useAuth();
console.log(isLoginDialogOpen);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

View File

@@ -1,5 +1,5 @@
import { AppBar, Button, Toolbar, Typography } from "@mui/material";
import { useAuth } from "../providers/AuthProvider";
import { useAuth } from "../hooks/useAuth";
export const Topbar = () => {
const { authenticated, logout, setIsLoginDialogOpen } = useAuth();

View File

@@ -0,0 +1,14 @@
import { useAuth } from "../hooks/useAuth";
export type UnautorizedBlockProps = {
protectedContent: React.ReactNode;
unauthenticatedContent: React.ReactNode;
};
export const UnautorizedBlock = (props: UnautorizedBlockProps) => {
const { authenticated } = useAuth();
return (
<>{authenticated ? props.protectedContent : props.unauthenticatedContent}</>
);
};

View File

@@ -0,0 +1,12 @@
import { useContext } from "react";
import { AuthContext } from "../providers/AuthProvider";
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("You probably forgot to put <AuthProvider>.");
}
return context;
};

View File

@@ -0,0 +1,84 @@
import { LoadingButton } from "@mui/lab";
import { Alert, Stack, TextField } from "@mui/material";
import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { useAuth } from "../../hooks/useAuth";
import { createApiClient } from "../../utils/apiFetch";
type NewAccountForm = {
email: string;
password: string;
};
export const NewAccount = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const apiClient = createApiClient();
const { login } = useAuth();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<NewAccountForm>();
const onSubmit: SubmitHandler<NewAccountForm> = (data) => {
setLoading(true);
apiClient("users", {
method: "POST",
body: JSON.stringify(data),
})
.then(async (res) => {
const result = await res.json();
if (result.error) {
setError(result.error);
} else {
login(data.email, data.password);
}
})
.finally(() => setLoading(false));
};
return (
<div>
<h1>New Account Page</h1>
{error && <Alert severity="error">{error}</Alert>}
<form onSubmit={handleSubmit(onSubmit)}>
<TextField
error={!!errors.email}
helperText={errors.email?.message}
disabled={loading}
autoFocus
margin="dense"
fullWidth
label="Email Address"
type="email"
variant="standard"
{...register("email", {
required: true,
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
},
})}
/>
<TextField
error={!!errors.password}
helperText={errors.password?.message}
disabled={loading}
margin="dense"
fullWidth
label="Password"
type="password"
variant="standard"
{...register("password", { required: true, minLength: 8 })}
/>
<Stack direction="row-reverse" spacing={3}>
<LoadingButton type="submit" loading={loading} variant="contained">
Create account
</LoadingButton>
</Stack>
</form>
</div>
);
};

View File

@@ -1,10 +1,5 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import { createContext, useCallback, useMemo, useState } from "react";
import { createApiClient } from "../utils/apiFetch";
type LoginCallback = (email: string, password: string) => void;
type LogoutCallback = () => void;
@@ -16,19 +11,10 @@ type AuthProviderValue = {
logout: LogoutCallback;
setIsLoginDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
isLoginDialogOpen: boolean;
apiClient: typeof fetch;
};
const AuthContext = createContext<AuthProviderValue | null>(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("You probably forgot to put <PaymentProvider>.");
}
return context;
};
export const AuthContext = createContext<AuthProviderValue | null>(null);
type AuthProviderProps = {
children: React.ReactNode;
@@ -36,16 +22,29 @@ type AuthProviderProps = {
export const AuthProvider = ({ children, ...rest }: AuthProviderProps) => {
const [loading] = useState(true);
const [token] = useState<AuthProviderValue["token"]>(null);
const [token, setToken] = useState<AuthProviderValue["token"]>(null);
const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false);
const apiClient = createApiClient(token ?? "");
const login = useCallback<LoginCallback>((_email, __password) => {
throw new Error("Not implemented yet");
}, []);
const login = useCallback<LoginCallback>((email, password) => {
apiClient("users/sign_in", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
}).then(async (res) => {
setToken(await res.json());
});
}, [apiClient, setToken]);
const logout = useCallback<LogoutCallback>(() => {
throw new Error("Not implemented yet");
}, []);
apiClient("users/sign_out", {
method: "DELETE",
}).then(() => {
setToken(null);
});
}, [apiClient, setToken]);
const authenticated = !!token;
@@ -65,6 +64,7 @@ export const AuthProvider = ({ children, ...rest }: AuthProviderProps) => {
value={{
...providerValue,
...rest,
apiClient,
isLoginDialogOpen,
setIsLoginDialogOpen,
}}

View File

@@ -0,0 +1,18 @@
const host = "http://localhost:5000/";
export const createApiClient =
(token?: string): typeof fetch =>
(input, init = {}): Promise<Response> => {
const { headers, ...rest } = init;
const customInt: RequestInit = {
headers: {
"Content-Type": "application/json",
"X-Access-Token": token ?? "",
...headers,
},
...rest,
};
return fetch(host + input, customInt);
};

View File

@@ -1169,6 +1169,39 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
"@date-io/core@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.14.0.tgz#03e9b9b9fc8e4d561c32dd324df0f3ccd967ef14"
integrity sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw==
"@date-io/date-fns@^2.11.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.14.0.tgz#92ab150f488f294c135c873350d154803cebdbea"
integrity sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA==
dependencies:
"@date-io/core" "^2.14.0"
"@date-io/dayjs@^2.11.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.14.0.tgz#8d4e93e1d473bb5f25210866204dc33384ca4c20"
integrity sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og==
dependencies:
"@date-io/core" "^2.14.0"
"@date-io/luxon@^2.11.1":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.14.0.tgz#cd1641229e00a899625895de3a31e3aaaf66629f"
integrity sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg==
dependencies:
"@date-io/core" "^2.14.0"
"@date-io/moment@^2.11.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.14.0.tgz#8300abd6ae8c55d8edee90d118db3cef0b1d4f58"
integrity sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA==
dependencies:
"@date-io/core" "^2.14.0"
"@emotion/babel-plugin@^11.7.1":
version "11.9.2"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95"
@@ -1604,6 +1637,22 @@
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/lab@^5.0.0-alpha.89":
version "5.0.0-alpha.89"
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.89.tgz#cc61560f16eebc3f44890835ee22cdc560ce4d48"
integrity sha512-u5bMi/V+Utwouo9awVzGasj/LudlRqPFyMo2L5/y60uFo0EaG17bt1jh/U7smQCdjd+7tvJ39HNMkEmIoGr7BQ==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/base" "5.0.0-alpha.88"
"@mui/system" "^5.8.7"
"@mui/utils" "^5.8.6"
"@mui/x-date-pickers" "5.0.0-alpha.1"
clsx "^1.2.0"
prop-types "^15.8.1"
react-is "^17.0.2"
react-transition-group "^4.4.2"
rifm "^0.12.1"
"@mui/material@^5.8.7":
version "5.8.7"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.8.7.tgz#28617c5b8a9e354e300f19fc38e1286ba1e15ad3"
@@ -1659,7 +1708,7 @@
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.4.tgz#4185c05d6df63ec673cda15feab80440abadc764"
integrity sha512-uveM3byMbthO+6tXZ1n2zm0W3uJCQYtwt/v5zV5I77v2v18u0ITkb8xwhsDD2i3V2Kye7SaNR6FFJ6lMuY/WqQ==
"@mui/utils@^5.8.6":
"@mui/utils@^5.6.0", "@mui/utils@^5.8.6":
version "5.8.6"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.8.6.tgz#543de64a64bb9135316ecfd91d75a8740544d79f"
integrity sha512-QM2Sd1xZo2jOt2Vz5Rmro+pi2FLJyiv4+OjxkUwXR3oUM65KSMAMLl/KNYU55s3W3DLRFP5MVwE4FhAbHseHAg==
@@ -1670,6 +1719,21 @@
prop-types "^15.8.1"
react-is "^17.0.2"
"@mui/x-date-pickers@5.0.0-alpha.1":
version "5.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-5.0.0-alpha.1.tgz#7450b5544b9ed655db41891c74e2c5f652fbedb7"
integrity sha512-dLPkRiIn2Gr0momblxiOnIwrxn4SijVix+8e08mwAGWhiWcmWep1O9XTRDpZsjB0kjHYCf+kZjlRX4dxnj2acg==
dependencies:
"@date-io/date-fns" "^2.11.0"
"@date-io/dayjs" "^2.11.0"
"@date-io/luxon" "^2.11.1"
"@date-io/moment" "^2.11.0"
"@mui/utils" "^5.6.0"
clsx "^1.1.1"
prop-types "^15.7.2"
react-transition-group "^4.4.2"
rifm "^0.12.1"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -3225,7 +3289,7 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clsx@^1.2.0:
clsx@^1.1.1, clsx@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@@ -7346,7 +7410,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.2, prop-types@^15.8.1:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -7479,6 +7543,11 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-hook-form@^7.33.1:
version "7.33.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.33.1.tgz#8c4410e3420788d3b804d62cc4c142915c2e46d0"
integrity sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -7781,6 +7850,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rifm@^0.12.1:
version "0.12.1"
resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4"
integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"