Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a custom endpoint for generate a Direct Line token #142

Merged
merged 16 commits into from
Jan 24, 2023
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
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ REMINDER_CRON_EXPR=0 0 8 * * ? * # At 08:00:00am every day.
MICROSOFT_APP_TYPE=
MICROSOFT_APP_ID=
MICROSOFT_APP_PASSWORD=
MICROSOFT_APP_TENANT_ID=
MICROSOFT_APP_TENANT_ID=

#
# Direct Line settings
#
DIRECT_LINE_SECRET=
DIRECT_LINE_BASE_URL=http://localhost:5000/
1 change: 1 addition & 0 deletions src/Constants/ResponseMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class ResponseMessages
public const string ResourceNotFoundMessage = "Recurso no encontrado.";
public const string ResourceFromAnotherUserMessage = "El recurso es de otro usuario.";

public const string DirectLineTokenFailedMessage = "Direct Line token API call failed.";
public const string InactiveUserAccountMessage = "Cuenta de usuario inactiva.";
public const string EmailSuccessfullyVerifiedMessage = "Correo electrónico verificado con éxito.";
public const string SuccessfulLoginMessage = "Ha iniciado la sesión con éxito.";
Expand Down
2 changes: 1 addition & 1 deletion src/DentallApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<PackageReference Include="Scriban" Version="5.5.0" />
<PackageReference Include="SendGrid.Extensions.DependencyInjection" Version="1.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
<PackageReference Include="DotEnv.Core" Version="2.3.0" />
<PackageReference Include="DotEnv.Core" Version="2.3.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
9 changes: 9 additions & 0 deletions src/Features/Chatbot/DirectLine/DTOs/DirectLineGetTokenDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace DentallApp.Features.Chatbot.DirectLine.DTOs;

public class DirectLineGetTokenDto
{
public string ConversationId { get; set; }
public string Token { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
}
23 changes: 23 additions & 0 deletions src/Features/Chatbot/DirectLine/DirectLineController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace DentallApp.Features.Chatbot.DirectLine;

[Route("directline/token")]
[ApiController]
public class DirectLineController : ControllerBase
{
private readonly DirectLineService _directLineService;

public DirectLineController(DirectLineService directLineService)
{
_directLineService = directLineService;
}

[AuthorizeByRole(RolesName.BasicUser)]
[HttpGet]
public async Task<ActionResult> Get()
{
var response = await _directLineService.GetTokenAsync(User.GetUserId());
return response.Success ?
Ok(new { response.Data.Token }) :
BadRequest(new { response.Message });
}
}
27 changes: 27 additions & 0 deletions src/Features/Chatbot/DirectLine/DirectLineExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace DentallApp.Features.Chatbot.DirectLine;

public static class DirectLineExtensions
{
public static IServiceCollection AddDirectLineService(this IServiceCollection services)
{
var settings = new EnvBinder()
.AllowBindNonPublicProperties()
.Bind<DirectLineSettings>();

services.AddSingleton(settings);
var serviceName = settings.GetServiceName();
services.AddHttpClient(
name: serviceName,
httpClient => httpClient.BaseAddress = new Uri(settings.GetDirectLineBaseUrl())
);

services.AddTransient(
serviceType: typeof(DirectLineService),
implementationType: Type.GetType(typeName: GetServiceNameWithNamespace(serviceName))
);
return services;
}

private static string GetServiceNameWithNamespace(string serviceName)
=> $"{typeof(DirectLineService).Namespace}.{serviceName}";
}
41 changes: 41 additions & 0 deletions src/Features/Chatbot/DirectLine/DirectLineSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace DentallApp.Features.Chatbot.DirectLine;

public class DirectLineSettings
{
public const string DirectLineSecretSetting = "DIRECT_LINE_SECRET";
public const string DirectLineBaseUrlSetting = "DIRECT_LINE_BASE_URL";
public const string DefaultBaseUrl = "https://directline.botframework.com/";
public const string DefaultServiceName = nameof(DirectLineAzureService);

public string DirectLineSecret { get; set; }
private string DirectLineBaseUrl { get; set; }

public string GetDirectLineBaseUrl()
=> string.IsNullOrWhiteSpace(DirectLineBaseUrl) ?
DefaultBaseUrl :
DirectLineBaseUrl.TrimEnd('/') + "/";

/// <summary>
/// Gets the name of the Direct Line service based on the URL loaded from the .env file.
/// </summary>
/// <remarks>
/// Available services:
/// <list type="bullet">
/// <item><see cref="InDirectLineService"/></item>
/// <item><see cref="DirectLineAzureService"/></item>
/// </list>
/// </remarks>
/// <returns>The name of the Direct Line service.</returns>
public string GetServiceName()
{
var baseUrl = GetDirectLineBaseUrl();
return string.IsNullOrWhiteSpace(baseUrl) ?
DefaultServiceName :
GetServiceName(baseUrl);
}

private string GetServiceName(string baseUrl)
=> baseUrl.StartsWith(DefaultBaseUrl) ?
DefaultServiceName :
nameof(InDirectLineService);
}
60 changes: 60 additions & 0 deletions src/Features/Chatbot/DirectLine/Services/DirectLineAzureService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace DentallApp.Features.Chatbot.DirectLine.Services;

/// <summary>
/// Represents the Direct Line channel of Azure Bot.
/// </summary>
public class DirectLineAzureService : DirectLineService
{
private readonly DirectLineSettings _directLineSettings;

public DirectLineAzureService(IHttpClientFactory httpFactory, DirectLineSettings directLineSettings)
: base(httpFactory, namedClient: nameof(DirectLineAzureService))
{
_directLineSettings = directLineSettings;
}

/// <summary>
/// Adds the prefix <c>dl_</c> to the user ID, as required by the Direct Line API.
/// </summary>
/// <param name="userId">The user ID to add the prefix.</param>
/// <returns>The user ID with the prefix.</returns>
private static string AddPrefixToUserId(int userId)
=> $"dl_{userId}";

/// <inheritdoc />
public async override Task<Response<DirectLineGetTokenDto>> GetTokenAsync(int userId)
{
var requestBody = new
{
user = new
{
id = AddPrefixToUserId(userId)
}
};
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri)
{
Headers =
{
{ "Authorization", $"Bearer {_directLineSettings.DirectLineSecret}" },
},
Content = new StringContent(content: JsonConvert.SerializeObject(requestBody),
Encoding.UTF8,
MediaTypeNames.Application.Json),
};

var tokenResponseMessage = await Client.SendAsync(tokenRequest, default);

if (!tokenResponseMessage.IsSuccessStatusCode)
return new Response<DirectLineGetTokenDto> { Message = DirectLineTokenFailedMessage };

var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<DirectLineGetTokenDto>(responseContentString);

return new Response<DirectLineGetTokenDto>
{
Success = true,
Data = tokenResponse,
Message = GetResourceMessage
};
}
}
33 changes: 33 additions & 0 deletions src/Features/Chatbot/DirectLine/Services/DirectLineService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace DentallApp.Features.Chatbot.DirectLine.Services;

