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 ( -
-
-
- - -
-
- {Object.values(errors || {}).map((error) => ( -

- {error} -

- ))} -
-
-
-
-
+
+ + +
); } 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 (
-
- -
-
- {Object.values(errors || {}).map((error) => ( -

- {error} -

- ))} -
-
-
+
); 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() {
)}
- - Remix - +

+ 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;