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

@@ -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);
};