Skip to content

Commit 8a19925

Browse files
authored
Fix a bunch of broken behaviors around authentication, basic homepage (#87)
1 parent b6efec3 commit 8a19925

File tree

9 files changed

+216
-221
lines changed

9 files changed

+216
-221
lines changed

app/components/login.tsx

+22-26
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
1-
import { Form, useSearchParams } from "@remix-run/react";
1+
import { Form } from "@remix-run/react";
2+
import type { ButtonHTMLAttributes } from "react";
23

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

9+
export function Login({
10+
children = "Log in with Discord",
11+
errors,
12+
redirectTo,
13+
...props
14+
}: LoginProps) {
715
return (
8-
<div className="flex min-h-full flex-col justify-center">
9-
<div className="mx-auto w-full max-w-md px-8">
10-
<Form method="post" className="space-y-6" action="/auth">
11-
<input type="hidden" name="redirectTo" value={redirectTo} />
12-
<button
13-
type="submit"
14-
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
15-
>
16-
Log in with Discord
17-
</button>
18-
<div className="flex items-center justify-between">
19-
<div className="flex items-center">
20-
{Object.values(errors || {}).map((error) => (
21-
<p key={error} className="text-red-500">
22-
{error}
23-
</p>
24-
))}
25-
</div>
26-
</div>
27-
</Form>
28-
</div>
29-
</div>
16+
<Form method="post" className="space-y-6" action="/auth">
17+
<input type="hidden" name="redirectTo" value={redirectTo} />
18+
<button
19+
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
20+
{...props}
21+
type="submit"
22+
>
23+
{children}
24+
</button>
25+
</Form>
3026
);
3127
}

app/components/logout.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Form } from "@remix-run/react";
2+
import type { ButtonHTMLAttributes } from "react";
3+
4+
interface LoginProps extends ButtonHTMLAttributes<Element> {
5+
errors?: { [k: string]: string };
6+
redirectTo?: string;
7+
}
8+
9+
export function Logout({
10+
children = "Log out",
11+
errors,
12+
redirectTo,
13+
...props
14+
}: LoginProps) {
15+
return (
16+
<Form method="post" action="/logout" className="space-y-6">
17+
<button
18+
type="submit"
19+
{...props}
20+
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
21+
>
22+
{children}
23+
</button>
24+
</Form>
25+
);
26+
}

app/models/session.server.ts

+26-25
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,18 @@ const {
7575
return result.id!;
7676
},
7777
async readData(id) {
78-
const result = await db
78+
const result = (await db
7979
.selectFrom("sessions")
8080
.where("id", "=", id)
8181
.selectAll()
82-
.executeTakeFirst();
83-
return result?.data ? JSON.parse(result.data) : null;
82+
.executeTakeFirst()) ?? { data: {} as any, expires: undefined };
83+
return result.data;
8484
},
8585
async updateData(id, data, expires) {
8686
await db
8787
.updateTable("sessions")
8888
.set("data", JSON.stringify(data))
89-
.set("expires", expires!.toString())
89+
.set("expires", expires?.toString() || null)
9090
.where("id", "=", id)
9191
.execute();
9292
},
@@ -158,9 +158,8 @@ export async function getUser(request: Request) {
158158
if (userId === undefined) return null;
159159

160160
const user = await getUserById(userId);
161-
if (user) return user;
162-
163-
throw await logout(request);
161+
if (!user) throw await logout(request);
162+
return user;
164163
}
165164

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

