ソースを参照

allow wildcard emails in email whitelist

Milo Schwartz 5 ヶ月 前
コミット
61b34c8b16

+ 39 - 13
server/routers/resource/authWithWhitelist.ts

@@ -13,9 +13,7 @@ import { NextFunction, Request, Response } from "express";
 import createHttpError from "http-errors";
 import { z } from "zod";
 import { fromError } from "zod-validation-error";
-import {
-    createResourceSession,
-} from "@server/auth/sessions/resource";
+import { createResourceSession } from "@server/auth/sessions/resource";
 import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
 import logger from "@server/logger";
 
@@ -90,20 +88,48 @@ export async function authWithWhitelist(
             .leftJoin(orgs, eq(orgs.orgId, resources.orgId))
             .limit(1);
 
-        const resource = result?.resources;
-        const org = result?.orgs;
-        const whitelistedEmail = result?.resourceWhitelist;
+        let resource = result?.resources;
+        let org = result?.orgs;
+        let whitelistedEmail = result?.resourceWhitelist;
 
         if (!whitelistedEmail) {
-            return next(
-                createHttpError(
-                    HttpCode.UNAUTHORIZED,
-                    createHttpError(
-                        HttpCode.BAD_REQUEST,
-                        "Email is not whitelisted"
+            // if email is not found, check for wildcard email
+            const wildcard = "*@" + email.split("@")[1];
+
+            logger.debug("Checking for wildcard email: " + wildcard)
+
+            const [result] = await db
+                .select()
+                .from(resourceWhitelist)
+                .where(
+                    and(
+                        eq(resourceWhitelist.resourceId, resourceId),
+                        eq(resourceWhitelist.email, wildcard)
                     )
                 )
-            );
+                .leftJoin(
+                    resources,
+                    eq(resources.resourceId, resourceWhitelist.resourceId)
+                )
+                .leftJoin(orgs, eq(orgs.orgId, resources.orgId))
+                .limit(1);
+
+            resource = result?.resources;
+            org = result?.orgs;
+            whitelistedEmail = result?.resourceWhitelist;
+
+            // if wildcard is still not found, return unauthorized
+            if (!whitelistedEmail) {
+                return next(
+                    createHttpError(
+                        HttpCode.UNAUTHORIZED,
+                        createHttpError(
+                            HttpCode.BAD_REQUEST,
+                            "Email is not whitelisted"
+                        )
+                    )
+                );
+            }
         }
 
         if (!org) {

+ 11 - 1
server/routers/resource/setResourceWhitelist.ts

@@ -12,7 +12,17 @@ import { and, eq } from "drizzle-orm";
 const setResourceWhitelistBodySchema = z
     .object({
         emails: z
-            .array(z.string().email())
+            .array(
+                z
+                    .string()
+                    .email()
+                    .or(
+                        z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
+                            message:
+                                "Invalid email address. Wildcard (*) must be the entire local part."
+                        })
+                    )
+            )
             .max(50)
             .transform((v) => v.map((e) => e.toLowerCase()))
     })

+ 16 - 2
src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx

@@ -48,6 +48,7 @@ import {
     SettingsSectionFooter
 } from "@app/components/Settings";
 import { SwitchInput } from "@app/components/SwitchInput";
+import { InfoPopup } from "@app/components/ui/info-popup";
 
 const UsersRolesFormSchema = z.object({
     roles: z.array(
@@ -665,10 +666,12 @@ export default function ResourceAuthenticationPage() {
                                             render={({ field }) => (
                                                 <FormItem>
                                                     <FormLabel>
-                                                        Whitelisted Emails
+                                                        <InfoPopup
+                                                            text="Whitelisted Emails"
+                                                            info="Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain."
+                                                        />
                                                     </FormLabel>
                                                     <FormControl>
-                                                        {/* @ts-ignore */}
                                                         {/* @ts-ignore */}
                                                         <TagInput
                                                             {...field}
@@ -681,6 +684,17 @@ export default function ResourceAuthenticationPage() {
                                                                 return z
                                                                     .string()
                                                                     .email()
+                                                                    .or(
+                                                                        z
+                                                                            .string()
+                                                                            .regex(
+                                                                                /^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
+                                                                                {
+                                                                                    message:
+                                                                                        "Invalid email address. Wildcard (*) must be the entire local part."
+                                                                                }
+                                                                            )
+                                                                    )
                                                                     .safeParse(
                                                                         tag
                                                                     ).success;

+ 38 - 0
src/components/ui/info-popup.tsx

@@ -0,0 +1,38 @@
+"use client";
+
+import React from "react";
+import { Info } from "lucide-react";
+import {
+    Popover,
+    PopoverContent,
+    PopoverTrigger
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+
+interface InfoPopupProps {
+    text: string;
+    info: string;
+}
+
+export function InfoPopup({ text, info }: InfoPopupProps) {
+    return (
+        <div className="flex items-center space-x-2">
+            <span>{text}</span>
+            <Popover>
+                <PopoverTrigger asChild>
+                    <Button
+                        variant="ghost"
+                        size="icon"
+                        className="h-6 w-6 rounded-full p-0"
+                    >
+                        <Info className="h-4 w-4" />
+                        <span className="sr-only">Show info</span>
+                    </Button>
+                </PopoverTrigger>
+                <PopoverContent className="w-80">
+                    <p className="text-sm text-muted-foreground">{info}</p>
+                </PopoverContent>
+            </Popover>
+        </div>
+    );
+}