diff --git a/apps/webapp/app/models/user.server.ts b/apps/webapp/app/models/user.server.ts index 19d5a2ee75..13054b8bf1 100644 --- a/apps/webapp/app/models/user.server.ts +++ b/apps/webapp/app/models/user.server.ts @@ -66,6 +66,10 @@ export async function findOrCreateGithubUser({ authenticationProfile, authenticationExtraParams, }: FindOrCreateGithub): Promise<LoggedInUser> { + if (env.WHITELISTED_EMAILS && !new RegExp(env.WHITELISTED_EMAILS).test(email)) { + throw new Error("This email is unauthorized"); + } + const name = authenticationProfile._json.name; let avatarUrl: string | undefined = undefined; if (authenticationProfile.photos[0]) { diff --git a/apps/webapp/app/routes/login._index/route.tsx b/apps/webapp/app/routes/login._index/route.tsx index 84297410ff..54f546b23b 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -11,6 +11,7 @@ import { import { LoginPageLayout } from "~/components/LoginPageLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Fieldset } from "~/components/primitives/Fieldset"; +import { FormError } from "~/components/primitives/FormError"; import { Header1 } from "~/components/primitives/Headers"; import { NamedIcon } from "~/components/primitives/NamedIcon"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -19,6 +20,10 @@ import type { LoaderType as RootLoader } from "~/root"; import { isGithubAuthSupported } from "~/services/auth.server"; import { commitSession, setRedirectTo } from "~/services/redirectTo.server"; import { getUserId } from "~/services/session.server"; +import { + getUserSession, + commitSession as commitAuthSession, +} from "~/services/sessionStorage.server"; import { appEnvTitleTag } from "~/utils"; import { requestUrl } from "~/utils/requestUrl.server"; @@ -41,11 +46,23 @@ export async function loader({ request }: LoaderFunctionArgs) { const url = requestUrl(request); const redirectTo = url.searchParams.get("redirectTo"); + const session = await getUserSession(request); + const error = session.get("auth:error"); + + let githubError: string | undefined; + if (error) { + if ("message" in error) { + githubError = error.message; + } else { + githubError = JSON.stringify(error, null, 2); + } + } + if (redirectTo) { const session = await setRedirectTo(request, redirectTo); return typedjson( - { redirectTo, showGithubAuth: isGithubAuthSupported }, + { redirectTo, showGithubAuth: isGithubAuthSupported, githubError }, { headers: { "Set-Cookie": await commitSession(session), @@ -53,10 +70,12 @@ export async function loader({ request }: LoaderFunctionArgs) { } ); } else { - return typedjson({ - redirectTo: null, - showGithubAuth: isGithubAuthSupported, - }); + return typedjson( + { redirectTo: null, showGithubAuth: isGithubAuthSupported, githubError }, + { + headers: { "Set-Cookie": await commitAuthSession(session) }, + } + ); } } @@ -78,7 +97,7 @@ export default function LoginPage() { Create an account or login </Paragraph> <Fieldset className="w-full"> - <div className="flex flex-col gap-y-2"> + <div className="flex flex-col items-center gap-y-2"> {data.showGithubAuth && ( <Button type="submit" @@ -102,6 +121,8 @@ export default function LoginPage() { /> Continue with Email </LinkButton> + + {data.githubError && <FormError>{data.githubError}</FormError>} </div> <Paragraph variant="extra-small" className="mt-2 text-center"> By signing up you agree to our{" "} diff --git a/apps/webapp/app/routes/login.magic/route.tsx b/apps/webapp/app/routes/login.magic/route.tsx index c19b684658..1af4a83107 100644 --- a/apps/webapp/app/routes/login.magic/route.tsx +++ b/apps/webapp/app/routes/login.magic/route.tsx @@ -1,7 +1,7 @@ import { InboxArrowDownIcon } from "@heroicons/react/24/solid"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; -import { Form, useNavigation } from "@remix-run/react"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; import { getMatchesData, metaV1 } from "@remix-run/v1-meta"; import { TypedMetaFunction, @@ -74,6 +74,18 @@ export async function action({ request }: ActionFunctionArgs) { .parse(payload); if (action === "send") { + const { email } = z + .object({ + email: z.string().email(), + }) + .parse(payload); + + if (process.env.WHITELISTED_EMAILS && !new RegExp(process.env.WHITELISTED_EMAILS).test(email)) { + return { + error: "This email is unauthorized", + }; + } + return authenticator.authenticate("email-link", request, { successRedirect: "/login/magic", failureRedirect: "/login/magic", @@ -92,6 +104,7 @@ export async function action({ request }: ActionFunctionArgs) { export default function LoginMagicLinkPage() { const { magicLinkSent, magicLinkError } = useTypedLoaderData<typeof loader>(); + const actionData = useActionData<typeof action>(); const navigate = useNavigation(); const isLoading = @@ -176,6 +189,7 @@ export default function LoginMagicLinkPage() { /> {isLoading ? "Sending…" : "Send a magic link"} </Button> + {actionData?.error && <FormError>{actionData?.error}</FormError>} {magicLinkError && <FormError>{magicLinkError}</FormError>} </Fieldset> <Paragraph variant="extra-small" className="mb-4 mt-6 text-center">