finish authorization flow
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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("");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
14
client/src/components/UnautorizedBlock.tsx
Normal file
14
client/src/components/UnautorizedBlock.tsx
Normal 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}</>
|
||||
);
|
||||
};
|
||||
12
client/src/hooks/useAuth.ts
Normal file
12
client/src/hooks/useAuth.ts
Normal 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;
|
||||
};
|
||||
84
client/src/pages/NewAccount/NewAccount.tsx
Normal file
84
client/src/pages/NewAccount/NewAccount.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
18
client/src/utils/apiFetch.ts
Normal file
18
client/src/utils/apiFetch.ts
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user