Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Graph;

/// <summary>
/// Adds services and implements methods to use Microsoft Graph SDK.
/// </summary>
internal static class GraphClientExtensions
{
/// <summary>
/// Extension method for adding the Microsoft Graph SDK to IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="scopes">The MS Graph scopes to request</param>
/// <returns></returns>
public static IServiceCollection AddMicrosoftGraphClient(this IServiceCollection services, params string[] scopes)
{
services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(options =>
{
foreach (var scope in scopes)
{
options.ProviderOptions.AdditionalScopesToConsent.Add(scope);
}
});

services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();
services.AddScoped<IHttpProvider, HttpClientHttpProvider>(sp => new HttpClientHttpProvider(new HttpClient()));
services.AddScoped(sp => new GraphServiceClient(
sp.GetRequiredService<IAuthenticationProvider>(),
sp.GetRequiredService<IHttpProvider>()));
return services;
}

/// <summary>
/// Implements IAuthenticationProvider interface.
/// Tries to get an access token for Microsoft Graph.
/// </summary>
private class GraphAuthenticationProvider : IAuthenticationProvider
{
public GraphAuthenticationProvider(IAccessTokenProvider provider)
{
Provider = provider;
}

public IAccessTokenProvider Provider { get; }

public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var result = await Provider.RequestAccessToken(new AccessTokenRequestOptions()
{
Scopes = new[] { "https://graph.microsoft.com/User.Read" }
});

if (result.TryGetToken(out var token))
{
request.Headers.Authorization ??= new AuthenticationHeaderValue("Bearer", token.Value);
}
}
}

