From 8f2ef5ba632a6958467561402ae9ecb56b730676 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Wed, 14 Feb 2024 17:45:28 -0600 Subject: [PATCH 1/6] GitHub SSO --- .../Users/Services/UserIdentity.cs | 18 ++++++-- .../Users/Services/UserService.cs | 6 +-- .../BotSharp.OpenAPI/BotSharp.OpenAPI.csproj | 13 +++++- .../BotSharpOpenApiExtensions.cs | 40 ++++++++++++++-- .../Controllers/UserController.cs | 46 ++++++++++++++++++- .../ViewModels/Users/UserViewModel.cs | 4 +- .../BotSharp.Plugin.ChatHub.csproj | 4 -- .../WebSocketsMiddleware.cs | 2 +- src/WebStarter/appsettings.json | 7 +++ 9 files changed, 119 insertions(+), 21 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs index b9b8b932..8e5852cc 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs @@ -14,17 +14,27 @@ public class UserIdentity : IUserIdentity } - public string Id + public string Id => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value!; public string UserName => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value!; - public string Email + public string Email => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value!; - public string FirstName - => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value!; + public string FirstName + { + get + { + var givenName = _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName); + if (givenName == null) + { + return UserName; + } + return givenName.Value; + } + } public string LastName => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index ce9a9d60..7dd9b327 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -48,7 +48,7 @@ public class UserService : IUserService } record = user; - record.Email = user.Email.ToLower(); + record.Email = user.Email?.ToLower(); record.Salt = Guid.NewGuid().ToString("N"); record.Password = Utilities.HashText(user.Password, record.Salt); @@ -167,7 +167,7 @@ public class UserService : IUserService Issuer = issuer, Audience = audience, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha512Signature) + SecurityAlgorithms.HmacSha256Signature) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); @@ -178,7 +178,7 @@ public class UserService : IUserService public async Task GetMyProfile() { var db = _services.GetRequiredService(); - var user = db.GetUserById(_user.Id); + var user = db.GetUserByUserName(_user.UserName); return user; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj index 6ea73972..e1c23384 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj @@ -18,9 +18,18 @@ - - + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 19584d5a..7d6796c5 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -1,12 +1,14 @@ using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core.Users.Services; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; using System.IdentityModel.Tokens.Jwt; namespace BotSharp.OpenAPI; @@ -29,11 +31,14 @@ public static class BotSharpOpenApiExtensions services.AddScoped(); // Add bearer authentication + var schema = "MIXED_SCHEME"; services.AddAuthentication(options => { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + // custom scheme defined in .AddPolicyScheme() below + // inspired from https://weblog.west-wind.com/posts/2022/Mar/29/Combining-Bearer-Token-and-Cookie-Auth-in-ASPNET + options.DefaultScheme = schema; + options.DefaultChallengeScheme = schema; + options.DefaultAuthenticateScheme = schema; }).AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters @@ -52,6 +57,32 @@ public static class BotSharpOpenApiExtensions o.TokenValidationParameters.SignatureValidator = (string token, TokenValidationParameters parameters) => new JwtSecurityToken(token); } + }).AddCookie(options => + { + }).AddGitHub(options => + { + options.ClientId = config["OAuth:GitHub:ClientId"]; + options.ClientSecret = config["OAuth:GitHub:ClientSecret"]; + options.Scope.Add("user:email"); + }).AddPolicyScheme(schema, "Mixed authentication", options => + { + // runs on each request + options.ForwardDefaultSelector = context => + { + // filter by auth type + string authorization = context.Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ")) + return JwtBearerDefaults.AuthenticationScheme; + else if (context.Request.Cookies.ContainsKey(".AspNetCore.Cookies")) + return CookieAuthenticationDefaults.AuthenticationScheme; + else if (context.Request.Path.StartsWithSegments("/sso") && context.Request.Method == "GET") + return CookieAuthenticationDefaults.AuthenticationScheme; + else if (context.Request.Path.ToString().StartsWith("/signin-") && context.Request.Method == "GET") + return CookieAuthenticationDefaults.AuthenticationScheme; + + // otherwise always check for cookie auth + return JwtBearerDefaults.AuthenticationScheme; + }; }); // Add services to the container. @@ -123,6 +154,7 @@ public static class BotSharpOpenApiExtensions if (env.IsDevelopment()) { + IdentityModelEventSource.ShowPII = true; app.UseSwaggerUI(); app.UseDeveloperExceptionPage(); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index 13dd6c78..34ed7a0e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -1,3 +1,6 @@ +using AspNet.Security.OAuth.GitHub; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using System.ComponentModel.DataAnnotations; namespace BotSharp.OpenAPI.Controllers; @@ -6,9 +9,11 @@ namespace BotSharp.OpenAPI.Controllers; [ApiController] public class UserController : ControllerBase { + private readonly IServiceProvider _services; private readonly IUserService _userService; - public UserController(IUserService userService) + public UserController(IUserService userService, IServiceProvider services) { + _services = services; _userService = userService; } @@ -30,6 +35,25 @@ public class UserController : ControllerBase return Ok(token); } + [AllowAnonymous] + [HttpGet("/sso/{provider}")] + public async Task Authorize([FromRoute] string provider) + { + return Challenge(new AuthenticationProperties { RedirectUri = $"page/user/me" }, provider); + } + + [AllowAnonymous] + [HttpGet("/signout")] + [HttpPost("/signout")] + public IActionResult SignOutCurrentUser() + { + // Instruct the cookies middleware to delete the local cookie created + // when the user agent is redirected from the external identity provider + // after a successful authentication flow (e.g Google or Facebook). + return SignOut(new AuthenticationProperties { RedirectUri = "/" }, + CookieAuthenticationDefaults.AuthenticationScheme); + } + [AllowAnonymous] [HttpPost("/user")] public async Task CreateUser(UserCreationModel user) @@ -38,10 +62,28 @@ public class UserController : ControllerBase return UserViewModel.FromUser(createdUser); } - [HttpGet("/user/my")] + [HttpGet("/user/me")] public async Task GetMyUserProfile() { var user = await _userService.GetMyProfile(); + if (user == null) + { + var identiy = _services.GetRequiredService(); + var accessor = _services.GetRequiredService(); + var claims = accessor.HttpContext.User.Claims; + if (claims.Any(x => x.Type == GitHubAuthenticationConstants.Claims.Name)) + { + user = await _userService.CreateUser(new User + { + Email = identiy.Email, + UserName = identiy.UserName, + FirstName = identiy.FirstName, + LastName = identiy.LastName, + Source = "GitHub", + ExternalId = identiy.Id, + }); + } + } return UserViewModel.FromUser(user); } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index 2d7d2922..eeb1a8a2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -15,7 +15,8 @@ public class UserViewModel public string Email { get; set; } public string Role { get; set; } = UserRole.Client; [JsonPropertyName("full_name")] - public string FullName => $"{FirstName} {LastName}"; + public string FullName => $"{FirstName} {LastName}".Trim(); + public string Source { get; set; } [JsonPropertyName("external_id")] public string? ExternalId { get; set; } [JsonPropertyName("create_date")] @@ -43,6 +44,7 @@ public class UserViewModel LastName = user.LastName, Email = user.Email, Role = user.Role, + Source = user.Source, ExternalId = user.ExternalId, CreateDate = user.CreatedTime, UpdateDate = user.UpdatedTime diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj index 59c67182..43faa831 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj +++ b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj @@ -16,10 +16,6 @@ \ - - - - diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs index c151a62e..bd3e90aa 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -20,7 +20,7 @@ public class WebSocketsMiddleware if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) && request.Query.TryGetValue("access_token", out var accessToken)) { - request.Headers.Add("Authorization", $"Bearer {accessToken}"); + request.Headers["Authorization"] = $"Bearer {accessToken}"; } await _next(httpContext); diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 426a141c..c5be322c 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -13,6 +13,13 @@ "Key": "31ba6052aa6f4569901facc3a41fcb4a" }, + "OAuth": { + "GitHub": { + "ClientId": "", + "ClientSecret": "" + } + }, + "LlmProviders": [ { "Provider": "azure-openai", From 45fc4c91d852f8ba77b4712848055b516603bb45 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 14 Feb 2024 19:13:35 -0600 Subject: [PATCH 2/6] add role in content log --- .../Loggers/Models/StreamingLogModel.cs | 2 ++ .../BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StreamingLogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StreamingLogModel.cs index 51941138..e0986272 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StreamingLogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StreamingLogModel.cs @@ -6,6 +6,8 @@ public class StreamingLogModel public string ConversationId { get; set; } [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("role")] + public string Role { get; set; } [JsonPropertyName("content")] public string Content { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 8b0aa2cd..c4e74ec4 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -37,7 +37,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook { var conversationId = _state.GetConversationId(); var log = $"MessageId: {message.MessageId} ==>\r\n{message.Role}: {message.Content}"; - await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, _user.UserName, log)); + await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, _user.UserName, message.Role, log)); } public async Task BeforeGenerating(Agent agent, List conversations) @@ -64,21 +64,22 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook var conversationId = _state.GetConversationId(); var agent = await agentService.LoadAgent(message.CurrentAgentId); - await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, tokenStats.Prompt)); + await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, message.Role, tokenStats.Prompt)); var log = message.Role == AgentRole.Function ? $"[{agent?.Name}]: {message.FunctionName}({message.FunctionArgs})" : $"[{agent?.Name}]: {message.Content}"; log += $"\r\n<== MessageId: {message.MessageId}"; - await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, log)); + await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, message.Role, log)); } - private string BuildLog(string conversationId, string? name, string content) + private string BuildLog(string conversationId, string? name, string role, string content) { var log = new StreamingLogModel { ConversationId = conversationId, Name = name, + Role = role, Content = content, CreateTime = DateTime.UtcNow }; From 7d1aa8fcf5c2c9eb10172b65de7b907565b93702 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 14 Feb 2024 19:48:03 -0600 Subject: [PATCH 3/6] fix oauth config --- .../FileRepository.Conversation.cs | 7 +++- .../BotSharpOpenApiExtensions.cs | 37 +++++++++++-------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 0b17b492..2ae0d1a3 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -206,8 +206,13 @@ namespace BotSharp.Core.Repository var records = new List(); var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); var pager = filter?.Pager ?? new Pagination(); - var totalDirs = Directory.GetDirectories(dir); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var totalDirs = Directory.GetDirectories(dir); foreach (var d in totalDirs) { var path = Path.Combine(d, CONVERSATION_FILE); diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 7d6796c5..5d744f8e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -32,7 +32,7 @@ public static class BotSharpOpenApiExtensions // Add bearer authentication var schema = "MIXED_SCHEME"; - services.AddAuthentication(options => + var builder = services.AddAuthentication(options => { // custom scheme defined in .AddPolicyScheme() below // inspired from https://weblog.west-wind.com/posts/2022/Mar/29/Combining-Bearer-Token-and-Cookie-Auth-in-ASPNET @@ -59,11 +59,6 @@ public static class BotSharpOpenApiExtensions } }).AddCookie(options => { - }).AddGitHub(options => - { - options.ClientId = config["OAuth:GitHub:ClientId"]; - options.ClientSecret = config["OAuth:GitHub:ClientSecret"]; - options.Scope.Add("user:email"); }).AddPolicyScheme(schema, "Mixed authentication", options => { // runs on each request @@ -85,6 +80,16 @@ public static class BotSharpOpenApiExtensions }; }); + if (!string.IsNullOrWhiteSpace(config["OAuth:GitHub:ClientId"]) && !string.IsNullOrWhiteSpace(config["OAuth:GitHub:ClientSecret"])) + { + builder = builder.AddGitHub(options => + { + options.ClientId = config["OAuth:GitHub:ClientId"]; + options.ClientSecret = config["OAuth:GitHub:ClientSecret"]; + options.Scope.Add("user:email"); + }); + } + // Add services to the container. services.AddControllers() .AddJsonOptions(options => @@ -106,18 +111,18 @@ public static class BotSharpOpenApiExtensions Type = SecuritySchemeType.ApiKey }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() } - }, - Array.Empty() - } - }); + }); } ); From 96f3b61921a14ee147756850e60615be10e37237 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 15 Feb 2024 15:48:13 +0800 Subject: [PATCH 4/6] Update installation.md --- docs/quick-start/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick-start/installation.md b/docs/quick-start/installation.md index 319bdf64..1a16341f 100644 --- a/docs/quick-start/installation.md +++ b/docs/quick-start/installation.md @@ -10,7 +10,7 @@ Building solution using dotnet CLI (preferred). ### Clone the source code and build ```powershell -PS D:\> git clone https://github.com/Oceania2018/BotSharp +PS D:\> git clone https://github.com/SciSharp/BotSharp PS D:\> cd BotSharp PS D:\> dotnet build ``` @@ -86,4 +86,4 @@ If you don't want to use the source code to experience this framework, you can a ```powershell PS D:\> Install-Package BotSharp.Core -``` \ No newline at end of file +``` From ccb825794b8c8175c5e5421df9802d4386c04d30 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 15 Feb 2024 18:07:07 +0800 Subject: [PATCH 5/6] Update installation.md Launch a BotSharp UI (Optional) --- docs/quick-start/installation.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/quick-start/installation.md b/docs/quick-start/installation.md index 1a16341f..4f6bb08b 100644 --- a/docs/quick-start/installation.md +++ b/docs/quick-start/installation.md @@ -62,24 +62,24 @@ So far, you have set up the Bot's running and development environment, but you c **Ignore below section if you're going to just use REST API to interact with your bot.** -### Launch a chatbot UI (Optional) -You can use a third-party open source UI for debugging and development, or you can directly use the REST API to integrate with your system. -If you want to use the [Chatbot UI](https://github.com/mckaywrigley/chatbot-ui) as a front end. +### Launch a BotSharp UI (Optional) +BotSharp has an official front-end project to be used in conjunction with the backend. The main function of this project is to allow developers to visualize various configurations of the backend. ```powershell -PS D:\> git clone https://github.com/mckaywrigley/chatbot-ui -PS D:\> cd chatbot-ui -PS D:\> cd npm i -PS D:\> cd npm run dev +PS D:\> git clone https://github.com/SciSharp/BotSharp-UI +PS D:\> cd BotSharp-UI +PS D:\> npm install +PS D:\> npm run dev ``` Update API url in `.env.local` to your localhost BotSharp backend service. ```config -OPENAI_API_HOST=http://localhost:5500 +PUBLIC_SERVICE_URL=http://localhost:5500 +PUBLIC_LIVECHAT_HOST=http://localhost:5015 ``` -Point your web browser at http://localhost:3000 and enjoy Chatbot with BotSharp. +Point your web browser at http://localhost:5015 and enjoy Chatbot with BotSharp. -![alt text](assets/ChatbotUIHome.png "Title") +![BotSharp UI Router](assets/BotSharp-UI-Router.png) ## Install in NuGet If you don't want to use the source code to experience this framework, you can also directly install the [NuGet packages](https://www.nuget.org/packages?q=BotSharp) released by BotSharp, and install different function packages according to the needs of your project. Before installing, please read the documentation carefully to understand the functions that different modules can provide. From 3891dfb53c1918db14d2657aacb8793e700e9cda Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 15 Feb 2024 18:07:43 +0800 Subject: [PATCH 6/6] Update installation.md --- docs/quick-start/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick-start/installation.md b/docs/quick-start/installation.md index 4f6bb08b..d54d2f97 100644 --- a/docs/quick-start/installation.md +++ b/docs/quick-start/installation.md @@ -71,7 +71,7 @@ PS D:\> npm install PS D:\> npm run dev ``` -Update API url in `.env.local` to your localhost BotSharp backend service. +Update API url in `.env` to your localhost BotSharp backend service. ```config PUBLIC_SERVICE_URL=http://localhost:5500 PUBLIC_LIVECHAT_HOST=http://localhost:5015