Skip to content

Commit

Permalink
Fix a bunch of broken behaviors around authentication, basic homepage (
Browse files Browse the repository at this point in the history
  • Loading branch information
vcarl authored Nov 19, 2024
1 parent b6efec3 commit 8a19925
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 221 deletions.
48 changes: 22 additions & 26 deletions app/components/login.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
import { Form, useSearchParams } from "@remix-run/react";
import { Form } from "@remix-run/react";
import type { ButtonHTMLAttributes } from "react";

export function Login({ errors }: { errors?: { [k: string]: string } }) {
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") || "/notes";
interface LoginProps extends ButtonHTMLAttributes<Element> {
errors?: { [k: string]: string };
redirectTo?: string;
}

export function Login({
children = "Log in with Discord",
errors,
redirectTo,
...props
}: LoginProps) {
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" action="/auth">
<input type="hidden" name="redirectTo" value={redirectTo} />
<button
type="submit"
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
>
Log in with Discord
</button>
<div className="flex items-center justify-between">
<div className="flex items-center">
{Object.values(errors || {}).map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
))}
</div>
</div>
</Form>
</div>
</div>
<Form method="post" className="space-y-6" action="/auth">
<input type="hidden" name="redirectTo" value={redirectTo} />
<button
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
{...props}
type="submit"
>
{children}
</button>
</Form>
);
}
26 changes: 26 additions & 0 deletions app/components/logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Form } from "@remix-run/react";
import type { ButtonHTMLAttributes } from "react";

interface LoginProps extends ButtonHTMLAttributes<Element> {
errors?: { [k: string]: string };
redirectTo?: string;
}

export function Logout({
children = "Log out",
errors,
redirectTo,
...props
}: LoginProps) {
return (
<Form method="post" action="/logout" className="space-y-6">
<button
type="submit"
{...props}
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
>
{children}
</button>
</Form>
);
}
51 changes: 26 additions & 25 deletions app/models/session.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,18 @@ const {
return result.id!;
},
async readData(id) {
const result = await db
const result = (await db
.selectFrom("sessions")
.where("id", "=", id)
.selectAll()
.executeTakeFirst();
return result?.data ? JSON.parse(result.data) : null;
.executeTakeFirst()) ?? { data: {} as any, expires: undefined };
return result.data;
},
async updateData(id, data, expires) {
await db
.updateTable("sessions")
.set("data", JSON.stringify(data))
.set("expires", expires!.toString())
.set("expires", expires?.toString() || null)
.where("id", "=", id)
.execute();
},
Expand Down Expand Up @@ -158,9 +158,8 @@ export async function getUser(request: Request) {
if (userId === undefined) return null;

const user = await getUserById(userId);
if (user) return user;

throw await logout(request);
if (!user) throw await logout(request);
return user;
}

