mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-12 23:15:00 +02:00
allow wildcard emails in email whitelist
This commit is contained in:
parent
9f1f2910e4
commit
61b34c8b16
4 changed files with 104 additions and 16 deletions
|
@ -13,9 +13,7 @@ import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||||
createResourceSession,
|
|
||||||
} from "@server/auth/sessions/resource";
|
|
||||||
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
@ -90,20 +88,48 @@ export async function authWithWhitelist(
|
||||||
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
|
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const resource = result?.resources;
|
let resource = result?.resources;
|
||||||
const org = result?.orgs;
|
let org = result?.orgs;
|
||||||
const whitelistedEmail = result?.resourceWhitelist;
|
let whitelistedEmail = result?.resourceWhitelist;
|
||||||
|
|
||||||
if (!whitelistedEmail) {
|
if (!whitelistedEmail) {
|
||||||
return next(
|
// if email is not found, check for wildcard email
|
||||||
createHttpError(
|
const wildcard = "*@" + email.split("@")[1];
|
||||||
HttpCode.UNAUTHORIZED,
|
|
||||||
createHttpError(
|
logger.debug("Checking for wildcard email: " + wildcard)
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Email is not whitelisted"
|
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) {
|
if (!org) {
|
||||||
|
|
|
@ -12,7 +12,17 @@ import { and, eq } from "drizzle-orm";
|
||||||
const setResourceWhitelistBodySchema = z
|
const setResourceWhitelistBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
emails: z
|
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)
|
.max(50)
|
||||||
.transform((v) => v.map((e) => e.toLowerCase()))
|
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
SettingsSectionFooter
|
SettingsSectionFooter
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
|
||||||
const UsersRolesFormSchema = z.object({
|
const UsersRolesFormSchema = z.object({
|
||||||
roles: z.array(
|
roles: z.array(
|
||||||
|
@ -665,10 +666,12 @@ export default function ResourceAuthenticationPage() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<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>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
{/* @ts-ignore */}
|
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<TagInput
|
<TagInput
|
||||||
{...field}
|
{...field}
|
||||||
|
@ -681,6 +684,17 @@ export default function ResourceAuthenticationPage() {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
.email()
|
.email()
|
||||||
|
.or(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Invalid email address. Wildcard (*) must be the entire local part."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
.safeParse(
|
.safeParse(
|
||||||
tag
|
tag
|
||||||
).success;
|
).success;
|
||||||
|
|
38
src/components/ui/info-popup.tsx
Normal file
38
src/components/ui/info-popup.tsx
Normal file
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue