feat: Whitelist user's from workflow spam scanning (#20717)
* Add whitelistWorkflows to user * Do not autolock whitelisted users * Add endpoint to whitelist a user * Get users data - include whitelistWorkflows * Whitelist a user on the user admin page * Fix typo * Add test * Use translations * Add `updateWhitelistWorkflows` to `UserRepository` * Use i18 for strings * Rename to whitelistWorkflows --------- Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
co-authored by
Benny Joo
parent
515f3263e0
commit
a82d616b79
@@ -3124,7 +3124,10 @@
|
||||
"resend_in_seconds": "Resend in {{seconds}} seconds",
|
||||
"resend": "Resend",
|
||||
"verification_email_sent": "Verification email sent",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
|
||||
"remove_whitelist_status": "Remove whitelist status",
|
||||
"whitelist_user_workflows": "Whitelist user workflows",
|
||||
"user_workflows_whitelisted": "User workflows whitelisted",
|
||||
"user_workflows_unwhitelisted": "User workflows unwhitelisted",
|
||||
"date_and_time": "Date & Time",
|
||||
"email_reminder_upcoming_event_notice": "This is a reminder about your upcoming event.",
|
||||
"email_reminder_triggered_by_workflow": "This reminder was triggered by a Workflow in Cal.",
|
||||
@@ -3136,5 +3139,6 @@
|
||||
"meeting_not_joined_question": "didn't join the meeting?",
|
||||
"reschedule_cta_short": "Reschedule here.",
|
||||
"you_and_conjunction": "You &",
|
||||
"email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal."
|
||||
"email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal.",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -122,6 +122,16 @@ function UsersTableBare() {
|
||||
const verifyWorkflows = trpc.viewer.admin.verifyWorkflows.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast("Workflows verified", "success");
|
||||
utils.viewer.admin.listPaginated.invalidate();
|
||||
},
|
||||
});
|
||||
const whitelistUserWorkflows = trpc.viewer.admin.whitelistUserWorkflows.useMutation({
|
||||
onSuccess: (data) => {
|
||||
showToast(
|
||||
data.whitelistWorkflows ? t("user_workflows_whitelisted") : t("user_workflows_unwhitelisted"),
|
||||
"success"
|
||||
);
|
||||
utils.viewer.admin.listPaginated.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -248,6 +258,19 @@ function UsersTableBare() {
|
||||
onClick: () => verifyWorkflows.mutate({ userId: user.id }),
|
||||
icon: "check",
|
||||
},
|
||||
{
|
||||
id: "whitelist-user-workflows",
|
||||
label: user.whitelistWorkflows
|
||||
? t("remove_whitelist_status")
|
||||
: t("whitelist_user_workflows"),
|
||||
onClick: () => {
|
||||
whitelistUserWorkflows.mutate({
|
||||
userId: user.id,
|
||||
whitelistWorkflows: !user.whitelistWorkflows,
|
||||
});
|
||||
},
|
||||
icon: "check",
|
||||
},
|
||||
{
|
||||
id: "impersonation",
|
||||
label: "Impersonate",
|
||||
|
||||
@@ -172,4 +172,23 @@ describe("scanWorkflowBody", () => {
|
||||
|
||||
expect(scheduleWorkflowNotifications).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle whitelisted user being flagged as spam", async () => {
|
||||
const payload = JSON.stringify({
|
||||
userId: 1,
|
||||
workflowStepIds: [1],
|
||||
});
|
||||
|
||||
prismaMock.workflowStep.findMany.mockResolvedValue([
|
||||
{ ...mockWorkflowStep, workflow: { user: { whitelistWorkflows: true } } },
|
||||
]);
|
||||
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
|
||||
mockAkismetCheckSpam.mockResolvedValue(true);
|
||||
|
||||
await scanWorkflowBody(payload);
|
||||
|
||||
expect(mockAkismetCheckSpam).toHaveBeenCalled();
|
||||
expect(prismaMock.workflowStep.update).not.toHaveBeenCalled();
|
||||
expect(lockUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ export async function scanWorkflowBody(payload: string) {
|
||||
select: {
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
whitelistWorkflows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -100,6 +101,13 @@ export async function scanWorkflowBody(payload: string) {
|
||||
const isSpam = await client.checkSpam(comment);
|
||||
|
||||
if (isSpam) {
|
||||
if (workflowStep.workflow.user?.whitelistWorkflows) {
|
||||
log.warn(
|
||||
`For whitelisted user, workflow step ${workflowStep.id} is spam with body ${workflowStep.reminderBody}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We won't delete the workflow step incase it is flagged as a false positive
|
||||
log.warn(`Workflow step ${workflowStep.id} is spam with body ${workflowStep.reminderBody}`);
|
||||
await lockUser("userId", userId.toString(), LockReason.SPAM_WORKFLOW_BODY);
|
||||
|
||||
@@ -927,4 +927,17 @@ export class UserRepository {
|
||||
data: { metadata: { ...existingMetadata, stripeCustomerId } },
|
||||
});
|
||||
}
|
||||
|
||||
static async updateWhitelistWorkflows({
|
||||
id,
|
||||
whitelistWorkflows,
|
||||
}: {
|
||||
id: number;
|
||||
whitelistWorkflows: boolean;
|
||||
}) {
|
||||
return prisma.user.update({
|
||||
where: { id },
|
||||
data: { whitelistWorkflows },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "whitelistWorkflows" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -384,6 +384,7 @@ model User {
|
||||
creationSource CreationSource?
|
||||
createdOrganizationOnboardings OrganizationOnboarding[] @relation("CreatedOrganizationOnboardings")
|
||||
filterSegments FilterSegment[]
|
||||
whitelistWorkflows Boolean @default(false)
|
||||
|
||||
@@unique([email])
|
||||
@@unique([email, username])
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ZAdminPasswordResetSchema } from "./sendPasswordReset.schema";
|
||||
import { ZSetSMSLockState } from "./setSMSLockState.schema";
|
||||
import { toggleFeatureFlag } from "./toggleFeatureFlag.procedure";
|
||||
import { ZAdminVerifyWorkflowsSchema } from "./verifyWorkflows.schema";
|
||||
import { ZWhitelistUserWorkflows } from "./whitelistUserWorkflows.schema";
|
||||
import {
|
||||
workspacePlatformCreateSchema,
|
||||
workspacePlatformUpdateSchema,
|
||||
@@ -55,6 +56,10 @@ export const adminRouter = router({
|
||||
const { default: handler } = await import("./verifyWorkflows.handler");
|
||||
return handler(opts);
|
||||
}),
|
||||
whitelistUserWorkflows: authedAdminProcedure.input(ZWhitelistUserWorkflows).mutation(async (opts) => {
|
||||
const { default: handler } = await import("./whitelistUserWorkflows.handler");
|
||||
return handler(opts);
|
||||
}),
|
||||
workspacePlatform: router({
|
||||
list: authedAdminProcedure.query(async () => {
|
||||
const { default: handler } = await import("./workspacePlatform/list.handler");
|
||||
|
||||
@@ -72,6 +72,7 @@ const listPaginatedHandler = async ({ input }: GetOptions) => {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
whitelistWorkflows: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
import type { TWhitelistUserWorkflows } from "./whitelistUserWorkflows.schema";
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TWhitelistUserWorkflows;
|
||||
};
|
||||
|
||||
export const whitelistUserWorkflows = async ({ input }: GetOptions) => {
|
||||
const { userId, whitelistWorkflows } = input;
|
||||
|
||||
const user = await UserRepository.updateWhitelistWorkflows({
|
||||
id: userId,
|
||||
whitelistWorkflows,
|
||||
});
|
||||
|
||||
return { whitelistWorkflows: user.whitelistWorkflows };
|
||||
};
|
||||
|
||||
export default whitelistUserWorkflows;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZWhitelistUserWorkflows = z.object({
|
||||
userId: z.number(),
|
||||
whitelistWorkflows: z.boolean(),
|
||||
});
|
||||
|
||||
export type TWhitelistUserWorkflows = z.infer<typeof ZWhitelistUserWorkflows>;
|
||||
Reference in New Issue
Block a user