From be2b1fd1ce55182054ca728ef2af337c0165b216 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 20:26:17 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20email=20whitelist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.mail.yml | 13 + server/db/pg/schema/schema.ts | 22 +- server/routers/policy/getResourcePolicy.ts | 15 +- .../EditPolicyAuthMethodsSectionForm.tsx | 6 +- .../resource-policy/EditPolicyForm.tsx | 9 +- .../EditPolicyNameSectionForm.tsx | 5 +- .../EditPolicyOtpEmailSectionForm.tsx | 270 +++++++++++------- .../EditPolicyUserRolesSectionForm.tsx | 5 +- 8 files changed, 220 insertions(+), 125 deletions(-) create mode 100644 docker-compose.mail.yml diff --git a/docker-compose.mail.yml b/docker-compose.mail.yml new file mode 100644 index 000000000..49aaead9f --- /dev/null +++ b/docker-compose.mail.yml @@ -0,0 +1,13 @@ +services: + mailer: + image: axllent/mailpit + ports: + - 8025:8025 + - 1025:1025 + volumes: + - mailpit-storage:/data + environment: + - MP_DATABASE=/data/mailpit.db + +volumes: + mailpit-storage: diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 935fab7c1..80fa24ac9 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -469,6 +469,16 @@ export const userPolicies = pgTable("userPolicies", { }) }); +export const resourcePolicyWhiteList = pgTable("resourcePolicyWhitelist", { + whitelistId: serial("id").primaryKey(), + email: varchar("email").notNull(), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + export const userInvites = pgTable("userInvites", { inviteId: varchar("inviteId").primaryKey(), orgId: varchar("orgId") @@ -621,12 +631,7 @@ export const resourceWhitelist = pgTable("resourceWhitelist", { email: varchar("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { - onDelete: "cascade" - }) + .references(() => resources.resourceId, { onDelete: "cascade" }) }); export const resourceOtp = pgTable("resourceOtp", { @@ -634,11 +639,6 @@ export const resourceOtp = pgTable("resourceOtp", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { - onDelete: "cascade" - }), email: varchar("email").notNull(), otpHash: varchar("otpHash").notNull(), expiresAt: bigint("expiresAt", { mode: "number" }).notNull() diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index cba2a9dba..124f67718 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -5,6 +5,7 @@ import { resourcePolicyHeaderAuth, resourcePolicyPassword, resourcePolicyPincode, + resourcePolicyWhiteList, rolePolicies, roles, userPolicies, @@ -116,7 +117,19 @@ async function query(params: z.infer) { ) .where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId)); - return { ...res, roles: policyRoles, users: policyUsers }; + const policyEmailWhiteList = await db + .select() + .from(resourcePolicyWhiteList) + .where( + eq(resourcePolicyWhiteList.resourcePolicyId, res.resourcePolicyId) + ); + + return { + ...res, + roles: policyRoles, + users: policyUsers, + emailWhiteList: policyEmailWhiteList + }; } export type GetResourcePolicyResponse = NonNullable< diff --git a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx index 957d60978..8a7efce1d 100644 --- a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -136,7 +137,6 @@ export function EditPolicyAuthMethodsSectionForm() { if (!isValid) return; const payload = form.getValues(); - console.log({ payload, policy }); const responseArray: Array | void>> = []; @@ -640,7 +640,7 @@ export function EditPolicyAuthMethodsSectionForm() { -
+ -
+ diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 8b0376107..4647c84f1 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -21,6 +21,7 @@ import { useMemo } from "react"; import { EditPolicyAuthMethodsSectionForm } from "./EditPolicyAuthMethodsSectionForm"; import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm"; import { EditPolicyUsersRolesSectionForm } from "./EditPolicyUserRolesSectionForm"; +import { EditPolicyOtpEmailSectionForm } from "./EditPolicyOtpEmailSectionForm"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── @@ -107,11 +108,11 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { /> + + {/* - -
+ -
+ diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index 917001158..93cb2b295 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -13,13 +14,14 @@ import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { + Form, FormControl, FormDescription, FormField, @@ -30,27 +32,64 @@ import { InfoPopup } from "@app/components/ui/info-popup"; import { InfoIcon, Plus } from "lucide-react"; -import { useState } from "react"; -import { UseFormReturn } from "react-hook-form"; +import { useActionState, useState } from "react"; +import { useForm, UseFormReturn, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; // ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── type PolicyOtpEmailSectionProps = { - form: UseFormReturn; emailEnabled: boolean; }; -export function PolicyOtpEmailSection({ - form, +export function EditPolicyOtpEmailSectionForm({ emailEnabled }: PolicyOtpEmailSectionProps) { const t = useTranslations(); - const [isExpanded, setIsExpanded] = useState(false); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); + + const { policy } = useResourcePolicyContext(); + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + emailWhitelistEnabled: true, + emails: true + }) + ), + defaultValues: { + emailWhitelistEnabled: policy.emailWhitelistEnabled, + emails: policy.emailWhiteList.map((email) => ({ + id: email.whitelistId.toString(), + text: email.email + })) + } + }); + + const whitelistEnabled = useWatch({ + control: form.control, + name: "emailWhitelistEnabled" + }); + + const [isExpanded, setIsExpanded] = useState(whitelistEnabled); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + console.log({ payload, policy }); + } + if (!isExpanded) { return ( @@ -77,100 +116,127 @@ export function PolicyOtpEmailSection({ } return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - { - setWhitelistEnabled(val); - form.setValue("emailWhitelistEnabled", val); - }} - disabled={!emailEnabled} - /> - - {whitelistEnabled && emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t("otpEmailEnter")} - tags={form.getValues().emails} - setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [Tag, ...Tag[]] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - +
+ + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + )} - /> - )} - - - + { + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag) + .success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t( + "otpEmailEnter" + )} + tags={ + form.getValues() + .emails ?? [] + } + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + + + + + + ); } diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx index 19c5bb719..d4d9b2de2 100644 --- a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -342,7 +343,7 @@ export function EditPolicyUsersRolesSectionForm({
-
+ -
+