Skip to content

Commit

Permalink
Add MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
hariombalhara committed Sep 13, 2024
1 parent 9cc1fc9 commit cf74351
Show file tree
Hide file tree
Showing 32 changed files with 920 additions and 5,841 deletions.
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- TODO
- Google Meet App to handle Domain wide delegation
- Disable multiple installations of Google Calendar app if Domain wide delegation is enabled(If Domain wide delegation is enabled, should we disable multiple installations of Google Calendar app?)
22 changes: 19 additions & 3 deletions apps/web/components/apps/AppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ import { Badge, Button, Icon, SkeletonButton, SkeletonText, showToast } from "@c

import { InstallAppButtonChild } from "./InstallAppButtonChild";

function isAllowedMultipleInstalls({
categories,
variant,
}: {
categories: string[];
variant: string;
}): boolean {
// TODO: We could disable it for Domain-wide delegation cases here but for now backend does it when someone tries to install the app
const isCalendarApp = categories.includes("calendar");
const isOtherVariant = variant === "other";
return isCalendarApp && !isOtherVariant;
}

export type AppPageProps = {
name: string;
description: AppType["description"];
Expand Down Expand Up @@ -79,12 +92,14 @@ export const AppPage = ({
const searchParams = useCompatSearchParams();

const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const utils = trpc.useUtils();

const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
onSuccess: async (data) => {
if (data?.setupPending) return;
setIsLoading(false);
showToast(t("app_successfully_installed"), "success");
showToast(data?.message || t("app_successfully_installed"), "success");
await utils.viewer.appCredentialsByType.invalidate({ appType: type });
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
Expand Down Expand Up @@ -161,7 +176,8 @@ export const AppPage = ({

// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";

const allowedMultipleInstalls = isAllowedMultipleInstalls({ categories, variant });
useEffect(() => {
if (searchParams?.get("defaultInstall") === "true") {
mutation.mutate({ type, variant, slug, defaultInstall: true });
Expand Down
287 changes: 287 additions & 0 deletions apps/web/pages/settings/admin/domainWideDelegation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
"use client";

import { useState } from "react";
import { useForm, Controller } from "react-hook-form";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Form,
Meta,
Switch,
showToast,
Dialog,
DialogContent,
DialogFooter,
DialogClose,
List,
TextField,
SelectField,
TextAreaField,
} from "@calcom/ui";

import PageWrapper from "@components/PageWrapper";
import { getLayout } from "@components/auth/layouts/AdminLayout";

const DomainWideDelegationPage = () => {
const { t } = useLocale();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingDelegation, setEditingDelegation] = useState(null);

const {
data: domainWideDelegations,
isLoading,
error,
} = trpc.viewer.admin.domainWideDelegation.list.useQuery();

if (error) {
return <ErrorState />;
}
if (isLoading) return <LoadingState />;
const defaultValues = !editingDelegation
? {
workspacePlatform: "GOOGLE",
serviceAccountKey: "",
organizationId: null,
enabled: true,
}
: editingDelegation;
return (
<>
<Meta title={t("domain_wide_delegation")} description={t("domain_wide_delegation_description")} />
<PageContent
domainWideDelegations={domainWideDelegations}
onAdd={handleAdd}
onEdit={handleEdit}
onToggle={handleToggle}
/>
<CreateUpdateDelegationDialog
isOpen={isDialogOpen}
onOpenChange={setIsDialogOpen}
editingDelegation={editingDelegation}
defaultValues={defaultValues}
key={editingDelegation?.id}
/>
</>
);

function handleAdd() {
setEditingDelegation(null);
setIsDialogOpen(true);
}

function handleEdit(delegation) {
setEditingDelegation(delegation);
setIsDialogOpen(true);
}

function handleToggle(delegation, checked) {
updateMutation.mutate({ ...delegation, enabled: checked });
}
};

function LoadingState() {
return <div>Loading...</div>;
}

function ErrorState() {
return <div>Some error occurred</div>;
}

function PageContent({ domainWideDelegations, onAdd, onEdit, onToggle }) {
const { t } = useLocale();
return (
<div className="mt-6 flex flex-col space-y-8">
<div className="flex flex-col space-y-4">
<h2 className="font-cal text-2xl">{t("domain_wide_delegation")}</h2>
<p>{t("domain_wide_delegation_description")}</p>
</div>
{domainWideDelegations.length === 0 ? (
<EmptyState onAdd={onAdd} />
) : (
<DelegationList
delegations={domainWideDelegations}
onEdit={onEdit}
onToggle={onToggle}
onAdd={onAdd}
/>
)}
</div>
);
}

function EmptyState({ onAdd }) {
const { t } = useLocale();

return (
<div className="flex flex-col items-center justify-center space-y-4 py-10">
<p className="text-gray-500">{t("no_domain_wide_delegations")}</p>
<Button color="secondary" onClick={onAdd}>
{t("add_domain_wide_delegation")}
</Button>
</div>
);
}

function DelegationList({ delegations, onEdit, onToggle, onAdd }) {
const { t } = useLocale();

return (
<>
<List>
{delegations.map((delegation) => (
<DelegationListItem
key={delegation.id}
delegation={delegation}
onEdit={onEdit}
onToggle={onToggle}
/>
))}
</List>
<div className="flex justify-end">
<Button color="secondary" onClick={onAdd}>
{t("add_domain_wide_delegation")}
</Button>
</div>
</>
);
}

function DelegationListItem({ delegation, onEdit, onToggle }) {
const { t } = useLocale();
const utils = trpc.useContext();
const deleteMutation = trpc.viewer.admin.domainWideDelegation.delete.useMutation({
onSuccess: () => {
showToast(t("domain_wide_delegation_deleted_successfully"), "success");
utils.viewer.admin.domainWideDelegation.list.invalidate();
},
onError: (error) => {
showToast(error.message, "error");
},
});

const handleDelete = () => {
if (window.confirm(t("confirm_delete_domain_wide_delegation"))) {
deleteMutation.mutate({ id: delegation.id });
}
};

return (
<li className="flex items-center justify-between py-4">
<div>
<p className="font-medium">{delegation.organizationId}</p>
<p className="text-sm text-gray-500">{delegation.workspacePlatform}</p>
</div>
<div className="flex items-center space-x-2">
<Switch checked={delegation.enabled} onCheckedChange={(checked) => onToggle(delegation, checked)} />
<Button color="secondary" onClick={() => onEdit(delegation)}>
{t("edit")}
</Button>
<Button color="destructive" onClick={handleDelete}>
{t("delete")}
</Button>
</div>
</li>
);
}

function CreateUpdateDelegationDialog({ isOpen, onOpenChange, editingDelegation, defaultValues }) {
const { t } = useLocale();
const form = useForm({
defaultValues,
});

const utils = trpc.useContext();

const updateMutation = trpc.viewer.admin.domainWideDelegation.update.useMutation({
onSuccess: handleMutationSuccess,
onError: handleMutationError,
});

const addMutation = trpc.viewer.admin.domainWideDelegation.add.useMutation({
onSuccess: handleMutationSuccess,
onError: handleMutationError,
});

function onSubmit(values) {
if (editingDelegation) {
updateMutation.mutate({ ...values, id: editingDelegation.id });
} else {
addMutation.mutate(values);
}
}

function handleMutationSuccess() {
showToast(t("domain_wide_delegation_updated_successfully"), "success");
onOpenChange(false);
utils.viewer.admin.domainWideDelegation.list.invalidate();
}

function handleMutationError(error) {
showToast(error.message, "error");
}

return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent
title={editingDelegation ? t("edit_domain_wide_delegation") : t("add_domain_wide_delegation")}>
<Form form={form} handleSubmit={onSubmit}>
<div className="space-y-4">
<TextField
label={t("organization_id")}
placeholder={t("enter_organization_id")}
{...form.register("organizationId", { valueAsNumber: true })}
/>
<Controller
name="workspacePlatform"
render={({ field: { value, onChange } }) => (
<SelectField
label={t("workspace_platform")}
onChange={(option) => {
onChange(option?.value);
}}
value={[
{ value: "GOOGLE", label: "Google" },
{ value: "MICROSOFT", label: "Microsoft" },
].find((opt) => opt.value === value)}
options={[
{ value: "GOOGLE", label: "Google" },
{ value: "MICROSOFT", label: "Microsoft" },
]}
/>
)}
/>
{!editingDelegation && (
<TextAreaField
label={t("service_account_key")}
placeholder={t("paste_service_account_key_here")}
{...form.register("serviceAccountKey")}
/>
)}
<Controller
control={form.control}
name="enabled"
render={({ field: { value, onChange } }) => (
<Switch
checked={value}
onCheckedChange={onChange}
label={t("enable_domain_wide_delegation")}
/>
)}
/>
</div>
<DialogFooter>
<DialogClose />
<Button type="submit">{editingDelegation ? t("save_changes") : t("add_delegation")}</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}

DomainWideDelegationPage.getLayout = getLayout;
DomainWideDelegationPage.PageWrapper = PageWrapper;

export default DomainWideDelegationPage;
3 changes: 2 additions & 1 deletion apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"@pages/*": ["pages/*"],
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
"@prisma/client/*": ["@calcom/prisma/client/*"]
"@prisma/client/*": ["@calcom/prisma/client/*"],
"@calcom/repository/*": ["@calcom/lib/server/repository/*"]
},
"plugins": [
{
Expand Down
12 changes: 1 addition & 11 deletions domain-wide-delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,9 @@ A service account is needed to act on behalf of users
- Give your service account a name and description
- Click Create and Continue

Step 3: Connecting Deployment Environment To Google Cloud Project using Workload Identity Federation
Vercel: Follow this guide: https://vercel.com/docs/security/secure-backend-access/oidc/gcp
First setup Google Cloud using the above guide and then paste the environment variables to your Vercel project.
GCP_PROJECT_ID=domain-wide-delegation-testing
GCP_PROJECT_NUMBER=777450754675
GCP_SERVICE_ACCOUNT_EMAIL=vercel-cal-staging@domain-wide-delegation-testing.iam.gserviceaccount.com
GCP_WORKLOAD_IDENTITY_POOL_ID=vercel
GCP_WORKLOAD_IDENTITY_POOL_PROVIDER_ID=vercel

Last Step (To Be Taken By Cal.com organization Owner/Admin): Assign Specific API Permissions via OAuth Scopes:
- Go to your Google Admin Console (admin-google-com)
- Navigate to Security → API Controls → Manage Domain-Wide Delegation
- Here, you'll authorize the service account's client ID(Unique ID) to access the Google Calendar API
- Add the necessary API scopes for Google Calendar(Full access to Google Calendar)
https://www.googleapis.com/auth/calendar

https://www.googleapis.com/auth/calendar
Loading

0 comments on commit cf74351

Please sign in to comment.