feat(frontend): log in with Discord
This commit is contained in:
		
							parent
							
								
									e4d028bbad
								
							
						
					
					
						commit
						4a8e1bb54f
					
				
					 8 changed files with 158 additions and 3 deletions
				
			
		
							
								
								
									
										10
									
								
								frontend/components/Loading.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/components/Loading.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ThreeDots } from "react-bootstrap-icons"; | ||||||
|  | 
 | ||||||
|  | export default function Loading() { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-col pt-32 items-center"> | ||||||
|  |       <ThreeDots size={64} className="animate-bounce" aria-hidden="true" /> | ||||||
|  |       <span className="font-bold text-xl">Loading...</span> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -56,7 +56,9 @@ export default function Navigation() { | ||||||
| 
 | 
 | ||||||
|   const nav = user ? ( |   const nav = user ? ( | ||||||
|     <> |     <> | ||||||
|       <NavItem href={`/u/${user.username}`}>@{user.username}</NavItem> |       <NavItem href={`/u/${user.username}`}> | ||||||
|  |         <a>@{user.username}</a> | ||||||
|  |       </NavItem> | ||||||
|       <NavItem href="/settings">Settings</NavItem> |       <NavItem href="/settings">Settings</NavItem> | ||||||
|       <NavItem href="/logout">Log out</NavItem> |       <NavItem href="/logout">Log out</NavItem> | ||||||
|     </> |     </> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import type { APIError } from "./types"; | import type { APIError } from "./types"; | ||||||
| 
 | 
 | ||||||
| const apiBase = process.env.API_BASE ?? "http://localhost:8080"; | const apiBase = process.env.API_BASE ?? "/api"; | ||||||
| 
 | 
 | ||||||
| export default async function fetchAPI<T>( | export default async function fetchAPI<T>( | ||||||
|   path: string, |   path: string, | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ | ||||||
|     "react-dom": "18.2.0", |     "react-dom": "18.2.0", | ||||||
|     "react-markdown": "^8.0.3", |     "react-markdown": "^8.0.3", | ||||||
|     "react-sortablejs": "^6.1.4", |     "react-sortablejs": "^6.1.4", | ||||||
|  |     "react-toast": "^1.0.3", | ||||||
|     "recoil": "^0.7.5", |     "recoil": "^0.7.5", | ||||||
|     "sortablejs": "^1.15.0" |     "sortablejs": "^1.15.0" | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
							
								
								
									
										74
									
								
								frontend/pages/login/discord.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/pages/login/discord.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | ||||||
|  | import { useEffect } from "react"; | ||||||
|  | import { useRouter } from "next/router"; | ||||||
|  | import { GetServerSideProps } from "next"; | ||||||
|  | import { useRecoilState } from "recoil"; | ||||||
|  | import fetchAPI from "../../lib/fetch"; | ||||||
|  | import { userState } from "../../lib/state"; | ||||||
|  | import { MeUser } from "../../lib/types"; | ||||||
|  | 
 | ||||||
|  | interface CallbackResponse { | ||||||
|  |   has_account: boolean; | ||||||
|  |   token?: string; | ||||||
|  |   user?: MeUser; | ||||||
|  | 
 | ||||||
|  |   discord?: string; | ||||||
|  |   ticket?: string; | ||||||
|  |   require_invite?: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface State { | ||||||
|  |   hasAccount: boolean; | ||||||
|  |   isLoading: boolean; | ||||||
|  |   token?: string; | ||||||
|  |   user?: MeUser; | ||||||
|  |   discord?: string; | ||||||
|  |   ticket?: string; | ||||||
|  |   error?: any; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function Discord(props: State) { | ||||||
|  |   const router = useRouter(); | ||||||
|  | 
 | ||||||
|  |   const [user, setUser] = useRecoilState(userState); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     // we got a token + user, save it and return to the home page
 | ||||||
|  |     if (props.token) { | ||||||
|  |       localStorage.setItem("pronouns-token", props.token); | ||||||
|  |       setUser(props.user!); | ||||||
|  | 
 | ||||||
|  |       router.push("/"); | ||||||
|  |     } | ||||||
|  |   }, [props.token, props.user, setUser, router]); | ||||||
|  | 
 | ||||||
|  |   return <>wow such login</>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const getServerSideProps: GetServerSideProps<State> = async ( | ||||||
|  |   context | ||||||
|  | ) => { | ||||||
|  |   try { | ||||||
|  |     const resp = await fetchAPI<CallbackResponse>( | ||||||
|  |       "/auth/discord/callback", | ||||||
|  |       "POST", | ||||||
|  |       { | ||||||
|  |         callback_domain: process.env.DOMAIN, | ||||||
|  |         code: context.query.code, | ||||||
|  |         state: context.query.state, | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       props: { | ||||||
|  |         hasAccount: resp.has_account, | ||||||
|  |         isLoading: false, | ||||||
|  |         token: resp.token, | ||||||
|  |         user: resp.user, | ||||||
|  |         discord: resp.discord || null, | ||||||
|  |         ticket: resp.ticket || null, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } catch (e) { | ||||||
|  |     return { props: { error: e } }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										41
									
								
								frontend/pages/login/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/pages/login/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | import { GetServerSideProps } from "next"; | ||||||
|  | import { useRouter } from "next/router"; | ||||||
|  | import { useRecoilValue } from "recoil"; | ||||||
|  | import Head from "next/head"; | ||||||
|  | import fetchAPI from "../../lib/fetch"; | ||||||
|  | import { userState } from "../../lib/state"; | ||||||
|  | 
 | ||||||
|  | interface URLsResponse { | ||||||
|  |   discord: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function Login({ urls }: { urls: URLsResponse }) { | ||||||
|  |   const router = useRouter(); | ||||||
|  | 
 | ||||||
|  |   if (useRecoilValue(userState) !== null) { | ||||||
|  |     router.push("/"); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Head> | ||||||
|  |         <title key="title">Login - pronouns.cc</title> | ||||||
|  |       </Head> | ||||||
|  |       <a href={urls.discord}>Login with Discord</a> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const getServerSideProps: GetServerSideProps = async (context) => { | ||||||
|  |   try { | ||||||
|  |     const urls = await fetchAPI<URLsResponse>("/auth/urls", "POST", { | ||||||
|  |       callback_domain: process.env.DOMAIN, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return { props: { urls } }; | ||||||
|  |   } catch (e) { | ||||||
|  |     console.log(e); | ||||||
|  | 
 | ||||||
|  |     return { notFound: true }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | @ -6,23 +6,45 @@ import FieldCard from "../../../components/FieldCard"; | ||||||
| import Card from "../../../components/Card"; | import Card from "../../../components/Card"; | ||||||
| import ReactMarkdown from "react-markdown"; | import ReactMarkdown from "react-markdown"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
|  | import { userState } from "../../../lib/state"; | ||||||
|  | import { useRecoilValue } from "recoil"; | ||||||
|  | import Link from "next/link"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   user: User; |   user: User; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default function Index({ user }: Props) { | export default function Index({ user }: Props) { | ||||||
|  |   const isMeUser = useRecoilValue(userState)?.id === user.id; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Head> |       <Head> | ||||||
|         <title key="title">@{user.username} - pronouns.cc</title> |         <title key="title">@{user.username} - pronouns.cc</title> | ||||||
|       </Head> |       </Head> | ||||||
|  |       {isMeUser && ( | ||||||
|  |         <div className="lg:w-1/3 mx-auto bg-slate-100 dark:bg-slate-700 shadow rounded-md p-2"> | ||||||
|  |           <span> | ||||||
|  |             You are currently viewing your{" "} | ||||||
|  |             <span className="font-bold">public</span> profile. | ||||||
|  |           </span> | ||||||
|  |           <br /> | ||||||
|  |           <Link | ||||||
|  |             href="/edit/profile" | ||||||
|  |             className="hover:underline text-sky-500 dark:text-sky-400" | ||||||
|  |           > | ||||||
|  |             Edit your profile | ||||||
|  |           </Link> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|       <div className="container mx-auto"> |       <div className="container mx-auto"> | ||||||
|         <div className="flex flex-col m-2 p-2 lg:flex-row justify-center lg:justify-start items-center space-y-4 lg:space-y-0 lg:space-x-16 lg:items-start border-b border-slate-200 dark:border-slate-700"> |         <div className="flex flex-col m-2 p-2 lg:flex-row justify-center lg:justify-start items-center space-y-4 lg:space-y-0 lg:space-x-16 lg:items-start border-b border-slate-200 dark:border-slate-700"> | ||||||
|           {user.avatar_url && ( |           {user.avatar_url && ( | ||||||
|             <Image |             <img | ||||||
|               className="max-w-xs rounded-full" |               className="max-w-xs rounded-full" | ||||||
|               src={user.avatar_url} |               src={user.avatar_url} | ||||||
|  |               //width="20rem"
 | ||||||
|  |               //height="20rem"
 | ||||||
|               alt={`@${user.username}'s avatar`} |               alt={`@${user.username}'s avatar`} | ||||||
|             /> |             /> | ||||||
|           )} |           )} | ||||||
|  |  | ||||||
|  | @ -2097,6 +2097,11 @@ react-sortablejs@^6.1.4: | ||||||
|     classnames "2.3.1" |     classnames "2.3.1" | ||||||
|     tiny-invariant "1.2.0" |     tiny-invariant "1.2.0" | ||||||
| 
 | 
 | ||||||
|  | react-toast@^1.0.3: | ||||||
|  |   version "1.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-toast/-/react-toast-1.0.3.tgz#cbe2cd946c5762736642dd2981a7e5d666c5448e" | ||||||
|  |   integrity sha512-gL3+O5hlLaoBmd36oXWKrjFeUyLCMQ04AIh48LrnUvdeg2vhJQ0E803TgVemgJvYUXKlutMVn9+/QS2DDnk26Q== | ||||||
|  | 
 | ||||||
| react@18.2.0: | react@18.2.0: | ||||||
|   version "18.2.0" |   version "18.2.0" | ||||||
|   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" |   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue