yep
This commit is contained in:
@@ -60,21 +60,29 @@ public class AccountController(
|
||||
}
|
||||
}
|
||||
|
||||
return this.Ok();
|
||||
return this.NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("/backend/logout")]
|
||||
public async Task<IActionResult> Logout([FromBody] object empty)
|
||||
[HttpPost("Login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromForm] string email, [FromForm] string password)
|
||||
{
|
||||
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
|
||||
if (empty is not null)
|
||||
var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: true, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
return Redirect("/login?error=Invalid login attempt.");
|
||||
}
|
||||
|
||||
[HttpGet("Logout")]
|
||||
[HttpPost("Logout")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
|
||||
return this.Ok();
|
||||
}
|
||||
|
||||
return this.Unauthorized();
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
[HttpGet("/backend/roles")]
|
||||
|
||||
72
Shogi/BackEnd/Identity/ServerAccountManager.cs
Normal file
72
Shogi/BackEnd/Identity/ServerAccountManager.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace Shogi.BackEnd.Identity;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Shogi.FrontEnd.Client;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
public class ServerAccountManager(
|
||||
UserManager<ShogiUser> userManager,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IEmailSender emailSender) : IAccountManagement
|
||||
{
|
||||
public async Task<FormResult> RegisterAsync(string email, string password)
|
||||
{
|
||||
var user = new ShogiUser { UserName = email, Email = email };
|
||||
var result = await userManager.CreateAsync(user, password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var userId = await userManager.GetUserIdAsync(user);
|
||||
var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
var request = httpContextAccessor.HttpContext?.Request;
|
||||
var host = request?.Host.Value ?? "localhost";
|
||||
var scheme = request?.Scheme ?? "https";
|
||||
var callbackUrl = $"{scheme}://{host}/backend/confirmEmail?userId={userId}&code={code}";
|
||||
|
||||
await emailSender.SendEmailAsync(email, "Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
return new FormResult { Succeeded = false, ErrorList = result.Errors.Select(e => e.Description).ToArray() };
|
||||
}
|
||||
|
||||
public Task<FormResult> LoginAsync(string email, string password)
|
||||
{
|
||||
throw new NotSupportedException("Login must be performed via Form POST to /backend/Account/Login in Server-Side Rendering.");
|
||||
}
|
||||
|
||||
public Task LogoutAsync()
|
||||
{
|
||||
// Logout should be performed via Form POST or Link to /backend/Account/Logout.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> CheckAuthenticatedAsync()
|
||||
{
|
||||
return Task.FromResult(httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> RequestPasswordReset(string email)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
// Generate token and send email logic would go here.
|
||||
}
|
||||
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user == null) return new FormResult { Succeeded = false, ErrorList = ["User not found"] };
|
||||
var result = await userManager.ResetPasswordAsync(user, resetCode, newPassword);
|
||||
return new FormResult { Succeeded = result.Succeeded, ErrorList = result.Errors.Select(e => e.Description).ToArray() };
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
namespace Shogi.FrontEnd.Client;
|
||||
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Handles state for cookie-based auth.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Create a new instance of the auth provider.
|
||||
/// </remarks>
|
||||
/// <param name="httpClientFactory">Factory to retrieve auth client.</param>
|
||||
public class CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory) : AuthenticationStateProvider, IAccountManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Map the JavaScript-formatted properties to C#-formatted classes.
|
||||
/// </summary>
|
||||
private readonly JsonSerializerOptions jsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Special auth client.
|
||||
/// </summary>
|
||||
private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth");
|
||||
|
||||
/// <summary>
|
||||
/// Authentication state.
|
||||
/// </summary>
|
||||
private bool authenticated = false;
|
||||
|
||||
/// <summary>
|
||||
/// Default principal for anonymous (not authenticated) users.
|
||||
/// </summary>
|
||||
private readonly ClaimsPrincipal Unauthenticated =
|
||||
new(new ClaimsIdentity());
|
||||
|
||||
/// <summary>
|
||||
/// Register a new user.
|
||||
/// </summary>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="password">The user's password.</param>
|
||||
/// <returns>The result serialized to a <see cref="FormResult"/>.
|
||||
/// </returns>
|
||||
public async Task<FormResult> RegisterAsync(string email, string password)
|
||||
{
|
||||
string[] defaultDetail = ["An unknown error prevented registration from succeeding."];
|
||||
|
||||
try
|
||||
{
|
||||
// make the request
|
||||
var result = await httpClient.PostAsJsonAsync("backend/register", new
|
||||
{
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// successful?
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
|
||||
// body should contain details about why it failed
|
||||
var details = await result.Content.ReadAsStringAsync();
|
||||
var problemDetails = JsonDocument.Parse(details);
|
||||
var errors = new List<string>();
|
||||
var errorList = problemDetails.RootElement.GetProperty("errors");
|
||||
|
||||
foreach (var errorEntry in errorList.EnumerateObject())
|
||||
{
|
||||
if (errorEntry.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
errors.Add(errorEntry.Value.GetString()!);
|
||||
}
|
||||
else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
errors.AddRange(
|
||||
errorEntry.Value.EnumerateArray().Select(
|
||||
e => e.GetString() ?? string.Empty)
|
||||
.Where(e => !string.IsNullOrEmpty(e)));
|
||||
}
|
||||
}
|
||||
|
||||
// return the error list
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = problemDetails == null ? defaultDetail : [.. errors]
|
||||
};
|
||||
}
|
||||
catch { }
|
||||
|
||||
// unknown error
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = defaultDetail
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User login.
|
||||
/// </summary>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="password">The user's password.</param>
|
||||
/// <returns>The result of the login request serialized to a <see cref="FormResult"/>.</returns>
|
||||
public async Task<FormResult> LoginAsync(string email, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
// login with cookies
|
||||
var result = await httpClient.PostAsJsonAsync("backend/login?useCookies=true", new
|
||||
{
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// success?
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
// need to refresh auth state
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
|
||||
// success!
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// unknown error
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = ["Invalid email and/or password."]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get authentication state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Called by Blazor anytime and authentication-based decision needs to be made, then cached
|
||||
/// until the changed state notification is raised.
|
||||
/// </remarks>
|
||||
/// <returns>The authentication state asynchronous request.</returns>
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
authenticated = false;
|
||||
|
||||
// default to not authenticated
|
||||
var user = Unauthenticated;
|
||||
|
||||
try
|
||||
{
|
||||
// the user info endpoint is secured, so if the user isn't logged in this will fail
|
||||
var userResponse = await httpClient.GetAsync("backend/manage/info");
|
||||
|
||||
// throw if user info wasn't retrieved
|
||||
userResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// user is authenticated,so let's build their authenticated identity
|
||||
var userJson = await userResponse.Content.ReadAsStringAsync();
|
||||
var userInfo = JsonSerializer.Deserialize<UserInfo>(userJson, jsonSerializerOptions);
|
||||
|
||||
if (userInfo != null)
|
||||
{
|
||||
// in our system name and email are the same
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, userInfo.Email),
|
||||
new(ClaimTypes.Email, userInfo.Email)
|
||||
};
|
||||
|
||||
// add any additional claims
|
||||
claims.AddRange(
|
||||
userInfo.Claims
|
||||
.Where(c => c.Key != ClaimTypes.Name && c.Key != ClaimTypes.Email)
|
||||
.Select(c => new Claim(c.Key, c.Value)));
|
||||
|
||||
// tap the roles endpoint for the user's roles
|
||||
var rolesResponse = await httpClient.GetAsync("backend/roles");
|
||||
|
||||
// throw if request fails
|
||||
rolesResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// read the response into a string
|
||||
var rolesJson = await rolesResponse.Content.ReadAsStringAsync();
|
||||
|
||||
// deserialize the roles string into an array
|
||||
var roles = JsonSerializer.Deserialize<RoleClaim[]>(rolesJson, jsonSerializerOptions);
|
||||
|
||||
// if there are roles, add them to the claims collection
|
||||
if (roles?.Length > 0)
|
||||
{
|
||||
foreach (var role in roles)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(role.Type) && !string.IsNullOrEmpty(role.Value))
|
||||
{
|
||||
claims.Add(new Claim(role.Type, role.Value, role.ValueType, role.Issuer, role.OriginalIssuer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the principal
|
||||
var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
|
||||
user = new ClaimsPrincipal(id);
|
||||
authenticated = true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// return the state
|
||||
return new AuthenticationState(user);
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
const string Empty = "{}";
|
||||
var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json");
|
||||
await httpClient.PostAsync("backend/logout", emptyContent);
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
|
||||
public async Task<bool> CheckAuthenticatedAsync()
|
||||
{
|
||||
await GetAuthenticationStateAsync();
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ask for an email to be sent which contains a reset code. This reset code is used during <see cref="ChangePassword"/>
|
||||
/// </summary>
|
||||
/// <remarks>Do not surface errors from this to users which may tell bad actors if emails do or do not exist in the system.</remarks>
|
||||
public async Task<HttpResponseMessage> RequestPasswordReset(string email)
|
||||
{
|
||||
return await httpClient.PostAsJsonAsync("backend/forgotPassword", new { email });
|
||||
}
|
||||
|
||||
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
email,
|
||||
resetCode,
|
||||
newPassword
|
||||
};
|
||||
var response = await httpClient.PostAsJsonAsync("backend/resetPassword", body);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = [await response.Content.ReadAsStringAsync()]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class RoleClaim
|
||||
{
|
||||
public string? Issuer { get; set; }
|
||||
public string? OriginalIssuer { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? Value { get; set; }
|
||||
public string? ValueType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,14 @@
|
||||
<HeadOutlet @rendermode="RenderMode.InteractiveServer" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!--
|
||||
data-enhance:
|
||||
Enables Blazor's "Enhanced Form Handling" globally for the document body.
|
||||
This intercepts standard <form> POST requests (like our login implementation) and performs them via fetch,
|
||||
patching the DOM with the response instead of triggering a full page reload.
|
||||
This provides a SPA-like experience even when using server-side endpoints for Cookies/Auth.
|
||||
-->
|
||||
<body data-enhance>
|
||||
<Routes @rendermode="RenderMode.InteractiveServer" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<span>@context.User.Identity?.Name</span>
|
||||
<a href="logout">Logout</a>
|
||||
<a href="backend/Account/Logout">Logout</a>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="login">Login</a>
|
||||
@@ -53,7 +53,7 @@
|
||||
<button class="href" @onclick="CreateSession">Create</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="logout">Logout</a>
|
||||
<a href="backend/Account/Logout">Logout</a>
|
||||
</li>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
@page "/login"
|
||||
@inject IAccountManagement Acct
|
||||
@inject NavigationManager navigator
|
||||
|
||||
<main class="PrimaryTheme">
|
||||
|
||||
<form class="LoginForm" @onsubmit="HandleSubmit">
|
||||
<form class="LoginForm" method="post" action="backend/Account/Login">
|
||||
<h1 style="grid-area: h1">Login</h1>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div>You're logged in as @context.User.Identity?.Name.</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
@if (errorList.Length > 0)
|
||||
@if (!string.IsNullOrEmpty(Error))
|
||||
{
|
||||
<div class="Errors" style="grid-area: errors">
|
||||
<ul>
|
||||
@foreach (var error in errorList)
|
||||
{
|
||||
<li>@error</li>
|
||||
}
|
||||
<li>@Error</li>
|
||||
</ul>
|
||||
<div class="ErrorProgress" @key="errorKey"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<label for="email" style="grid-area: emailLabel">Email</label>
|
||||
<input required id="email" name="emailInput" type="text" style="grid-area: emailControl" @bind-value="email" readonly="@isEmailSubmitted" />
|
||||
<input required id="email" name="email" type="text" style="grid-area: emailControl" />
|
||||
|
||||
@if (isEmailSubmitted)
|
||||
{
|
||||
<label for="password" style="grid-area: passLabel">Password</label>
|
||||
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" />
|
||||
<input required id="password" name="password" type="password" style="grid-area: passControl" />
|
||||
|
||||
<a href="forgot" style="grid-area: resetLink; place-self: end;">Reset password</a>
|
||||
}
|
||||
|
||||
<button style="grid-area: button" type="submit">@(isEmailSubmitted ? "Login" : "Next")</button>
|
||||
<button style="grid-area: button" type="submit">Login</button>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</form>
|
||||
@@ -43,95 +35,6 @@
|
||||
</main>
|
||||
|
||||
@code {
|
||||
|
||||
private string email = string.Empty;
|
||||
private string password = string.Empty;
|
||||
private string[] errorList = [];
|
||||
private System.Threading.CancellationTokenSource? _clearErrorCts;
|
||||
private bool isEmailSubmitted = false;
|
||||
private Guid errorKey;
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
if (!isEmailSubmitted)
|
||||
{
|
||||
SubmitEmail();
|
||||
}
|
||||
else
|
||||
{
|
||||
await DoLoginAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void SubmitEmail()
|
||||
{
|
||||
SetErrors([]);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
SetErrors(["Email is required."]);
|
||||
return;
|
||||
}
|
||||
|
||||
isEmailSubmitted = true;
|
||||
}
|
||||
|
||||
public async Task DoLoginAsync()
|
||||
{
|
||||
SetErrors([]);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
SetErrors(["Email is required."]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
SetErrors(["Password is required."]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Acct.LoginAsync(email, password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
email = password = string.Empty;
|
||||
|
||||
navigator.NavigateTo("");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetErrors(result.ErrorList);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetErrors(string[] errors)
|
||||
{
|
||||
_clearErrorCts?.Cancel();
|
||||
errorList = errors;
|
||||
|
||||
if (errors.Length > 0)
|
||||
{
|
||||
errorKey = Guid.NewGuid();
|
||||
_clearErrorCts = new System.Threading.CancellationTokenSource();
|
||||
_ = ClearErrorsAfterDelay(_clearErrorCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearErrorsAfterDelay(System.Threading.CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(10000, token);
|
||||
errorList = [];
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
[SupplyParameterFromQuery]
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
@@ -39,9 +39,8 @@ builder.Services.Configure<ApiKeys>(builder.Configuration.GetSection("ApiKeys"))
|
||||
builder.Services.AddTransient<ILocalStorage, LocalStorage>();
|
||||
builder.Services.AddScoped<ShogiService>();
|
||||
builder.Services.AddScoped<GameHubNode>();
|
||||
builder.Services.AddScoped<CookieAuthenticationStateProvider>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CookieAuthenticationStateProvider>());
|
||||
builder.Services.AddScoped<IAccountManagement>(sp => sp.GetRequiredService<CookieAuthenticationStateProvider>());
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<IAccountManagement, ServerAccountManager>();
|
||||
|
||||
AddIdentity(builder, builder.Configuration);
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Kestrel": {
|
||||
"profiles": {
|
||||
"Shogi UI": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/",
|
||||
"AZURE_USERNAME": "Hauth@live.com"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
},
|
||||
"Swagger": {
|
||||
"Swagger UI": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/",
|
||||
"AZURE_USERNAME": "Hauth@live.com"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50728/",
|
||||
"sslPort": 44315
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
Reference in New Issue
Block a user