diff --git a/apps/web/pages/api/teams/googleworkspace/add.ts b/apps/web/pages/api/teams/googleworkspace/add.ts new file mode 100644 index 0000000000..2955a03454 --- /dev/null +++ b/apps/web/pages/api/teams/googleworkspace/add.ts @@ -0,0 +1,35 @@ +import { google } from "googleapis"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; +import { WEBAPP_URL } from "@calcom/lib/constants"; + +const scopes = [ + "https://www.googleapis.com/auth/admin.directory.user.readonly", + "https://www.googleapis.com/auth/admin.directory.customer.readonly", +]; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") { + // Get appKeys from google-calendar + const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar"); + if (!client_id || typeof client_id !== "string") + return res.status(400).json({ message: "Google client_id missing." }); + if (!client_secret || typeof client_secret !== "string") + return res.status(400).json({ message: "Google client_secret missing." }); + + // use differnt callback to normal calendar connection + const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback"; + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); + + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: "offline", + scope: scopes, + + prompt: "consent", + state: JSON.stringify({ teamId: req.query.teamId }), + }); + + res.status(200).json({ url: authUrl }); + } +} diff --git a/apps/web/pages/api/teams/googleworkspace/callback.ts b/apps/web/pages/api/teams/googleworkspace/callback.ts new file mode 100644 index 0000000000..079353b4af --- /dev/null +++ b/apps/web/pages/api/teams/googleworkspace/callback.ts @@ -0,0 +1,63 @@ +import { google } from "googleapis"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import prisma from "@calcom/prisma"; + +const stateSchema = z.object({ + teamId: z.string(), +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession({ req, res }); + + if (!session?.user?.id) { + return res.status(401).json({ message: "You must be logged in to do this" }); + } + + const { code, state } = req.query; + const parsedState = stateSchema.parse(JSON.parse(state as string)); + const { teamId } = parsedState; + + if (code && typeof code !== "string") { + res.status(400).json({ message: "`code` must be a string" }); + return; + } + + const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar"); + + if (!client_id || typeof client_id !== "string") + return res.status(400).json({ message: "Google client_id missing." }); + if (!client_secret || typeof client_secret !== "string") + return res.status(400).json({ message: "Google client_secret missing." }); + + const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback"; + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); + + if (!code) { + throw new Error("No code provided"); + } + + const credentials = await oAuth2Client.getToken(code); + + await prisma.credential.create({ + data: { + type: "google_workspace_directory", + key: credentials.res?.data, + userId: session.user.id, + }, + }); + + if (!teamId) { + res.redirect(getSafeRedirectUrl(WEBAPP_URL + "/settings") ?? `${WEBAPP_URL}/teams`); + } + + res.redirect( + getSafeRedirectUrl(WEBAPP_URL + `/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ?? + `${WEBAPP_URL}/teams` + ); +} diff --git a/apps/web/pages/api/trpc/googleWorkspace/[trpc].ts b/apps/web/pages/api/trpc/googleWorkspace/[trpc].ts new file mode 100644 index 0000000000..e6f07468dd --- /dev/null +++ b/apps/web/pages/api/trpc/googleWorkspace/[trpc].ts @@ -0,0 +1,4 @@ +import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler"; +import { googleWorkspaceRouter } from "@calcom/trpc/server/routers/viewer/googleWorkspace/_router"; + +export default createNextApiHandler(googleWorkspaceRouter); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 3c862b1202..b406c9ece6 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -26,6 +26,8 @@ "rejection_confirmation": "Reject the booking", "manage_this_event": "Manage this event", "invite_team_member": "Invite team member", + "invite_team_individual_segment": "Invite individual", + "invite_team_bulk_segment": "Bulk import", "invite_team_notifcation_badge": "Inv.", "your_event_has_been_scheduled": "Your event has been scheduled", "your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled", @@ -222,6 +224,7 @@ "go_back_login": "Go back to the login page", "error_during_login": "An error occurred when logging you in. Head back to the login screen and try again.", "request_password_reset": "Send reset email", + "send_invite": "Send invite", "forgot_password": "Forgot Password?", "forgot": "Forgot?", "done": "Done", @@ -1809,6 +1812,7 @@ "charge_attendee": "Charge attendee {{amount, currency}}", "payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)", "email_invite_team": "{{email}} has been invited", + "email_invite_team_bulk": "{{userCount}} users have been invited", "error_collecting_card": "Error collecting card", "image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit", "inline_embed": "Inline Embed", @@ -1819,12 +1823,16 @@ "open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.", "need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.", "book_my_cal": "Book my Cal", + "invite_as":"Invite as", "form_updated_successfully":"Form updated successfully.", "email_not_cal_member_cta": "Join your team", "disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees", "disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.", "disable_host_confirmation_emails": "Disable default confirmation emails for host", "disable_host_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the host when the event is booked.", + "import_from_google_workspace":"Import users from Google Workspace", + "connect_google_workspace":"Connect Google Workspace", + "google_workspace_admin_tooltip":"You must be a Workspace Admin to use this feature", "first_event_type_webhook_description": "Create your first webhook for this event type", "create_for": "Create for" } diff --git a/packages/features/ee/teams/components/AddNewTeamMembers.tsx b/packages/features/ee/teams/components/AddNewTeamMembers.tsx index f83836c074..cb9b48d628 100644 --- a/packages/features/ee/teams/components/AddNewTeamMembers.tsx +++ b/packages/features/ee/teams/components/AddNewTeamMembers.tsx @@ -40,20 +40,30 @@ export const AddNewTeamMembersForm = ({ teamId: number; }) => { const { t, i18n } = useLocale(); - const [memberInviteModal, setMemberInviteModal] = useState(false); - const utils = trpc.useContext(); const router = useRouter(); + const showDialog = router.query.inviteModal === "true"; + const [memberInviteModal, setMemberInviteModal] = useState(showDialog); + const utils = trpc.useContext(); const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({ async onSuccess(data) { await utils.viewer.teams.get.invalidate(); setMemberInviteModal(false); if (data.sendEmailInvitation) { - showToast( - t("email_invite_team", { - email: data.usernameOrEmail, - }), - "success" - ); + if (Array.isArray(data.usernameOrEmail)) { + showToast( + t("email_invite_team_bulk", { + userCount: data.usernameOrEmail.length, + }), + "success" + ); + } else { + showToast( + t("email_invite_team", { + email: data.usernameOrEmail, + }), + "success" + ); + } } }, onError: (error) => { diff --git a/packages/features/ee/teams/components/GoogleWorkspaceInviteButton.tsx b/packages/features/ee/teams/components/GoogleWorkspaceInviteButton.tsx new file mode 100644 index 0000000000..1c14fe4659 --- /dev/null +++ b/packages/features/ee/teams/components/GoogleWorkspaceInviteButton.tsx @@ -0,0 +1,126 @@ +import { UsersIcon, XIcon } from "lucide-react"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import type { PropsWithChildren } from "react"; + +import { useFlagMap } from "@calcom/features/flags/context/provider"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc"; +import { Button, Tooltip, showToast } from "@calcom/ui"; + +const GoogleIcon = () => ( + +); + +function gotoUrl(url: string, newTab?: boolean) { + if (newTab) { + window.open(url, "_blank"); + return; + } + window.location.href = url; +} + +export function GoogleWorkspaceInviteButton( + props: PropsWithChildren<{ onSuccess: (data: string[]) => void }> +) { + const router = useRouter(); + const featureFlags = useFlagMap(); + const utils = trpc.useContext(); + const { t } = useLocale(); + const teamId = Number(router.query.id); + const [googleWorkspaceLoading, setGoogleWorkspaceLoading] = useState(false); + const { data: credential } = trpc.viewer.googleWorkspace.checkForGWorkspace.useQuery(); + const { data: hasGcalInstalled } = trpc.viewer.appsRouter.checkGlobalKeys.useQuery({ + slug: "google-calendar", + }); + const mutation = trpc.viewer.googleWorkspace.getUsersFromGWorkspace.useMutation({ + onSuccess: (data) => { + if (Array.isArray(data) && data.length !== 0) { + props.onSuccess(data); + } + }, + }); + + const removeConnectionMutation = + trpc.viewer.googleWorkspace.removeCurrentGoogleWorkspaceConnection.useMutation({ + onSuccess: () => { + showToast(t("app_removed_successfully"), "success"); + }, + }); + + if (featureFlags["google-workspace-directory"] == false || !hasGcalInstalled) { + return null; + } + + // Show populate input button if they do + if (credential && credential?.id) { + return ( +