Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a bunch of broken behaviors around authentication, basic homepage #87

Merged
merged 5 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading