From b47c75304eb63ff92111ee29acda12092dd8a4c8 Mon Sep 17 00:00:00 2001 From: alannnc Date: Wed, 20 Jul 2022 18:12:18 +0000 Subject: [PATCH 01/29] Fixed action button display while responsive and fix translations (#3469) --- .../ui/UsernameAvailability/PremiumTextfield.tsx | 6 ++---- .../ui/UsernameAvailability/UsernameTextfield.tsx | 8 +++----- apps/web/public/static/locales/en/common.json | 7 ++++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx index 8f86a9465e..6044eac72c 100644 --- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx @@ -274,16 +274,14 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
-

- {t("current")} {t("username")} -

+

{t("current_username")}

{currentUsername}

- {t("new")} {t("username")} + {t("new_username")}

{inputUsernameValue}

diff --git a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx index ce7c337ba9..325647f3be 100644 --- a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx @@ -144,7 +144,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
)} -
+
@@ -171,16 +171,14 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
-

- {t("current")} {t("username").toLocaleLowerCase()} -

+

{t("current_username")}

{currentUsername}

- {t("new")} {t("username").toLocaleLowerCase()} + {t("new_username")}

{inputUsernameValue}

diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 990879319a..077fd6163d 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -952,7 +952,7 @@ "nr_event_type_one": "{{count}} Event Type", "nr_event_type_other": "{{count}} Event Types", "add_action": "Add action", - "set_whereby_link":"Set Whereby link", + "set_whereby_link": "Set Whereby link", "invalid_whereby_link": "Please enter a valid Whereby Link", "set_around_link": "Set Around.Co link", "invalid_around_link": "Please enter a valid Around Link", @@ -993,6 +993,7 @@ "open": "Open", "close": "Close", "pro_feature_teams": "This is a Pro feature. Upgrade to Pro to see your team's availability.", - "pro_feature_workflows": "This is a Pro feature. Upgrade to Pro to automate your event notifications and reminders with Workflows." - + "pro_feature_workflows": "This is a Pro feature. Upgrade to Pro to automate your event notifications and reminders with Workflows.", + "current_username": "Current Username", + "new_username": "New Username" } From 5b7cd476a75db2c92758c5663a18b744d0d85048 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 21 Jul 2022 00:00:57 +0530 Subject: [PATCH 02/29] Feature: Routing Forms Webhook for Form Responses (#3444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../components/webhook/WebhookDialogForm.tsx | 17 ++++++--- .../webhook/WebhookListContainer.tsx | 12 ++++-- apps/web/lib/webhooks/constants.ts | 17 +++++++-- apps/web/lib/webhooks/sendPayload.tsx | 32 ++++++++++++++-- apps/web/pages/settings/developer.tsx | 17 ++++++++- apps/web/public/static/locales/en/common.json | 1 + apps/web/server/routers/viewer/webhook.tsx | 6 ++- .../routing_forms/api/responses/[formId].ts | 2 +- .../components/RoutingNavBar.tsx | 2 +- .../pages/form-edit/[...appPages].tsx | 35 +++++++++++++++++ .../playwright/tests/basic.test.ts | 3 ++ .../app-store/ee/routing_forms/trpc-router.ts | 38 ++++++++++++++++++- packages/app-store/ee/routing_forms/zod.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + 15 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 packages/prisma/migrations/20220719110415_form_submitted_webhook/migration.sql diff --git a/apps/web/components/webhook/WebhookDialogForm.tsx b/apps/web/components/webhook/WebhookDialogForm.tsx index 9fa9487952..4ca59e5c6b 100644 --- a/apps/web/components/webhook/WebhookDialogForm.tsx +++ b/apps/web/components/webhook/WebhookDialogForm.tsx @@ -3,14 +3,13 @@ import { Controller, useForm } from "react-hook-form"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; -import { Tooltip } from "@calcom/ui"; import Button from "@calcom/ui/Button"; import { DialogFooter } from "@calcom/ui/Dialog"; import Switch from "@calcom/ui/Switch"; import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@calcom/ui/form/fields"; import { trpc } from "@lib/trpc"; -import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants"; +import { WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP } from "@lib/webhooks/constants"; import customTemplate, { hasTemplateIntegration } from "@lib/webhooks/integrationTemplate"; import { TWebhook } from "@components/webhook/WebhookListItem"; @@ -19,14 +18,20 @@ import WebhookTestDisclosure from "@components/webhook/WebhookTestDisclosure"; export default function WebhookDialogForm(props: { eventTypeId?: number; defaultValues?: TWebhook; + app?: string; handleClose: () => void; }) { const { t } = useLocale(); const utils = trpc.useContext(); + const appId = props.app; + + const triggers = !appId + ? WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP["core"] + : WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP[appId as keyof typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP]; const { defaultValues = { id: "", - eventTriggers: WEBHOOK_TRIGGER_EVENTS, + eventTriggers: triggers, subscriberUrl: "", active: true, payloadTemplate: null, @@ -60,8 +65,8 @@ export default function WebhookDialogForm(props: { form={form} handleSubmit={async (event) => { const e = changeSecret - ? { ...event, eventTypeId: props.eventTypeId } - : { ...event, secret: currentSecret, eventTypeId: props.eventTypeId }; + ? { ...event, eventTypeId: props.eventTypeId, appId } + : { ...event, secret: currentSecret, eventTypeId: props.eventTypeId, appId }; if (!useCustomPayloadTemplate && event.payloadTemplate) { event.payloadTemplate = null; } @@ -115,7 +120,7 @@ export default function WebhookDialogForm(props: {
{t("event_triggers")} - {WEBHOOK_TRIGGER_EVENTS.map((key) => ( + {triggers.map((key) => ( (null); @@ -66,6 +70,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) { !isOpen && setNewWebhookModal(false)}> setNewWebhookModal(false)} /> @@ -76,6 +81,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) { {editing && ( setEditModalOpen(false)} diff --git a/apps/web/lib/webhooks/constants.ts b/apps/web/lib/webhooks/constants.ts index ddd5a44507..ee02568bce 100644 --- a/apps/web/lib/webhooks/constants.ts +++ b/apps/web/lib/webhooks/constants.ts @@ -1,8 +1,17 @@ import { WebhookTriggerEvents } from "@prisma/client"; // this is exported as we can't use `WebhookTriggerEvents` in the frontend straight-off + +export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = { + core: [ + WebhookTriggerEvents.BOOKING_CANCELLED, + WebhookTriggerEvents.BOOKING_CREATED, + WebhookTriggerEvents.BOOKING_RESCHEDULED, + ] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"], + routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"], +}; + export const WEBHOOK_TRIGGER_EVENTS = [ - WebhookTriggerEvents.BOOKING_CANCELLED, - WebhookTriggerEvents.BOOKING_CREATED, - WebhookTriggerEvents.BOOKING_RESCHEDULED, -] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"]; + ...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core, + ...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms, +] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"]; diff --git a/apps/web/lib/webhooks/sendPayload.tsx b/apps/web/lib/webhooks/sendPayload.tsx index 4bfa6b440c..5eb1d019cf 100644 --- a/apps/web/lib/webhooks/sendPayload.tsx +++ b/apps/web/lib/webhooks/sendPayload.tsx @@ -34,10 +34,7 @@ const sendPayload = async ( bookingId?: number; } ) => { - const { subscriberUrl, appId, payloadTemplate: template } = webhook; - if (!subscriberUrl || !data) { - throw new Error("Missing required elements to send webhook payload."); - } + const { appId, payloadTemplate: template } = webhook; const contentType = !template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded"; @@ -59,6 +56,33 @@ const sendPayload = async ( }); } + return _sendPayload(secretKey, triggerEvent, createdAt, webhook, body, contentType); +}; + +export const sendGenericWebhookPayload = async ( + secretKey: string | null, + triggerEvent: string, + createdAt: string, + webhook: Pick, + data: Record +) => { + const body = JSON.stringify(data); + return _sendPayload(secretKey, triggerEvent, createdAt, webhook, body, "application/json"); +}; + +const _sendPayload = async ( + secretKey: string | null, + triggerEvent: string, + createdAt: string, + webhook: Pick, + body: string, + contentType: "application/json" | "application/x-www-form-urlencoded" +) => { + const { subscriberUrl } = webhook; + if (!subscriberUrl || !body) { + throw new Error("Missing required elements to send webhook payload."); + } + const secretSignature = secretKey ? createHmac("sha256", secretKey).update(`${body}`).digest("hex") : "no-secret-provided"; diff --git a/apps/web/pages/settings/developer.tsx b/apps/web/pages/settings/developer.tsx index b032c4be5f..2c052e6bee 100644 --- a/apps/web/pages/settings/developer.tsx +++ b/apps/web/pages/settings/developer.tsx @@ -1,15 +1,30 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import ApiKeyListContainer from "@ee/components/apiKeys/ApiKeyListContainer"; +import { trpc } from "@lib/trpc"; + import SettingsShell from "@components/SettingsShell"; import WebhookListContainer from "@components/webhook/WebhookListContainer"; export default function Settings() { const { t } = useLocale(); + const { data: routingForms } = trpc.useQuery([ + "viewer.appById", + { + appId: "routing_forms", + }, + ]); return ( - + + {routingForms && ( + + )} ); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 077fd6163d..6e8e59f94a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -344,6 +344,7 @@ "booking_cancelled": "Booking Cancelled", "booking_rescheduled": "Booking Rescheduled", "booking_created": "Booking Created", + "form_submitted": "Form Submitted", "event_triggers": "Event Triggers", "subscriber_url": "Subscriber Url", "create_new_webhook": "Create a new webhook", diff --git a/apps/web/server/routers/viewer/webhook.tsx b/apps/web/server/routers/viewer/webhook.tsx index 1456260bd7..39700287aa 100644 --- a/apps/web/server/routers/viewer/webhook.tsx +++ b/apps/web/server/routers/viewer/webhook.tsx @@ -71,11 +71,13 @@ export const webhookRouter = createProtectedRouter() input: z .object({ eventTypeId: z.number().optional(), + appId: z.string().optional(), }) .optional(), async resolve({ ctx, input }) { const where: Prisma.WebhookWhereInput = { - AND: [{ appId: null /* Don't mixup zapier webhooks with normal ones */ }], + /* Don't mixup zapier webhooks with normal ones */ + AND: [{ appId: !input?.appId ? null : input.appId }], }; if (Array.isArray(where.AND)) { if (input?.eventTypeId) { @@ -84,6 +86,7 @@ export const webhookRouter = createProtectedRouter() where.AND?.push({ userId: ctx.user.id }); } } + return await ctx.prisma.webhook.findMany({ where, }); @@ -108,6 +111,7 @@ export const webhookRouter = createProtectedRouter() }, }); } + return await ctx.prisma.webhook.create({ data: { id: v4(), diff --git a/packages/app-store/ee/routing_forms/api/responses/[formId].ts b/packages/app-store/ee/routing_forms/api/responses/[formId].ts index 9abd79c39b..af73e86941 100644 --- a/packages/app-store/ee/routing_forms/api/responses/[formId].ts +++ b/packages/app-store/ee/routing_forms/api/responses/[formId].ts @@ -38,7 +38,6 @@ async function* getResponses(formId: string) { } else { serializedValue = escapeCsvText(value); } - csvLineColumns.push(`"Field Label :=> Value"`); csvLineColumns.push(`"${label} :=> ${serializedValue}"`); } csv.push(csvLineColumns.join(",")); @@ -48,6 +47,7 @@ async function* getResponses(formId: string) { } return ""; } + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { args } = req.query; if (!args) { diff --git a/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx b/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx index 0cd701cc83..7eb792ecd4 100644 --- a/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx +++ b/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx @@ -11,7 +11,7 @@ export default function RoutingNavBar({ }) { const tabs = [ { - name: "Fields", + name: "Form", href: `${appUrl}/form-edit/${form?.id}`, }, { diff --git a/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx b/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx index 5f4e1ac110..57de747685 100644 --- a/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx +++ b/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx @@ -1,5 +1,6 @@ import { TrashIcon, PlusIcon, ArrowUpIcon, CollectionIcon, ArrowDownIcon } from "@heroicons/react/solid"; import { useRouter } from "next/router"; +import { useState, useEffect } from "react"; import { useForm, UseFormReturn, useFieldArray, Controller } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; @@ -72,6 +73,22 @@ function Field({ fn: () => void; }; }) { + const [identifier, _setIdentifier] = useState(hookForm.getValues(`${hookFieldNamespace}.identifier`)); + + const setUserChangedIdentifier = (val: string) => { + _setIdentifier(val); + // Also, update the form identifier so tha it can be persisted + hookForm.setValue(`${hookFieldNamespace}.identifier`, val); + }; + + const label = hookForm.watch(`${hookFieldNamespace}.label`); + + useEffect(() => { + if (!hookForm.getValues(`${hookFieldNamespace}.identifier`)) { + _setIdentifier(label); + } + }, [label, hookFieldNamespace, hookForm]); + return (
+
+
+ +
+
+ setUserChangedIdentifier(e.target.value)} + className="block w-full rounded-sm border-gray-300 text-sm" + /> +
+
@@ -2281,6 +2287,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }) : []; + // Find the current users memebership so we can check role to enable/disable deletion. + // Sets to null if no membership is found - this must mean we are in a none team event type + const currentUserMembership = + eventTypeObject.team?.members.find((el) => el.user.id === session.user.id) ?? null; + return { props: { session, @@ -2292,6 +2303,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => hasPaymentIntegration, hasGiphyIntegration, currency, + currentUserMembership, }, }; }; diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index b9bc4dcacb..2e28209fab 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -49,6 +49,8 @@ import Avatar from "@components/ui/Avatar"; import AvatarGroup from "@components/ui/AvatarGroup"; import Badge from "@components/ui/Badge"; +import { TRPCClientError } from "@trpc/react"; + type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"]; type EventTypeGroupProfile = EventTypeGroups[number]["profile"]; interface EventTypeListHeadingProps { @@ -193,6 +195,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); setDeleteDialogOpen(false); + } else if (err instanceof TRPCClientError) { + showToast(err.message, "error"); } }, }); @@ -332,19 +336,22 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL /> - - - + {/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */} + {(group.metadata?.readOnly === false || group.metadata.readOnly === null) && ( + + + + )} From 5a9ca309d3542d0a691a0387415a1ad3c264eba7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 17:41:57 +0200 Subject: [PATCH 11/29] New Crowdin translations by Github Action (#3461) Co-authored-by: Crowdin Bot --- apps/web/public/static/locales/es/common.json | 6 +++--- apps/web/public/static/locales/pt/common.json | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 24a97916bb..5ff9d02b60 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -92,7 +92,7 @@ "meeting_password": "Contraseña de la reunión", "meeting_url": "URL de la reunión", "meeting_request_rejected": "Su solicitud de reunión ha sido rechazada", - "rescheduled_event_type_subject": "Reprogramado: {{eventType}} con {{name}} el {{date}}", + "rescheduled_event_type_subject": "Solicitud de reprogramación enviada: {{eventType}} con {{name}} el {{date}}", "requested_to_reschedule_subject_attendee": "Reprogramar acción requerida: reserva una nueva hora para {{eventType}} con {{name}}", "rejected_event_type_with_organizer": "Rechazado: {{eventType}} con {{organizer}} en {{date}}", "hi": "Hola", @@ -934,9 +934,9 @@ "attendee_name": "Nombre del asistente", "broken_integration": "Integración rota", "problem_adding_video_link": "Hubo un problema al añadir un enlace de vídeo", - "problem_updating_calendar": "Hubo un problema al actualizar su calendario", + "problem_updating_calendar": "Hubo un problema al actualizar tu calendario", "new_seat_subject": "Nuevo asistente {{name}} en {{eventType}} el {{date}}", "new_seat_title": "Alguien se ha añadido a un evento", "app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.", - "invalid_number": "Número de teléfono inválido" + "invalid_number": "Número de teléfono no válido" } diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index a56df548cd..d58c8be836 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -286,6 +286,8 @@ "cannot_cancel_booking": "Não pode cancelar esta reserva", "reschedule_instead": "Em vez disso, também poderia reagendar", "event_is_in_the_past": "O evento já passou", + "cancelling_event_recurring": "O evento é uma instância de um evento recorrente.", + "cancelling_all_recurring": "Estas são todas as restantes instâncias do evento recorrente.", "error_with_status_code_occured": "Ocorreu um erro com o código de estado {{status}}", "booking_already_cancelled": "Esta reserva já foi cancelada", "go_back_home": "Voltar à Página Inicial", @@ -342,6 +344,7 @@ "booking_cancelled": "Reserva Cancelada", "booking_rescheduled": "Reserva Reagendada", "booking_created": "Reserva Criada", + "form_submitted": "Formulário enviado", "event_triggers": "Causadores de eventos", "subscriber_url": "URL do assinante", "create_new_webhook": "Criar um novo webhook", @@ -991,5 +994,7 @@ "open": "Abrir", "close": "Fechar", "pro_feature_teams": "Esta é uma funcionalidade Pro. Actualize para o Pro para ver a disponibilidade da sua equipa.", - "pro_feature_workflows": "Esta é uma funcionalidade Pro. Actualize para o Pro para automatizar as suas notificações de eventos e lembretes com os fluxos de trabalho." + "pro_feature_workflows": "Esta é uma funcionalidade Pro. Actualize para o Pro para automatizar as suas notificações de eventos e lembretes com os fluxos de trabalho.", + "current_username": "Nome de utilizador actual", + "new_username": "Novo nome de utilizador" } From 2eedf74eb73a5254b880d5176a74d894704ec46d Mon Sep 17 00:00:00 2001 From: GitStart <1501599+gitstart@users.noreply.github.com> Date: Thu, 21 Jul 2022 17:05:13 +0100 Subject: [PATCH 12/29] Apply suggestions from code review (#3480) Co-authored-by: gitstart Co-authored-by: Peer Richelsen --- apps/web/components/availability/Schedule.tsx | 27 ++++++++++--------- apps/web/public/static/locales/en/common.json | 1 + apps/web/public/static/locales/fr/common.json | 1 + 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/web/components/availability/Schedule.tsx b/apps/web/components/availability/Schedule.tsx index 22a60acb19..01df625fc6 100644 --- a/apps/web/components/availability/Schedule.tsx +++ b/apps/web/components/availability/Schedule.tsx @@ -8,7 +8,8 @@ import { GroupBase, Props } from "react-select"; import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/Button"; -import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/Dropdown"; +import Dropdown, { DropdownMenuContent } from "@calcom/ui/Dropdown"; +import { Tooltip } from "@calcom/ui/Tooltip"; import { defaultDayRange } from "@lib/availability"; import { weekdayNames } from "@lib/core/i18n/weekday"; @@ -202,7 +203,7 @@ export const DayRanges = ({ const { setValue, watch } = useFormContext(); // XXX: Hack to make copying times work; `fields` is out of date until save. const watcher = watch(name); - + const { t } = useLocale(); const { fields, replace, append, remove } = useFieldArray({ name, }); @@ -242,16 +243,18 @@ export const DayRanges = ({ {index === 0 && (
- +
+ + + + ); +}; + +export default DisableTeamImpersonation; diff --git a/apps/web/components/team/MemberListItem.tsx b/apps/web/components/team/MemberListItem.tsx index f77e022228..952308e3bd 100644 --- a/apps/web/components/team/MemberListItem.tsx +++ b/apps/web/components/team/MemberListItem.tsx @@ -1,6 +1,7 @@ -import { PencilIcon, UserRemoveIcon } from "@heroicons/react/outline"; +import { LockClosedIcon, PencilIcon, UserRemoveIcon } from "@heroicons/react/outline"; import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon } from "@heroicons/react/solid"; import { MembershipRole } from "@prisma/client"; +import { signIn } from "next-auth/react"; import Link from "next/link"; import { useState } from "react"; @@ -39,6 +40,7 @@ export default function MemberListItem(props: Props) { const utils = trpc.useContext(); const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false); const [showTeamAvailabilityModal, setShowTeamAvailabilityModal] = useState(false); + const [showImpersonateModal, setShowImpersonateModal] = useState(false); const removeMemberMutation = trpc.useMutation("viewer.teams.removeMember", { async onSuccess() { @@ -147,6 +149,24 @@ export default function MemberListItem(props: Props) { + {/* Only show impersonate box if - The user has impersonation enabled, + They have accepted the team invite, and it is enabled for this instance */} + {!props.member.disableImpersonation && + props.member.accepted && + process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true" && ( + <> + + + + + + )} @@ -185,6 +205,39 @@ export default function MemberListItem(props: Props) { onExit={() => setShowChangeMemberRoleModal(false)} /> )} + {showImpersonateModal && props.member.username && ( + setShowImpersonateModal(false)}> + <> +
+
+ +
+
+
{ + e.preventDefault(); + await signIn("impersonation-auth", { + username: props.member.username, + teamId: props.team.id, + }); + }}> +

+ {t("impersonate_user_tip")} +

+
+ + +
+
+ +
+ )} {showTeamAvailabilityModal && ( , + impersonatedByUID: number +) => { + // Log impersonations for audit purposes + await prisma.impersonations.create({ + data: { + impersonatedBy: { + connect: { + id: impersonatedByUID, + }, + }, + impersonatedUser: { + connect: { + id: impersonatedUser.id, + }, + }, + }, + }); + + const obj = { + id: impersonatedUser.id, + username: impersonatedUser.username, + email: impersonatedUser.email, + name: impersonatedUser.name, + role: impersonatedUser.role, + impersonatedByUID, + }; + + return obj; +}; + const ImpersonationProvider = CredentialsProvider({ id: "impersonation-auth", name: "Impersonation", type: "credentials", credentials: { - username: { label: "Username", type: "text " }, + username: { type: "text" }, + teamId: { type: "text" }, }, async authorize(creds, req) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore need to figure out how to correctly type this const session = await getSession({ req }); - if (session?.user.role !== "ADMIN") { - throw new Error("You do not have permission to do this."); - } + const teamId = creds?.teamId ? asNumberOrThrow(creds.teamId) : undefined; if (session?.user.username === creds?.username) { throw new Error("You cannot impersonate yourself."); } - const user = await prisma.user.findUnique({ + if (!creds?.username) throw new Error("Username must be present"); + // If you are an ADMIN we return way before team impersonation logic is executed, so NEXT_PUBLIC_TEAM_IMPERSONATION certainly true + if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") { + throw new Error("You do not have permission to do this."); + } + + // Get user who is being impersonated + const impersonatedUser = await prisma.user.findUnique({ where: { username: creds?.username, }, - }); - - if (!user) { - throw new Error("This user does not exist"); - } - - if (user.disableImpersonation) { - throw new Error("This user has disabled Impersonation."); - } - - // Log impersonations for audit purposes - await prisma.impersonations.create({ - data: { - impersonatedBy: { - connect: { - id: session.user.id, + select: { + id: true, + username: true, + role: true, + name: true, + email: true, + disableImpersonation: true, + teams: { + where: { + disableImpersonation: false, // Ensure they have impersonation enabled + accepted: true, // Ensure they are apart of the team and not just invited. + team: { + id: teamId, // Bring back only the right team + }, }, - }, - impersonatedUser: { - connect: { - id: user.id, + select: { + teamId: true, + disableImpersonation: true, }, }, }, }); - const obj = { - id: user.id, - username: user.username, - email: user.email, - name: user.name, - role: user.role, - impersonatedByUID: session?.user.id, - }; - return obj; + // Check if impersonating is allowed for this user + if (!impersonatedUser) { + throw new Error("This user does not exist"); + } + + if (session?.user.role === "ADMIN") { + if (impersonatedUser.disableImpersonation) { + throw new Error("This user has disabled Impersonation."); + } + return auditAndReturnNextUser(impersonatedUser, session?.user.id as number); + } + + // Check session + const sessionUserFromDb = await prisma.user.findUnique({ + where: { + id: session?.user.id, + }, + include: { + teams: { + where: { + AND: [ + { + role: { + in: ["ADMIN", "OWNER"], + }, + }, + { + team: { + id: teamId, + }, + }, + ], + }, + }, + }, + }); + + if (sessionUserFromDb?.teams.length === 0 || impersonatedUser.teams.length === 0) { + throw new Error("You do not have permission to do this."); + } + + return auditAndReturnNextUser(impersonatedUser, session?.user.id as number); }, }); diff --git a/apps/web/lib/queries/teams/index.ts b/apps/web/lib/queries/teams/index.ts index b60081a0da..7b3c2f3932 100644 --- a/apps/web/lib/queries/teams/index.ts +++ b/apps/web/lib/queries/teams/index.ts @@ -63,6 +63,7 @@ export async function getTeamWithMembers(id?: number, slug?: string) { isMissingSeat: obj.user.plan === UserPlan.FREE, role: membership?.role, accepted: membership?.accepted, + disableImpersonation: membership?.disableImpersonation, }; }); diff --git a/apps/web/pages/settings/teams/[id]/index.tsx b/apps/web/pages/settings/teams/[id]/index.tsx index 2f1a4d417e..7032bd7568 100644 --- a/apps/web/pages/settings/teams/[id]/index.tsx +++ b/apps/web/pages/settings/teams/[id]/index.tsx @@ -11,10 +11,12 @@ import SAMLConfiguration from "@ee/components/saml/Configuration"; import { QueryCell } from "@lib/QueryCell"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; +import useCurrentUserId from "@lib/hooks/useCurrentUserId"; import { useLocale } from "@lib/hooks/useLocale"; import { trpc } from "@lib/trpc"; import Shell from "@components/Shell"; +import DisableTeamImpersonation from "@components/team/DisableTeamImpersonation"; import MemberInvitationModal from "@components/team/MemberInvitationModal"; import MemberList from "@components/team/MemberList"; import TeamSettings from "@components/team/TeamSettings"; @@ -25,6 +27,7 @@ import Avatar from "@components/ui/Avatar"; export function TeamSettingsPage() { const { t } = useLocale(); const router = useRouter(); + const userId = useCurrentUserId(); const upgraded = router.query.upgraded as string; @@ -165,6 +168,7 @@ export function TeamSettingsPage() { {isAdmin && } + {userId && }
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a0c35c80ac..62881b9158 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -838,6 +838,7 @@ "impersonate": "Impersonate", "user_impersonation_heading": "User Impersonation", "user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.", + "team_impersonation_description": "Allows your team admins to temporarily sign in as you.", "impersonate_user_tip": "All uses of this feature is audited.", "impersonating_user_warning": "Impersonating username \"{{user}}\".", "impersonating_stop_instructions": "<0>Click Here to stop.", diff --git a/apps/web/server/routers/viewer/teams.tsx b/apps/web/server/routers/viewer/teams.tsx index 7a32ae2c98..f6e7238669 100644 --- a/apps/web/server/routers/viewer/teams.tsx +++ b/apps/web/server/routers/viewer/teams.tsx @@ -1,5 +1,6 @@ import { MembershipRole, Prisma, UserPlan } from "@prisma/client"; import { randomBytes } from "crypto"; +import { resolve } from "path"; import { z } from "zod"; import { getUserAvailability } from "@calcom/core/getUserAvailability"; @@ -460,4 +461,54 @@ export const viewerTeamsRouter = createProtectedRouter() async resolve({ ctx, input }) { return await ensureSubscriptionQuantityCorrectness(ctx.user.id, input.teamId); }, + }) + .query("getMembershipbyUser", { + input: z.object({ + teamId: z.number(), + memberId: z.number(), + }), + async resolve({ ctx, input }) { + if (ctx.user.id !== input.memberId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You cannot view memberships that are not your own.", + }); + } + + return await ctx.prisma.membership.findUnique({ + where: { + userId_teamId: { + userId: input.memberId, + teamId: input.teamId, + }, + }, + }); + }, + }) + .mutation("updateMembership", { + input: z.object({ + teamId: z.number(), + memberId: z.number(), + disableImpersonation: z.boolean(), + }), + async resolve({ ctx, input }) { + if (ctx.user.id !== input.memberId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You cannot edit memberships that are not your own.", + }); + } + + return await ctx.prisma.membership.update({ + where: { + userId_teamId: { + userId: input.memberId, + teamId: input.teamId, + }, + }, + data: { + disableImpersonation: input.disableImpersonation, + }, + }); + }, }); diff --git a/packages/prisma/migrations/20220719144253_disabled_impersonation_teams/migration.sql b/packages/prisma/migrations/20220719144253_disabled_impersonation_teams/migration.sql new file mode 100644 index 0000000000..9a38e15bf7 --- /dev/null +++ b/packages/prisma/migrations/20220719144253_disabled_impersonation_teams/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Membership" ADD COLUMN "disableImpersonation" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 6e1ea1a6b5..752857ef33 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -206,12 +206,13 @@ enum MembershipRole { } model Membership { - teamId Int - userId Int - accepted Boolean @default(false) - role MembershipRole - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + teamId Int + userId Int + accepted Boolean @default(false) + role MembershipRole + team Team @relation(fields: [teamId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + disableImpersonation Boolean @default(false) @@id([userId, teamId]) } diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index 30c7991f1f..812bf9fd10 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -1,4 +1,4 @@ -import { BookingStatus, MembershipRole, Prisma, UserPlan } from "@prisma/client"; +import { BookingStatus, MembershipRole, Prisma, UserPermissionRole, UserPlan } from "@prisma/client"; import { uuid } from "short-uuid"; import dayjs from "@calcom/dayjs"; @@ -18,6 +18,7 @@ async function createUserAndEventType(opts: { name: string; completedOnboarding?: boolean; timeZone?: string; + role?: UserPermissionRole; }; eventTypes: Array< Prisma.EventTypeCreateInput & { @@ -468,6 +469,18 @@ async function main() { eventTypes: [], }); + await createUserAndEventType({ + user: { + email: "admin@example.com", + password: "admin", + username: "admin", + name: "Admin Example", + plan: "PRO", + role: "ADMIN", + }, + eventTypes: [], + }); + const pro2UserTeam = await createUserAndEventType({ user: { email: "teampro2@example.com", @@ -536,6 +549,7 @@ async function main() { { id: pro2UserTeam.id, username: pro2UserTeam.name || "Unknown", + role: "MEMBER", }, { id: pro3UserTeam.id, From c93e238e1521a61ce1f1bd2a0317c979175532a4 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:56:20 -0400 Subject: [PATCH 15/29] Fixes bug that new line is not working in workflow reminder emails (#3452) * add html format to email template * remove workflow reminder email * fix that text was used instead of html * fixes that email subject was used instead of body * remove \n\n from text template Co-authored-by: CarinaWolli Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../reminders/emailReminderManager.ts | 32 ++++++++++------- .../templates/emailReminderTemplate.ts | 16 ++++++--- .../cron/workflows/scheduleEmailReminders.ts | 27 +++++++-------- packages/emails/email-manager.ts | 13 ------- .../templates/workflow-reminder-email.ts | 34 ------------------- packages/prisma/schema.prisma | 32 ++++++++--------- 6 files changed, 60 insertions(+), 94 deletions(-) delete mode 100644 packages/emails/templates/workflow-reminder-email.ts diff --git a/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts b/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts index 76bbb0a809..18b434e443 100644 --- a/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts +++ b/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts @@ -9,7 +9,6 @@ import client from "@sendgrid/client"; import sgMail from "@sendgrid/mail"; import dayjs from "@calcom/dayjs"; -import { sendWorkflowReminderEmail } from "@calcom/emails"; import prisma from "@calcom/prisma"; import { BookingInfo, timeUnitLowerCase } from "@ee/lib/workflows/reminders/smsReminderManager"; import emailReminderTemplate from "@ee/lib/workflows/reminders/templates/emailReminderTemplate"; @@ -60,11 +59,17 @@ export const scheduleEmailReminder = async ( const attendeeName = action === WorkflowActions.EMAIL_HOST ? evt.attendees[0].name : evt.organizer.name; const timeZone = action === WorkflowActions.EMAIL_HOST ? evt.organizer.timeZone : evt.attendees[0].timeZone; + let emailContent = { + emailSubject, + emailBody: { + text: emailBody, + html: `${emailBody}`, + }, + }; + switch (template) { case WorkflowTemplates.REMINDER: - const emailTemplate = emailReminderTemplate(startTime, evt.title, timeZone, attendeeName, name); - emailSubject = emailTemplate.subject; - emailBody = emailTemplate.body; + emailContent = emailReminderTemplate(startTime, evt.title, timeZone, attendeeName, name); break; } @@ -73,7 +78,14 @@ export const scheduleEmailReminder = async ( triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED ) { try { - await sendWorkflowReminderEmail(evt, sendTo, emailSubject, emailBody); + await sgMail.send({ + to: sendTo, + from: senderEmail, + subject: emailContent.emailSubject, + text: emailContent.emailBody.text, + html: emailContent.emailBody.html, + batchId: batchIdResponse[1].batch_id, + }); } catch (error) { console.log("Error sending Email"); } @@ -88,13 +100,9 @@ export const scheduleEmailReminder = async ( await sgMail.send({ to: sendTo, from: senderEmail, - subject: emailSubject, - content: [ - { - type: "text/html", - value: emailBody, - }, - ], + subject: emailContent.emailSubject, + text: emailContent.emailBody.text, + html: emailContent.emailBody.html, batchId: batchIdResponse[1].batch_id, sendAt: scheduledDate.unix(), }); diff --git a/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts b/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts index 60368f797a..f3c18b6d3b 100644 --- a/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts +++ b/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts @@ -7,19 +7,25 @@ const emailReminderTemplate = ( attendee: string, name: string ) => { - const templateSubject = `Reminder: ${eventName} at ${dayjs(startTime) + const emailSubject = `Reminder: ${eventName} on ${dayjs(startTime) .tz(timeZone) - .format("YYYY MMM D h:mmA")}`; + .format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.`; - const templateBody = `Hi ${name},\n\nThis is a reminder that your meeting (${eventName}) with ${attendee} is on ${dayjs( + const templateBodyText = `Hi ${name}, this is a reminder that your meeting (${eventName}) with ${attendee} is on ${dayjs( startTime ) .tz(timeZone) .format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.`; - const emailContent = { subject: templateSubject, body: templateBody }; + const templateBodyHtml = `Hi ${name},

