From f2bcdd774321477cd372481fdabe537435388920 Mon Sep 17 00:00:00 2001
From: Carl Vitullo
Date: Mon, 18 Nov 2024 23:49:15 -0500
Subject: [PATCH 1/5] Fix some auth issues
---
app/models/session.server.ts | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/app/models/session.server.ts b/app/models/session.server.ts
index f1d573d..8249b5a 100644
--- a/app/models/session.server.ts
+++ b/app/models/session.server.ts
@@ -80,13 +80,21 @@ const {
.where("id", "=", id)
.selectAll()
.executeTakeFirst();
- return result?.data ? JSON.parse(result.data) : null;
+ return (result?.data as { state: string } | null) || null;
},
async updateData(id, data, expires) {
await db
.updateTable("sessions")
.set("data", JSON.stringify(data))
- .set("expires", expires!.toString())
+ .set(
+ "expires",
+ expires?.toString() ||
+ (() => {
+ const soon = new Date();
+ soon.setMinutes(soon.getMinutes() + 15);
+ return soon.toString();
+ })(),
+ )
.where("id", "=", id)
.execute();
},
@@ -251,7 +259,6 @@ export async function completeOauthLogin(request: Request) {
// 401 if the state arg doesn't match
const state = url.searchParams.get("state");
- console.log({ state, dbState: dbSession.get("state") });
if (dbSession.get("state") !== state) {
throw redirect("/login", 401);
}
From 2504d53ff4399ffd722b3debdb9cc86e49d2beea Mon Sep 17 00:00:00 2001
From: Carl Vitullo
Date: Tue, 19 Nov 2024 00:46:37 -0500
Subject: [PATCH 2/5] Fix login/logout button experience
---
app/components/login.tsx | 48 ++++++++++++++++++---------------------
app/components/logout.tsx | 26 +++++++++++++++++++++
app/routes/__auth.tsx | 12 ++++++++--
app/routes/auth.tsx | 8 ++++++-
app/routes/index.tsx | 23 ++++---------------
app/routes/logout.tsx | 19 +---------------
6 files changed, 70 insertions(+), 66 deletions(-)
create mode 100644 app/components/logout.tsx
diff --git a/app/components/login.tsx b/app/components/login.tsx
index 7b1430f..8008232 100644
--- a/app/components/login.tsx
+++ b/app/components/login.tsx
@@ -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 {
+ errors?: { [k: string]: string };
+ redirectTo?: string;
+}
+export function Login({
+ children = "Log in with Discord",
+ errors,
+ redirectTo,
+ ...props
+}: LoginProps) {
return (
-
+
);
}
diff --git a/app/components/logout.tsx b/app/components/logout.tsx
new file mode 100644
index 0000000..bad0755
--- /dev/null
+++ b/app/components/logout.tsx
@@ -0,0 +1,26 @@
+import { Form } from "@remix-run/react";
+import type { ButtonHTMLAttributes } from "react";
+
+interface LoginProps extends ButtonHTMLAttributes {
+ errors?: { [k: string]: string };
+ redirectTo?: string;
+}
+
+export function Logout({
+ children = "Log out",
+ errors,
+ redirectTo,
+ ...props
+}: LoginProps) {
+ return (
+
+ );
+}
diff --git a/app/routes/__auth.tsx b/app/routes/__auth.tsx
index b1c9bc8..9c8250f 100644
--- a/app/routes/__auth.tsx
+++ b/app/routes/__auth.tsx
@@ -1,12 +1,20 @@
-import { Outlet } from "@remix-run/react";
+import { Outlet, useLocation } from "@remix-run/react";
import { Login } from "~/components/login";
import { useOptionalUser } from "~/utils";
export default function Auth() {
const user = useOptionalUser();
+ const location = useLocation();
+
if (!user) {
- return ;
+ return (
+
+ );
}
return ;
diff --git a/app/routes/auth.tsx b/app/routes/auth.tsx
index 31d419f..d7c82e1 100644
--- a/app/routes/auth.tsx
+++ b/app/routes/auth.tsx
@@ -20,5 +20,11 @@ export const action: ActionFunction = async ({ request }) => {
};
export default function LoginPage() {
- return ;
+ return (
+
+ );
}
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
index 1cf4613..461c76c 100644
--- a/app/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -1,4 +1,5 @@
-import { Link } from "@remix-run/react";
+import { Login } from "~/components/login";
+import { Logout } from "~/components/logout";
import { useOptionalUser } from "~/utils";
@@ -29,26 +30,10 @@ export default function Index() {
{user ? (
-
- Log out {user.email}
-
+
) : (
-
- Sign up
-
-
- Log In
-
+ Log in
)}
diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx
index 75184c9..5515218 100644
--- a/app/routes/logout.tsx
+++ b/app/routes/logout.tsx
@@ -1,5 +1,4 @@
import type { ActionFunction } from "@remix-run/node";
-import { Form } from "@remix-run/react";
import { logout } from "~/models/session.server";
@@ -15,23 +14,7 @@ export default function Logout({
return (
);
From f04555a288b1bd8dfc924f0dfc8d60b22164ddf8 Mon Sep 17 00:00:00 2001
From: Carl Vitullo
Date: Tue, 19 Nov 2024 01:32:01 -0500
Subject: [PATCH 3/5] Fix a bunch of dumb auth issues
---
app/models/session.server.ts | 53 ++++++++++++++++--------------------
app/routes/auth.tsx | 10 +++----
app/routes/discord-oauth.tsx | 20 ++++++++++++--
3 files changed, 47 insertions(+), 36 deletions(-)
diff --git a/app/models/session.server.ts b/app/models/session.server.ts
index 8249b5a..c3561ae 100644
--- a/app/models/session.server.ts
+++ b/app/models/session.server.ts
@@ -75,26 +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 as { state: string } | null) || 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() ||
- (() => {
- const soon = new Date();
- soon.setMinutes(soon.getMinutes() + 15);
- return soon.toString();
- })(),
- )
+ .set("expires", expires?.toString() || null)
.where("id", "=", id)
.execute();
},
@@ -196,14 +188,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,
@@ -212,21 +211,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,
@@ -253,14 +248,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");
+ // 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);
@@ -276,7 +271,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) {
diff --git a/app/routes/auth.tsx b/app/routes/auth.tsx
index d7c82e1..573e3c9 100644
--- a/app/routes/auth.tsx
+++ b/app/routes/auth.tsx
@@ -1,21 +1,21 @@
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,
});
};
diff --git a/app/routes/discord-oauth.tsx b/app/routes/discord-oauth.tsx
index c2b67e3..6b92d68 100644
--- a/app/routes/discord-oauth.tsx
+++ b/app/routes/discord-oauth.tsx
@@ -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,
+ );
};
From df943414a64643777ee5638a4f7a48d31833d632 Mon Sep 17 00:00:00 2001
From: Carl Vitullo
Date: Tue, 19 Nov 2024 02:53:35 -0500
Subject: [PATCH 4/5] Fix auth gate
---
app/models/session.server.ts | 5 ++---
app/routes/__auth.tsx | 5 +++++
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/app/models/session.server.ts b/app/models/session.server.ts
index c3561ae..fe9a584 100644
--- a/app/models/session.server.ts
+++ b/app/models/session.server.ts
@@ -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(
diff --git a/app/routes/__auth.tsx b/app/routes/__auth.tsx
index 9c8250f..d0e8f49 100644
--- a/app/routes/__auth.tsx
+++ b/app/routes/__auth.tsx
@@ -1,8 +1,13 @@
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();
From 394ce2efbdb9678c64b13f14e5703efd4a1d707c Mon Sep 17 00:00:00 2001
From: Carl Vitullo
Date: Tue, 19 Nov 2024 03:29:36 -0500
Subject: [PATCH 5/5] Basic homepage that isn't egregiously inaccurate
---
app/routes/index.tsx | 28 +++---
app/styles/tailwind.css | 187 +++++++++++++++++-----------------------
2 files changed, 92 insertions(+), 123 deletions(-)
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
index 461c76c..2e61bb7 100644
--- a/app/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -18,16 +18,13 @@ export default function Index() {
/>
-
+
+
- Job bot prototype
+ Euno
-
- Check the README.md file for instructions on how to get this
- project deployed.
-
{user ? (
@@ -37,13 +34,18 @@ export default function Index() {
)}
-
-
-
+
+ This is a development placeholder for Euno, a Discord moderation
+ bot.
+
+
+ Coming soon:
+
+ - ticketing??
+ - activity reports
+ - other fun things
+
+
diff --git a/app/styles/tailwind.css b/app/styles/tailwind.css
index 9f069ae..f8a8a64 100644
--- a/app/styles/tailwind.css
+++ b/app/styles/tailwind.css
@@ -1,5 +1,5 @@
/*
-! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com
+! tailwindcss v3.2.1 | MIT License | https://tailwindcss.com
*/
/*
@@ -187,6 +187,8 @@ textarea {
/* 1 */
font-size: 100%;
/* 1 */
+ font-weight: inherit;
+ /* 1 */
line-height: inherit;
/* 1 */
color: inherit;
@@ -353,13 +355,6 @@ input::-moz-placeholder, textarea::-moz-placeholder {
/* 2 */
}
-input:-ms-input-placeholder, textarea:-ms-input-placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
input::placeholder,
textarea::placeholder {
opacity: 1;
@@ -415,15 +410,62 @@ video {
height: auto;
}
-/*
-Ensure the default browser behavior of the `hidden` attribute.
-*/
+/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@@ -504,14 +546,14 @@ Ensure the default browser behavior of the `hidden` attribute.
margin-top: 2.5rem;
}
-.mt-16 {
- margin-top: 4rem;
-}
-
.block {
display: block;
}
+.inline {
+ display: inline;
+}
+
.flex {
display: flex;
}
@@ -548,34 +590,22 @@ Ensure the default browser behavior of the `hidden` attribute.
max-width: 80rem;
}
-.max-w-lg {
- max-width: 32rem;
+.max-w-xl {
+ max-width: 36rem;
}
.max-w-sm {
max-width: 24rem;
}
-.max-w-\[12rem\] {
- max-width: 12rem;
-}
-
.flex-col {
flex-direction: column;
}
-.items-center {
- align-items: center;
-}
-
.justify-center {
justify-content: center;
}
-.justify-between {
- justify-content: space-between;
-}
-
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -592,18 +622,6 @@ Ensure the default browser behavior of the `hidden` attribute.
border-radius: 0.25rem;
}
-.rounded-md {
- border-radius: 0.375rem;
-}
-
-.border {
- border-width: 1px;
-}
-
-.border-transparent {
- border-color: transparent;
-}
-
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
@@ -618,9 +636,13 @@ Ensure the default browser behavior of the `hidden` attribute.
background-color: rgba(254,204,27,0.5);
}
-.bg-yellow-500 {
+.bg-slate-700 {
--tw-bg-opacity: 1;
- background-color: rgb(234 179 8 / var(--tw-bg-opacity));
+ background-color: rgb(51 65 85 / var(--tw-bg-opacity));
+}
+
+.bg-opacity-40 {
+ --tw-bg-opacity: 0.4;
}
.object-cover {
@@ -628,9 +650,9 @@ Ensure the default browser behavior of the `hidden` attribute.
object-fit: cover;
}
-.px-8 {
- padding-left: 2rem;
- padding-right: 2rem;
+.px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
}
.py-2 {
@@ -638,24 +660,19 @@ Ensure the default browser behavior of the `hidden` attribute.
padding-bottom: 0.5rem;
}
-.px-4 {
- padding-left: 1rem;
- padding-right: 1rem;
+.px-8 {
+ padding-left: 2rem;
+ padding-right: 2rem;
}
-.py-3 {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
+.pb-8 {
+ padding-bottom: 2rem;
}
.pt-16 {
padding-top: 4rem;
}
-.pb-8 {
- padding-bottom: 2rem;
-}
-
.text-center {
text-align: center;
}
@@ -670,19 +687,10 @@ Ensure the default browser behavior of the `hidden` attribute.
line-height: 1.75rem;
}
-.text-base {
- font-size: 1rem;
- line-height: 1.5rem;
-}
-
.font-extrabold {
font-weight: 800;
}
-.font-medium {
- font-weight: 500;
-}
-
.uppercase {
text-transform: uppercase;
}
@@ -696,21 +704,11 @@ Ensure the default browser behavior of the `hidden` attribute.
color: rgb(255 255 255 / var(--tw-text-opacity));
}
-.text-red-500 {
- --tw-text-opacity: 1;
- color: rgb(239 68 68 / var(--tw-text-opacity));
-}
-
.text-yellow-500 {
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity));
}
-.text-yellow-700 {
- --tw-text-opacity: 1;
- color: rgb(161 98 7 / var(--tw-text-opacity));
-}
-
.mix-blend-multiply {
mix-blend-mode: multiply;
}
@@ -721,12 +719,6 @@ Ensure the default browser behavior of the `hidden` attribute.
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
-.shadow-sm {
- --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
- --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
.drop-shadow-md {
--tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
@@ -741,16 +733,6 @@ Ensure the default browser behavior of the `hidden` attribute.
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
-.hover\:bg-yellow-50:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(254 252 232 / var(--tw-bg-opacity));
-}
-
-.hover\:bg-yellow-600:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(202 138 4 / var(--tw-bg-opacity));
-}
-
.focus\:bg-blue-400:focus {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
@@ -770,10 +752,6 @@ Ensure the default browser behavior of the `hidden` attribute.
display: inline-grid;
}
- .sm\:max-w-3xl {
- max-width: 48rem;
- }
-
.sm\:max-w-none {
max-width: none;
}
@@ -813,11 +791,6 @@ Ensure the default browser behavior of the `hidden` attribute.
padding-right: 1.5rem;
}
- .sm\:px-8 {
- padding-left: 2rem;
- padding-right: 2rem;
- }
-
.sm\:pb-16 {
padding-bottom: 4rem;
}
@@ -826,26 +799,20 @@ Ensure the default browser behavior of the `hidden` attribute.
padding-top: 2rem;
}
- .sm\:pt-24 {
- padding-top: 6rem;
- }
-
.sm\:pb-14 {
padding-bottom: 3.5rem;
}
+ .sm\:pt-24 {
+ padding-top: 6rem;
+ }
+
.sm\:text-8xl {
font-size: 6rem;
line-height: 1;
}
}
-@media (min-width: 768px) {
- .md\:max-w-\[16rem\] {
- max-width: 16rem;
- }
-}
-
@media (min-width: 1024px) {
.lg\:px-8 {
padding-left: 2rem;