Recently I started to create Microsoft Teams App samples with the Teams Toolkit and Visual Studio using C# as backend programming language for instance. Here is my second rebuilt sample, a PDF conversion on Drag&Drop upload in a Teams Tab.
Content
Setup
First you need to setup your Teams application. You should have Visual studio 2022 in the version 17.3 or above installed. This enables you to install Teams Toolkit in GA version. Then a Teams App, here a Tab, can be created. This was already described in my last post so I won’t repeat it here again.
After the Teams App Dependencies were prepared a basic app registration was created for you. Next there is a need to slightly adjust permissions according to this solution.
Azure App registration
The app registration created is already pre-configured to support the Teams SSO process. The following steps can already be found after “Prepare Teams App Dependencies” (see above):
- A redirect uri https://localhost/blank-auth-end.html
- Multi-tenant enabled
- A client secret
- An exposed Api “access_as_user” and App ID Uri api://localhost/
- With the client IDs for Teams App and Teams Web App 1fec8e78-bce4-4aaf-ab1b-5451cc387264 and 5e3ce6c0-2b1f-4285-8d4b-75ee78787346
Additionally for the upload and download operations ReadWrite permissions to OneDrive and SharePoint are needed.
Client-Side
Drag&Drop
The drag&drop handling needs to be enabled for a specific component and also visualized by highlighting the “landing zone”. Therefore, different event receivers are set and implemented in JavaScript.
<div class="dropZone"
ondragenter="TabPDF.Drag.enableHighlight(event)"
ondragleave="TabPDF.Drag.disableHighlight(event)"
ondragover="TabPDF.Drag.allowDrop(event)"
ondrop="TabPDF.executeUpload(event)">
...
</div>
TabPDF.Drag = {};
{
TabPDF.Drag.allowDrop = function (event) {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = 'copy';
}
TabPDF.Drag.enableHighlight = function (event) {
TabPDF.Drag.allowDrop(event);
const bgDIV = document.getElementsByClassName('dropZone');
bgDIV[0].classList.add('dropZoneHighlight');
}
TabPDF.Drag.disableHighlight = function (event) {
TabPDF.Drag.allowDrop(event);
const bgDIV = document.getElementsByClassName('dropZone');
bgDIV[0].classList.remove('dropZoneHighlight');
}
}
Progress Indicator
As the conversion process consists of 3 major up-/download operations and may take a while showing a progress indicator to the user is a good and polite idea. Therefore, usage of a FluentUI Blazor component is a suitable idea:
<div style="display: none;" class="loader">
<FluentProgressRing />
</div>
This is enabled by the following <script />
tag in the root HTML page:
<script type="module" src="https://unpkg.com/@@fluentui/web-components"></script>
In the JavaScript code at the beginning of the upload operation the whole <div/> containing the progress indicator is made visible while it’s turned off again at the very end.
TabPDF.executeUpload = function (event) {
TabPDF.Drag.allowDrop(event);
const loaderDIV = document.getElementsByClassName('loader')[0];
loaderDIV.style.display = 'flex';
const dt = event.dataTransfer;
const files = Array.prototype.slice.call(dt.files);
files.forEach(fileToUpload => {
const extensions = fileToUpload.name.split('.');
const fileExtension = extensions[extensions.length - 1];
TabPDF.Drag.disableHighlight(event);
if (TabPDF.Utilities.isFileTypeSupported(fileExtension, 'PDF')) {
const formData = new FormData();
formData.append('file', fileToUpload);
formData.append('Name', fileToUpload.name);
formData.append('SiteUrl', TabPDF.siteUrl);
fetch("/api/Upload", {
method: "post",
headers: {
"Authorization": "Bearer " + TabPDF.ssoToken
},
body: formData
})
.then((response) => {
response.text().then(resp => {
TabPDF.addConvertedFile(resp);
loaderDIV.style.display = 'none';
});
});
}
else {
alert('File type not supported!')
}
});
}
In between the file and several parameters are collected in a form and with it the backend is called. Authentication is done with a bootstrap token (generation see next).
SSO
(function (TabPDF) {
ssoToken = "";
siteUrl = "";
TabPDF.getSSOToken = function () {
if (microsoftTeams) {
microsoftTeams.initialize();
microsoftTeams.authentication.getAuthToken({
successCallback: (token, event) => {
TabPDF.ssoToken = token;
},
failureCallback: (error) => {
renderError(error);
}
});
}
}
}
}(window.TabPDF = window.TabPDF || {}));
This is a self invoking function that stores itself in a global variable. The TabPDF.getSSOToken
function is executed onload
of the whole script
<script onload="(function () { TabPDF.getSSOToken(); TabPDF.getContext();}).call(this)" src="/js/TabPDF.js">
The function uses the TeamsJS SDK 2.0 to get an AuthToken
. That is, the so called ID or bootstrap token which needs to be exchanged to an access token for getting access to Microsoft Graph. But this happens later server-side. For the moment it is stored in a class variable where it’s taken from “on action” when a file is dropped.
Additionally TeamsJS SDK 2.0 provides either the SharePoint siteUrl
for the current Teams’ site (if Tab is running as a Configurable inside a team) or the SharePoint host url (if Tab is running as static or personal one). That siteUrl
will later be used as the target siteUrl
for final upload of the converted PDF.
TabPDF.getContext = function () {
if (microsoftTeams) {
microsoftTeams.app.getContext()
.then(context => {
if (context.sharePointSite.teamSiteUrl !== "") {
TabPDF.siteUrl = context.sharePointSite.teamSiteUrl;
}
else {
TabPDF.siteUrl = "https://" + context.sharePointSite.teamSiteDomain;
}
});
}
}
Alternatively, a dedicated site url could be stored with the configuration option. This is another story and for Teams Toolkit and Visual Studio I will handle later.
Backend Graph Controller
Authentication / SSO
In the call above the bootstrap token was used for authentication with “Bearer “. Any backend controller in our solution now simply gets added UI authentication and additionally a Microsoft Graph client established by automatic use of the on-behalf flow. This is done in the program.cs
builder.Services
// Use Web API authentication (default JWT bearer token scheme)
.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration)
// Enable token acquisition via on-behalf-of flow
.EnableTokenAcquisitionToCallDownstreamApi()
// Add authenticated Graph client via dependency injection
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
// Use in-memory token cache
// See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization
.AddInMemoryTokenCaches();
Token acquisition 8not needed here, but…) but also the Microsoft Graph SDK client they can then directly be used inside our controller:
[Route("api/[controller]")]
[ApiController]
public class UploadController : ControllerBase
{
private readonly GraphServiceClient _graphClient;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly ILogger<UploadController> _logger;
public UploadController(ITokenAcquisition tokenAcquisition, GraphServiceClient graphClient, ILogger<UploadController> logger)
{
_tokenAcquisition = tokenAcquisition;
_graphClient = graphClient;
_logger = logger;
}
...
Temporary upload to OneDrive
The backend operation first takes the file and the other parameters, and uploads the file directly to OneDrive for a temporary purpos.
public async Task<ActionResult<string>> Post([FromForm] UploadRequest fileUpload)
{
string userID = User.GetObjectId();
DriveItem uploadResult = await this._graphClient.Users[userID]
.Drive.Root
.ItemWithPath(fileUpload.file.FileName)
.Content.Request()
.PutAsync<DriveItem>(fileUpload.file.OpenReadStream());
Download with PDF conversion
Having a file uploaded to OneDrive, and received the driveItemID, it is quite simple to download it once again, but this time with the format conversion to PDF. Therefore, “?format=PDF” is attached to the request, here with the Microsoft Graph SDK as QueryOption
–
private async Task<Stream> GetPDF(string userID, string itemID)
{
var queryOptions = new List<QueryOption>()
{
new QueryOption("format", "PDF")
};
Stream pdfResult = await this._graphClient.Users[userID]
.Drive.Items[itemID]
.Content
.Request(queryOptions)
.GetAsync();
return pdfResult;
}
Final upload to target
Finally, the PDF converted Stream
needs to be uploaded to the target siteUrl
(definition depending on current context, see above). First, a new pdfFileName
needs to be constructed by exchanging former file extension with .pdf
The upload itself is easy (omitting UploadSession and big file sizes here). After evaluating the siteId
based on the given siteUrl
the upload itself can be executed and the webUrl
of the new DriveItem
can be returned for display.
private async Task<string> UploadPDF(string userID, string orgFileName, Stream fileStream, string siteUrl)
{
string pdfFileName = Path.GetFileNameWithoutExtension(orgFileName);
pdfFileName += ".pdf";
string siteId = await EvaluateSiteID(siteUrl);
DriveItem uploadResult = await this._graphClient.Sites[siteId]
.Drive.Root
.ItemWithPath(pdfFileName)
.Content.Request()
.PutAsync<DriveItem>(fileStream);
return uploadResult.WebUrl;
}
private async Task<string> EvaluateSiteID(string siteUrl)
{
Uri siteUri = new Uri(siteUrl);
Site site = await this._graphClient.Sites.GetByPath(siteUri.PathAndQuery, siteUri.Host).Request().GetAsync();
return site.Id;
}
Delete temporary file
Finally to clean up resources the temporary first upload file to OneDrive can be deleted. That’s quite easy because we still have the driveItem ID.
private async Task DeleteTempFile(string userID, string itemID)
{
await this._graphClient.Users[userID]
.Drive.Items[itemID]
.Request()
.DeleteAsync();
}
Finally, the app in action looks like the following:
I hope I explained the essential topics drag&drop in HTML5, Teams SSO, Microsoft Graph file upload and PDF conversion in an understandable way. Also, you understood once again, the benefit of such a backend solution, when having the need for a chain of upload and download operations. In SPFx I once upon a time realized a similar solution, but that is not the best idea. Nevertheless for your reference, the whole solution is available in my GitHub repository.
Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 Development. He loves SharePoint Framework but also has a passion for Microsoft Graph and Teams Development. He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich. In 2021 he received his first Microsoft MVP award in M365 Development for his continuous community contributions. Although if partially inspired by his daily work opinions are always personal. |