This is a reminder that your meeting (${eventName}) with ${attendee} is on ${dayjs( + startTime + ) + .tz(timeZone) + .format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.`; - return emailContent; + const emailBody = { text: templateBodyText, html: templateBodyHtml }; + + return { emailSubject, emailBody }; }; export default emailReminderTemplate; diff --git a/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts b/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts index 4dabde6756..fbdafb64e2 100644 --- a/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts +++ b/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts @@ -72,11 +72,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ? reminder.booking?.user?.email : reminder.booking?.attendees[0].email; - let emailTemplate = { - subject: reminder.workflowStep.emailSubject || "", - body: reminder.workflowStep.reminderBody || "", - }; - const name = reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ? reminder.booking?.attendees[0].name @@ -92,9 +87,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ? reminder.booking?.attendees[0].timeZone : reminder.booking?.user?.timeZone; + let emailContent = { + emailSubject: reminder.workflowStep.emailSubject || "", + emailBody: { + text: reminder.workflowStep.reminderBody || "", + html: `${reminder.workflowStep.reminderBody || ""}`, + }, + }; + switch (reminder.workflowStep.template) { case WorkflowTemplates.REMINDER: - emailTemplate = emailReminderTemplate( + emailContent = emailReminderTemplate( reminder.booking?.startTime.toISOString() || "", reminder.booking?.eventType?.title || "", timeZone || "", @@ -103,17 +106,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ); break; } - if (emailTemplate.subject.length > 0 && emailTemplate.body.length > 0 && sendTo) { + if (emailContent.emailSubject.length > 0 && emailContent.emailBody.text.length > 0 && sendTo) { await sgMail.send({ to: sendTo, from: senderEmail, - subject: emailTemplate.subject, - content: [ - { - type: "text/html", - value: emailTemplate.body, - }, - ], + subject: emailContent.emailSubject, + text: emailContent.emailBody.text, + html: emailContent.emailBody.html, batchId: batchIdResponse[1].batch_id, sendAt: dayjs(reminder.scheduledDate).unix(), }); diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 2247e26919..db1f8c7c9e 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -10,7 +10,6 @@ import AttendeeRescheduledEmail from "./templates/attendee-rescheduled-email"; import AttendeeScheduledEmail from "./templates/attendee-scheduled-email"; import BrokenIntegrationEmail from "./templates/broken-integration-email"; import FeedbackEmail, { Feedback } from "./templates/feedback-email"; -import WorkflowReminderEmail from "./templates/workflow-reminder-email"; import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email"; import OrganizerCancelledEmail from "./templates/organizer-cancelled-email"; import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email"; @@ -21,7 +20,6 @@ import OrganizerRequestRescheduleEmail from "./templates/organizer-request-resch import OrganizerRescheduledEmail from "./templates/organizer-rescheduled-email"; import OrganizerScheduledEmail from "./templates/organizer-scheduled-email"; import TeamInviteEmail, { TeamInvite } from "./templates/team-invite-email"; -import { BookingInfo } from "@calcom/web/ee/lib/workflows/reminders/smsReminderManager"; export const sendScheduledEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; @@ -329,14 +327,3 @@ export const sendBrokenIntegrationEmail = async (evt: CalendarEvent, type: "vide } }); }; - -export const sendWorkflowReminderEmail = async (evt: BookingInfo, sendTo: string, emailSubject: string, emailBody: string) => { - await new Promise((resolve, reject) => { - try { - const workflowReminderEmail = new WorkflowReminderEmail(evt, sendTo, emailSubject, emailBody); - resolve(workflowReminderEmail.sendEmail()); - } catch (e) { - reject(console.error("WorkflowReminderEmail.sendEmail failed", e)); - } - }); -} diff --git a/packages/emails/templates/workflow-reminder-email.ts b/packages/emails/templates/workflow-reminder-email.ts deleted file mode 100644 index e9faa079a0..0000000000 --- a/packages/emails/templates/workflow-reminder-email.ts +++ /dev/null @@ -1,34 +0,0 @@ -import BaseEmail from "./_base-email"; -import { BookingInfo } from "@calcom/web/ee/lib/workflows/reminders/smsReminderManager"; -export default class WorkflowReminderEmail extends BaseEmail { - sendTo: string; - body: string; - emailSubject: string; - evt: BookingInfo; - - constructor(evt: BookingInfo, sendTo: string, emailSubject: string, body: string) { - super(); - this.sendTo = sendTo; - this.body = body; - this.evt = evt; - this.emailSubject = emailSubject; - } - - protected getNodeMailerPayload(): Record { - let from =""; - let replyTo =""; - - if(this.evt.organizer) { - from = this.evt.organizer.name || ""; - replyTo = this.evt.organizer.email; - } - return { - to: `<${this.sendTo}>`, - from: `${from} <${this.getMailerOptions().from}>`, - replyTo: replyTo, - subject: this.emailSubject, - text: this.body, - }; - } - -} diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 752857ef33..b6a84f337c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -19,14 +19,14 @@ generator zod { } enum SchedulingType { - ROUND_ROBIN @map("roundRobin") - COLLECTIVE @map("collective") + ROUND_ROBIN @map("roundRobin") + COLLECTIVE @map("collective") } enum PeriodType { - UNLIMITED @map("unlimited") - ROLLING @map("rolling") - RANGE @map("range") + UNLIMITED @map("unlimited") + ROLLING @map("rolling") + RANGE @map("range") } model EventType { @@ -253,10 +253,10 @@ model Attendee { } enum BookingStatus { - CANCELLED @map("cancelled") - ACCEPTED @map("accepted") - REJECTED @map("rejected") - PENDING @map("pending") + CANCELLED @map("cancelled") + ACCEPTED @map("accepted") + REJECTED @map("rejected") + PENDING @map("pending") } model DailyEventReference { @@ -335,10 +335,10 @@ model SelectedCalendar { } enum EventTypeCustomInputType { - TEXT @map("text") - TEXTLONG @map("textLong") - NUMBER @map("number") - BOOL @map("bool") + TEXT @map("text") + TEXTLONG @map("textLong") + NUMBER @map("number") + BOOL @map("bool") } model EventTypeCustomInput { @@ -575,9 +575,9 @@ model WorkflowsOnEventTypes { } enum TimeUnit { - DAY @map("day") - HOUR @map("hour") - MINUTE @map("minute") + DAY @map("day") + HOUR @map("hour") + MINUTE @map("minute") } model WorkflowReminder { From ee14423f4c2899ab16fb1a39b35047e37d3b9d1d Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 21 Jul 2022 16:29:24 -0300 Subject: [PATCH 16/29] Self-hosted onboarding first admin wizard (#3393) * WIP * API and step done fallback * Finishing up tweaks * Inline comment * Translations --- apps/web/pages/api/auth/setup.ts | 58 ++++ apps/web/pages/auth/login.tsx | 14 +- apps/web/pages/auth/setup.tsx | 293 ++++++++++++++++++ apps/web/public/static/locales/en/common.json | 3 + packages/lib/auth.ts | 16 + packages/ui/Stepper.tsx | 50 +++ packages/ui/WizardForm.tsx | 59 ++++ 7 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 apps/web/pages/api/auth/setup.ts create mode 100644 apps/web/pages/auth/setup.tsx create mode 100644 packages/ui/Stepper.tsx create mode 100644 packages/ui/WizardForm.tsx diff --git a/apps/web/pages/api/auth/setup.ts b/apps/web/pages/api/auth/setup.ts new file mode 100644 index 0000000000..e87953ec0a --- /dev/null +++ b/apps/web/pages/api/auth/setup.ts @@ -0,0 +1,58 @@ +import { IdentityProvider } from "@prisma/client"; +import { NextApiRequest, NextApiResponse } from "next"; +import z from "zod"; + +import { isPasswordValid } from "@calcom/lib/auth"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +import { hashPassword } from "@lib/auth"; +import prisma from "@lib/prisma"; +import slugify from "@lib/slugify"; + +const querySchema = z.object({ + username: z.string().min(1), + fullname: z.string(), + email: z.string().email({ message: "Please enter a valid email" }), + password: z.string().refine((val) => isPasswordValid(val.trim()), { + message: + "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", + }), +}); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const userCount = await prisma.user.count(); + if (userCount !== 0) { + throw new HttpError({ statusCode: 400, message: "No setup needed." }); + } + + const parsedQuery = querySchema.safeParse(req.body); + if (!parsedQuery.success) { + throw new HttpError({ statusCode: 422, message: parsedQuery.error.message }); + } + + const username = slugify(parsedQuery.data.username); + const userEmail = parsedQuery.data.email.toLowerCase(); + + const hashedPassword = await hashPassword(parsedQuery.data.password); + + await prisma.user.create({ + data: { + username, + email: userEmail, + password: hashedPassword, + role: "ADMIN", + name: parsedQuery.data.fullname, + emailVerified: new Date(), + locale: "en", // TODO: We should revisit this + plan: "PRO", + identityProvider: IdentityProvider.CAL, + }, + }); + + res.status(201).json({ message: "First admin user created successfuly." }); +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 1558bbbaa3..54c4132eaf 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -8,13 +8,14 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { EmailField, Form, PasswordField } from "@calcom/ui/form/fields"; +import prisma from "@calcom/web/lib/prisma"; import { ErrorCode, getSession } from "@lib/auth"; import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants"; -import { useLocale } from "@lib/hooks/useLocale"; import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@lib/saml"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -217,6 +218,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } + const userCount = await prisma.user.count(); + if (userCount === 0) { + // Proceed to new onboarding to create first admin user + return { + redirect: { + destination: "/auth/setup", + permanent: false, + }, + }; + } + return { props: { csrfToken: await getCsrfToken(context), diff --git a/apps/web/pages/auth/setup.tsx b/apps/web/pages/auth/setup.tsx new file mode 100644 index 0000000000..c11def4ba6 --- /dev/null +++ b/apps/web/pages/auth/setup.tsx @@ -0,0 +1,293 @@ +import { CheckIcon } from "@heroicons/react/solid"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/router"; +import { Controller, useForm } from "react-hook-form"; +import * as z from "zod"; + +import { isPasswordValid } from "@calcom/lib/auth"; +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { inferSSRProps } from "@calcom/types/inferSSRProps"; +import WizardForm from "@calcom/ui/WizardForm"; +import { Input } from "@calcom/ui/form/fields"; +import { Form } from "@calcom/ui/form/fields"; + +import prisma from "@lib/prisma"; + +const schema = z.object({ + username: z.string().min(1), + email: z.string().email({ message: "Please enter a valid email" }), + fullname: z.string(), + password: z.string().refine((val) => isPasswordValid(val.trim()), { + message: + "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", + }), +}); + +const StepDone = () => { + const { t } = useLocale(); + + return ( +
+
+ +
+
+

{t("all_done")}

+
+
+ ); +}; + +const SetupFormStep1 = () => { + const router = useRouter(); + const { t } = useLocale(); + + const formMethods = useForm<{ + username: string; + email: string; + fullname: string; + password: string; + }>({ + resolver: zodResolver(schema), + }); + + return ( +
{ + const response = await fetch("/api/auth/setup", { + method: "POST", + body: JSON.stringify({ + username: data.username, + fullname: data.fullname, + email: data.email.toLowerCase(), + password: data.password, + }), + headers: { + "Content-Type": "application/json", + }, + }); + if (response.status === 201) { + router.replace("/auth/login"); + } else { + router.replace(`/auth/setup`); + } + }}> +
+ +
+ + cal.com/ + + + ( + { + onChange(e.target.value); + formMethods.setValue("username", e.target.value); + await formMethods.trigger("username"); + }} + defaultValue={router.query.email} + color={formMethods.formState.errors.username ? "warn" : ""} + type="text" + name="username" + autoComplete="off" + autoCorrect="off" + autoCapitalize="none" + placeholder={t("username")} + className="rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm" + /> + )} + /> +
+ {formMethods.formState.errors.username && ( +

+ {formMethods.formState.errors.username.message} +

+ )} +
+
+ +
+ ( + { + onChange(e.target.value); + formMethods.setValue("fullname", e.target.value); + await formMethods.trigger("fullname"); + }} + defaultValue={router.query.fullname} + color={formMethods.formState.errors.fullname ? "warn" : ""} + type="text" + name="fullname" + autoCapitalize="none" + autoComplete="name" + autoCorrect="off" + placeholder={t("full_name")} + className={classNames( + "rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm", + formMethods.formState.errors.fullname + ? "border-r-0 focus:border-l focus:border-gray-300 focus:ring-0" + : "focus:border-gray-900 focus:ring-gray-900" + )} + /> + )} + /> +
+ {formMethods.formState.errors.fullname && ( +

+ {formMethods.formState.errors.fullname.message} +

+ )} +
+
+ +
+ ( + { + onChange(e.target.value); + formMethods.setValue("email", e.target.value); + await formMethods.trigger("email"); + }} + defaultValue={router.query.email} + color={formMethods.formState.errors.email ? "warn" : ""} + type="email" + name="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + placeholder={t("email_address")} + className={classNames( + "rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm", + formMethods.formState.errors.email + ? "border-r-0 focus:border-l focus:border-gray-300 focus:ring-0" + : "focus:border-gray-900 focus:ring-gray-900" + )} + /> + )} + /> +
+ {formMethods.formState.errors.email && ( +

+ {formMethods.formState.errors.email.message} +

+ )} +
+
+ +
+ ( + { + onChange(e.target.value); + formMethods.setValue("password", e.target.value); + await formMethods.trigger("password"); + }} + color={formMethods.formState.errors.password ? "warn" : ""} + type="password" + name="password" + autoComplete="off" + placeholder={t("password")} + className={classNames( + "rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm", + formMethods.formState.errors.password + ? "border-r-0 focus:border-l focus:border-gray-300 focus:ring-0" + : "focus:border-gray-900 focus:ring-gray-900" + )} + /> + )} + /> +
+ {formMethods.formState.errors.password && ( +

+ {formMethods.formState.errors.password.message} +

+ )} +
+ + +
+ ); +}; + +export default function Setup(props: inferSSRProps) { + const { t } = useLocale(); + + const steps = [ + { + title: t("administrator_user"), + description: t("lets_create_first_administrator_user"), + content: props.userCount !== 0 ? : , + enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps + }, + ]; + + return ( + <> +
+ +
+ + ); +} + +export const getServerSideProps = async () => { + const userCount = await prisma.user.count(); + return { + props: { + userCount, + }, + }; +}; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 62881b9158..3950eb5722 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -213,6 +213,7 @@ "forgot_password": "Forgot Password", "forgot": "Forgot?", "done": "Done", + "all_done": "All done!", "check_email_reset_password": "Check your email. We sent you a link to reset your password.", "finish": "Finish", "few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.", @@ -477,6 +478,8 @@ "member": "Member", "owner": "Owner", "admin": "Admin", + "administrator_user": "Administrator user", + "lets_create_first_administrator_user": "Let's create the first administrator user.", "new_member": "New Member", "invite": "Invite", "invite_new_member": "Invite a new member", diff --git a/packages/lib/auth.ts b/packages/lib/auth.ts index 976b6c32ce..893e659da2 100644 --- a/packages/lib/auth.ts +++ b/packages/lib/auth.ts @@ -9,3 +9,19 @@ export async function verifyPassword(password: string, hashedPassword: string) { const isValid = await compare(password, hashedPassword); return isValid; } + +export function isPasswordValid(password: string) { + let cap = false, + low = false, + num = false, + min = false; + if (password.length > 6) min = true; + for (let i = 0; i < password.length; i++) { + if (!isNaN(parseInt(password[i]))) num = true; + else { + if (password[i] === password[i].toUpperCase()) cap = true; + if (password[i] === password[i].toLowerCase()) low = true; + } + } + return cap && low && num && min; +} diff --git a/packages/ui/Stepper.tsx b/packages/ui/Stepper.tsx new file mode 100644 index 0000000000..4e51d7752d --- /dev/null +++ b/packages/ui/Stepper.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +type DefaultStep = { + title: string; +}; + +function Stepper(props: { href: string; step: number; steps: T[] }) { + const { href, steps } = props; + return ( + <> + {steps.length > 1 && ( +
+ )} + + ); +} + +export default Stepper; diff --git a/packages/ui/WizardForm.tsx b/packages/ui/WizardForm.tsx new file mode 100644 index 0000000000..47dbbce7ce --- /dev/null +++ b/packages/ui/WizardForm.tsx @@ -0,0 +1,59 @@ +import { useRouter } from "next/router"; + +import Stepper from "./Stepper"; + +type DefaultStep = { + title: string; + description: string; + content: JSX.Element; + enabled?: boolean; +}; + +function WizardForm(props: { href: string; steps: T[] }) { + const { href, steps } = props; + const router = useRouter(); + const step = parseInt((router.query.step as string) || "1"); + const currentStep = steps[step - 1]; + const setStep = (newStep: number) => { + router.replace(`${href}?step=${newStep || 1}`, undefined, { shallow: true }); + }; + + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Cal.com Logo +
+
+

{currentStep.title}

+

{currentStep.description}

+
+
{currentStep.content}
+ {currentStep.enabled !== false && ( +
+ {step > 1 && ( + + )} + + +
+ )} +
+
+ +
+
+ ); +} + +export default WizardForm; From 0a125b6900236e7b5fe3ad3645ab1c2569a1913e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Thu, 21 Jul 2022 15:05:52 -0600 Subject: [PATCH 17/29] Revert "Self-hosted onboarding first admin wizard (#3393)" (#3485) This reverts commit ee14423f4c2899ab16fb1a39b35047e37d3b9d1d. --- apps/web/pages/api/auth/setup.ts | 58 ---- apps/web/pages/auth/login.tsx | 14 +- apps/web/pages/auth/setup.tsx | 293 ------------------ apps/web/public/static/locales/en/common.json | 3 - packages/lib/auth.ts | 16 - packages/ui/Stepper.tsx | 50 --- packages/ui/WizardForm.tsx | 59 ---- 7 files changed, 1 insertion(+), 492 deletions(-) delete mode 100644 apps/web/pages/api/auth/setup.ts delete mode 100644 apps/web/pages/auth/setup.tsx delete mode 100644 packages/ui/Stepper.tsx delete mode 100644 packages/ui/WizardForm.tsx diff --git a/apps/web/pages/api/auth/setup.ts b/apps/web/pages/api/auth/setup.ts deleted file mode 100644 index e87953ec0a..0000000000 --- a/apps/web/pages/api/auth/setup.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IdentityProvider } from "@prisma/client"; -import { NextApiRequest, NextApiResponse } from "next"; -import z from "zod"; - -import { isPasswordValid } from "@calcom/lib/auth"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultHandler, defaultResponder } from "@calcom/lib/server"; - -import { hashPassword } from "@lib/auth"; -import prisma from "@lib/prisma"; -import slugify from "@lib/slugify"; - -const querySchema = z.object({ - username: z.string().min(1), - fullname: z.string(), - email: z.string().email({ message: "Please enter a valid email" }), - password: z.string().refine((val) => isPasswordValid(val.trim()), { - message: - "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", - }), -}); - -async function handler(req: NextApiRequest, res: NextApiResponse) { - const userCount = await prisma.user.count(); - if (userCount !== 0) { - throw new HttpError({ statusCode: 400, message: "No setup needed." }); - } - - const parsedQuery = querySchema.safeParse(req.body); - if (!parsedQuery.success) { - throw new HttpError({ statusCode: 422, message: parsedQuery.error.message }); - } - - const username = slugify(parsedQuery.data.username); - const userEmail = parsedQuery.data.email.toLowerCase(); - - const hashedPassword = await hashPassword(parsedQuery.data.password); - - await prisma.user.create({ - data: { - username, - email: userEmail, - password: hashedPassword, - role: "ADMIN", - name: parsedQuery.data.fullname, - emailVerified: new Date(), - locale: "en", // TODO: We should revisit this - plan: "PRO", - identityProvider: IdentityProvider.CAL, - }, - }); - - res.status(201).json({ message: "First admin user created successfuly." }); -} - -export default defaultHandler({ - POST: Promise.resolve({ default: defaultResponder(handler) }), -}); diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 54c4132eaf..1558bbbaa3 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -8,14 +8,13 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { EmailField, Form, PasswordField } from "@calcom/ui/form/fields"; -import prisma from "@calcom/web/lib/prisma"; import { ErrorCode, getSession } from "@lib/auth"; import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants"; +import { useLocale } from "@lib/hooks/useLocale"; import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@lib/saml"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -218,17 +217,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const userCount = await prisma.user.count(); - if (userCount === 0) { - // Proceed to new onboarding to create first admin user - return { - redirect: { - destination: "/auth/setup", - permanent: false, - }, - }; - } - return { props: { csrfToken: await getCsrfToken(context), diff --git a/apps/web/pages/auth/setup.tsx b/apps/web/pages/auth/setup.tsx deleted file mode 100644 index c11def4ba6..0000000000 --- a/apps/web/pages/auth/setup.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { CheckIcon } from "@heroicons/react/solid"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useRouter } from "next/router"; -import { Controller, useForm } from "react-hook-form"; -import * as z from "zod"; - -import { isPasswordValid } from "@calcom/lib/auth"; -import classNames from "@calcom/lib/classNames"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { inferSSRProps } from "@calcom/types/inferSSRProps"; -import WizardForm from "@calcom/ui/WizardForm"; -import { Input } from "@calcom/ui/form/fields"; -import { Form } from "@calcom/ui/form/fields"; - -import prisma from "@lib/prisma"; - -const schema = z.object({ - username: z.string().min(1), - email: z.string().email({ message: "Please enter a valid email" }), - fullname: z.string(), - password: z.string().refine((val) => isPasswordValid(val.trim()), { - message: - "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", - }), -}); - -const StepDone = () => { - const { t } = useLocale(); - - return ( -
-
- -
-
-

{t("all_done")}

-
-
- ); -}; - -const SetupFormStep1 = () => { - const router = useRouter(); - const { t } = useLocale(); - - const formMethods = useForm<{ - username: string; - email: string; - fullname: string; - password: string; - }>({ - resolver: zodResolver(schema), - }); - - return ( -
{ - const response = await fetch("/api/auth/setup", { - method: "POST", - body: JSON.stringify({ - username: data.username, - fullname: data.fullname, - email: data.email.toLowerCase(), - password: data.password, - }), - headers: { - "Content-Type": "application/json", - }, - }); - if (response.status === 201) { - router.replace("/auth/login"); - } else { - router.replace(`/auth/setup`); - } - }}> -
- -
- - cal.com/ - - - ( - { - onChange(e.target.value); - formMethods.setValue("username", e.target.value); - await formMethods.trigger("username"); - }} - defaultValue={router.query.email} - color={formMethods.formState.errors.username ? "warn" : ""} - type="text" - name="username" - autoComplete="off" - autoCorrect="off" - autoCapitalize="none" - placeholder={t("username")} - className="rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm" - /> - )} - /> -
- {formMethods.formState.errors.username && ( -

- {formMethods.formState.errors.username.message} -

- )} -
-
- -
- ( - { - onChange(e.target.value); - formMethods.setValue("fullname", e.target.value); - await formMethods.trigger("fullname"); - }} - defaultValue={router.query.fullname} - color={formMethods.formState.errors.fullname ? "warn" : ""} - type="text" - name="fullname" - autoCapitalize="none" - autoComplete="name" - autoCorrect="off" - placeholder={t("full_name")} - className={classNames( - "rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm", - formMethods.formState.errors.fullname - ? "border-r-0 focus:border-l focus:border-gray-300 focus:ring-0" - : "focus:border-gray-900 focus:ring-gray-900" - )} - /> - )} - /> -
- {formMethods.formState.errors.fullname && ( -

- {formMethods.formState.errors.fullname.message} -

- )} -
-
- -
- ( - { - onChange(e.target.value); - formMethods.setValue("email", e.target.value); - await formMethods.trigger("email"); - }} - defaultValue={router.query.email} - color={formMethods.formState.errors.email ? "warn" : ""} - type="email" - name="email" - autoCapitalize="none" - autoComplete="email" - autoCorrect="off" - placeholder={t("email_address")} - className={classNames( - "rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm", - formMethods.formState.errors.email - ? "border-r-0 focus:border-l focus:border-gray-300 focus:ring-0" - : "focus:border-gray-900 focus:ring-gray-900" - )} - /> - )} - /> -
- {formMethods.formState.errors.email && ( -

- {formMethods.formState.errors.email.message} -

- )} -
-
- -
- ( - { - onChange(e.target.value); - formMethods.setValue("password", e.target.value); - await formMethods.trigger("password"); - }} - color={formMethods.formState.errors.password ? "warn" : ""} - type="password" - name="password" - autoComplete="off" - placeholder={t("password")} - className={classNames( - "rounded-r-s mt-0 block min-w-0 flex-1 rounded-none border-gray-300 px-3 py-2 sm:text-sm", - formMethods.formState.errors.password - ? "border-r-0 focus:border-l focus:border-gray-300 focus:ring-0" - : "focus:border-gray-900 focus:ring-gray-900" - )} - /> - )} - /> -
- {formMethods.formState.errors.password && ( -

- {formMethods.formState.errors.password.message} -

- )} -
- - -
- ); -}; - -export default function Setup(props: inferSSRProps) { - const { t } = useLocale(); - - const steps = [ - { - title: t("administrator_user"), - description: t("lets_create_first_administrator_user"), - content: props.userCount !== 0 ? : , - enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps - }, - ]; - - return ( - <> -
- -
- - ); -} - -export const getServerSideProps = async () => { - const userCount = await prisma.user.count(); - return { - props: { - userCount, - }, - }; -}; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 3950eb5722..62881b9158 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -213,7 +213,6 @@ "forgot_password": "Forgot Password", "forgot": "Forgot?", "done": "Done", - "all_done": "All done!", "check_email_reset_password": "Check your email. We sent you a link to reset your password.", "finish": "Finish", "few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.", @@ -478,8 +477,6 @@ "member": "Member", "owner": "Owner", "admin": "Admin", - "administrator_user": "Administrator user", - "lets_create_first_administrator_user": "Let's create the first administrator user.", "new_member": "New Member", "invite": "Invite", "invite_new_member": "Invite a new member", diff --git a/packages/lib/auth.ts b/packages/lib/auth.ts index 893e659da2..976b6c32ce 100644 --- a/packages/lib/auth.ts +++ b/packages/lib/auth.ts @@ -9,19 +9,3 @@ export async function verifyPassword(password: string, hashedPassword: string) { const isValid = await compare(password, hashedPassword); return isValid; } - -export function isPasswordValid(password: string) { - let cap = false, - low = false, - num = false, - min = false; - if (password.length > 6) min = true; - for (let i = 0; i < password.length; i++) { - if (!isNaN(parseInt(password[i]))) num = true; - else { - if (password[i] === password[i].toUpperCase()) cap = true; - if (password[i] === password[i].toLowerCase()) low = true; - } - } - return cap && low && num && min; -} diff --git a/packages/ui/Stepper.tsx b/packages/ui/Stepper.tsx deleted file mode 100644 index 4e51d7752d..0000000000 --- a/packages/ui/Stepper.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import Link from "next/link"; - -type DefaultStep = { - title: string; -}; - -function Stepper(props: { href: string; step: number; steps: T[] }) { - const { href, steps } = props; - return ( - <> - {steps.length > 1 && ( - - )} - - ); -} - -export default Stepper; diff --git a/packages/ui/WizardForm.tsx b/packages/ui/WizardForm.tsx deleted file mode 100644 index 47dbbce7ce..0000000000 --- a/packages/ui/WizardForm.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useRouter } from "next/router"; - -import Stepper from "./Stepper"; - -type DefaultStep = { - title: string; - description: string; - content: JSX.Element; - enabled?: boolean; -}; - -function WizardForm(props: { href: string; steps: T[] }) { - const { href, steps } = props; - const router = useRouter(); - const step = parseInt((router.query.step as string) || "1"); - const currentStep = steps[step - 1]; - const setStep = (newStep: number) => { - router.replace(`${href}?step=${newStep || 1}`, undefined, { shallow: true }); - }; - - return ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Cal.com Logo -
-
-

{currentStep.title}

-

{currentStep.description}

-
-
{currentStep.content}
- {currentStep.enabled !== false && ( -
- {step > 1 && ( - - )} - - -
- )} -
-
- -
-
- ); -} - -export default WizardForm; From b94f78f6e1949197ec31e7eb71a87d2617f9025e Mon Sep 17 00:00:00 2001 From: zomars Date: Thu, 21 Jul 2022 16:11:57 -0600 Subject: [PATCH 18/29] Update labeler.yml --- .github/workflows/labeler.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2f10275dfe..ff514935a7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,5 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/labeler@v4 + # Checking the actor will prevent your Action run failing on non-Dependabot + # PRs but also ensures that it only does work for Dependabot PRs. + if: ${{ github.actor == 'github-actions[bot]' }} with: repo-token: "${{ secrets.GITHUB_TOKEN }}" From da29b8893a0b7dcd904de53973ad44848bd267fd Mon Sep 17 00:00:00 2001 From: zomars Date: Thu, 21 Jul 2022 16:12:10 -0600 Subject: [PATCH 19/29] Adds missing migration --- .../prisma/migrations/20220721183745_/migration.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/prisma/migrations/20220721183745_/migration.sql diff --git a/packages/prisma/migrations/20220721183745_/migration.sql b/packages/prisma/migrations/20220721183745_/migration.sql new file mode 100644 index 0000000000..3ee21c8100 --- /dev/null +++ b/packages/prisma/migrations/20220721183745_/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "DestinationCalendar" DROP CONSTRAINT "DestinationCalendar_eventTypeId_fkey"; + +-- DropForeignKey +ALTER TABLE "DestinationCalendar" DROP CONSTRAINT "DestinationCalendar_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "DestinationCalendar" ADD CONSTRAINT "DestinationCalendar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DestinationCalendar" ADD CONSTRAINT "DestinationCalendar_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE SET NULL ON UPDATE CASCADE; From 9709ee4e71ba2084b97c692459411061b968bc88 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 21 Jul 2022 18:39:39 -0400 Subject: [PATCH 20/29] fix reschedule translation (#3487) Co-authored-by: CarinaWolli --- apps/web/pages/success.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 79a9196968..3575e84c61 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -425,7 +425,7 @@ export default function Success(props: SuccessProps) { <>
{t("or_lowercase")}
- {t("Reschedule")} + {t("reschedule")}
)} From c2d6c302219aeefd9de9308f85122281252e8dc2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 18:00:21 -0600 Subject: [PATCH 21/29] New Crowdin translations by Github Action (#3483) Co-authored-by: Crowdin Bot --- apps/web/public/static/locales/fr/common.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 579c41423e..6e55d6984d 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -130,7 +130,6 @@ "dismiss": "Ignorer", "no_data_yet": "Aucune donnée pour l'instant", "ping_test": "Test ping", - "add_time_availability": "Ajouter une disponibilité horaire", "add_to_homescreen": "Ajoutez cette application à l'écran d'accueil de votre mobile pour un accès plus rapide et une meilleure expérience.", "upcoming": "À venir", "recurring": "Récurrent", @@ -376,6 +375,7 @@ "enable": "Activer", "code": "Code", "code_is_incorrect": "Code incorrect.", + "add_time_availability": "Ajouter une disponibilité horaire", "add_an_extra_layer_of_security": "Ajoutez une sécurité supplémentaire à votre compte en cas de vol de votre mot de passe.", "2fa": "Double authentification", "enable_2fa": "Activer la double authentification", From 9b5d311136c0dbf1c9be8f840f9800848e16e8e8 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 22 Jul 2022 12:59:13 +0530 Subject: [PATCH 22/29] Hotfix: Embed ModalBox - Hidden Content (#3491) --- .../embed-core/src/ModalBox/ModalBoxHtml.ts | 34 ++++++------------- packages/embeds/embed-core/src/embed.ts | 7 ++-- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts b/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts index bd8f76110f..c0985c9731 100644 --- a/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts +++ b/packages/embeds/embed-core/src/ModalBox/ModalBoxHtml.ts @@ -10,30 +10,16 @@ const html = `