diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 884f31e03d..38360c93c3 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server"; import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; +import { BoolEnv } from "./utils/boolEnv"; const EnvironmentSchema = z.object({ NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]), @@ -50,7 +51,7 @@ const EnvironmentSchema = z.object({ RESEND_API_KEY: z.string().optional(), SMTP_HOST: z.string().optional(), SMTP_PORT: z.coerce.number().optional(), - SMTP_SECURE: z.coerce.boolean().optional(), + SMTP_SECURE: BoolEnv.optional(), SMTP_USER: z.string().optional(), SMTP_PASSWORD: z.string().optional(), @@ -338,7 +339,7 @@ const EnvironmentSchema = z.object({ ALERT_RESEND_API_KEY: z.string().optional(), ALERT_SMTP_HOST: z.string().optional(), ALERT_SMTP_PORT: z.coerce.number().optional(), - ALERT_SMTP_SECURE: z.coerce.boolean().optional(), + ALERT_SMTP_SECURE: BoolEnv.optional(), ALERT_SMTP_USER: z.string().optional(), ALERT_SMTP_PASSWORD: z.string().optional(), ALERT_RATE_LIMITER_EMISSION_INTERVAL: z.coerce.number().int().default(2_500), @@ -378,7 +379,7 @@ const EnvironmentSchema = z.object({ MAX_SEQUENTIAL_INDEX_FAILURE_COUNT: z.coerce.number().default(96), LOOPS_API_KEY: z.string().optional(), - MARQS_DISABLE_REBALANCING: z.coerce.boolean().default(false), + MARQS_DISABLE_REBALANCING: BoolEnv.default(false), MARQS_VISIBILITY_TIMEOUT_MS: z.coerce .number() .int() @@ -456,7 +457,7 @@ const EnvironmentSchema = z.object({ .number() .int() .default(60_000 * 10), - RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: z.coerce.boolean().default(false), + RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: BoolEnv.default(false), RUN_ENGINE_PARENT_QUEUE_LIMIT: z.coerce.number().int().default(1000), RUN_ENGINE_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), RUN_ENGINE_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), diff --git a/apps/webapp/app/models/user.server.ts b/apps/webapp/app/models/user.server.ts index 2e16f2e956..8a381a8394 100644 --- a/apps/webapp/app/models/user.server.ts +++ b/apps/webapp/app/models/user.server.ts @@ -7,7 +7,7 @@ import { getDashboardPreferences, } from "~/services/dashboardPreferences.server"; export type { User } from "@trigger.dev/database"; - +import { assertEmailAllowed } from "~/utils/email"; type FindOrCreateMagicLink = { authenticationMethod: "MAGIC_LINK"; email: string; @@ -38,31 +38,29 @@ export async function findOrCreateUser(input: FindOrCreateUser): Promise { - if (env.WHITELISTED_EMAILS && !new RegExp(env.WHITELISTED_EMAILS).test(input.email)) { - throw new Error("This email is unauthorized"); - } +export async function findOrCreateMagicLinkUser({ + email, +}: FindOrCreateMagicLink): Promise { + assertEmailAllowed(email); const existingUser = await prisma.user.findFirst({ where: { - email: input.email, + email, }, }); const adminEmailRegex = env.ADMIN_EMAILS ? new RegExp(env.ADMIN_EMAILS) : undefined; - const makeAdmin = adminEmailRegex ? adminEmailRegex.test(input.email) : false; + const makeAdmin = adminEmailRegex ? adminEmailRegex.test(email) : false; const user = await prisma.user.upsert({ where: { - email: input.email, + email, }, update: { - email: input.email, + email, }, create: { - email: input.email, + email, authenticationMethod: "MAGIC_LINK", admin: makeAdmin, // only on create, to prevent automatically removing existing admins }, @@ -79,6 +77,8 @@ export async function findOrCreateGithubUser({ authenticationProfile, authenticationExtraParams, }: FindOrCreateGithub): Promise { + assertEmailAllowed(email); + 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 8511a889eb..b92c1bb39e 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -6,12 +6,14 @@ import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; 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 { Paragraph } from "~/components/primitives/Paragraph"; import { TextLink } from "~/components/primitives/TextLink"; import { isGithubAuthSupported } from "~/services/auth.server"; import { commitSession, setRedirectTo } from "~/services/redirectTo.server"; import { getUserId } from "~/services/session.server"; +import { getUserSession } from "~/services/sessionStorage.server"; import { requestUrl } from "~/utils/requestUrl.server"; export const meta: MetaFunction = ({ matches }) => { @@ -48,7 +50,11 @@ export async function loader({ request }: LoaderFunctionArgs) { const session = await setRedirectTo(request, redirectTo); return typedjson( - { redirectTo, showGithubAuth: isGithubAuthSupported }, + { + redirectTo, + showGithubAuth: isGithubAuthSupported, + authError: null, + }, { headers: { "Set-Cookie": await commitSession(session), @@ -56,9 +62,22 @@ export async function loader({ request }: LoaderFunctionArgs) { } ); } else { + const session = await getUserSession(request); + const error = session.get("auth:error"); + + let authError: string | undefined; + if (error) { + if ("message" in error) { + authError = error.message; + } else { + authError = JSON.stringify(error, null, 2); + } + } + return typedjson({ redirectTo: null, showGithubAuth: isGithubAuthSupported, + authError, }); } } @@ -81,7 +100,7 @@ export default function LoginPage() { Create an account or login
-
+
{data.showGithubAuth && (
By signing up you agree to our{" "} diff --git a/apps/webapp/app/services/email.server.ts b/apps/webapp/app/services/email.server.ts index 0f14fb28b0..425e2d4497 100644 --- a/apps/webapp/app/services/email.server.ts +++ b/apps/webapp/app/services/email.server.ts @@ -3,11 +3,11 @@ import { EmailClient, MailTransportOptions } from "emails"; import type { SendEmailOptions } from "remix-auth-email-link"; import { redirect } from "remix-typedjson"; import { env } from "~/env.server"; -import type { User } from "~/models/user.server"; import type { AuthUser } from "./authUser"; import { workerQueue } from "./worker.server"; import { logger } from "./logger.server"; import { singleton } from "~/utils/singleton"; +import { assertEmailAllowed } from "~/utils/email"; const client = singleton( "email-client", @@ -66,6 +66,8 @@ function buildTransportOptions(alerts?: boolean): MailTransportOptions { } export async function sendMagicLinkEmail(options: SendEmailOptions): Promise { + assertEmailAllowed(options.emailAddress); + // Auto redirect when in development mode if (env.NODE_ENV === "development") { throw redirect(options.magicLink); diff --git a/apps/webapp/app/utils/boolEnv.ts b/apps/webapp/app/utils/boolEnv.ts new file mode 100644 index 0000000000..a2609034e3 --- /dev/null +++ b/apps/webapp/app/utils/boolEnv.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const BoolEnv = z.preprocess((val) => { + if (typeof val !== "string") { + return val; + } + + return ["true", "1"].includes(val.toLowerCase().trim()); +}, z.boolean()); diff --git a/apps/webapp/app/utils/email.ts b/apps/webapp/app/utils/email.ts new file mode 100644 index 0000000000..de41ae592a --- /dev/null +++ b/apps/webapp/app/utils/email.ts @@ -0,0 +1,13 @@ +import { env } from "~/env.server"; + +export function assertEmailAllowed(email: string) { + if (!env.WHITELISTED_EMAILS) { + return; + } + + const regexp = new RegExp(env.WHITELISTED_EMAILS); + + if (!regexp.test(email)) { + throw new Error("This email is unauthorized"); + } +}