refactor: magic link auth

This commit is contained in:
Tristan Yang
2022-06-20 17:41:58 +08:00
parent b25907f7fc
commit b938b6b3aa
17 changed files with 349 additions and 111 deletions
+29 -8
View File
@@ -45,6 +45,13 @@ export const authApi = createApi({
}
}
}),
register: builder.mutation({
query: (data) => ({
url: `user/register`,
method: "POST",
body: data
})
}),
// 更新token
renew: builder.mutation({
query: ({ token, refreshToken }) => ({
@@ -87,9 +94,9 @@ export const authApi = createApi({
})
}),
checkInviteTokenValid: builder.mutation({
checkMagicTokenValid: builder.mutation({
query: (token) => ({
url: "user/check_invite_magic_token",
url: "user/check_magic_token",
method: "POST",
body: { magic_token: token }
})
@@ -101,11 +108,23 @@ export const authApi = createApi({
body: { old_password, new_password }
})
}),
sendMagicLink: builder.mutation({
sendLoginMagicLink: builder.mutation({
query: (email) => ({
url: "token/send_magic_link",
headers: {
// "content-type": "text/plain",
accept: "text/plain"
},
url: `user/send_login_magic_link?email=${encodeURIComponent(email)}`,
method: "POST",
body: { email }
responseHandler: (response) => response.text()
// body: { email }
})
}),
sendRegMagicLink: builder.mutation({
query: (data) => ({
url: `user/send_reg_magic_link`,
method: "POST",
body: data
})
}),
getMetamaskNonce: builder.query({
@@ -147,7 +166,8 @@ export const authApi = createApi({
export const {
useGetInitializedQuery,
useSendMagicLinkMutation,
useSendLoginMagicLinkMutation,
useSendRegMagicLinkMutation,
useGetCredentialsQuery,
useUpdateDeviceTokenMutation,
useGetOpenidMutation,
@@ -155,6 +175,7 @@ export const {
useLazyGetMetamaskNonceQuery,
useLoginMutation,
useLazyLogoutQuery,
useCheckInviteTokenValidMutation,
useUpdatePasswordMutation
useCheckMagicTokenValidMutation,
useUpdatePasswordMutation,
useRegisterMutation
} = authApi;
+3 -2
View File
@@ -6,8 +6,9 @@ import BASE_URL, { tokenHeader } from "../config";
const whiteList = [
"login",
"register",
"sendMagicLink",
"checkInviteTokenValid",
"sendLoginMagicLink",
"sendRegMagicLink",
"checkMagicTokenValid",
"getGoogleAuthConfig",
"getGithubAuthConfig",
"getSMTPStatus",
+2 -9
View File
@@ -89,13 +89,7 @@ export const contactApi = createApi({
body: data
})
}),
register: builder.mutation({
query: (data) => ({
url: `user/register`,
method: "POST",
body: data
})
}),
sendMsg: builder.mutation({
query: ({ id, content, type = "text", properties = "" }) => ({
headers: {
@@ -139,6 +133,5 @@ export const {
useUpdateAvatarMutation,
useGetContactsQuery,
useLazyGetContactsQuery,
useSendMsgMutation,
useRegisterMutation
useSendMsgMutation
} = contactApi;
@@ -1,7 +1,23 @@
// import { useState, useEffect } from "react";
// import { useGoogleLogin } from "react-google-login";
import IconGithub from "../../assets/icons/github.svg";
import { StyledSocialButton } from "./styled";
import styled from "styled-components";
import Button from "./styled/Button";
const StyledSocialButton = styled(Button)`
width: 100%;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: #344054;
border: 1px solid #d0d5dd;
background: none !important;
.icon {
width: 24px;
height: 24px;
}
`;
export default function GithubLoginButton({ config = {} }) {
const { client_id } = config;
const handleGithubLogin = () => {
+58
View File
@@ -0,0 +1,58 @@
import { useEffect } from "react";
import { useGoogleLogin } from "react-google-login";
import googleSvg from "../../assets/icons/google.svg?url";
import styled from "styled-components";
import Button from "./styled/Button";
import { useLoginMutation } from "../../app/services/auth";
import toast from "react-hot-toast";
const StyledSocialButton = styled(Button)`
width: 100%;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: #344054;
border: 1px solid #d0d5dd;
background: none !important;
.icon {
width: 24px;
height: 24px;
}
`;
export default function GoogleLoginButton({ clientId }) {
const [login, { isSuccess, isLoading }] = useLoginMutation();
const { signIn, loaded } = useGoogleLogin({
onScriptLoadFailure: (wtf) => {
console.error("google login script load failure", wtf);
},
clientId,
onSuccess: ({ tokenId, ...rest }) => {
console.info("google oauth success", tokenId, rest);
login({
id_token: tokenId,
type: "google"
});
},
onFailure: (wtf) => {
console.error("google login failure", wtf);
}
});
useEffect(() => {
if (isSuccess) {
toast.success("Login Successfully");
// navigateTo("/");
}
}, [isSuccess]);
const handleGoogleLogin = () => {
signIn();
};
// console.log("google login ", loaded);
return (
<StyledSocialButton disabled={!loaded || isLoading} onClick={handleGoogleLogin}>
<img className="icon" src={googleSvg} alt="google icon" />
{loaded ? `Sign in with Google` : `Initailizing`}
</StyledSocialButton>
);
}
+2 -2
View File
@@ -75,9 +75,9 @@ const PageRoutes = () => {
}
>
<Route index element={<RegPage />} />
<Route path="magiclink">
<Route path="set_name">
<Route index element={<RegWithUsernamePage />} />
<Route path=":token" element={<RegWithUsernamePage />} />
<Route path=":from" element={<RegWithUsernamePage />} />
</Route>
</Route>
<Route
+3 -3
View File
@@ -3,8 +3,8 @@ import StyledWrapper from "./styled";
import { Navigate } from "react-router-dom";
import toast from "react-hot-toast";
import BASE_URL from "../../app/config";
import { useRegisterMutation } from "../../app/services/contact";
import { useCheckInviteTokenValidMutation } from "../../app/services/auth";
import { useRegisterMutation } from "../../app/services/auth";
import { useCheckMagicTokenValidMutation } from "../../app/services/auth";
import { useSelector } from "react-redux";
export default function InvitePage() {
@@ -17,7 +17,7 @@ export default function InvitePage() {
// const navigateTo = useNavigate();
const [register, { data, isLoading, isSuccess, isError, error }] = useRegisterMutation();
const [checkToken, { data: isValid, isLoading: checkLoading, isSuccess: checkSuccess }] =
useCheckInviteTokenValidMutation();
useCheckMagicTokenValidMutation();
useEffect(() => {
// console.log(search);
const query = new URLSearchParams(location.search);
-33
View File
@@ -1,33 +0,0 @@
// import { useState, useEffect } from "react";
import { useGoogleLogin } from "react-google-login";
import googleSvg from "../../assets/icons/google.svg?url";
import { StyledSocialButton } from "./styled";
export default function GoogleLoginButton({ login, clientId }) {
const { signIn, loaded } = useGoogleLogin({
onScriptLoadFailure: (wtf) => {
console.error("google login script load failure", wtf);
},
clientId,
onSuccess: ({ tokenId, ...rest }) => {
console.info("google oauth success", tokenId, rest);
login({
id_token: tokenId,
type: "google"
});
},
onFailure: (wtf) => {
console.error("google login failure", wtf);
}
});
const handleGoogleLogin = () => {
signIn();
};
// console.log("google login ", loaded);
return (
<StyledSocialButton disabled={!loaded} onClick={handleGoogleLogin}>
<img className="icon" src={googleSvg} alt="google icon" />
{loaded ? `Sign in with Google` : `Initailizing`}
</StyledSocialButton>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ const StyledSignUpLink = styled.p`
export default function MagicLinkLogin() {
const navigate = useNavigate();
const handleSignUp = () => {
navigate("/");
navigate("/register");
};
return (
<StyledSignUpLink>
+9 -9
View File
@@ -8,14 +8,14 @@ import MetamaskLoginButton from "./MetamaskLoginButton";
import OidcLoginButton from "./OidcLoginButton";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
import GoogleLoginButton from "./GoogleLoginButton";
import MagicLinkLogin from "./MagicLinkLogin";
import SignUpLink from "./SignUpLink";
import { useLoginMutation } from "../../app/services/auth";
import { useGetLoginConfigQuery, useGetSMTPStatusQuery } from "../../app/services/server";
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
import GithubLoginButton from "./GithubLoginButton";
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
import GithubLoginButton from "../../common/component/GithubLoginButton";
export default function LoginPage() {
const { data: enableSMTP } = useGetSMTPStatusQuery();
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
@@ -32,7 +32,7 @@ export default function LoginPage() {
const oauth = query.get("oauth");
const code = query.get("code");
const state = query.get("state");
const token = query.get("token");
const magic_token = query.get("magic_token");
const exists = query.get("exists");
if (oauth) {
switch (oauth) {
@@ -58,18 +58,18 @@ export default function LoginPage() {
}
}
// magic link
if (token && typeof exists !== "undefined") {
console.log("tokken", token, exists);
if (magic_token && typeof exists !== "undefined") {
// console.log("tokken", token, exists);
const isLogin = exists == "true";
if (isLogin) {
// login
login({
token,
magic_token,
type: "magiclink"
});
} else {
// reg
location.href = `/#/reg/magiclink/${token}`;
// reg with magic link and set name only
location.href = `?magic_token=${magic_token}#/register/set_name/login`;
}
}
}, []);
@@ -167,7 +167,7 @@ export default function LoginPage() {
</form>
{hasDivider && <hr className="or" />}
{enableMagicLink && <MagicLinkLogin />}
{googleLogin && <GoogleLoginButton login={login} clientId={clientId} />}
{googleLogin && <GoogleLoginButton clientId={clientId} />}
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
{oidc.length > 0 && <OidcLoginButton issuers={oidc} />}
+33
View File
@@ -0,0 +1,33 @@
// import React from "react";
import styled from "styled-components";
const Styled = styled.div`
display: flex;
flex-direction: column;
align-items: center;
.title {
font-weight: 600;
font-size: 30px;
line-height: 38px;
color: #101828;
margin-bottom: 12px;
}
.desc {
text-align: center;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: #667085;
&:not(:last-child) {
margin-bottom: 24px;
}
}
`;
export default function EmailNextTip() {
return (
<Styled>
<div className="title">Magic link Sent</div>
<p className="desc">Login to your email client, and continue next step</p>
<p className="desc">You can close this window now.</p>
</Styled>
);
}
+30 -15
View File
@@ -1,25 +1,30 @@
/* eslint-disable no-undef */
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
// import { useNavigate } from "react-router-dom";
// import toast from "react-hot-toast";
import { useParams } from "react-router-dom";
import { setAuthData } from "../../app/slices/auth.data";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
import { useLoginMutation, useCheckInviteTokenValidMutation } from "../../app/services/auth";
import { useLoginMutation, useCheckMagicTokenValidMutation } from "../../app/services/auth";
import toast from "react-hot-toast";
import ExpiredTip from "./ExpiredTip";
import { useRegisterMutation } from "../../app/services/auth";
export default function RegWithUsername() {
const { token } = useParams();
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
useCheckInviteTokenValidMutation();
const [login, { isLoading, error, isSuccess, data }] = useLoginMutation();
useCheckMagicTokenValidMutation();
const [login, { isLoading: loginLoading, error, isSuccess: loginSuccess, data: loginData }] =
useLoginMutation();
const [register, { isLoading: regLoading, isSuccess: regSuccess, data: regData }] =
useRegisterMutation();
// const navigateTo = useNavigate();
const { from = "reg" } = useParams();
const dispatch = useDispatch();
const [username, setUsername] = useState("");
const query = new URLSearchParams(location.search);
// const githubCode = query.get("gcode");
const token = query.get("magic_token");
useEffect(() => {
if (token) {
checkTokenInvalid(token);
@@ -38,32 +43,42 @@ export default function RegWithUsername() {
}
}, [error]);
useEffect(() => {
const isSuccess = loginSuccess || regSuccess;
const data = loginData || regData;
if (isSuccess && data) {
// 更新本地认证信息
console.log("login data", data);
toast.success("Login Successfully");
dispatch(setAuthData(data));
// tricky
location.href = `/#/`;
}
}, [isSuccess, data]);
}, [loginSuccess, regSuccess, loginData, regData]);
const handleLogin = (evt) => {
const handleAuth = (evt) => {
evt.preventDefault();
if (from == "reg") {
register({
magic_token: token,
name: username
});
} else {
login({
token,
username,
magic_token: token,
extra_name: username,
type: "magiclink"
});
// sendMagicLink(email);
}
};
const handleInput = (evt) => {
const { value } = evt.target;
setUsername(value);
};
if (!token) return "no token";
if (checkingToken) return "checking Magic Link...";
if (!token) return "No Token";
if (checkingToken) return "Checking Magic Link...";
if (!isTokenValid) return <ExpiredTip />;
const isLoading = loginLoading || regLoading;
const isSuccess = loginSuccess || regSuccess;
return (
<>
<div className="tips">
@@ -73,7 +88,7 @@ export default function RegWithUsername() {
visible to others in spaces you joined.
</span>
</div>
<form onSubmit={handleLogin}>
<form onSubmit={handleAuth}>
<Input
className="large"
name="username"
+63 -19
View File
@@ -1,42 +1,86 @@
/* eslint-disable no-undef */
import { useState } from "react";
import { useState, useEffect } from "react";
// import { useDispatch } from "react-redux";
// import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
// import toast from "react-hot-toast";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
// import { useSendMagicLinkMutation } from "../../app/services/auth";
import { useSendRegMagicLinkMutation } from "../../app/services/auth";
import EmailNextTip from "./EmailNextStepTip";
// import { useSendLoginMagicLinkMutation } from "../../app/services/auth";
export default function Reg() {
// const [
// sendMagicLink,
// { data, isSuccess, isLoading, error },
// ] = useSendMagicLinkMutation();
// const navigateTo = useNavigate();
// const dispatch = useDispatch();
const [username, setUsername] = useState("");
const [sendRegMagicLink, { isLoading, data, isSuccess }] = useSendRegMagicLinkMutation();
const navigateTo = useNavigate();
const [magicToken, setMagicToken] = useState("");
const [input, setInput] = useState({
email: "",
password: "",
confirmPassword: ""
});
useEffect(() => {
const query = new URLSearchParams(location.search);
// const githubCode = query.get("gcode");
const token = query.get("magic_token");
if (token) {
setMagicToken(token);
}
}, []);
useEffect(() => {
if (isSuccess && data) {
const { new_magic_token, mail_is_sent } = data;
if (!mail_is_sent && new_magic_token) {
// 直接进入set_name流程
navigateTo(`?magic_token=${new_magic_token}#/register/set_name`);
}
}
}, [isSuccess, data]);
const handleLogin = (evt) => {
const handleReg = (evt) => {
evt.preventDefault();
const { email, password } = input;
sendRegMagicLink({
magic_token: magicToken,
email,
password
});
// sendMagicLink(email);
};
const handleInput = (evt) => {
const { type } = evt.target.dataset;
const { value } = evt.target;
console.log(value);
setUsername(value);
// console.log(type, value);
setInput((prev) => {
prev[type] = value;
return { ...prev };
});
};
const { email, password } = input;
if (data?.mail_is_sent) return <EmailNextTip />;
return (
<form onSubmit={handleLogin}>
<form onSubmit={handleReg}>
<Input
className="large"
name="username"
value={username}
name="email"
value={email}
required
placeholder="Enter your Name22"
placeholder="Enter your email"
data-type="email"
onChange={handleInput}
/>
<Button type="submit">{isLoading ? "Sending" : `Register`}</Button>
<Input
className="large"
type="password"
value={password}
name="password"
required
data-type="password"
onChange={handleInput}
placeholder="Enter your password"
/>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Signing Up" : `Sign Up`}
</Button>
</form>
);
}
+34
View File
@@ -0,0 +1,34 @@
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
const StyledSignInLink = styled.p`
text-align: center;
margin: 24px 0 8px;
> span {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #667085;
margin-right: 4px;
}
> a {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #22d3ee;
cursor: pointer;
}
`;
export default function SignInLink() {
const navigate = useNavigate();
const handleSignIn = () => {
navigate("/login");
};
return (
<StyledSignInLink>
<span>Have an account?</span>
<a onClick={handleSignIn}>Sign In</a>
</StyledSignInLink>
);
}
+38 -1
View File
@@ -1,12 +1,49 @@
/* eslint-disable no-undef */
import { Outlet } from "react-router-dom";
import { useMatch } from "react-router-dom";
import BASE_URL from "../../app/config";
import SignInLink from "./SignInLink";
import { useGetLoginConfigQuery } from "../../app/services/server";
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
import GithubLoginButton from "../../common/component/GithubLoginButton";
import StyledWrapper from "./styled";
export default function Reg() {
const isRegHome = useMatch(`/register`);
const { clientId } = useGoogleAuthConfig();
const { config: githubAuthConfig } = useGithubAuthConfig();
const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery();
if (!loginConfigSuccess) return null;
const {
github: enableGithubLogin,
google: enableGoogleLogin,
who_can_sign_up: whoCanSignUp
} = loginConfig;
const googleLogin = enableGoogleLogin && clientId;
// 没有开放注册
if (whoCanSignUp !== "EveryOne") return `Open Register is Closed!`;
return (
<StyledWrapper>
<div className="form">
{isRegHome && (
<>
<div className="tips">
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
<h2 className="title">Sign Up to Rustchat</h2>
<span className="desc">Please enter your details.</span>
</div>
</>
)}
<Outlet />
{isRegHome && (
<>
<hr className="or" />
{googleLogin && <GoogleLoginButton clientId={clientId} />}
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
<SignInLink />
</>
)}
</div>
</StyledWrapper>
);
+19
View File
@@ -45,6 +45,25 @@ const StyledWrapper = styled.div`
width: 360px;
}
}
.or {
border: none;
position: relative;
height: 1px;
background-color: #e4e7ec;
margin: 26px 0;
&:after {
padding: 4px;
background-color: #fff;
content: "OR";
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
font-size: 14px;
line-height: 20px;
color: #667085;
}
}
`;
export default StyledWrapper;
+5 -5
View File
@@ -7,12 +7,12 @@ import BASE_URL from "../../app/config";
import StyledWrapper from "./styled";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
import { useSendMagicLinkMutation } from "../../app/services/auth";
import { useSendLoginMagicLinkMutation } from "../../app/services/auth";
import SentTip from "./SentTip";
export default function SendMagicLinkPage() {
const [sent, setSent] = useState(false);
const [sendMagicLink, { isSuccess, isLoading, error }] = useSendMagicLinkMutation();
const [sendMagicLink, { isSuccess, isLoading, error }] = useSendLoginMagicLinkMutation();
const navigateTo = useNavigate();
// const dispatch = useDispatch();
@@ -32,13 +32,13 @@ export default function SendMagicLinkPage() {
toast.error(error.data);
break;
case 401:
toast.error("username or password incorrect");
toast.error("Username or Password Incorrect");
break;
case 404:
toast.error("account not exsit");
toast.error("Account not exsit");
break;
default:
toast.error("something error");
toast.error("Something Error");
break;
}
return;