Skip to content

Commit

Permalink
Meeting Signing: Prevent multiple graph consent pop-ups (OfficeDev#465)
Browse files Browse the repository at this point in the history
* Meeting Signing: Prevent Multiple Graph Consents
  • Loading branch information
eoinobrien authored Oct 17, 2022
1 parent 121b373 commit 4cf61d9
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { merge } from 'lodash';
import { ApiErrorResponse, ErrorCode } from 'models/ApiErrorResponse';
import * as microsoftTeams from '@microsoft/teams-js';

// This function is for callers where authentication is required.
// It makes a fetch client call with an AAD token.
export async function authFetch<T>(urlPath: string, init?: RequestInit) {
export async function authFetch<T>(
urlPath: string,
init?: RequestInit,
) {
const token = await microsoftTeams.authentication
.getAuthToken()
.catch((err) => {
Expand All @@ -18,31 +20,24 @@ export async function authFetch<T>(urlPath: string, init?: RequestInit) {
'Content-Type': 'application/json;charset=UTF-8',
},
});

return (await fetchClient(urlPath, mergedInit)) as T;
}

async function fetchClient(urlPath: string, mergedInit: RequestInit): Promise<any> {
async function fetchClient(
urlPath: string,
mergedInit: RequestInit,
): Promise<any> {
const response = await fetch(`/${urlPath}`, mergedInit);

if (!response.ok) {
const errorJson: ApiErrorResponse = await response.json();
const errorJson: any = await response.json();
if (!errorJson) {
throw new Error('Error fetching data.');
}

if (errorJson.ErrorCode === ErrorCode.AuthConsentRequired) {
try {
await microsoftTeams.authentication.authenticate({
url: `${window.location.origin}/auth-start`,
width: 600,
height: 535,
});

console.log('Consent provided.');
return await fetchClient(urlPath, mergedInit);
} catch (error) {
console.error("Failed to get consent: '" + error + "'");
}
if (errorJson.title) {
throw new Error(`${errorJson.title}`);
}

throw new Error(`${errorJson.ErrorCode}: ${errorJson.Message}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as microsoftTeams from '@microsoft/teams-js';
import { Button, Flex, Header, Text } from '@fluentui/react-northstar';

type ConsentRequestProps = {
callback: (error?: string, result?: string) => void;
};

function ConsentRequest({ callback }: ConsentRequestProps) {
const callConsentAuth = async () => {
try {
const result = await microsoftTeams.authentication.authenticate({
url: `${window.location.origin}/auth-start`,
width: 600,
height: 535,
});

console.log('Consent provided.');
callback(undefined, result);
} catch (error: any) {
console.error(`Failed to get consent: "${error}"`);
callback(error);
}
};

return (
<Flex column>
<Header as="h2" content="To complete that action you must consent" />
<Text
as="p"
content="We need your permission to access some data from your account."
/>
<Button primary content="Consent" onClick={callConsentAuth} />
</Flex>
);
}

export { ConsentRequest };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ConsentRequest } from './ConsentRequest';

export { ConsentRequest };
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Alert, Button, Flex } from '@fluentui/react-northstar';
import { useState } from 'react';
import { useMutation } from 'react-query';
import { TaskInfo } from '@microsoft/teams-js';
import * as microsoftTeams from '@microsoft/teams-js';
import * as ACData from 'adaptivecards-templating';
import { CreateDocumentCard } from 'adaptive-cards';
import { createDocument } from 'api/documentApi';
import {
ApiErrorCode,
Document,
DocumentInput,
DocumentType,
User,
} from 'models';
import { apiRetryQuery, isApiErrorCode } from 'utils/UtilsFunctions';
import { ConsentRequest } from 'components/ConsentRequest';

type Choice = {
name: string;
value: string;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createTaskInfo = (card: any): TaskInfo => {
return {
card: JSON.stringify(card),
};
};

const createDocumentTypeArray = () => {
const documents: Choice[] = Object.entries(DocumentType).map(
([value, name]) => {
return { name, value } as Choice;
},
);

return documents;
};

const createUserArray = (
commaArrayOfUsers?: string,
isEmail?: boolean,
): User[] => {
if (!commaArrayOfUsers) {
return [];
}

return commaArrayOfUsers.split(',').map((u: string) => {
return {
userId: isEmail ? undefined : u,
name: '',
} as User;
});
};

/**
* Content that is shown in the Meeting Tab
* Includes the ability to open a Task Module to create a Document.
*
* @returns a component with a simple header and button to create a document
*/
export function CreateDocumentButton() {
const [userHasConsented, setUserHasConsented] = useState<boolean>(false);
const [documentInput, setDocumentInput] = useState<DocumentInput | undefined>(
undefined,
);

const createDocumentMutation = useMutation<Document, Error, DocumentInput>(
(documentInput: DocumentInput) => createDocument(documentInput),
{
retry: (failureCount: number, error: Error) =>
apiRetryQuery(
failureCount,
error,
userHasConsented,
setUserHasConsented,
),
},
);

const createDocumentsTaskModule = () => {
const template = new ACData.Template(CreateDocumentCard);
const documentsCard = template.expand({
$root: {
title: 'Select the documents that needs to be reviewed in the meeting',
error: 'At least one document is required',
choices: createDocumentTypeArray(),
successButtonText: 'Next',
id: 'documents',
},
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createDocumentsSubmitHandler = (error: string, result: any) => {
if (error !== null) {
console.log(`Document handler - error: '${error}'`);
} else if (result !== undefined) {
const documents: string[] = result.documentsValue.split(',');

const viewers: User[] = createUserArray(result.viewersValue);
const signers: User[] = createUserArray(result.signersValue);

documents.forEach(async (d: string) => {
const documentInput: DocumentInput = {
documentType: DocumentType[d as keyof typeof DocumentType],
viewers: viewers,
signers: signers,
};

setDocumentInput(documentInput);
createDocumentMutation.mutate(documentInput);
});
}
};

// tasks.startTasks is deprecated, but the 2.0 of SDK's dialog.open does not support opening adaptive cards yet.
microsoftTeams.tasks.startTask(
createTaskInfo(documentsCard),
createDocumentsSubmitHandler,
);
};

const consentCallback = (error?: string, result?: string) => {
if (error) {
console.log(`Error: ${error}`);
}
if (result) {
setUserHasConsented(true);
if (documentInput !== undefined) {
createDocumentMutation.mutate(documentInput);
}
}
};

const displayConsentRequest =
isApiErrorCode(
ApiErrorCode.AuthConsentRequired,
createDocumentMutation.error,
) && !userHasConsented;

return (
<Flex column>
{createDocumentMutation.isError && displayConsentRequest && (
<ConsentRequest callback={consentCallback} />
)}

{!displayConsentRequest && (
<Button
content="Create Documents"
onClick={() => createDocumentsTaskModule()}
primary
loading={createDocumentMutation.isLoading}
/>
)}
{createDocumentMutation.isError && !displayConsentRequest && (
<Alert
header="Error"
content={
createDocumentMutation.error?.message ??
'Something went wrong while creating your document'
}
danger
visible
/>
)}

{createDocumentMutation.data && (
<Alert header="Success" content="Document Created" success visible />
)}
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { CreateDocumentButton } from './CreateDocumentButton';
export { CreateDocumentButton };
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import { Alert, Button, Flex, Header } from '@fluentui/react-northstar';
import { TaskInfo } from '@microsoft/teams-js';
import * as microsoftTeams from '@microsoft/teams-js';
import * as ACData from 'adaptivecards-templating';
import { CreateDocumentCard } from 'adaptive-cards';
import { createDocument } from 'api/documentApi';
import { Flex, Header } from '@fluentui/react-northstar';
import { useTeamsContext } from 'utils/TeamsProvider/hooks';
import { Document, DocumentInput, DocumentType, User } from 'models';
import { CreateDocumentButton } from 'components/CreateDocumentButton';
import styles from './TabContent.module.css';
import { useMutation } from 'react-query';

type Choice = {
name: string;
value: string;
};

/**
* Content that is shown in the Meeting Tab
Expand All @@ -23,75 +12,6 @@ type Choice = {
export function TabContent() {
const context = useTeamsContext();

const createDocumentMutation = useMutation<Document, Error, DocumentInput>(
(documentInput: DocumentInput) => createDocument(documentInput),
);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createTaskInfo = (card: any): TaskInfo => {
return {
card: JSON.stringify(card),
};
};

const createDocumentTypeArray = () => {
const documents: Choice[] = Object.entries(DocumentType).map(
([value, name]) => {
return { name, value } as Choice;
},
);

return documents;
};

const createDocumentsTaskModule = () => {
const template = new ACData.Template(CreateDocumentCard);
const documentsCard = template.expand({
$root: {
title: 'Select the documents that needs to be reviewed in the meeting',
error: 'At least one document is required',
choices: createDocumentTypeArray(),
successButtonText: 'Next',
id: 'documents',
},
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createDocumentsSubmitHandler = (error: string, result: any) => {
if (error !== null) {
console.log(`Document handler - error: '${error}'`);
} else if (result !== undefined) {
const documents: string[] = result.documentsValue.split(',');
const viewers: User[] = result.viewersValue
.split(',')
.map((s: string) => {
return { userId: s, name: '' };
});
const signers: User[] = result.signersValue
.split(',')
.map((s: string) => {
return { userId: s, name: '' };
});

documents.forEach(async (d: string) => {
const documentInput: DocumentInput = {
documentType: DocumentType[d as keyof typeof DocumentType],
viewers: viewers,
signers: signers,
};

createDocumentMutation.mutate(documentInput);
});
}
};

// tasks.startTasks is deprecated, but the 2.0 of SDK's dialog.open does not support opening adaptive cards yet.
microsoftTeams.tasks.startTask(
createTaskInfo(documentsCard),
createDocumentsSubmitHandler,
);
};

return (
<Flex column className={styles.tabContent}>
<Header
Expand All @@ -101,22 +21,7 @@ export function TabContent() {
content: `FrameContext: ${context?.page.frameContext}`,
}}
/>
<Button
content="Create Documents"
onClick={() => createDocumentsTaskModule()}
primary
/>
{createDocumentMutation.isError && (
<Alert
header="Error"
content={createDocumentMutation.error.message}
danger
visible
/>
)}
{createDocumentMutation.data && (
<Alert header="Success" content="Document Created" success visible />
)}
<CreateDocumentButton />
</Flex>
);
}
Loading

0 comments on commit 4cf61d9

Please sign in to comment.