export async function requireUserId(
Expand Down Expand Up @@ -188,14 +187,21 @@ const OAUTH_REDIRECT = "http://localhost:3000/discord-oauth";

export async function initOauthLogin({
request,
redirectTo,
}: {
request: Request;
redirectTo: string;
redirectTo?: string;
}) {
const dbSession = await getDbSession(request.headers.get("Cookie"));

const state = randomUUID();
dbSession.set("state", state);
if (redirectTo) {
dbSession.set("redirectTo", redirectTo);
}
const cookie = await commitDbSession(dbSession, {
maxAge: 60 * 60 * 1, // 1 hour
});
return redirect(
authorization.authorizeURL({
redirect_uri: OAUTH_REDIRECT,
Expand All @@ -204,21 +210,17 @@ export async function initOauthLogin({
}),
{
headers: {
"Set-Cookie": await commitDbSession(dbSession, {
maxAge: 60 * 60 * 1, // 1 hour
}),
"Set-Cookie": cookie,
},
},
);
}

export async function completeOauthLogin(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
if (!code) {
throw json({ message: `Discord didn't send an auth code` }, 500);
}

export async function completeOauthLogin(
code: string,
reqCookie: string,
state?: string,
) {
const token = await authorization.getToken({
scope: SCOPE,
code,
Expand All @@ -245,15 +247,14 @@ export async function completeOauthLogin(request: Request) {
}

const [cookieSession, dbSession] = await Promise.all([
getCookieSession(request.headers.get("Cookie")),
getDbSession(request.headers.get("Cookie")),
getCookieSession(reqCookie),
getDbSession(reqCookie),
]);

// 401 if the state arg doesn't match
const state = url.searchParams.get("state");
console.log({ state, dbState: dbSession.get("state") });
// Redirect to login if the state arg doesn't match
if (dbSession.get("state") !== state) {
throw redirect("/login", 401);
console.error("DB state didn’t match cookie state");
throw redirect("/login");
}

cookieSession.set(USER_SESSION_KEY, userId);
Expand All @@ -269,7 +270,7 @@ export async function completeOauthLogin(request: Request) {
headers.append("Set-Cookie", cookie);
headers.append("Set-Cookie", dbCookie);

return redirect("/", { headers });
return redirect(dbSession.get("redirectTo") ?? "/", { headers });
}

export async function refreshSession(request: Request) {
Expand Down
17 changes: 15 additions & 2 deletions app/routes/__auth.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { Outlet } from "@remix-run/react";
import { Outlet, useLocation } from "@remix-run/react";

import { Login } from "~/components/login";
import { getUser } from "~/models/session.server";
import { useOptionalUser } from "~/utils";

export function loader({ request }: { request: Request }) {
return getUser(request);
}

export default function Auth() {
const user = useOptionalUser();
const location = useLocation();

if (!user) {
return <Login />;
return (
<div className="flex min-h-full flex-col justify-center">
<div className="mx-auto w-full max-w-md px-8">
<Login redirectTo={location.pathname} />;
</div>
</div>
);
}

return <Outlet />;
Expand Down
18 changes: 12 additions & 6 deletions app/routes/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";

import { getUser, initOauthLogin } from "~/models/session.server";
import { initOauthLogin } from "~/models/session.server";
import { Login } from "~/components/login";

export const loader: LoaderFunction = async ({ request }) => {
const user = await getUser(request);
if (user) return redirect("/");
return redirect("/login");
return redirect("/");
};

export const action: ActionFunction = async ({ request }) => {
// fetch user from db
// if doesn't exist, create it with discord ID + email
const form = await request.formData();

return initOauthLogin({
request,
redirectTo: "http://localhost:3000/discord-oauth",
redirectTo: form.get("redirectTo")?.toString() ?? undefined,
});
};

export default function LoginPage() {
return <Login errors={undefined} />;
return (
<div className="flex min-h-full flex-col justify-center">
<div className="mx-auto w-full max-w-md px-8">
<Login redirectTo="/dashboard" />;
</div>
</div>
);
}
20 changes: 18 additions & 2 deletions app/routes/discord-oauth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import type { LoaderFunction } from "@remix-run/server-runtime";
import { redirect, type LoaderFunction } from "@remix-run/node";
import { completeOauthLogin } from "~/models/session.server";

export const loader: LoaderFunction = async ({ request }) => {
return await completeOauthLogin(request);
const url = new URL(request.url);
const code = url.searchParams.get("code");
const cookie = request.headers.get("Cookie");
if (!code) {
console.error("No code provided by Discord");
return redirect("/");
}
if (!cookie) {
console.error("No cookie found when responding to Discord oauth");
throw redirect("/login", 500);
}

return await completeOauthLogin(
code,
cookie,
url.searchParams.get("state") ?? undefined,
);
};
51 changes: 19 additions & 32 deletions app/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Link } from "@remix-run/react";
import { Login } from "~/components/login";
import { Logout } from "~/components/logout";

import { useOptionalUser } from "~/utils";

Expand All @@ -17,48 +18,34 @@ export default function Index() {
/>
<div className="absolute inset-0 bg-[color:rgba(254,204,27,0.5)] mix-blend-multiply" />
</div>
<div className="lg:pb-18 relative px-4 pb-8 pt-16 sm:px-6 sm:pb-14 sm:pt-24 lg:px-8 lg:pt-32">

<div className="lg:pb-18 relative w-full max-w-xl px-4 pb-8 pt-16 sm:px-6 sm:pb-14 sm:pt-24 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-yellow-500 drop-shadow-md">
Job bot prototype
Euno
</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="/logout"
className="flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8"
>
Log out {user.email}
</Link>
<Logout />
) : (
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
<Link
to="/login"
className="flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8"
>
Sign up
</Link>
<Link
to="/login"
className="flex items-center justify-center rounded-md bg-yellow-500 px-4 py-3 font-medium text-white hover:bg-yellow-600 "
>
Log In
</Link>
<Login>Log in</Login>
</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>
<p className="mx-auto mt-6 max-w-md text-center text-xl text-white">
This is a development placeholder for Euno, a Discord moderation
bot.
</p>
<p className="mx-auto mt-6 max-w-md text-center text-xl text-white drop-shadow-md">
Coming soon:
<ul>
<li>ticketing??</li>
<li>activity reports</li>
<li>other fun things</li>
</ul>
</p>
</div>
</div>
</div>
Expand Down
19 changes: 1 addition & 18 deletions app/routes/logout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ActionFunction } from "@remix-run/node";
import { Form } from "@remix-run/react";

import { logout } from "~/models/session.server";

Expand All @@ -15,23 +14,7 @@ export default function Logout({
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">
<button
type="submit"
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
>
Log out
</button>
<div className="flex items-center justify-between">
<div className="flex items-center">
{Object.values(errors || {}).map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
))}
</div>
</div>
</Form>
<Logout />
</div>
</div>
);
Expand Down
Loading

0 comments on commit 8a19925

Please sign in to comment.