/// <summary>
/// Represents the client that obtains the Direct Line token.
/// </summary>
public abstract class DirectLineService
{
public const string RequestUri = "v3/directline/tokens/generate";
private readonly HttpClient _client;
protected HttpClient Client => _client;

/// <summary>
/// Initializes a new instance of the <see cref="DirectLineService" /> class with a specified <see cref="HttpClient" />.
/// </summary>
/// <param name="client">An instance of <see cref="HttpClient" />.</param>
public DirectLineService(HttpClient client)
=> _client = client;

/// <summary>
/// Initializes a new instance of the <see cref="DirectLineService" /> class with a specified factory and a reference with named client.
/// </summary>
/// <param name="httpFactory">A factory that can create <see cref="HttpClient" /> instances.</param>
/// <param name="namedClient">The logical name of the client to create.</param>
public DirectLineService(IHttpClientFactory httpFactory, string namedClient)
=> _client = httpFactory.CreateClient(namedClient);

/// <summary>
/// Gets the Direct Line token to access a single conversation associated with the bot.
/// </summary>
/// <param name="userId">The ID of the authenticated user.</param>
/// <returns></returns>
public abstract Task<Response<DirectLineGetTokenDto>> GetTokenAsync(int userId);
}
42 changes: 42 additions & 0 deletions src/Features/Chatbot/DirectLine/Services/InDirectLineService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace DentallApp.Features.Chatbot.DirectLine.Services;

