React嵌入GitHub OAuth
React中实现GitHub OAuth
GitHub开发者设置页面里创建OAuth应用程序、配置应用程序的客户端ID、和重定向URI。
OAuth是基于令牌的身份验证和授权的开放标准。GitHub OAuth让用户向第三方应用程序授予对GitHub资源拉取请求、问题、个人资料信息访问权限的方式而不用共享登录凭据。
运行yarn或yarn install来安装项目的依赖项。
使用Vite工具引导React。
npm create vite@latest github-oauth-reactjs -- --template react
# or
yarn create vite github-oauth-reactjs -- --template react
从NPM存储库下载Vite CLI二进制文件,生成React模板,将文件输出到github-reactjs中。
运行yarn或npm install安装依赖项。然后设置 Tailwind CSS 来处理项目的样式方面。
npm install -D tailwindcss postcss autoprefixer
yarn add -D tailwindcss postcss autoprefixer
安装完成后,运行Tailwind CSS init生成tailwind.config.cjs和postcss.config.cjs。
npx tailwindcss init -p
tailwind.config.cjs替换文件Tailwind CSS配置内容。
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
'ct-dark-600': '#222',
'ct-dark-200': '#e5e7eb',
'ct-dark-100': '#f5f6f7',
'ct-blue-600': '#2363eb',
'ct-yellow-600': '#f9d13e',
},
fontFamily: {
Poppins: ['Poppins, sans-serif'],
},
container: {
center: true,
padding: '1rem',
screens: {
lg: '1125px',
xl: '1125px',
'2xl': '1125px',
},
},
},
},
plugins: [],
};
src/index.css另外,CSS和Tailwind CSS替换内容。
src/index.css
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
html{
font-family: 'Poppins', sans-serif;
}
安装项目所需的依赖项。
yarn add react-hook-form @hookform/resolvers react-router-dom react-toastify tailwind-merge zod zustand
npm install react-hook-form @hookform/resolvers react-router-dom react-toastify tailwind-merge zod zustand
react-hook-form– React 的表单验证库
@hookform/resolvers– React-Hook-Form 的验证解析器
react-router-dom– 为 React Web 应用程序提供路由
react-toastify– 在 React 中显示警报通知
tailwind-merge– 合并 Tailwind CSS 类,避免样式冲突
zod– 架构验证库
zustand React 的状态管理库
打开package.json文件并将dev脚本更改为端口3000
"vite --port 3000"启动开发服务器。
获取 GitHub OAuth 凭证
VITE_SERVER_ENDPOINT=http://localhost:8000
VITE_GITHUB_OAUTH_CLIENT_ID=
VITE_GITHUB_OAUTH_CLIENT_SECRET=
VITE_GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github
生成 GitHub 同意屏幕链接
创建一个辅助函数,以便从客户端 ID 和密钥生成 GitHub OAuth 同意屏幕链接。在src目录中创建一个utils文件夹。
在src/utils/文件夹中,创建一个文件添加 TypeScript
src/utils/getGithubUrl.ts
export function getGitHubUrl(from: string) {
const rootURl = "https://github.com/login/oauth/authorize";
const options = {
client_id: import.meta.env.VITE_GITHUB_OAUTH_CLIENT_ID as string,
redirect_uri: import.meta.env.VITE_GITHUB_OAUTH_REDIRECT_URL as string,
scope: "user:email",
state: from,
};
const qs = new URLSearchParams(options);
return `${rootURl}?${qs.toString()}`;
}
创建了一个函数,根据 OAuth 应用程序凭据生成 OAuth 同意屏幕 URL。
client_id– GitHub OAuth API 将使用此参数来识别发出请求的 OAuth 应用。在 GitHub 开发者设置中创建 OAuth 应用时,已为其分配了此唯一标识符。
redirect_uri– 这是 GitHub OAuth API 在授予或拒绝权限后将用户重定向到的 URL。此回调 URL 必须与我们在 GitHub 开发者设置中配置的授权重定向 URL 匹配。
scope– 此参数指定应用程序请求的访问级别。user:email范围授予对用户电子邮件地址的读取权限。
state– 此参数允许用户授予或拒绝权限后将数据传回应用程序。受保护页面的路径传递给后端 API。state参数可用于防止跨站点请求伪造 ( CSRF ) 攻击。
使用 Zusand 设置 React Store
Zustand 是一个小型且简单的 React 状态管理库,允许通过 React hooks 进行状态访问和变异。
在src目录中创建一个store文件夹。在src/store/文件夹中,创建一个文件并添加下面的接口
types.ts
export interface IUser {
id: string;
name: string;
email: string;
role: string;
photo: string;
provider: string;
verified: string;
}
import { create } from "zustand";
import { IUser } from "./types";
type Store = {
authUser: IUser | null;
requestLoading: boolean;
setAuthUser: (user: IUser | null) => void;
setRequestLoading: (isLoading: boolean) => void;
};
const useStore = create<Store>((set) => ({
authUser: null,
requestLoading: false,
setAuthUser: (user) => set((state) => ({ ...state, authUser: user })),
setRequestLoading: (isLoading) =>
set((state) => ({ ...state, requestLoading: isLoading })),
}));
export default useStore;
pinner 组件
每当有请求时都会显示。默认情况下,此组件是隐藏的当后端API正在处理请求时将显示出来。
在src/components/文件夹中创建一个Spinner.tsx文件。
import React from 'react';
import { twMerge } from 'tailwind-merge';
type SpinnerProps = {
width?: number;
height?: number;
color?: string;
bgColor?: string;
};
const Spinner: React.FC<SpinnerProps> = ({
width = 5,
height = 5,
color,
bgColor,
}) => {
return (
<svg
role='status'
className={twMerge(
'w-5 h-5 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600',
`w-${width} h-${height} ${color} ${bgColor}`
)}
viewBox='0 0 100 101'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
fill='currentColor'
/>
<path
d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
fill='currentFill'
/>
</svg>
);
};
export default Spinner;
提交表单时,React-Hook-Form 将根据 Zod 模式验证字段,如果表单有效,registerUser则调用该函数将表单数据提交给后端 API。
src/pages/register.page.tsx
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import useStore from "../store";
import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const registerSchema = object({
name: string().min(1, "Full name is required").max(100),
email: string()
.min(1, "Email address is required")
.email("Email Address is invalid"),
password: string()
.min(1, "Password is required")
.min(8, "Password must be more than 8 characters")
.max(32, "Password must be less than 32 characters"),
passwordConfirm: string().min(1, "Please confirm your password"),
}).refine((data) => data.password === data.passwordConfirm, {
path: ["passwordConfirm"],
message: "Passwords do not match",
});
export type RegisterInput = TypeOf<typeof registerSchema>;
const RegisterPage = () => {
const navigate = useNavigate();
const store = useStore();
const registerUser = async (data: RegisterInput) => {
try {
store.setRequestLoading(true);
const VITE_SERVER_ENDPOINT = import.meta.env.VITE_SERVER_ENDPOINT;
const response = await fetch(
`${VITE_SERVER_ENDPOINT}/api/auth/register`,
{
method: "POST",
credentials: "include",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw await response.json();
}
toast.success("Account created successfully", {
position: "top-right",
});
store.setRequestLoading(false);
navigate("/login");
} catch (error: any) {
store.setRequestLoading(false);
if (error.error) {
error.error.forEach((err: any) => {
toast.error(err.message, {
position: "top-right",
});
});
return;
}
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
toast.error(resMessage, {
position: "top-right",
});
}
};
const methods = useForm<RegisterInput>({
resolver: zodResolver(registerSchema),
});
const {
reset,
handleSubmit,
register,
formState: { isSubmitSuccessful, errors },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
const onSubmitHandler: SubmitHandler<RegisterInput> = (values) => {
registerUser(values);
};
return (
<section className="bg-ct-blue-600 min-h-screen pt-20">
<div className="container mx-auto px-6 py-12 h-full flex justify-center items-center">
<div className="md:w-8/12 lg:w-5/12 bg-white px-8 py-10">
<form onSubmit={handleSubmit(onSubmitHandler)}>
<div className="mb-6">
<input
type="text"
className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
placeholder="Name"
{...register("name")}
/>
{errors.name && (
<p className="text-red-700 text-sm mt-1">
{errors.name?.message}
</p>
)}
</div>
<div className="mb-6">
<input
type="email"
className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
placeholder="Email address"
{...register("email")}
/>
{errors.email && (
<p className="text-red-700 text-sm mt-1">
{errors.email?.message}
</p>
)}
</div>
<div className="mb-6">
<input
type="password"
className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
placeholder="Password"
{...register("password")}
/>
{errors.password && (
<p className="text-red-700 text-sm mt-1">
{errors.password?.message}
</p>
)}
</div>
<div className="mb-6">
<input
type="password"
className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
placeholder="Confirm Password"
{...register("passwordConfirm")}
/>
{errors.passwordConfirm && (
<p className="text-red-700 text-sm mt-1">
{errors.passwordConfirm?.message}
</p>
)}
</div>
<button
type="submit"
className="inline-block px-7 py-4 bg-blue-600 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out w-full"
data-mdb-ripple="true"
data-mdb-ripple-color="light"
>
Sign up
</button>
</form>
</div>
</div>
</section>
);
};
export default RegisterPage;
你的反应是什么?