feat: magic link login/reg

This commit is contained in:
zerosoul
2022-05-09 21:17:49 +08:00
parent f3b82295d6
commit 1b1cfb956e
16 changed files with 760 additions and 350 deletions
+16
View File
@@ -90,6 +90,13 @@ export const authApi = createApi({
body: { old_password, new_password },
}),
}),
sendMagicLink: builder.mutation({
query: (email) => ({
url: "token/send_magic_link",
method: "POST",
body: { email },
}),
}),
getMetamaskNonce: builder.query({
query: (address) => ({
url: `/token/metamask/nonce?public_address=${address}`,
@@ -102,11 +109,20 @@ export const authApi = createApi({
}),
logout: builder.query({
query: () => ({ url: `token/logout` }),
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
await queryFulfilled;
dispatch(resetAuthData());
} catch {
console.log("logout error");
}
},
}),
}),
});
export const {
useSendMagicLinkMutation,
useGetCredentialsQuery,
useUpdateDeviceTokenMutation,
useGetOpenidMutation,
+1
View File
@@ -6,6 +6,7 @@ import BASE_URL, { tokenHeader } from "../config";
const whiteList = [
"login",
"register",
"sendMagicLink",
"checkInviteTokenValid",
"getLoginConfig",
"getServer",
-8
View File
@@ -1,6 +1,4 @@
import { useEffect } from "react";
import { useDispatch, batch } from "react-redux";
import { resetAuthData } from "../../app/slices/auth.data";
import { resetFootprint } from "../../app/slices/footprint";
import { resetChannels } from "../../app/slices/channels";
import { resetContacts } from "../../app/slices/contacts";
@@ -26,11 +24,5 @@ export default function useLogout() {
});
};
useEffect(() => {
if (isSuccess) {
dispatch(resetAuthData());
}
}, [isSuccess]);
return { clearLocalData, logout, exited: isSuccess, exiting: isLoading };
}
+5 -5
View File
@@ -12,7 +12,9 @@ import useStreaming from "../../common/hook/useStreaming";
// let request = null;
export default function usePreload() {
const { rehydrate, rehydrated } = useRehydrate();
const loginUid = useSelector((store) => store.authData.uid);
const { loginUid, token } = useSelector((store) => {
return { loginUid: store.authData.uid, token: store.authData.token };
});
const { setStreamingReady } = useStreaming();
const [
getFavorites,
@@ -56,11 +58,9 @@ export default function usePreload() {
getFavorites();
}
}, [rehydrated]);
const canStreaming = loginUid && rehydrated;
const canStreaming = loginUid && rehydrated && !!token;
useEffect(() => {
if (canStreaming) {
setStreamingReady(true);
}
setStreamingReady(canStreaming);
}, [canStreaming]);
return {
+128 -91
View File
@@ -1,100 +1,137 @@
import { useEffect } from 'react';
import { Route, Routes, HashRouter } from 'react-router-dom';
import { Provider, useSelector } from 'react-redux';
import { useEffect } from "react";
import { Route, Routes, HashRouter } from "react-router-dom";
import { Provider, useSelector } from "react-redux";
// import Welcome from './Welcome'
import NotFoundPage from './404';
import OAuthPage from './oauth';
import LoginPage from './login';
import HomePage from './home';
import ChatPage from './chat';
import FavoritesPage from './favs';
import ContactsPage from './contacts';
import RequireAuth from '../common/component/RequireAuth';
import RequireNoAuth from '../common/component/RequireNoAuth';
import Meta from '../common/component/Meta';
import NotFoundPage from "./404";
import OAuthPage from "./oauth";
import LoginPage from "./login";
import SendMagicLinkPage from "./sendMagicLink";
import RegBasePage from "./reg";
import RegPage from "./reg/Register";
import RegWithUsernamePage from "./reg/RegWithUsername";
import HomePage from "./home";
import ChatPage from "./chat";
import FavoritesPage from "./favs";
import ContactsPage from "./contacts";
import RequireAuth from "../common/component/RequireAuth";
import RequireNoAuth from "../common/component/RequireNoAuth";
import Meta from "../common/component/Meta";
import store from '../app/store';
import InvitePage from './invite';
import SettingPage from './setting';
import SettingChannelPage from './settingChannel';
import toast from 'react-hot-toast';
import ResourceManagement from './resources';
import store from "../app/store";
import InvitePage from "./invite";
import SettingPage from "./setting";
import SettingChannelPage from "./settingChannel";
import toast from "react-hot-toast";
import ResourceManagement from "./resources";
const PageRoutes = () => {
const {
ui: { online },
fileMessages
} = useSelector((store) => {
return { ui: store.ui, fileMessages: store.fileMessage };
});
// 掉线检测
useEffect(() => {
let toastId = 0;
if (!online) {
toast.error('Network Offline!', { duration: Infinity });
} else {
toast.dismiss(toastId);
}
}, [online]);
const {
ui: { online },
fileMessages,
} = useSelector((store) => {
return { ui: store.ui, fileMessages: store.fileMessage };
});
// 掉线检测
useEffect(() => {
let toastId = 0;
if (!online) {
toast.error("Network Offline!", { duration: Infinity });
} else {
toast.dismiss(toastId);
}
}, [online]);
return (
<HashRouter>
<Routes>
<Route path="/oauth/:token" element={<OAuthPage />} />
<Route
path="/login"
element={
<RequireNoAuth>
<LoginPage />
</RequireNoAuth>
}
/>
<Route path="/invite" element={<InvitePage />} />
<Route
path="/"
element={
<RequireAuth>
<HomePage />
</RequireAuth>
}
>
<Route path="setting">
<Route index element={<SettingPage />} />
<Route path="channel/:cid" element={<SettingChannelPage />} />
</Route>
<Route index element={<ChatPage />} />
<Route path="chat">
<Route index element={<ChatPage />} />
<Route path="channel/:channel_id" element={<ChatPage />} />
<Route path="dm/:user_id" element={<ChatPage />} />
</Route>
<Route path="contacts">
<Route index element={<ContactsPage />} />
<Route path=":user_id" element={<ContactsPage />} />
</Route>
<Route path="favs" element={<FavoritesPage />}></Route>
<Route path="files" element={<ResourceManagement fileMessages={fileMessages} />}></Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</HashRouter>
);
return (
<HashRouter>
<Routes>
<Route path="/oauth/:token" element={<OAuthPage />} />
<Route
path="/login"
element={
<RequireNoAuth>
<LoginPage />
</RequireNoAuth>
}
/>
<Route
path="/send_magic_link"
element={
<RequireNoAuth>
<SendMagicLinkPage />
</RequireNoAuth>
}
/>
<Route
path="/reg"
element={
<RequireNoAuth>
<RegBasePage />
</RequireNoAuth>
}
>
<Route index element={<RegPage />} />
<Route path="magiclink">
<Route index element={<RegWithUsernamePage />} />
<Route path=":token" element={<RegWithUsernamePage />} />
</Route>
</Route>
<Route
path="/email_login"
element={
<RequireNoAuth>
<SendMagicLinkPage />
</RequireNoAuth>
}
/>
<Route path="/invite" element={<InvitePage />} />
<Route
path="/"
element={
<RequireAuth>
<HomePage />
</RequireAuth>
}
>
<Route path="setting">
<Route index element={<SettingPage />} />
<Route path="channel/:cid" element={<SettingChannelPage />} />
</Route>
<Route index element={<ChatPage />} />
<Route path="chat">
<Route index element={<ChatPage />} />
<Route path="channel/:channel_id" element={<ChatPage />} />
<Route path="dm/:user_id" element={<ChatPage />} />
</Route>
<Route path="contacts">
<Route index element={<ContactsPage />} />
<Route path=":user_id" element={<ContactsPage />} />
</Route>
<Route path="favs" element={<FavoritesPage />}></Route>
<Route
path="files"
element={<ResourceManagement fileMessages={fileMessages} />}
></Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</HashRouter>
);
};
// const local_key = "AUTH_DATA";
export default function ReduxRoutes() {
// const [authData, setAuthData] = useState(
// JSON.parse(localStorage.getItem(local_key))
// );
// const updateAuthData = (data) => {
// localStorage.setItem(local_key, JSON.stringify(data));
// setAuthData(data);
// };
return (
<Provider store={store}>
{/* <PersistGate loading={null} persistor={persistStore(store)}> */}
<Meta />
<PageRoutes />
{/* </PersistGate> */}
</Provider>
);
// const [authData, setAuthData] = useState(
// JSON.parse(localStorage.getItem(local_key))
// );
// const updateAuthData = (data) => {
// localStorage.setItem(local_key, JSON.stringify(data));
// setAuthData(data);
// };
return (
<Provider store={store}>
{/* <PersistGate loading={null} persistor={persistStore(store)}> */}
<Meta />
<PageRoutes />
{/* </PersistGate> */}
</Provider>
);
}
+20
View File
@@ -0,0 +1,20 @@
// import { useState, useEffect } from "react";
import Button from "../../common/component/styled/Button";
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
const StyledMagicButton = styled(Button)`
width: 100%;
margin-bottom: 16px;
`;
export default function MagicLinkLogin() {
const navigate = useNavigate();
const handleGoogleLogin = () => {
navigate("/send_magic_link");
// signIn();
};
return (
<StyledMagicButton onClick={handleGoogleLogin} href="#">
Sign in with Magic Link
</StyledMagicButton>
);
}
+20
View File
@@ -11,6 +11,7 @@ import SolidLoginButton from "./SolidLoginButton";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
import GoogleLoginButton from "./GoogleLoginButton";
import MagicLinkLogin from "./MagicLinkLogin";
import { useLoginMutation } from "../../app/services/auth";
import { useGetLoginConfigQuery } from "../../app/services/server";
import { setAuthData } from "../../app/slices/auth.data";
@@ -31,6 +32,9 @@ export default function LoginPage() {
const query = new URLSearchParams(location.search);
const code = query.get("code");
const state = query.get("state");
const token = query.get("token");
const exists = query.get("exists");
// oidc login
if (code && state) {
login({
code,
@@ -38,6 +42,21 @@ export default function LoginPage() {
type: "oidc",
});
}
// magic link
if (token && typeof exists !== "undefined") {
console.log("tokken", token, exists);
const isLogin = exists == "true";
if (isLogin) {
// login
login({
token,
type: "magiclink",
});
} else {
// reg
location.href = `/#/reg/magiclink/${token}`;
}
}
}, []);
useEffect(() => {
@@ -134,6 +153,7 @@ export default function LoginPage() {
{(enableGoogleLogin || enableMetamaskLogin || oidc.length > 0) && (
<hr className="or" />
)}
<MagicLinkLogin />
{enableGoogleLogin && <GoogleLoginButton login={login} />}
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
{oidc.length > 0 && <SolidLoginButton issuers={oidc} />}
+80
View File
@@ -0,0 +1,80 @@
/* 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 } from "../../app/services/auth";
import toast from "react-hot-toast";
export default function RegWithUsername() {
const { token } = useParams();
const [login, { isLoading, error, isSuccess, data }] = useLoginMutation();
// const navigateTo = useNavigate();
const dispatch = useDispatch();
const [username, setUsername] = useState("");
useEffect(() => {
console.log("errr", error);
switch (error?.status) {
case 401:
toast.error("Invalided Token");
break;
default:
break;
}
}, [error]);
useEffect(() => {
if (isSuccess && data) {
// 更新本地认证信息
console.log("login data", data);
toast.success("login success");
dispatch(setAuthData(data));
location.href = `/#/`;
}
}, [isSuccess, data]);
const handleLogin = (evt) => {
evt.preventDefault();
login({
token,
username,
type: "magiclink",
});
// sendMagicLink(email);
};
const handleInput = (evt) => {
const { value } = evt.target;
setUsername(value);
};
if (!token) return "no token";
return (
<>
<div className="tips">
<h2 className="title">Whats your name</h2>
<span className="desc">
Enter a name or handle so people know how youd like to be called.
Your name will only be visible to others in spaces you joined.
</span>
</div>
<form onSubmit={handleLogin}>
<Input
className="large"
name="username"
value={username}
required
placeholder="Type a name"
onChange={handleInput}
/>
<Button type="submit" disabled={isLoading || !username || isSuccess}>
{isLoading ? "Logining" : `Continue`}
</Button>
</form>
</>
);
}
+42
View File
@@ -0,0 +1,42 @@
/* 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 Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
// import { useSendMagicLinkMutation } 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 handleLogin = (evt) => {
evt.preventDefault();
// sendMagicLink(email);
};
const handleInput = (evt) => {
const { value } = evt.target;
console.log(value);
setUsername(value);
};
return (
<form onSubmit={handleLogin}>
<Input
className="large"
name="username"
value={username}
required
placeholder="Enter your Name22"
onChange={handleInput}
/>
<Button type="submit">{isLoading ? "Sending" : `Register`}</Button>
</form>
);
}
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable no-undef */
import { Outlet } from "react-router-dom";
import StyledWrapper from "./styled";
export default function Reg() {
return (
<StyledWrapper>
<div className="form">
<Outlet />
</div>
</StyledWrapper>
);
}
+49
View File
@@ -0,0 +1,49 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
.form {
max-width: 440px;
padding: 36px 40px 32px 40px;
/* border: 1px solid #eee; */
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1),
0px 2px 4px -2px rgba(16, 24, 40, 0.06);
border-radius: 12px;
.tips {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 24px;
.logo {
width: 56px;
height: 56px;
margin-bottom: 28px;
border-radius: 50%;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 32px;
color: #101828;
margin-bottom: 8px;
}
.desc {
text-align: center;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #667085;
}
}
form {
display: flex;
flex-direction: column;
gap: 20px;
width: 360px;
}
}
`;
export default StyledWrapper;
+86
View File
@@ -0,0 +1,86 @@
/* 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 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";
export default function SendMagicLinkPage() {
const [
sendMagicLink,
{ isSuccess, isLoading, error },
] = useSendMagicLinkMutation();
// const navigateTo = useNavigate();
// const dispatch = useDispatch();
const [email, setEmail] = useState("");
useEffect(() => {
if (isSuccess) {
toast.success("Send Email Successfully!");
}
}, [isSuccess]);
useEffect(() => {
if (error) {
console.log(error);
switch (error.status) {
case "PARSING_ERROR":
toast.error(error.data);
break;
case 401:
toast.error("username or password incorrect");
break;
case 404:
toast.error("account not exsit");
break;
default:
toast.error("something error");
break;
}
return;
}
}, [error]);
const handleLogin = (evt) => {
evt.preventDefault();
sendMagicLink(email);
};
const handleInput = (evt) => {
const { value } = evt.target;
console.log(value);
setEmail(value);
};
return (
<StyledWrapper>
<div className="form">
<div className="tips">
<img
src={`${BASE_URL}/resource/organization/logo`}
alt="logo"
className="logo"
/>
<h2 className="title">Login to Rustchat</h2>
<span className="desc">Please enter your Email</span>
</div>
<form onSubmit={handleLogin}>
<Input
type="email"
className="large"
name="email"
value={email}
required
placeholder="Enter your email"
onChange={handleInput}
/>
<Button type="submit" disabled={isLoading || !email}>
{isLoading ? "Sending" : `Continue with Email`}
</Button>
</form>
</div>
</StyledWrapper>
);
}
+47
View File
@@ -0,0 +1,47 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
.form {
padding: 36px 40px 32px 40px;
/* border: 1px solid #eee; */
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1),
0px 2px 4px -2px rgba(16, 24, 40, 0.06);
border-radius: 12px;
.tips {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 24px;
.logo {
width: 56px;
height: 56px;
margin-bottom: 28px;
border-radius: 50%;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 32px;
color: #101828;
margin-bottom: 8px;
}
}
.desc {
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #667085;
}
form {
display: flex;
flex-direction: column;
gap: 20px;
width: 360px;
}
}
`;
export default StyledWrapper;
+7 -3
View File
@@ -1,7 +1,7 @@
// import React from "react";
import { useEffect, useState } from "react";
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
// import { useNavigate } from "react-router-dom";
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
@@ -27,8 +27,8 @@ const StyledConfirm = styled(StyledModal)`
}
`;
import Modal from "../../common/component/Modal";
import toast from "react-hot-toast";
export default function LogoutConfirmModal({ closeModal }) {
const navigate = useNavigate();
const [clearLocal, setClearLocal] = useState(false);
const { logout, exited, exiting, clearLocalData } = useLogout();
const handleLogout = () => {
@@ -43,7 +43,11 @@ export default function LogoutConfirmModal({ closeModal }) {
console.log("clear all store");
clearLocalData();
}
navigate("/");
toast.success("Logout Successfully");
setTimeout(() => {
location.href = `${location.origin}#/login`;
}, 500);
// location.reload();
}
}, [exited, clearLocal]);
return (