189188
export async function initOauthLogin({
190189
request,
190+
redirectTo,
191191
}: {
192192
request: Request;
193-
redirectTo: string;
193+
redirectTo?: string;
194194
}) {
195195
const dbSession = await getDbSession(request.headers.get("Cookie"));
196196

197197
const state = randomUUID();
198198
dbSession.set("state", state);
199+
if (redirectTo) {
200+
dbSession.set("redirectTo", redirectTo);
201+
}
202+
const cookie = await commitDbSession(dbSession, {
203+
maxAge: 60 * 60 * 1, // 1 hour
204+
});
199205
return redirect(
200206
authorization.authorizeURL({
201207
redirect_uri: OAUTH_REDIRECT,
@@ -204,21 +210,17 @@ export async function initOauthLogin({
204210
}),
205211
{
206212
headers: {
207-
"Set-Cookie": await commitDbSession(dbSession, {
208-
maxAge: 60 * 60 * 1, // 1 hour
209-
}),
213+
"Set-Cookie": cookie,
210214
},
211215
},
212216
);
213217
}
214218

215-
export async function completeOauthLogin(request: Request) {
216-
const url = new URL(request.url);
217-
const code = url.searchParams.get("code");
218-
if (!code) {
219-
throw json({ message: `Discord didn't send an auth code` }, 500);
220-
}
221-
219+
export async function completeOauthLogin(
220+
code: string,
221+
reqCookie: string,
222+
state?: string,
223+
) {
222224
const token = await authorization.getToken({
223225
scope: SCOPE,
224226
code,
@@ -245,15 +247,14 @@ export async function completeOauthLogin(request: Request) {
245247
}
246248

247249
const [cookieSession, dbSession] = await Promise.all([
248-
getCookieSession(request.headers.get("Cookie")),
249-
getDbSession(request.headers.get("Cookie")),
250+
getCookieSession(reqCookie),
251+
getDbSession(reqCookie),
250252
]);
251253

252-
// 401 if the state arg doesn't match
253-
const state = url.searchParams.get("state");
254-
console.log({ state, dbState: dbSession.get("state") });
254+
// Redirect to login if the state arg doesn't match
255255
if (dbSession.get("state") !== state) {
256-
throw redirect("/login", 401);
256+
console.error("DB state didn’t match cookie state");
257+
throw redirect("/login");
257258
}
258259

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

272-
return redirect("/", { headers });
273+
return redirect(dbSession.get("redirectTo") ?? "/", { headers });
273274
}
274275

275276
export async function refreshSession(request: Request) {

app/routes/__auth.tsx

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
import { Outlet } from "@remix-run/react";
1+
import { Outlet, useLocation } from "@remix-run/react";
22

33
import { Login } from "~/components/login";
4+
import { getUser } from "~/models/session.server";
45
import { useOptionalUser } from "~/utils";
56

7+
export function loader({ request }: { request: Request }) {
8+
return getUser(request);
9+
}
10+
611
export default function Auth() {
712
const user = useOptionalUser();
13+
const location = useLocation();
14+
815
if (!user) {
9-
return <Login />;
16+
return (
17+
<div className="flex min-h-full flex-col justify-center">
18+
<div className="mx-auto w-full max-w-md px-8">
19+
<Login redirectTo={location.pathname} />;
20+
</div>
21+
</div>
22+
);
1023
}
1124

1225
return <Outlet />;

app/routes/auth.tsx

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
22
import { redirect } from "@remix-run/node";
33

4-
import { getUser, initOauthLogin } from "~/models/session.server";
4+
import { initOauthLogin } from "~/models/session.server";
55
import { Login } from "~/components/login";
66

77
export const loader: LoaderFunction = async ({ request }) => {
8-
const user = await getUser(request);
9-
if (user) return redirect("/");
10-
return redirect("/login");
8+
return redirect("/");
119
};
1210

1311
export const action: ActionFunction = async ({ request }) => {
1412
// fetch user from db
1513
// if doesn't exist, create it with discord ID + email
14+
const form = await request.formData();
15+
1616
return initOauthLogin({
1717
request,
18-
redirectTo: "http://localhost:3000/discord-oauth",
18+
redirectTo: form.get("redirectTo")?.toString() ?? undefined,
1919
});
2020
};
2121

2222
export default function LoginPage() {
23-
return <Login errors={undefined} />;
23+
return (
24+
<div className="flex min-h-full flex-col justify-center">
25+
<div className="mx-auto w-full max-w-md px-8">
26+
<Login redirectTo="/dashboard" />;
27+
</div>
28+
</div>
29+
);
2430
}

app/routes/discord-oauth.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1-
import type { LoaderFunction } from "@remix-run/server-runtime";
1+
import { redirect, type LoaderFunction } from "@remix-run/node";
22
import { completeOauthLogin } from "~/models/session.server";
33

44
export const loader: LoaderFunction = async ({ request }) => {
5-
return await completeOauthLogin(request);
5+
const url = new URL(request.url);
6+
const code = url.searchParams.get("code");
7+
const cookie = request.headers.get("Cookie");
8+
if (!code) {
9+
console.error("No code provided by Discord");
10+
return redirect("/");
11+
}
12+
if (!cookie) {
13+
console.error("No cookie found when responding to Discord oauth");
14+
throw redirect("/login", 500);
15+
}
16+
17+
return await completeOauthLogin(
18+
code,
19+
cookie,
20+
url.searchParams.get("state") ?? undefined,
21+
);
622
};

app/routes/index.tsx

+19-32
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Link } from "@remix-run/react";
1+
import { Login } from "~/components/login";
2+
import { Logout } from "~/components/logout";
23

34
import { useOptionalUser } from "~/utils";
45

@@ -17,48 +18,34 @@ export default function Index() {
1718
/>
1819
<div className="absolute inset-0 bg-[color:rgba(254,204,27,0.5)] mix-blend-multiply" />
1920
</div>
20-
<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">
21+
22+
<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">
2123
<h1 className="text-center text-6xl font-extrabold tracking-tight sm:text-8xl lg:text-9xl">
2224
<span className="block uppercase text-yellow-500 drop-shadow-md">
23-
Job bot prototype
25+
Euno
2426
</span>
2527
</h1>
26-
<p className="mx-auto mt-6 max-w-lg text-center text-xl text-white sm:max-w-3xl">
27-
Check the README.md file for instructions on how to get this
28-
project deployed.
29-
</p>
3028
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
3129
{user ? (
32-
<Link
33-
to="/logout"
34-
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"
35-
>
36-
Log out {user.email}
37-
</Link>
30+
<Logout />
3831
) : (
3932
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
40-
<Link
41-
to="/login"
42-
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"
43-
>
44-
Sign up
45-
</Link>
46-
<Link
47-
to="/login"
48-
className="flex items-center justify-center rounded-md bg-yellow-500 px-4 py-3 font-medium text-white hover:bg-yellow-600 "
49-
>
50-
Log In
51-
</Link>
33+
<Login>Log in</Login>
5234
</div>
5335
)}
5436
</div>
55-
<a href="https://remix.run">
56-
<img
57-
src="https://user-images.githubusercontent.com/1500684/158298926-e45dafff-3544-4b69-96d6-d3bcc33fc76a.svg"
58-
alt="Remix"
59-
className="mx-auto mt-16 w-full max-w-[12rem] md:max-w-[16rem]"
60-
/>
61-
</a>
37+
<p className="mx-auto mt-6 max-w-md text-center text-xl text-white">
38+
This is a development placeholder for Euno, a Discord moderation
39+
bot.
40+
</p>
41+
<p className="mx-auto mt-6 max-w-md text-center text-xl text-white drop-shadow-md">
42+
Coming soon:
43+
<ul>
44+
<li>ticketing??</li>
45+
<li>activity reports</li>
46+
<li>other fun things</li>
47+
</ul>
48+
</p>
6249
</div>
6350
</div>
6451
</div>

app/routes/logout.tsx

+1-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ActionFunction } from "@remix-run/node";
2-
import { Form } from "@remix-run/react";
32

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

@@ -15,23 +14,7 @@ export default function Logout({
1514
return (
1615
<div className="flex min-h-full flex-col justify-center">
1716
<div className="mx-auto w-full max-w-md px-8">
18-
<Form method="post" className="space-y-6">
19-
<button
20-
type="submit"
21-
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
22-
>
23-
Log out
24-
</button>
25-
<div className="flex items-center justify-between">
26-
<div className="flex items-center">
27-
{Object.values(errors || {}).map((error) => (
28-
<p key={error} className="text-red-500">
29-
{error}
30-
</p>
31-
))}
32-
</div>
33-
</div>
34-
</Form>
17+
<Logout />
3518
</div>
3619
</div>
3720
);

0 commit comments

Comments
 (0)