feat: github login

This commit is contained in:
Tristan Yang
2022-10-30 22:12:25 +08:00
parent dec387ed5b
commit 4950a0663c
11 changed files with 149 additions and 75 deletions
+27 -42
View File
@@ -1,12 +1,7 @@
import { useEffect, FC } from "react";
import { FC, useEffect } from "react";
import IconGithub from "../../assets/icons/github.svg";
import styled from "styled-components";
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
import Button from "./styled/Button";
import { useLoginMutation } from "../../app/services/auth";
import toast from "react-hot-toast";
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
const StyledSocialButton = styled(Button)`
width: 100%;
@@ -25,52 +20,42 @@ const StyledSocialButton = styled(Button)`
`;
type Props = {
source?: "widget" | "webapp",
client_id?: string;
type?: "login" | "register";
};
const GithubLoginButton: FC<Props> = ({ type = "login", client_id }) => {
//拿本地存的magic token
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
const [login, { isLoading, isSuccess, error }] = useLoginMutation();
const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", client_id }) => {
useEffect(() => {
const query = new URLSearchParams(location.search);
const isGithub = query.get("oauth") === "github";
const code = query.get("code");
if (isGithub && code) {
login({
magic_token,
code: code,
type: "github"
});
}
}, []);
useEffect(() => {
if (isSuccess) {
toast.success("Login Successfully");
}
}, [isSuccess]);
useEffect(() => {
if (error) {
// todo: why?
switch ((error as FetchBaseQueryError).status) {
case 410:
toast.error(
"No associated account found, please user admin for an invitation link to join."
);
break;
default:
toast.error("Something Error");
break;
const handleGithubLoginStatusChange = (evt: StorageEvent) => {
const { key, newValue } = evt;
if (key == 'widget' && !!newValue) {
console.log("github logged in");
localStorage.removeItem("widget");
const parentWindow = window.parent;
if (parentWindow) {
parentWindow.postMessage("RELOAD_WITH_OPEN", '*');
}
}
return;
};
if (source == "widget") {
window.addEventListener("storage", handleGithubLoginStatusChange);
}
}, [error]);
return () => {
if (source == "widget") {
window.removeEventListener("storage", handleGithubLoginStatusChange);
}
};
}, [source]);
const handleGithubLogin = () => {
location.href = `https://github.com/login/oauth/authorize?client_id=${client_id}`;
window.open(`https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${location.origin}/#/cb/github/${source}`);
// location.href = `https://github.com/login/oauth/authorize?client_id=${client_id}`;
};
return (
<StyledSocialButton onClick={handleGithubLogin} disabled={isLoading}>
<StyledSocialButton onClick={handleGithubLogin}>
<IconGithub className="icon" />
{` ${type === "login" ? "Sign in" : "Sign up"} with Github`}
</StyledSocialButton>
+69
View File
@@ -0,0 +1,69 @@
import { useEffect, FC } from 'react';
import styled from 'styled-components';
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
import { useLoginMutation } from "../../app/services/auth";
import toast from "react-hot-toast";
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
const Styled = styled.section`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
.success{
font-size: 30px;
font-weight: bold;
color: green;
}
`;
export type GithubLoginSource = "widget" | "webapp"
type Props = {
code: string, from?: GithubLoginSource
}
const GithubCallback: FC<Props> = ({ code, from = "webapp" }) => {
//拿本地存的magic token
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
const [login, { isLoading, isSuccess, error }] = useLoginMutation();
useEffect(() => {
if (code) {
login({
magic_token,
code,
type: "github"
});
}
}, [code]);
useEffect(() => {
if (isSuccess) {
toast.success("Login Successfully");
// 通知widget
if (from == 'widget') {
localStorage.setItem("widget", `${new Date().getTime()}`);
}
}
}, [isSuccess, from]);
useEffect(() => {
if (error) {
// todo: why?
switch ((error as FetchBaseQueryError).status) {
case 410:
toast.error(
"No associated account found, please user admin for an invitation link to join."
);
break;
default:
toast.error("Something Error");
break;
}
return;
}
}, [error]);
if (error) return <span>Something Error</span>;
return <Styled>
{isSuccess && from == 'widget' && <h1>Please close this window and return widget window</h1>}
<span className='success'>{isLoading ? "Github Logging in..." : "Github Login Success!"}</span>
</Styled>;
};
export default GithubCallback;
+11 -1
View File
@@ -1,10 +1,11 @@
import { useParams } from "react-router-dom";
import PaymentSuccess from "./PaymentSuccess";
import GithubCallback, { GithubLoginSource } from "./GithubCallback";
import StyledWrapper from "./styled";
// type Props = {
// type: "payment_success";
// };
// 该页面服务于一些第三方服务的回调,比如stripe付款成功的回调
// 该页面服务于一些第三方服务的回调,比如stripe付款成功的回调GitHub登录成功的回调
export default function CallbackPage() {
const { type = "", payload = "" } = useParams();
if (type == "payment_success") {
@@ -14,5 +15,14 @@ export default function CallbackPage() {
</StyledWrapper>
);
}
if (type == "github") {
const query = new URLSearchParams(location.search);
const code = query.get("code") ?? "";
return (
<StyledWrapper>
<GithubCallback code={code} from={payload as GithubLoginSource} />
</StyledWrapper>
);
}
return <StyledWrapper>callback page</StyledWrapper>;
}
+5 -5
View File
@@ -7,15 +7,15 @@ type Props = {
};
const Index: FC<Props> = ({ handleClose }) => {
const { name, logo } = useAppSelector(store => store.server);
const { name, description, logo } = useAppSelector(store => store.server);
return (
<div className="flex gap-4 justify-between items-center p-2">
<div className="relative w-10 h-10">
<div className="flex gap-2 justify-between items-center p-2">
<div className="relative w-12 h-12">
<img src={logo} alt="logo" className="w-full h-full" />
</div>
<div className="flex flex-col flex-1">
<span className="text-gray-900 text-lg">{name}</span>
{/* <span className="text-gray-400 text-sm">{online ? 'Active now' : 'Offline now'}</span> */}
<span className="text-gray-900 text-lg font-bold">{name}</span>
{description && <span className="text-gray-400 text-sm">{description}</span>}
</div>
<IconClose className="w-5 h-5 mr-2 cursor-pointer" onClick={handleClose} />
</div>
+2 -2
View File
@@ -20,9 +20,9 @@ const Login = () => {
} = loginConfig;
const googleLogin = enableGoogleLogin && clientId;
return (
<div className="w-60 flex flex-col">
<div className="w-60 flex flex-col py-2">
{googleLogin && <GoogleLoginButton clientId={clientId} />}
{enableGithubLogin && <GithubLoginButton client_id={githubAuthConfig?.client_id} />}
{enableGithubLogin && <GithubLoginButton client_id={githubAuthConfig?.client_id} source="widget" />}
</div>
);
};
+10 -10
View File
@@ -28,18 +28,18 @@ export type MessageProps = {
const Index: FC<MessageProps> = (props) => {
const { server: { name, logo }, loginUser } = useAppSelector(store => { return { server: store.server, loginUser: store.authData.user }; });
const { hostId, from_uid, content, created_at, compact, isFirst = false } = props;
const { hostId, from_uid, content, created_at = 0, compact, isFirst = false } = props;
const isHost = from_uid == hostId;
return (
<div
className={clsx(
'flex flex-col relative pl-14 py-1 hover:bg-gray-100 rounded-sm',
compact ? 'mt-0' : 'mt-4',
'flex flex-col relative pl-14 py-2 hover:bg-gray-100 rounded-sm',
compact ? 'py-1' : 'mt-4',
isFirst ? 'mt-0' : ''
)}
>
{!compact && (
<div className="absolute left-0 top-2 w-12 h-12 rounded-full overflow-hidden">
<div className="absolute left-1 top-2 w-12 h-12 rounded-full overflow-hidden">
{isHost ? (
<img src={logo} alt="logo" className="w-full h-full" />
) : (
@@ -48,19 +48,19 @@ const Index: FC<MessageProps> = (props) => {
</div>
)}
{!compact && (
<span className="flex items-center gap-2 leading-6">
<em className="text-black not-italic font-bold text-md">{isHost ? name : loginUser?.name}</em>
<Time time={created_at ?? 0} />
<span className="flex items-center gap-2 text-md">
<em className="text-black not-italic font-bold">{isHost ? name : loginUser?.name}</em>
<Time time={created_at} />
</span>
)}
<p
className={clsx(
'text-gray-600 text-md whitespace-normal break-words mt-2 w-[80%]',
compact ? 'group relative mt-0' : ''
'text-gray-600 text-md whitespace-normal break-words w-[80%]',
compact ? 'group relative ' : 'pt-1'
)}
>
{compact && (
<time className="absolute -left-2 top-0 -translate-x-full text-gray-300 text-xs invisible group-hover:visible">{`${new Date(created_at)
<time className="absolute -left-2 top-1/2 -translate-x-full -translate-y-1/2 text-gray-300 text-xs invisible group-hover:visible">{`${new Date(created_at)
.toLocaleTimeString('en-US', {
second: 'numeric',
minute: 'numeric',
+4 -4
View File
@@ -4,7 +4,7 @@ import { useAppSelector } from '../../app/store';
import Login from './Login';
const TextMessage = ({ text, animate = '' }: { text: string; animate?: string }) => (
<p className={`text-gray-600 text-md mb-3 ${animate}`}>{text}</p>
<p className={`text-gray-600 text-md mb-1 ${animate}`}>{text}</p>
);
type Props = {
needLogin?: boolean
@@ -12,20 +12,20 @@ type Props = {
const Index = ({ needLogin = false }: Props) => {
const { name, logo } = useAppSelector(store => store.server);
return (
<div className="flex gap-2">
<div className="flex gap-1 pl-1">
<div className="w-12 h-12">
<img src={logo} alt="logo" className="w-full h-full" />
</div>
<div className="flex flex-col">
<span className="flex items-center gap-2 leading-6">
<em className="text-black not-italic font-bold text-lg">{name}</em>
<em className="text-black not-italic text-lg font-bold">{name}</em>
<time className="text-gray-300 text-sm">{`${new Date().toLocaleTimeString('en-US', {
minute: 'numeric',
hour: 'numeric',
hour12: true,
})}`}</time>
</span>
<TextMessage text="👋 Hi there, Nice to meet you!" animate="animate-[fadeInUp_.5s_ease-in-out_both]" />
<TextMessage text="Hi there, Nice to meet you! 👋👋👋" animate={needLogin ? "animate-[fadeInUp_.5s_ease-in-out_both]" : ''} />
{needLogin && <>
<TextMessage text="You need login before we have a nice talk 👇" animate="animate-[fadeInUp_.5s_.8s_ease-in-out_both]" />
<div className="animate-[fadeInUp_.5s_1.2s_ease-in-out_both]">
+4 -3
View File
@@ -1,4 +1,4 @@
import { useState, useRef, ChangeEvent } from 'react';
import { useState, ChangeEvent } from 'react';
import Header from './Header';
import Welcome from './Welcome';
import MessageFeed from './MessageFeed';
@@ -33,7 +33,7 @@ const Index = ({ handleClose, hostId }: Props) => {
// no token or guest login
const notLogin = !token || isGuest;
return (
<aside className="flex flex-col justify-between bg-white w-full h-full rounded-md overflow-hidden">
<aside className="flex flex-col justify-between bg-white w-[600px] h-[800px] rounded-md overflow-hidden">
<Header handleClose={handleClose} />
<Line />
{/* message list */}
@@ -45,10 +45,11 @@ const Index = ({ handleClose, hostId }: Props) => {
{/* message input */}
<div className="w-full px-2 py-3">
<textarea
disabled={notLogin}
value={input}
onChange={handleInput}
className="w-full h-full text-sm p-2 rounded-lg bg-gray-200 resize-none text-black outline-none"
placeholder="Write a message..."
placeholder={notLogin ? "Login first..." : "Write a message..."}
rows={3}
/>
</div>
+5 -7
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { useGetServerQuery } from "../app/services/server";
import Icon from "./Icon";
@@ -10,18 +10,16 @@ type Props = {
function Widget({ hostId }: Props) {
const { rehydrated } = usePreload();
const [visible, setVisible] = useState(false);
const [visible, setVisible] = useState(!!new URLSearchParams(location.search).get("open"));
const { isLoading, isError } = useGetServerQuery();
const toggleVisible = () => {
setVisible((prev) => !prev);
};
useEffect(() => {
// 有无iframe内嵌
const parentWindow = window.parent;
if (parentWindow) {
parentWindow.postMessage(visible ? 'OPEN' : 'CLOSE', '*');
parentWindow.postMessage(visible ? 'CLOSE' : 'OPEN', '*');
}
}, [visible]);
setVisible((prev) => !prev);
};
if (isLoading || isError || !rehydrated) return null;
return visible ? <Popup handleClose={toggleVisible} hostId={hostId} /> : <Icon handleClick={toggleVisible} />;
}