help me
This commit is contained in:
		
							parent
							
								
									52a0ba1b3b
								
							
						
					
					
						commit
						83bc981152
					
				
					 23 changed files with 9034 additions and 927 deletions
				
			
		
							
								
								
									
										124
									
								
								app/discord/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								app/discord/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,124 @@ | |||
| import axios from 'axios'; | ||||
| import { prisma } from "~/db.server"; | ||||
| import { createUserSession } from '~/session.server'; | ||||
| 
 | ||||
| export interface DiscordUser { | ||||
|     id: string; | ||||
|     username: string; | ||||
|     discriminator: string; | ||||
|     avatar: string; | ||||
|     bot?: boolean; | ||||
|     system?: boolean; | ||||
|     mfa_enabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface AccessTokenResponse { | ||||
|     access_token: string; | ||||
|     expires: number; | ||||
|     refresh_token: string; | ||||
| } | ||||
| 
 | ||||
| export function getAccessToken(code: string): Promise<AccessTokenResponse> { | ||||
|     return new Promise( | ||||
|         (resolve, reject) => { | ||||
|             const body = new FormData(); | ||||
|             body.append("grant_type", "authorization_code"); | ||||
|             body.append("code", code); | ||||
|             body.append("redirect_url", process.env.DISCORD_REDIRECT_URI || ""); | ||||
| 
 | ||||
|             axios.post("https://discord.com/api/oauth2/token", body) | ||||
|                 .then((res) => { | ||||
|                     const { access_token, refresh_token, expires_in } = res.data; | ||||
| 
 | ||||
|                     resolve({ | ||||
|                         access_token, | ||||
|                         refresh_token, | ||||
|                         expires: Date.now() + expires_in * 1000 | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(reject); | ||||
|         } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| export async function authorize(access_code: string): Promise<AccessTokenResponse | undefined> { | ||||
|     return new Promise( | ||||
|         (resolve, reject) => { | ||||
|             const formData = new FormData(); | ||||
| 
 | ||||
|             formData.append("client_id", process.env.DISCORD_CLIENT_ID || ""); | ||||
|             formData.append("client_secret", process.env.DISCORD_CLIENT_SECRET || ""); | ||||
|             formData.append("grant_type", "authorization_code"); | ||||
|             formData.append("redirect_uri", process.env.DISCORD_REDIRECT_URI || ""); | ||||
|             formData.append("scope", "identify"); | ||||
|             formData.append("code", access_code); | ||||
| 
 | ||||
|             axios.post("https://discord.com/api/oauth2/token", formData) | ||||
|                 .then((res) => { | ||||
|                     const { access_token, refresh_token, expires_in } = res.data; | ||||
| 
 | ||||
|                     resolve({ | ||||
|                         access_token, | ||||
|                         refresh_token, | ||||
|                         expires: Date.now() + expires_in * 1000 | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(reject); | ||||
|         } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| export function getDiscordUser(token: string): Promise<DiscordUser> { | ||||
|     return new Promise( | ||||
|         (resolve, reject) => { | ||||
|             axios.get("https://discord.com/api/users/@me", { | ||||
|                 headers: { | ||||
|                     Authorization: `Bearer ${token}` | ||||
|                 } | ||||
|             }) | ||||
|                 .then( | ||||
|                     (res) => { | ||||
|                         resolve(res.data); | ||||
|                     } | ||||
|                 ) | ||||
|                 .catch(reject); | ||||
|         } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| export async function discordLogin(request: Request, code: string): Promise<Response> { | ||||
|     return new Promise( | ||||
|         (resolve, reject) => { | ||||
|             getAccessToken(code) | ||||
|                 .then((token) => { | ||||
|                     getDiscordUser(token.access_token) | ||||
|                         .then(async (user) => { | ||||
|                             prisma.discordUser.upsert({ | ||||
|                                 create: { | ||||
|                                     discord_id: user.id, | ||||
|                                     access_token: token.access_token, | ||||
|                                     refresh_token: token.refresh_token, | ||||
|                                     expires: new Date(token.expires), | ||||
|                                     username: user.username, | ||||
|                                     discriminator: user.discriminator, | ||||
|                                 }, | ||||
|                                 update: { | ||||
|                                     discord_id: user.id, | ||||
|                                     access_token: token.access_token, | ||||
|                                     refresh_token: token.refresh_token, | ||||
|                                     expires: new Date(token.expires), | ||||
|                                     username: user.username, | ||||
|                                     discriminator: user.discriminator, | ||||
|                                 }, | ||||
|                                 where: { | ||||
|                                     discord_id: user.id | ||||
|                                 } | ||||
|                             }) | ||||
|                             .then(async () => { | ||||
|                                 resolve(await createUserSession({request, discord_id: user.id, redirectTo: '/'})); | ||||
|                             }) | ||||
|                         }) | ||||
|                 }) | ||||
|         } | ||||
|     ) | ||||
| } | ||||
|  | @ -1,53 +0,0 @@ | |||
| import type { User, Note } from "@prisma/client"; | ||||
| 
 | ||||
| import { prisma } from "~/db.server"; | ||||
| 
 | ||||
| export type { Note } from "@prisma/client"; | ||||
| 
 | ||||
| export function getNote({ | ||||
|   id, | ||||
|   userId, | ||||
| }: Pick<Note, "id"> & { | ||||
|   userId: User["id"]; | ||||
| }) { | ||||
|   return prisma.note.findFirst({ | ||||
|     where: { id, userId }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function getNoteListItems({ userId }: { userId: User["id"] }) { | ||||
|   return prisma.note.findMany({ | ||||
|     where: { userId }, | ||||
|     select: { id: true, title: true }, | ||||
|     orderBy: { updatedAt: "desc" }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function createNote({ | ||||
|   body, | ||||
|   title, | ||||
|   userId, | ||||
| }: Pick<Note, "body" | "title"> & { | ||||
|   userId: User["id"]; | ||||
| }) { | ||||
|   return prisma.note.create({ | ||||
|     data: { | ||||
|       title, | ||||
|       body, | ||||
|       user: { | ||||
|         connect: { | ||||
|           id: userId, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function deleteNote({ | ||||
|   id, | ||||
|   userId, | ||||
| }: Pick<Note, "id"> & { userId: User["id"] }) { | ||||
|   return prisma.note.deleteMany({ | ||||
|     where: { id, userId }, | ||||
|   }); | ||||
| } | ||||
|  | @ -1,59 +1,65 @@ | |||
| import type { Password, User } from "@prisma/client"; | ||||
| import bcrypt from "@node-rs/bcrypt"; | ||||
| 
 | ||||
| import { prisma } from "~/db.server"; | ||||
| import type {User} from "@prisma/client"; | ||||
| import { AccessTokenResponse, DiscordUser } from "~/discord/index"; | ||||
| export type {User, Border} from "@prisma/client"; | ||||
| 
 | ||||
| export type { User } from "@prisma/client"; | ||||
| export async function getUserByDiscordId(discord_id: User["discord_id"]) { | ||||
|   return prisma.user.findUnique({ where: { discord_id: discord_id || undefined }}); | ||||
| } | ||||
| 
 | ||||
| export async function getUserById(id: User["id"]) { | ||||
|   return prisma.user.findUnique({ where: { id } }); | ||||
| } | ||||
| 
 | ||||
| export async function getUserByEmail(email: User["email"]) { | ||||
|   return prisma.user.findUnique({ where: { email } }); | ||||
| } | ||||
| 
 | ||||
| export async function createUser(email: User["email"], password: string) { | ||||
|   const hashedPassword = await bcrypt.hash(password, 10); | ||||
| /// SHOULD ONLY BE USED WITH A CORRESPONDING OAUTH TOKEN
 | ||||
| export async function createUser(discord_id: User["discord_id"]) { | ||||
| 
 | ||||
|   return prisma.user.create({ | ||||
|     data: { | ||||
|       email, | ||||
|       password: { | ||||
|         create: { | ||||
|           hash: hashedPassword, | ||||
|         }, | ||||
|       }, | ||||
|       discord_id | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteUserByEmail(email: User["email"]) { | ||||
|   return prisma.user.delete({ where: { email } }); | ||||
| export async function createDiscordLogin(discord_id: DiscordUser["id"], token_response: AccessTokenResponse) { | ||||
|   return prisma.discordUser.create({ | ||||
|     data: { | ||||
|       discord_id, | ||||
|       access_token: token_response.access_token, | ||||
|       refresh_token: token_response.refresh_token, | ||||
|       expires: new Date(token_response.expires) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export async function verifyLogin( | ||||
|   email: User["email"], | ||||
|   password: Password["hash"] | ||||
| ) { | ||||
|   const userWithPassword = await prisma.user.findUnique({ | ||||
|     where: { email }, | ||||
|     include: { | ||||
|       password: true, | ||||
|     }, | ||||
|   }); | ||||
| export type SessionInformation = { | ||||
|   bearer_token: string, | ||||
|   refresh_token: string | ||||
| }; | ||||
| 
 | ||||
|   if (!userWithPassword || !userWithPassword.password) { | ||||
|     return null; | ||||
| export async function discordIdentify(bearer_token: string, refresh_token: string): Promise<DiscordUser | SessionInformation | undefined> { | ||||
|   let user_info = await ( | ||||
|     await fetch("https://discord.com/api/users/@me", { | ||||
|      headers: { | ||||
|        Authorization: `Bearer ${bearer_token}` | ||||
|       } | ||||
|     }) | ||||
|   ).json(); | ||||
| 
 | ||||
|   if (!user_info["id"]) { | ||||
|     const form_data = new FormData(); | ||||
|     form_data.append("client_id", process.env.DISCORD_CLIENT_ID || ""); | ||||
|     form_data.append("client_secret", process.env.DISCORD_CLIENT_SECRET || ""); | ||||
|     form_data.append("grant_type", "refresh_token"); | ||||
|     form_data.append("refresh_token", refresh_token); | ||||
|     let refresh_info = await (await fetch("https://discord.com/api/oauth2/token", { | ||||
|       method: "POST", | ||||
|       body: form_data | ||||
|     })).json(); | ||||
| 
 | ||||
|     return refresh_info; | ||||
|   } | ||||
| 
 | ||||
|   const isValid = await bcrypt.verify(password, userWithPassword.password.hash); | ||||
| 
 | ||||
|   if (!isValid) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   const { password: _password, ...userWithoutPassword } = userWithPassword; | ||||
| 
 | ||||
|   return userWithoutPassword; | ||||
| } | ||||
|   return user_info; | ||||
| } | ||||
							
								
								
									
										14
									
								
								app/root.tsx
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								app/root.tsx
									
										
									
									
									
								
							|  | @ -6,11 +6,11 @@ import { | |||
|   Outlet, | ||||
|   Scripts, | ||||
|   ScrollRestoration, | ||||
|   useLoaderData, | ||||
| } from "remix"; | ||||
| import type { LinksFunction, MetaFunction, LoaderFunction } from "remix"; | ||||
| 
 | ||||
| import tailwindStylesheetUrl from "./styles/tailwind.css"; | ||||
| import { getUser } from "./session.server"; | ||||
| 
 | ||||
| export const links: LinksFunction = () => { | ||||
|   return [{ rel: "stylesheet", href: tailwindStylesheetUrl }]; | ||||
|  | @ -18,20 +18,10 @@ export const links: LinksFunction = () => { | |||
| 
 | ||||
| export const meta: MetaFunction = () => ({ | ||||
|   charset: "utf-8", | ||||
|   title: "Remix Notes", | ||||
|   title: "Border Selector", | ||||
|   viewport: "width=device-width,initial-scale=1", | ||||
| }); | ||||
| 
 | ||||
| type LoaderData = { | ||||
|   user: Awaited<ReturnType<typeof getUser>>; | ||||
| }; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request }) => { | ||||
|   return json<LoaderData>({ | ||||
|     user: await getUser(request), | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export default function App() { | ||||
|   return ( | ||||
|     <html lang="en" className="h-full"> | ||||
|  |  | |||
|  | @ -1,137 +1,23 @@ | |||
| import { Link } from "remix"; | ||||
| import { json, Link, LoaderFunction, useLoaderData } from "remix"; | ||||
| import { getUser, getSession } from "~/session.server"; | ||||
| import { useOptionalUser } from "~/utils"; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request }) => { | ||||
|   const user_info = await getUser(request); | ||||
|   return json(user_info); | ||||
| } | ||||
| 
 | ||||
| export default function Index() { | ||||
|   const user = useOptionalUser(); | ||||
|   const discordUser = useLoaderData(); | ||||
|   console.log("discordUser is", discordUser); | ||||
|   return ( | ||||
|     <main className="relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center"> | ||||
|       <div className="relative sm:pb-16 sm:pt-8"> | ||||
|         <div className="mx-auto max-w-7xl sm:px-6 lg:px-8"> | ||||
|           <div className="relative shadow-xl sm:overflow-hidden sm:rounded-2xl"> | ||||
|             <div className="absolute inset-0"> | ||||
|               <img | ||||
|                 className="h-full w-full object-cover" | ||||
|                 src="https://user-images.githubusercontent.com/1500684/158276320-c46b661b-8eff-4a4d-82c6-cf296c987a12.jpg" | ||||
|                 alt="BB King playing blues on his Les Paul guitar" | ||||
|               /> | ||||
|               <div className="absolute inset-0 bg-[color:rgba(27,167,254,0.5)] mix-blend-multiply" /> | ||||
|             </div> | ||||
|             <div className="lg:pb-18 relative px-4 pt-16 pb-8 sm:px-6 sm:pt-24 sm:pb-14 lg:px-8 lg:pt-32"> | ||||
|               <h1 className="text-center text-6xl font-extrabold tracking-tight sm:text-8xl lg:text-9xl"> | ||||
|                 <span className="block uppercase text-blue-500 drop-shadow-md"> | ||||
|                   Blues Stack | ||||
|                 </span> | ||||
|               </h1> | ||||
|               <p className="mx-auto mt-6 max-w-lg text-center text-xl text-white sm:max-w-3xl"> | ||||
|                 Check the README.md file for instructions on how to get this | ||||
|                 project deployed. | ||||
|               </p> | ||||
|               <div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center"> | ||||
|                 {user ? ( | ||||
|                   <Link | ||||
|                     to="/notes" | ||||
|                     className="flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-blue-700 shadow-sm hover:bg-blue-50 sm:px-8" | ||||
|                   > | ||||
|                     View Notes for {user.email} | ||||
|                   </Link> | ||||
|                 ) : ( | ||||
|                   <div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0"> | ||||
|                     <Link | ||||
|                       to="/join" | ||||
|                       className="flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-blue-700 shadow-sm hover:bg-blue-50 sm:px-8" | ||||
|                     > | ||||
|                       Sign up | ||||
|                     </Link> | ||||
|                     <Link | ||||
|                       to="/login" | ||||
|                       className="flex items-center justify-center rounded-md bg-blue-500 px-4 py-3 font-medium text-white hover:bg-blue-600  " | ||||
|                     > | ||||
|                       Log In | ||||
|                     </Link> | ||||
|                   </div> | ||||
|                 )} | ||||
|               </div> | ||||
|               <a href="https://remix.run"> | ||||
|                 <img | ||||
|                   src="https://user-images.githubusercontent.com/1500684/158298926-e45dafff-3544-4b69-96d6-d3bcc33fc76a.svg" | ||||
|                   alt="Remix" | ||||
|                   className="mx-auto mt-16 w-full max-w-[12rem] md:max-w-[16rem]" | ||||
|                 /> | ||||
|               </a> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="mx-auto max-w-7xl py-2 px-4 sm:px-6 lg:px-8"> | ||||
|           <div className="mt-6 flex flex-wrap justify-center gap-8"> | ||||
|             {[ | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157764397-ccd8ea10-b8aa-4772-a99b-35de937319e1.svg", | ||||
|                 alt: "Fly.io", | ||||
|                 href: "https://fly.io", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/158238105-e7279a0c-1640-40db-86b0-3d3a10aab824.svg", | ||||
|                 alt: "PostgreSQL", | ||||
|                 href: "https://www.postgresql.org/", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157764484-ad64a21a-d7fb-47e3-8669-ec046da20c1f.svg", | ||||
|                 alt: "Prisma", | ||||
|                 href: "https://prisma.io", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157764276-a516a239-e377-4a20-b44a-0ac7b65c8c14.svg", | ||||
|                 alt: "Tailwind", | ||||
|                 href: "https://tailwindcss.com", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157764454-48ac8c71-a2a9-4b5e-b19c-edef8b8953d6.svg", | ||||
|                 alt: "Cypress", | ||||
|                 href: "https://www.cypress.io", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157772386-75444196-0604-4340-af28-53b236faa182.svg", | ||||
|                 alt: "MSW", | ||||
|                 href: "https://mswjs.io", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157772447-00fccdce-9d12-46a3-8bb4-fac612cdc949.svg", | ||||
|                 alt: "Vitest", | ||||
|                 href: "https://vitest.dev", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157772662-92b0dd3a-453f-4d18-b8be-9fa6efde52cf.png", | ||||
|                 alt: "Testing Library", | ||||
|                 href: "https://testing-library.com", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157772934-ce0a943d-e9d0-40f8-97f3-f464c0811643.svg", | ||||
|                 alt: "Prettier", | ||||
|                 href: "https://prettier.io", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157772990-3968ff7c-b551-4c55-a25c-046a32709a8e.svg", | ||||
|                 alt: "ESLint", | ||||
|                 href: "https://eslint.org", | ||||
|               }, | ||||
|               { | ||||
|                 src: "https://user-images.githubusercontent.com/1500684/157773063-20a0ed64-b9f8-4e0b-9d1e-0b65a3d4a6db.svg", | ||||
|                 alt: "TypeScript", | ||||
|                 href: "https://typescriptlang.org", | ||||
|               }, | ||||
|             ].map((img) => ( | ||||
|               <a | ||||
|                 key={img.href} | ||||
|                 href={img.href} | ||||
|                 className="flex h-16 w-32 justify-center p-1 grayscale transition hover:grayscale-0 focus:grayscale-0" | ||||
|               > | ||||
|                 <img alt={img.alt} src={img.src} /> | ||||
|               </a> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <h1>Do you love the color of the sky?</h1> | ||||
|       <br /> | ||||
|       <Link | ||||
|       to="/loginstart"> | ||||
|       Log in | ||||
|       </Link> | ||||
|     </main> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -1,179 +0,0 @@ | |||
| import * as React from "react"; | ||||
| import type { ActionFunction, LoaderFunction, MetaFunction } from "remix"; | ||||
| import { | ||||
|   Form, | ||||
|   Link, | ||||
|   redirect, | ||||
|   useSearchParams, | ||||
|   json, | ||||
|   useActionData, | ||||
| } from "remix"; | ||||
| 
 | ||||
| import { getUserId, createUserSession } from "~/session.server"; | ||||
| 
 | ||||
| import { createUser, getUserByEmail } from "~/models/user.server"; | ||||
| import { validateEmail } from "~/utils"; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request }) => { | ||||
|   const userId = await getUserId(request); | ||||
|   if (userId) return redirect("/"); | ||||
|   return json({}); | ||||
| }; | ||||
| 
 | ||||
| interface ActionData { | ||||
|   errors: { | ||||
|     email?: string; | ||||
|     password?: string; | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const action: ActionFunction = async ({ request }) => { | ||||
|   const formData = await request.formData(); | ||||
|   const email = formData.get("email"); | ||||
|   const password = formData.get("password"); | ||||
|   const redirectTo = formData.get("redirectTo"); | ||||
| 
 | ||||
|   if (!validateEmail(email)) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { email: "Email is invalid" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   if (typeof password !== "string") { | ||||
|     return json<ActionData>( | ||||
|       { errors: { password: "Password is required" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   if (password.length < 8) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { password: "Password is too short" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   const existingUser = await getUserByEmail(email); | ||||
|   if (existingUser) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { email: "A user already exists with this email" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   const user = await createUser(email, password); | ||||
| 
 | ||||
|   return createUserSession({ | ||||
|     request, | ||||
|     userId: user.id, | ||||
|     remember: false, | ||||
|     redirectTo: typeof redirectTo === "string" ? redirectTo : "/", | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export const meta: MetaFunction = () => { | ||||
|   return { | ||||
|     title: "Sign Up", | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export default function Join() { | ||||
|   const [searchParams] = useSearchParams(); | ||||
|   const redirectTo = searchParams.get("redirectTo") ?? undefined; | ||||
|   const actionData = useActionData() as ActionData; | ||||
|   const emailRef = React.useRef<HTMLInputElement>(null); | ||||
|   const passwordRef = React.useRef<HTMLInputElement>(null); | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     if (actionData?.errors?.email) { | ||||
|       emailRef.current?.focus(); | ||||
|     } else if (actionData?.errors?.password) { | ||||
|       passwordRef.current?.focus(); | ||||
|     } | ||||
|   }, [actionData]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex min-h-full flex-col justify-center"> | ||||
|       <div className="mx-auto w-full max-w-md px-8"> | ||||
|         <Form method="post" className="space-y-6" noValidate> | ||||
|           <div> | ||||
|             <label | ||||
|               htmlFor="email" | ||||
|               className="block text-sm font-medium text-gray-700" | ||||
|             > | ||||
|               Email address | ||||
|             </label> | ||||
|             <div className="mt-1"> | ||||
|               <input | ||||
|                 ref={emailRef} | ||||
|                 id="email" | ||||
|                 required | ||||
|                 autoFocus={true} | ||||
|                 name="email" | ||||
|                 type="email" | ||||
|                 autoComplete="email" | ||||
|                 aria-invalid={actionData?.errors?.email ? true : undefined} | ||||
|                 aria-describedby="email-error" | ||||
|                 className="w-full rounded border border-gray-500 px-2 py-1 text-lg" | ||||
|               /> | ||||
|               {actionData?.errors?.email && ( | ||||
|                 <div className="pt-1 text-red-700" id="email-error"> | ||||
|                   {actionData.errors.email} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <label | ||||
|               htmlFor="password" | ||||
|               className="block text-sm font-medium text-gray-700" | ||||
|             > | ||||
|               Password | ||||
|             </label> | ||||
|             <div className="mt-1"> | ||||
|               <input | ||||
|                 id="password" | ||||
|                 ref={passwordRef} | ||||
|                 name="password" | ||||
|                 type="password" | ||||
|                 autoComplete="new-password" | ||||
|                 aria-invalid={actionData?.errors?.password ? true : undefined} | ||||
|                 aria-describedby="password-error" | ||||
|                 className="w-full rounded border border-gray-500 px-2 py-1 text-lg" | ||||
|               /> | ||||
|               {actionData?.errors?.password && ( | ||||
|                 <div className="pt-1 text-red-700" id="password-error"> | ||||
|                   {actionData.errors.password} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <input type="hidden" name="redirectTo" value={redirectTo} /> | ||||
|           <button | ||||
|             type="submit" | ||||
|             className="w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400" | ||||
|           > | ||||
|             Create Account | ||||
|           </button> | ||||
|           <div className="flex items-center justify-center"> | ||||
|             <div className="text-center text-sm text-gray-500"> | ||||
|               Already have an account?{" "} | ||||
|               <Link | ||||
|                 className="text-blue-500 underline" | ||||
|                 to={{ | ||||
|                   pathname: "/login", | ||||
|                   search: searchParams.toString(), | ||||
|                 }} | ||||
|               > | ||||
|                 Log in | ||||
|               </Link> | ||||
|             </div> | ||||
|           </div> | ||||
|         </Form> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | @ -8,67 +8,20 @@ import { | |||
|   redirect, | ||||
|   useSearchParams, | ||||
| } from "remix"; | ||||
| import { discordLogin } from "~/discord"; | ||||
| 
 | ||||
| import { createUserSession, getUserId } from "~/session.server"; | ||||
| import { verifyLogin } from "~/models/user.server"; | ||||
| import { validateEmail } from "~/utils"; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request }) => { | ||||
|   const userId = await getUserId(request); | ||||
|   if (userId) return redirect("/"); | ||||
|   return json({}); | ||||
| }; | ||||
| 
 | ||||
| interface ActionData { | ||||
|   errors?: { | ||||
|     email?: string; | ||||
|     password?: string; | ||||
|   }; | ||||
| } | ||||
|   const url = new URL(await request.url); | ||||
|   const accessCode = url.searchParams.get("code") || ""; | ||||
| 
 | ||||
| export const action: ActionFunction = async ({ request }) => { | ||||
|   const formData = await request.formData(); | ||||
|   const email = formData.get("email"); | ||||
|   const password = formData.get("password"); | ||||
|   const redirectTo = formData.get("redirectTo"); | ||||
|   const remember = formData.get("remember"); | ||||
| 
 | ||||
|   if (!validateEmail(email)) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { email: "Email is invalid" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   if (typeof password !== "string") { | ||||
|     return json<ActionData>( | ||||
|       { errors: { password: "Password is required" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   if (password.length < 8) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { password: "Password is too short" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   const user = await verifyLogin(email, password); | ||||
| 
 | ||||
|   if (!user) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { email: "Invalid email or password" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return createUserSession({ | ||||
|     request, | ||||
|     userId: user.id, | ||||
|     remember: remember === "on" ? true : false, | ||||
|     redirectTo: typeof redirectTo === "string" ? redirectTo : "/notes", | ||||
|   }); | ||||
|   const response = await discordLogin(request, accessCode); | ||||
|   console.log(response); | ||||
|   return "" | ||||
| }; | ||||
| 
 | ||||
| export const meta: MetaFunction = () => { | ||||
|  | @ -78,115 +31,9 @@ export const meta: MetaFunction = () => { | |||
| }; | ||||
| 
 | ||||
| export default function LoginPage() { | ||||
|   const [searchParams] = useSearchParams(); | ||||
|   const redirectTo = searchParams.get("redirectTo") || "/notes"; | ||||
|   const actionData = useActionData() as ActionData; | ||||
|   const emailRef = React.useRef<HTMLInputElement>(null); | ||||
|   const passwordRef = React.useRef<HTMLInputElement>(null); | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     if (actionData?.errors?.email) { | ||||
|       emailRef.current?.focus(); | ||||
|     } else if (actionData?.errors?.password) { | ||||
|       passwordRef.current?.focus(); | ||||
|     } | ||||
|   }, [actionData]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex min-h-full flex-col justify-center"> | ||||
|       <div className="mx-auto w-full max-w-md px-8"> | ||||
|         <Form method="post" className="space-y-6" noValidate> | ||||
|           <div> | ||||
|             <label | ||||
|               htmlFor="email" | ||||
|               className="block text-sm font-medium text-gray-700" | ||||
|             > | ||||
|               Email address | ||||
|             </label> | ||||
|             <div className="mt-1"> | ||||
|               <input | ||||
|                 ref={emailRef} | ||||
|                 id="email" | ||||
|                 required | ||||
|                 autoFocus={true} | ||||
|                 name="email" | ||||
|                 type="email" | ||||
|                 autoComplete="email" | ||||
|                 aria-invalid={actionData?.errors?.email ? true : undefined} | ||||
|                 aria-describedby="email-error" | ||||
|                 className="w-full rounded border border-gray-500 px-2 py-1 text-lg" | ||||
|               /> | ||||
|               {actionData?.errors?.email && ( | ||||
|                 <div className="pt-1 text-red-700" id="email-error"> | ||||
|                   {actionData.errors.email} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <label | ||||
|               htmlFor="password" | ||||
|               className="block text-sm font-medium text-gray-700" | ||||
|             > | ||||
|               Password | ||||
|             </label> | ||||
|             <div className="mt-1"> | ||||
|               <input | ||||
|                 id="password" | ||||
|                 ref={passwordRef} | ||||
|                 name="password" | ||||
|                 type="password" | ||||
|                 autoComplete="new-password" | ||||
|                 aria-invalid={actionData?.errors?.password ? true : undefined} | ||||
|                 aria-describedby="password-error" | ||||
|                 className="w-full rounded border border-gray-500 px-2 py-1 text-lg" | ||||
|               /> | ||||
|               {actionData?.errors?.password && ( | ||||
|                 <div className="pt-1 text-red-700" id="password-error"> | ||||
|                   {actionData.errors.password} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <input type="hidden" name="redirectTo" value={redirectTo} /> | ||||
|           <button | ||||
|             type="submit" | ||||
|             className="w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400" | ||||
|           > | ||||
|             Log in | ||||
|           </button> | ||||
|           <div className="flex items-center justify-between"> | ||||
|             <div className="flex items-center"> | ||||
|               <input | ||||
|                 id="remember" | ||||
|                 name="remember" | ||||
|                 type="checkbox" | ||||
|                 className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" | ||||
|               /> | ||||
|               <label | ||||
|                 htmlFor="remember" | ||||
|                 className="ml-2 block text-sm text-gray-900" | ||||
|               > | ||||
|                 Remember me | ||||
|               </label> | ||||
|             </div> | ||||
|             <div className="text-center text-sm text-gray-500"> | ||||
|               Don't have an account?{" "} | ||||
|               <Link | ||||
|                 className="text-blue-500 underline" | ||||
|                 to={{ | ||||
|                   pathname: "/join", | ||||
|                   search: searchParams.toString(), | ||||
|                 }} | ||||
|               > | ||||
|                 Sign up | ||||
|               </Link> | ||||
|             </div> | ||||
|           </div> | ||||
|         </Form> | ||||
|       </div> | ||||
|       <p>Error logging in.</p>   | ||||
|     </div> | ||||
|   ); | ||||
|   ) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										25
									
								
								app/routes/loginstart.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/routes/loginstart.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| import type { LoaderFunction, MetaFunction } from "remix"; | ||||
| import { | ||||
|   json, | ||||
|   redirect, | ||||
| } from "remix"; | ||||
| import { getUserId } from "~/session.server"; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request }) => { | ||||
|   const userId = await getUserId(request); | ||||
|   if (userId) return redirect("/"); | ||||
|   const client_id = process.env.DISCORD_CLIENT_ID || ""; | ||||
|   const redirect_uri = process.env.DISCORD_REDIRECT_URI || ""; | ||||
|   return redirect(`https://discord.com/api/oauth2/authorize?client_id=${client_id}` + | ||||
|   `&response_type=code&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=identify`); | ||||
| }; | ||||
| 
 | ||||
| export const meta: MetaFunction = () => { | ||||
|   return { | ||||
|     title: "Login", | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export default function LoginPage() { | ||||
|   return <div></div> | ||||
| } | ||||
|  | @ -1,73 +0,0 @@ | |||
| import { Form, json, useLoaderData, Outlet, Link, NavLink } from "remix"; | ||||
| import type { LoaderFunction } from "remix"; | ||||
| 
 | ||||
| import { requireUserId } from "~/session.server"; | ||||
| import { useUser } from "~/utils"; | ||||
| import { getNoteListItems } from "~/models/note.server"; | ||||
| 
 | ||||
| type LoaderData = { | ||||
|   noteListItems: Awaited<ReturnType<typeof getNoteListItems>>; | ||||
| }; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request }) => { | ||||
|   const userId = await requireUserId(request); | ||||
|   const noteListItems = await getNoteListItems({ userId }); | ||||
|   return json<LoaderData>({ noteListItems }); | ||||
| }; | ||||
| 
 | ||||
| export default function NotesPage() { | ||||
|   const data = useLoaderData() as LoaderData; | ||||
|   const user = useUser(); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex h-full min-h-screen flex-col"> | ||||
|       <header className="flex items-center justify-between bg-slate-800 p-4 text-white"> | ||||
|         <h1 className="text-3xl font-bold"> | ||||
|           <Link to=".">Notes</Link> | ||||
|         </h1> | ||||
|         <p>{user.email}</p> | ||||
|         <Form action="/logout" method="post"> | ||||
|           <button | ||||
|             type="submit" | ||||
|             className="rounded bg-slate-600 py-2 px-4 text-blue-100 hover:bg-blue-500 active:bg-blue-600" | ||||
|           > | ||||
|             Logout | ||||
|           </button> | ||||
|         </Form> | ||||
|       </header> | ||||
| 
 | ||||
|       <main className="flex h-full bg-white"> | ||||
|         <div className="h-full w-80 border-r bg-gray-50"> | ||||
|           <Link to="new" className="block p-4 text-xl text-blue-500"> | ||||
|             + New Note | ||||
|           </Link> | ||||
| 
 | ||||
|           <hr /> | ||||
| 
 | ||||
|           {data.noteListItems.length === 0 ? ( | ||||
|             <p className="p-4">No notes yet</p> | ||||
|           ) : ( | ||||
|             <ol> | ||||
|               {data.noteListItems.map((note) => ( | ||||
|                 <li key={note.id}> | ||||
|                   <NavLink | ||||
|                     className={({ isActive }) => | ||||
|                       `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}` | ||||
|                     } | ||||
|                     to={note.id} | ||||
|                   > | ||||
|                     📝 {note.title} | ||||
|                   </NavLink> | ||||
|                 </li> | ||||
|               ))} | ||||
|             </ol> | ||||
|           )} | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="flex-1 p-6"> | ||||
|           <Outlet /> | ||||
|         </div> | ||||
|       </main> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | @ -1,68 +0,0 @@ | |||
| import type { LoaderFunction, ActionFunction } from "remix"; | ||||
| import { redirect } from "remix"; | ||||
| import { json, useLoaderData, useCatch, Form } from "remix"; | ||||
| import invariant from "tiny-invariant"; | ||||
| import type { Note } from "~/models/note.server"; | ||||
| import { deleteNote } from "~/models/note.server"; | ||||
| import { getNote } from "~/models/note.server"; | ||||
| import { requireUserId } from "~/session.server"; | ||||
| 
 | ||||
| type LoaderData = { | ||||
|   note: Note; | ||||
| }; | ||||
| 
 | ||||
| export const loader: LoaderFunction = async ({ request, params }) => { | ||||
|   const userId = await requireUserId(request); | ||||
|   invariant(params.noteId, "noteId not found"); | ||||
| 
 | ||||
|   const note = await getNote({ userId, id: params.noteId }); | ||||
|   if (!note) { | ||||
|     throw new Response("Not Found", { status: 404 }); | ||||
|   } | ||||
|   return json<LoaderData>({ note }); | ||||
| }; | ||||
| 
 | ||||
| export const action: ActionFunction = async ({ request, params }) => { | ||||
|   const userId = await requireUserId(request); | ||||
|   invariant(params.noteId, "noteId not found"); | ||||
| 
 | ||||
|   await deleteNote({ userId, id: params.noteId }); | ||||
| 
 | ||||
|   return redirect("/notes"); | ||||
| }; | ||||
| 
 | ||||
| export default function NoteDetailsPage() { | ||||
|   const data = useLoaderData() as LoaderData; | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <h3 className="text-2xl font-bold">{data.note.title}</h3> | ||||
|       <p className="py-6">{data.note.body}</p> | ||||
|       <hr className="my-4" /> | ||||
|       <Form method="post"> | ||||
|         <button | ||||
|           type="submit" | ||||
|           className="rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400" | ||||
|         > | ||||
|           Delete | ||||
|         </button> | ||||
|       </Form> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function ErrorBoundary({ error }: { error: Error }) { | ||||
|   console.error(error); | ||||
| 
 | ||||
|   return <div>An unexpected error occurred: {error.message}</div>; | ||||
| } | ||||
| 
 | ||||
| export function CatchBoundary() { | ||||
|   const caught = useCatch(); | ||||
| 
 | ||||
|   if (caught.status === 404) { | ||||
|     return <div>Note not found</div>; | ||||
|   } | ||||
| 
 | ||||
|   throw new Error(`Unexpected caught response with status: ${caught.status}`); | ||||
| } | ||||
|  | @ -1,12 +0,0 @@ | |||
| import { Link } from "remix"; | ||||
| 
 | ||||
| export default function NoteIndexPage() { | ||||
|   return ( | ||||
|     <p> | ||||
|       No note selected. Select a note on the left, or{" "} | ||||
|       <Link to="new" className="text-blue-500 underline"> | ||||
|         create a new note. | ||||
|       </Link> | ||||
|     </p> | ||||
|   ); | ||||
| } | ||||
|  | @ -1,116 +0,0 @@ | |||
| import * as React from "react"; | ||||
| import { Form, json, redirect, useActionData } from "remix"; | ||||
| import type { ActionFunction } from "remix"; | ||||
| import Alert from "@reach/alert"; | ||||
| 
 | ||||
| import { createNote } from "~/models/note.server"; | ||||
| import { requireUserId } from "~/session.server"; | ||||
| 
 | ||||
| type ActionData = { | ||||
|   errors?: { | ||||
|     title?: string; | ||||
|     body?: string; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export const action: ActionFunction = async ({ request }) => { | ||||
|   const userId = await requireUserId(request); | ||||
| 
 | ||||
|   const formData = await request.formData(); | ||||
|   const title = formData.get("title"); | ||||
|   const body = formData.get("body"); | ||||
| 
 | ||||
|   if (typeof title !== "string" || title.length === 0) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { title: "Title is required" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   if (typeof body !== "string" || body.length === 0) { | ||||
|     return json<ActionData>( | ||||
|       { errors: { body: "Body is required" } }, | ||||
|       { status: 400 } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   const note = await createNote({ title, body, userId }); | ||||
| 
 | ||||
|   return redirect(`/notes/${note.id}`); | ||||
| }; | ||||
| 
 | ||||
| export default function NewNotePage() { | ||||
|   const actionData = useActionData() as ActionData; | ||||
|   const titleRef = React.useRef<HTMLInputElement>(null); | ||||
|   const bodyRef = React.useRef<HTMLTextAreaElement>(null); | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     if (actionData?.errors?.title) { | ||||
|       titleRef.current?.focus(); | ||||
|     } else if (actionData?.errors?.body) { | ||||
|       bodyRef.current?.focus(); | ||||
|     } | ||||
|   }, [actionData]); | ||||
| 
 | ||||
|   return ( | ||||
|     <Form | ||||
|       method="post" | ||||
|       style={{ | ||||
|         display: "flex", | ||||
|         flexDirection: "column", | ||||
|         gap: 8, | ||||
|         width: "100%", | ||||
|       }} | ||||
|     > | ||||
|       <div> | ||||
|         <label className="flex w-full flex-col gap-1"> | ||||
|           <span>Title: </span> | ||||
|           <input | ||||
|             ref={titleRef} | ||||
|             name="title" | ||||
|             className="flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose" | ||||
|             aria-invalid={actionData?.errors?.title ? true : undefined} | ||||
|             aria-errormessage={ | ||||
|               actionData?.errors?.title ? "title-error" : undefined | ||||
|             } | ||||
|           /> | ||||
|         </label> | ||||
|         {actionData?.errors?.title && ( | ||||
|           <Alert className="pt-1 text-red-700" id="title=error"> | ||||
|             {actionData.errors.title} | ||||
|           </Alert> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       <div> | ||||
|         <label className="flex w-full flex-col gap-1"> | ||||
|           <span>Body: </span> | ||||
|           <textarea | ||||
|             ref={bodyRef} | ||||
|             name="body" | ||||
|             rows={8} | ||||
|             className="w-full flex-1 rounded-md border-2 border-blue-500 py-2 px-3 text-lg leading-6" | ||||
|             aria-invalid={actionData?.errors?.body ? true : undefined} | ||||
|             aria-errormessage={ | ||||
|               actionData?.errors?.body ? "body-error" : undefined | ||||
|             } | ||||
|           /> | ||||
|         </label> | ||||
|         {actionData?.errors?.body && ( | ||||
|           <Alert className="pt-1 text-red-700" id="body=error"> | ||||
|             {actionData.errors.body} | ||||
|           </Alert> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="text-right"> | ||||
|         <button | ||||
|           type="submit" | ||||
|           className="rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400" | ||||
|         > | ||||
|           Save | ||||
|         </button> | ||||
|       </div> | ||||
|     </Form> | ||||
|   ); | ||||
| } | ||||
|  | @ -1,8 +1,8 @@ | |||
| import { createCookieSessionStorage, redirect } from "remix"; | ||||
| import invariant from "tiny-invariant"; | ||||
| 
 | ||||
| import type { User } from "~/models/user.server"; | ||||
| import { getUserById } from "~/models/user.server"; | ||||
| import { discordIdentify, User, SessionInformation } from "~/models/user.server"; | ||||
| import { getUserByDiscordId } from "~/models/user.server"; | ||||
| 
 | ||||
| invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set"); | ||||
| 
 | ||||
|  | @ -19,6 +19,8 @@ export const sessionStorage = createCookieSessionStorage({ | |||
| }); | ||||
| 
 | ||||
| const USER_SESSION_KEY = "userId"; | ||||
| const BEARER_TOKEN_KEY = "bearerToken"; | ||||
| const REFRESH_TOKEN_KEY = "refreshToken"; | ||||
| 
 | ||||
| export async function getSession(request: Request) { | ||||
|   const cookie = request.headers.get("Cookie"); | ||||
|  | @ -35,7 +37,7 @@ export async function getUser(request: Request): Promise<null | User> { | |||
|   const userId = await getUserId(request); | ||||
|   if (userId === undefined) return null; | ||||
| 
 | ||||
|   const user = await getUserById(userId); | ||||
|   const user = await getUserByDiscordId(userId); | ||||
|   if (user) return user; | ||||
| 
 | ||||
|   throw await logout(request); | ||||
|  | @ -48,39 +50,38 @@ export async function requireUserId( | |||
|   const userId = await getUserId(request); | ||||
|   if (!userId) { | ||||
|     const searchParams = new URLSearchParams([["redirectTo", redirectTo]]); | ||||
|     throw redirect(`/login?${searchParams}`); | ||||
|     throw redirect(`/loginstart?${searchParams}`); | ||||
|   } | ||||
|   return userId; | ||||
| } | ||||
| 
 | ||||
| export async function requireUser(request: Request) { | ||||
|   const userId = await requireUserId(request); | ||||
| 
 | ||||
|   const user = await getUserById(userId); | ||||
|   if (user) return user; | ||||
| 
 | ||||
|   throw await logout(request); | ||||
|   const session = await getSession(request); | ||||
|   const user_id = session.get(USER_SESSION_KEY); | ||||
|   const client_id = process.env.DISCORD_CLIENT_ID || ""; | ||||
|   const redirect_uri = process.env.DISCORD_REDIRECT_URI || ""; | ||||
|   if (!user_id) { | ||||
|     return redirect(`https://discord.com/api/oauth2/authorize?client_id=${client_id}` + | ||||
|     `&response_type=code&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=identify`); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function createUserSession({ | ||||
|   request, | ||||
|   userId, | ||||
|   remember, | ||||
|   discord_id, | ||||
|   redirectTo, | ||||
| }: { | ||||
|   request: Request; | ||||
|   userId: string; | ||||
|   remember: boolean; | ||||
|   discord_id: string; | ||||
|   redirectTo: string; | ||||
| }) { | ||||
|   const session = await getSession(request); | ||||
|   session.set(USER_SESSION_KEY, userId); | ||||
|   session.set(USER_SESSION_KEY, discord_id); | ||||
|   console.log(discord_id); | ||||
|   return redirect(redirectTo, { | ||||
|     headers: { | ||||
|       "Set-Cookie": await sessionStorage.commitSession(session, { | ||||
|         maxAge: remember | ||||
|           ? 60 * 60 * 24 * 7 // 7 days
 | ||||
|           : undefined, | ||||
|         maxAge: 60 * 60 * 24 * 7 // 7 days
 | ||||
|       }), | ||||
|     }, | ||||
|   }); | ||||
|  |  | |||
|  | @ -1,13 +0,0 @@ | |||
| import { validateEmail } from "./utils"; | ||||
| 
 | ||||
| test("validateEmail returns false for non-emails", () => { | ||||
|   expect(validateEmail(undefined)).toBe(false); | ||||
|   expect(validateEmail(null)).toBe(false); | ||||
|   expect(validateEmail("")).toBe(false); | ||||
|   expect(validateEmail("not-an-email")).toBe(false); | ||||
|   expect(validateEmail("n@")).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test("validateEmail returns true for emails", () => { | ||||
|   expect(validateEmail("kody@example.com")).toBe(true); | ||||
| }); | ||||
|  | @ -21,11 +21,12 @@ export function useMatchesData( | |||
| } | ||||
| 
 | ||||
| function isUser(user: any): user is User { | ||||
|   return user && typeof user === "object" && typeof user.email === "string"; | ||||
|   return user && typeof user === "object" && typeof user.discord_id === "string"; | ||||
| } | ||||
| 
 | ||||
| export function useOptionalUser(): User | undefined { | ||||
|   const data = useMatchesData("root"); | ||||
|   console.log(data) | ||||
|   if (!data || !isUser(data.user)) { | ||||
|     return undefined; | ||||
|   } | ||||
|  | @ -41,7 +42,3 @@ export function useUser(): User { | |||
|   } | ||||
|   return maybeUser; | ||||
| } | ||||
| 
 | ||||
| export function validateEmail(email: unknown): email is string { | ||||
|   return typeof email === "string" && email.length > 3 && email.includes("@"); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue