зеркало из https://github.com/Azure/azure-saas.git
Bump MS Graph 4.x -> 5.x
This commit is contained in:
Родитель
aa93be8e60
Коммит
e5c701c071
|
@ -9,7 +9,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
AZURE_WEBAPP_NAME: 'admin-api-asdk-test-5yb3' # set this to your application's name
|
||||
AZURE_WEBAPP_NAME: 'admin-api-asdk-test-83sx' # set this to your application's name
|
||||
AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root
|
||||
DOTNET_VERSION: 7.x.x
|
||||
PROJECT_DIR: ./src/Saas.Admin/Saas.Admin.Service
|
||||
|
|
|
@ -9,7 +9,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
AZURE_WEBAPP_NAME: 'saas-app-asdk-test-aiuq' # set this to your application's name
|
||||
AZURE_WEBAPP_NAME: 'saas-app-asdk-test-83sx' # set this to your application's name
|
||||
AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root
|
||||
DOTNET_VERSION: 7.x.x
|
||||
PROJECT_DIR: ./src/Saas.Application/Saas.Application.Web
|
||||
|
|
|
@ -9,7 +9,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
AZURE_WEBAPP_NAME: 'signupadmin-app-asdk-test-aiuq' # set this to your application's name
|
||||
AZURE_WEBAPP_NAME: 'signupadmin-app-asdk-test-83sx' # set this to your application's name
|
||||
AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root
|
||||
DOTNET_VERSION: 7.x.x
|
||||
PROJECT_DIR: ./src/Saas.SignupAdministration/Saas.SignupAdministration.Web
|
||||
|
|
|
@ -12,23 +12,10 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.10.3" />
|
||||
<PackageReference Include="Azure.Security.KeyVault.Certificates" Version="4.5.1" />
|
||||
<PackageReference Include="Dawn.Guard" Version="1.12.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="6.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.UI" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
|
|
|
@ -10,7 +10,7 @@ permissions:
|
|||
|
||||
env:
|
||||
APP_NAME: admin-api
|
||||
AZURE_WEBAPP_NAME: admin-api-asdk-test-aiuq # set this to your application's name
|
||||
AZURE_WEBAPP_NAME: admin-api-asdk-test-83sx # set this to your application's name
|
||||
AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root
|
||||
DOTNET_VERSION: 7.x.x
|
||||
PROJECT_DIR: ./src/Saas.Admin/Saas.Admin.Service
|
||||
|
|
|
@ -9,17 +9,8 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="6.1.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.10.3" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.UI" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.11" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -10,7 +10,7 @@ permissions:
|
|||
|
||||
env:
|
||||
APP_NAME: saas-app
|
||||
AZURE_WEBAPP_NAME: saas-app-asdk-test-aiuq # set this to your application's name
|
||||
AZURE_WEBAPP_NAME: saas-app-asdk-test-83sx # set this to your application's name
|
||||
AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root
|
||||
DOTNET_VERSION: 7.x.x
|
||||
PROJECT_DIR: ./src/Saas.Application/Saas.Application.Web
|
||||
|
|
|
@ -16,24 +16,10 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.10.3" />
|
||||
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="6.1.1" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="4.54.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.13" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Graph.Models;
|
||||
using Saas.Permissions.Service.Exceptions;
|
||||
using Saas.Permissions.Service.Interfaces;
|
||||
using Saas.Permissions.Service.Models;
|
||||
|
@ -57,22 +58,24 @@ public class GraphAPIService : IGraphAPIService
|
|||
try
|
||||
{
|
||||
var graphUsers = await _graphServiceClient.Users
|
||||
.Request()
|
||||
.Filter($"identities/any(id: id/issuer eq '{_permissionOptions.Domain}' and id/issuerAssignedId eq '{userEmail}')")
|
||||
.Select("id, identitied, displayName")
|
||||
.GetAsync();
|
||||
.GetAsync(requestionConfiguration =>
|
||||
{
|
||||
requestionConfiguration.QueryParameters.Filter = $"identities/any(id: id/issuer eq '{_permissionOptions.Domain}' and id/issuerAssignedId eq '{userEmail}')";
|
||||
requestionConfiguration.QueryParameters.Select = new string[] { "id, identities, displayName" };
|
||||
});
|
||||
|
||||
if (graphUsers.Count > 1)
|
||||
if (graphUsers?.Value?.Count > 1)
|
||||
{
|
||||
throw new UserNotFoundException($"More than one user with the email {userEmail} exists in the Identity provider");
|
||||
}
|
||||
if (graphUsers.Count == 0)
|
||||
|
||||
if (graphUsers?.Value?.Count == 0 || graphUsers?.Value is null)
|
||||
{
|
||||
throw new UserNotFoundException($"The user with the email {userEmail} was not found in the Identity Provider");
|
||||
}
|
||||
|
||||
// Ok to just return first, because at this point we've verified we have exactly 1 user in the graphUsers object.
|
||||
return ToUserObjects(graphUsers).First();
|
||||
return ToUserObjects(graphUsers.Value).First();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -84,29 +87,34 @@ public class GraphAPIService : IGraphAPIService
|
|||
// Enriches the user object with data from Microsoft Graph.
|
||||
public async Task<IEnumerable<Models.User>> GetUsersByIds(ICollection<Guid> userIds)
|
||||
{
|
||||
// Build graph query: "id in ('id1', 'id2')"
|
||||
// https://docs.microsoft.com/en-us/graph/aad-advanced-queries?tabs=csharp
|
||||
StringBuilder filter = new();
|
||||
filter.Append("id in (");
|
||||
filter.Append(string.Join(",", userIds.Select(id => $"'{id}'")));
|
||||
filter.Append(')');
|
||||
|
||||
|
||||
List<Models.User> userList = new();
|
||||
|
||||
try
|
||||
{
|
||||
var graphUsers = await _graphServiceClient.Users
|
||||
.Request()
|
||||
.Filter(filter.ToString())
|
||||
.GetAsync();
|
||||
.GetAsync(requestConfiguration =>
|
||||
{
|
||||
requestConfiguration.QueryParameters.Filter = MakeUserFilter();
|
||||
});
|
||||
|
||||
userList.AddRange(ToUserObjects(graphUsers));
|
||||
|
||||
while (graphUsers.NextPageRequest is not null)
|
||||
if (graphUsers?.Value is null)
|
||||
{
|
||||
graphUsers = await graphUsers.NextPageRequest.GetAsync();
|
||||
userList.AddRange(ToUserObjects(graphUsers));
|
||||
};
|
||||
return userList;
|
||||
}
|
||||
|
||||
PageIterator<Microsoft.Graph.Models.User, UserCollectionResponse> pageIterator
|
||||
= PageIterator<Microsoft.Graph.Models.User, UserCollectionResponse>
|
||||
.CreatePageIterator(
|
||||
_graphServiceClient,
|
||||
graphUsers,
|
||||
(msg) =>
|
||||
{
|
||||
userList.Add(ToUserObject(msg));
|
||||
return true;
|
||||
});
|
||||
|
||||
await pageIterator.IterateAsync();
|
||||
|
||||
return userList;
|
||||
}
|
||||
|
@ -115,17 +123,31 @@ public class GraphAPIService : IGraphAPIService
|
|||
_logError(_logger, ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
string MakeUserFilter ()
|
||||
{
|
||||
// Build graph query: "id in ('id1', 'id2')"
|
||||
// https://docs.microsoft.com/en-us/graph/aad-advanced-queries?tabs=csharp
|
||||
StringBuilder filter = new();
|
||||
filter.Append("id in (");
|
||||
filter.Append(string.Join(",", userIds.Select(id => $"'{id}'")));
|
||||
filter.Append(')');
|
||||
|
||||
return filter.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ServicePrincipal?> GetServicePrincipalAsync(string clientId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var servicePrincipal = await _graphServiceClient.ServicePrincipals.Request()
|
||||
.Filter($"appId eq '{clientId}'")
|
||||
.GetAsync();
|
||||
var servicePrincipal = await _graphServiceClient.ServicePrincipals
|
||||
.GetAsync(requestConfiguration =>
|
||||
{
|
||||
requestConfiguration.QueryParameters.Filter = $"appId eq '{clientId}'";
|
||||
});
|
||||
|
||||
return servicePrincipal.SingleOrDefault();
|
||||
return servicePrincipal?.Value?.SingleOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -139,17 +161,38 @@ public class GraphAPIService : IGraphAPIService
|
|||
try
|
||||
{
|
||||
var userAppRoleAssignments = await _graphServiceClient.Users[userId].AppRoleAssignments
|
||||
.Request()
|
||||
.Filter($"resourceId eq {servicePrincipal.Id}")
|
||||
.GetAsync();
|
||||
.GetAsync(requestConfiguration =>
|
||||
{
|
||||
requestConfiguration.Equals($"resourceId eq {servicePrincipal.Id}");
|
||||
}) ?? throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
|
||||
|
||||
var appRoleIds = userAppRoleAssignments.Select(a => a.AppRoleId);
|
||||
var appRoleIds = userAppRoleAssignments?.Value?
|
||||
.Where(appRole => appRole is not null)
|
||||
.Where(appRole => appRole.AppRoleId is not null)
|
||||
.Select(appRole => appRole.AppRoleId);
|
||||
|
||||
var appRoles = servicePrincipal.AppRoles
|
||||
.Where(a => appRoleIds.Contains(a.Id))
|
||||
.Select(a => a.Value);
|
||||
if (appRoleIds is null || !appRoleIds.Any())
|
||||
{
|
||||
throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
|
||||
}
|
||||
|
||||
return appRoles.ToArray();
|
||||
var appRoles = servicePrincipal.AppRoles?
|
||||
.Where(appRole => appRole?.Id is not null)
|
||||
.Where(appRole => appRoleIds.Contains(appRole.Id));
|
||||
|
||||
if (appRoles is null || !appRoles.Any())
|
||||
{
|
||||
throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
|
||||
}
|
||||
|
||||
var roleClaimsArray = appRoles
|
||||
.Select(appRole => appRole.Value ?? string.Empty)
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.ToArray();
|
||||
|
||||
return roleClaimsArray is not null && roleClaimsArray.Any()
|
||||
? roleClaimsArray
|
||||
: throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -158,12 +201,19 @@ public class GraphAPIService : IGraphAPIService
|
|||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Models.User> ToUserObjects(IGraphServiceUsersCollectionPage graphUsers) =>
|
||||
private static IEnumerable<Models.User> ToUserObjects(IEnumerable<Microsoft.Graph.Models.User> graphUsers) =>
|
||||
graphUsers.Select(graphUser => new Models.User()
|
||||
{
|
||||
UserId = graphUser.Id,
|
||||
DisplayName = graphUser.DisplayName
|
||||
});
|
||||
|
||||
private static Models.User ToUserObject(Microsoft.Graph.Models.User graphUser) =>
|
||||
new()
|
||||
{
|
||||
UserId = graphUser.Id,
|
||||
DisplayName = graphUser.DisplayName
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using Microsoft.Graph;
|
||||
using Saas.Shared.Options;
|
||||
using Saas.Permissions.Service.Interfaces;
|
||||
using Microsoft.Kiota.Abstractions.Authentication;
|
||||
|
||||
namespace Saas.Permissions.Service.Services;
|
||||
|
||||
|
@ -21,10 +22,6 @@ public class GraphApiClientFactory : IGraphApiClientFactory
|
|||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public GraphServiceClient Create() =>
|
||||
new(_httpClient, _msGraphOptions.BaseUrl)
|
||||
{
|
||||
AuthenticationProvider = _authenticationProvider
|
||||
};
|
||||
|
||||
public GraphServiceClient Create() =>
|
||||
new(_httpClient, _authenticationProvider, _msGraphOptions.BaseUrl);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Kiota.Abstractions.Authentication;
|
||||
using Saas.Identity.Crypto;
|
||||
using Saas.Identity.Interface;
|
||||
using Saas.Identity.Provider;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Kiota.Abstractions;
|
||||
using Microsoft.Kiota.Abstractions.Authentication;
|
||||
using Saas.Shared.Interface;
|
||||
using Saas.Shared.Options;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Saas.Identity.Provider;
|
||||
public class SaasGraphClientCredentialsProvider<TOptions> : IAuthenticationProvider
|
||||
|
@ -26,12 +26,28 @@ public class SaasGraphClientCredentialsProvider<TOptions> : IAuthenticationProvi
|
|||
_authProvider = authProvider;
|
||||
}
|
||||
|
||||
public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage)
|
||||
//public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// requestMessage.Headers.Authorization =
|
||||
// new AuthenticationHeaderValue("bearer", await _authProvider.GetAccessTokenAsync());
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logError(_logger, ex);
|
||||
// throw;
|
||||
// }
|
||||
//}
|
||||
|
||||
public async Task AuthenticateRequestAsync(
|
||||
RequestInformation request,
|
||||
Dictionary<string, object>? additionalAuthenticationContext = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
requestMessage.Headers.Authorization =
|
||||
new AuthenticationHeaderValue("bearer", await _authProvider.GetAccessTokenAsync());
|
||||
request.Headers.Add("Authorization", $"bearer { await _authProvider.GetAccessTokenAsync() }");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="4.54.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.32.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
//using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
|
||||
//namespace Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Controllers;
|
||||
|
||||
//// https://damienbod.com/2022/05/16/using-multiple-azure-b2c-user-flows-from-asp-net-core/
|
||||
|
||||
//[AllowAnonymous]
|
||||
//[Route("MicrosoftIdentity/[controller]/[action]")]
|
||||
//public class AccountSignUpController : Controller
|
||||
//{
|
||||
// [HttpGet("{scheme?}")]
|
||||
// public IActionResult SignUpPolicy(
|
||||
// [FromRoute] string scheme,
|
||||
// [FromQuery] string redirectUri)
|
||||
// {
|
||||
// scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
|
||||
|
||||
// string redirect = !string.IsNullOrEmpty(redirectUri) && Url.IsLocalUrl(redirectUri)
|
||||
// ? redirectUri
|
||||
// : Url.Content("~/");
|
||||
|
||||
// AuthenticationProperties properties = new() { RedirectUri = redirect };
|
||||
|
||||
// properties.Items[Constants.Policy] = "B2C_1A_SIGNUP_SIGNIN";
|
||||
// return Challenge(properties, scheme);
|
||||
// }
|
||||
//}
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
<i class="fab fa-microsoft"></i> Microsoft - © 2022 - Azure SaaS - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
<i class="fab fa-microsoft"></i> Microsoft - © 2023 - Azure SaaS - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
|
|
|
@ -10,7 +10,7 @@ permissions:
|
|||
|
||||
env:
|
||||
APP_NAME: signupadmin-app
|
||||
AZURE_WEBAPP_NAME: signupadmin-app-asdk-test-aiuq # set this to your application's name
|
||||
AZURE_WEBAPP_NAME: signupadmin-app-asdk-test-83sx # set this to your application's name
|
||||
AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root
|
||||
DOTNET_VERSION: 7.x.x
|
||||
PROJECT_DIR: ./src/Saas.SignupAdministration/Saas.SignupAdministration.Web
|
||||
|
|
Загрузка…
Ссылка в новой задаче