/// <summary>
/// Represents an own implementation of Direct Line API that runs without Azure.
/// <para>See <see href="https://github.com/newbienewbie/InDirectLine" />.</para>
/// </summary>
public class InDirectLineService : DirectLineService
{
public InDirectLineService(IHttpClientFactory httpFactory)
: base(httpFactory, namedClient: nameof(InDirectLineService)) { }

/// <inheritdoc />
public async override Task<Response<DirectLineGetTokenDto>> GetTokenAsync(int userId)
{
var requestBody = new
{
userId = userId.ToString(),
password = ""
};
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri)
{
Content = new StringContent(content: JsonConvert.SerializeObject(requestBody),
Encoding.UTF8,
MediaTypeNames.Application.Json),
};

var tokenResponseMessage = await Client.SendAsync(tokenRequest, default);

if (!tokenResponseMessage.IsSuccessStatusCode)
return new Response<DirectLineGetTokenDto> { Message = DirectLineTokenFailedMessage };

var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<DirectLineGetTokenDto>(responseContentString);

return new Response<DirectLineGetTokenDto>
{
Success = true,
Data = tokenResponse,
Message = GetResourceMessage
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public static IServiceCollection AddBotServices(this IServiceCollection services
services.AddSingleton<IAppointmentBotService, AppointmentBotService>();
services.AddScoped<IBotQueryRepository, BotQueryRepository>();

services.AddDirectLineService();

// Create the Bot Framework Authentication to be used with the Bot Adapter.
services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

Expand All @@ -22,7 +24,6 @@ public static IServiceCollection AddBotServices(this IServiceCollection services

// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddTransient<IBot, AppointmentBotHandler<RootDialog>>();

return services;
}
}
5 changes: 4 additions & 1 deletion src/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
global using DentallApp.Features.Chatbot;
global using DentallApp.Features.Chatbot.DirectLine;
global using DentallApp.Features.Chatbot.DirectLine.Services;
global using DentallApp.Features.Chatbot.DirectLine.DTOs;
global using DentallApp.Features.Chatbot.Dialogs;
global using DentallApp.Features.Chatbot.Factories;
global using DentallApp.Features.Chatbot.Helpers;
Expand Down Expand Up @@ -90,6 +93,7 @@

global using System.Data;
global using System.Text;
global using System.Net.Mime;
global using System.Text.Encodings.Web;
global using System.Linq.Expressions;
global using System.Reflection;
Expand All @@ -112,7 +116,6 @@
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Authorization;


global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Diagnostics;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
Expand Down
78 changes: 78 additions & 0 deletions tests/Features/Chatbot/DirectLine/DirectLineSettingsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
namespace DentallApp.Tests.Features.Chatbot.DirectLine;

[TestClass]
public class DirectLineSettingsTests
{
private const string LocalHostBaseUrl = "http://localhost:5000";
private IEnvBinder _envBinder;

[TestInitialize]
public void TestInitialize()
{
_envBinder = new EnvBinder().AllowBindNonPublicProperties();
Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineSecretSetting, "SECRET");
}

[TestMethod]
public void GetDirectLineBaseUrl_WhenDirectLineBaseUrlIsEmptyOrWhiteSpace_ShouldReturnsDefaultBaseUrl()
{
Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, " ");
var settings = _envBinder.Bind<DirectLineSettings>();

var baseUrl = settings.GetDirectLineBaseUrl();

Assert.AreEqual(expected: DirectLineSettings.DefaultBaseUrl, actual: baseUrl);
}

[DataTestMethod]
[DataRow(LocalHostBaseUrl + "/")]
[DataRow(LocalHostBaseUrl)]
[DataRow(LocalHostBaseUrl + "///")]
public void GetDirectLineBaseUrl_WhenDirectLineBaseUrlIsNotEmpty_ShouldReturnsBaseUrlWithSlashAtEnd(string value)
{
Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, value);
var settings = _envBinder.Bind<DirectLineSettings>();

var baseUrl = settings.GetDirectLineBaseUrl();

Assert.AreEqual(expected: "http://localhost:5000/", actual: baseUrl);
}

[TestMethod]
public void GetServiceName_WhenBaseUrlIsEmptyOrWhiteSpace_ShouldReturnsDefaultServiceName()
{
Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, " ");
var settings = _envBinder.Bind<DirectLineSettings>();

var serviceName = settings.GetServiceName();

Assert.AreEqual(expected: DirectLineSettings.DefaultServiceName, actual: serviceName);
}

[DataTestMethod]
[DataRow(DirectLineSettings.DefaultBaseUrl)]
[DataRow(DirectLineSettings.DefaultBaseUrl + "///")]
public void GetServiceName_WhenBaseUrlStartsWithDefaultBaseUrl_ShouldReturnsDefaultServiceName(string baseUrl)
{
Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, baseUrl);
var settings = _envBinder.Bind<DirectLineSettings>();

var serviceName = settings.GetServiceName();

Assert.AreEqual(expected: DirectLineSettings.DefaultServiceName, actual: serviceName);
}

[DataTestMethod]
[DataRow(LocalHostBaseUrl + "/")]
[DataRow(LocalHostBaseUrl)]
[DataRow(LocalHostBaseUrl + "///")]
public void GetServiceName_WhenBaseUrlNotStartsWithDefaultBaseUrl_ShouldReturnsInDirectLineService(string baseUrl)
{
Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, baseUrl);
var settings = _envBinder.Bind<DirectLineSettings>();

var serviceName = settings.GetServiceName();

Assert.AreEqual(expected: nameof(InDirectLineService), actual: serviceName);
}
}
Loading