private class HttpClientHttpProvider : IHttpProvider
{
private readonly HttpClient _client;

public HttpClientHttpProvider(HttpClient client)
{
_client = client;
}

public ISerializer Serializer { get; } = new Serializer();

public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

public void Dispose()
{
}

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
return _client.SendAsync(request);
}

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
return _client.SendAsync(request, completionOption, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@page "/profile"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@inject Microsoft.Graph.GraphServiceClient GraphServiceClient
@attribute [Authorize]

<h3>User Profile</h3>
@if (user == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tr>
<td> DisplayName </td>
<td> @user.DisplayName </td>
</tr>
<tr>
<td> UserPrincipalName </td>
<td> @user.UserPrincipalName </td>
</tr>
</table>
}

@code {
User? user;

protected override async Task OnInitializedAsync()
{
try
{
user = await GraphServiceClient.Me.Request().GetAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
"Methods": {
"Global": {
"CodeChanges": [
{
"Block": "builder.Services.AddMicrosoftGraphClient(\"https://graph.microsoft.com/User.Read\")",
"Options": [ "MicrosoftGraph" ],
"InsertAfter": "builder.Services.AddScoped",
"CodeFormatting": {
"Newline": true
}
},
{
"Block": "builder.Services.AddMsalAuthentication()",
"InsertAfter": "builder.Services.AddScoped",
Expand All @@ -23,6 +31,16 @@
"Newline": true,
"NumberOfSpaces": 4
}
},
{
"Block": "options.ProviderOptions.DefaultAccessTokenScopes.Add(\"https://graph.microsoft.com/User.Read\")",
"Options": [ "MicrosoftGraph" ],
"CodeChangeType": "Lambda",
"Parameter": "options",
"Parent": "builder.Services.AddMsalAuthentication",
"CodeFormatting": {
"NumberOfSpaces": 4
}
}
]
}
Expand Down Expand Up @@ -71,7 +89,7 @@
"AddFilePath": "Pages/Authentication.razor"
},
{
"FileName": "LoginDisplay.razor",
"FileName": "LoginDisplay.razor",
"AddFilePath": "Shared/LoginDisplay.razor"
},
{
Expand All @@ -86,6 +104,27 @@
{
"FileName": "RedirectToLogin.razor",
"AddFilePath": "Shared/RedirectToLogin.razor"
},
{
"FileName": "NavMenu.razor",
"Options": [ "MicrosoftGraph" ],
"Replacements": [
{
"Block": "</NavLink>\r\n </div>\r\n <div class=\"nav-item px-3\">\r\n <NavLink class=\"nav-link\" href=\"profile\">\r\n <span class=\"oi oi-list-rich\" aria-hidden=\"true\"></span> Show profile\r\n </NavLink>\r\n </div>\r\n </nav>\r\n</div>",
"ReplaceSnippet": "</NavLink>\r\n </div>\r\n </nav>\r\n</div>",
"Options": [ "MicrosoftGraph" ]
}
]
},
{
"FileName": "UserProfile.razor",
"AddFilePath": "Pages/UserProfile.razor",
"Options": [ "MicrosoftGraph" ]
},
{
"FileName": "GraphClientExtensions.cs",
"AddFilePath": "Data/GraphClientExtensions.cs",
"Options": [ "MicrosoftGraph" ]
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.DotNet.MSIdentity.Properties;
using Microsoft.DotNet.MSIdentity.Shared;
using Microsoft.DotNet.MSIdentity.Tool;
using Microsoft.DotNet.Scaffolding.Shared.CodeModifier;
Expand All @@ -23,6 +24,7 @@ internal class ProjectModifier
private readonly ProvisioningToolOptions _toolOptions;
private readonly IEnumerable<string> _files;
private readonly IConsoleLogger _consoleLogger;
private PropertyInfo? _codeModifierConfigPropertyInfo;

public ProjectModifier(ProvisioningToolOptions toolOptions, IEnumerable<string> files, IConsoleLogger consoleLogger)
{
Expand Down Expand Up @@ -110,9 +112,14 @@ private PropertyInfo? CodeModifierConfigPropertyInfo
{
get
{
var identifier = _toolOptions.ProjectTypeIdentifier.Replace('-', '_');
var propertyInfo = AppProvisioningTool.Properties.FirstOrDefault(p => p.Name.StartsWith("cm") && p.Name.EndsWith(identifier));
return propertyInfo;
if (_codeModifierConfigPropertyInfo == null)
{
var codeModifierName = $"cm_{_toolOptions.ProjectTypeIdentifier.Replace('-', '_')}";
_codeModifierConfigPropertyInfo = AppProvisioningTool.Properties.FirstOrDefault(
p => p.Name.Equals(codeModifierName));
}

return _codeModifierConfigPropertyInfo;
}
}

Expand Down Expand Up @@ -177,36 +184,32 @@ private void AddFile(CodeFile file, string identifier)
{
Directory.CreateDirectory(fileDir);
File.WriteAllText(filePath, codeFileString);
_consoleLogger.LogMessage($"Added {filePath}.\n");
}
}

private string GetCodeFileString(CodeFile file, string identifier)
internal static string GetCodeFileString(CodeFile file, string identifier) // todo make all code files strings
{
var propertyInfo = GetPropertyInfo(file.FileName, identifier);
if (propertyInfo is null)
// Resource files cannot contain '-' (dash) or '.' (period)
var codeFilePropertyName = $"add_{identifier.Replace('-', '_')}_{file.FileName.Replace('.', '_')}";
var property = AppProvisioningTool.Properties.FirstOrDefault(
p => p.Name.Equals(codeFilePropertyName));

if (property is null)
{
throw new FormatException($"Resource file for {file.FileName} could not be found. ");
throw new FormatException($"Resource property for {file.FileName} could not be found. ");
}

byte[] content = (propertyInfo.GetValue(null) as byte[])!;
string codeFileString = Encoding.UTF8.GetString(content);
var codeFileString = property.GetValue(typeof(Resources))?.ToString();

if (string.IsNullOrEmpty(codeFileString))
{
throw new FormatException($"Resource file for {file.FileName} could not be parsed. ");
throw new FormatException($"CodeFile string for {file.FileName} was empty.");
}

return codeFileString;
}

private PropertyInfo? GetPropertyInfo(string fileName, string identifier)
{
return AppProvisioningTool.Properties.Where(
p => p.Name.StartsWith("add")
&& p.Name.Contains(identifier.Replace('-', '_')) // Resource files cannot have '-' (dash character)
&& p.Name.Contains(fileName.Replace('.', '_'))) // Resource files cannot have '.' (period character)
.FirstOrDefault();
}

internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options)
{
if (file.FileName.Equals("Startup.cs"))
Expand Down Expand Up @@ -249,7 +252,7 @@ internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, Co
var root = documentBuilder.AddUsings(options);
if (file.FileName.Equals("Program.cs") && file.Methods.TryGetValue("Global", out var globalChanges))
{

var filteredChanges = ProjectModifierHelper.FilterCodeSnippets(globalChanges.CodeChanges, options);
var updatedIdentifer = ProjectModifierHelper.GetBuilderVariableIdentifierTransformation(root.Members);
if (updatedIdentifer.HasValue)
Expand Down Expand Up @@ -281,7 +284,7 @@ node is ClassDeclarationSyntax cds &&
modifiedClassDeclarationSyntax = documentBuilder.AddClassAttributes(modifiedClassDeclarationSyntax, options);

modifiedClassDeclarationSyntax = ModifyMethods(modifiedClassDeclarationSyntax, documentBuilder, file.Methods, options);

//add code snippets/changes.

//replace class node with all the updates.
Expand Down
Loading