Skip to content

Commit

Permalink
Add setting of Signin/Generate-token to admin console (#317)
Browse files Browse the repository at this point in the history
Added setting panel to enable or disable the generate token endpoint via AdminConsole.
  • Loading branch information
jrmccannon authored Jan 9, 2024
1 parent b3ad660 commit 45677cb
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 41 deletions.
6 changes: 2 additions & 4 deletions src/AdminConsole/Components/Layouts/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Identity
@using Badge = Passwordless.AdminConsole.Components.Shared.Badge
@using Passwordless.AdminConsole.Middleware
@using System.Security.Policy
@using Passwordless.AdminConsole.TagHelpers
@using Badge = Passwordless.AdminConsole.Components.Shared.Badge
@using Microsoft.AspNetCore.Components.Authorization

@inject NavigationManager NavigationManager
@inject ICurrentContext CurrentContext
Expand Down
10 changes: 5 additions & 5 deletions src/AdminConsole/Middleware/CurrentContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ public class CurrentContext : ICurrentContext

public string? ApiKey { get; private set; }
public bool IsFrozen { get; private set; }
public FeaturesContext Features { get; private set; }
public ApplicationFeatureContext Features { get; private set; }
public Organization? Organization { get; private set; }
public int? OrgId { get; private set; }
public FeaturesContext OrganizationFeatures { get; private set; } = new(false, 0, null, null);
public OrganizationFeaturesContext OrganizationFeatures { get; private set; } = new(false, 0);

public void SetApp(Application application)
{
Expand All @@ -35,15 +35,15 @@ public void SetApp(Application application)
IsFrozen = application.DeleteAt.HasValue;
}

public void SetFeatures(FeaturesContext context)
public void SetFeatures(ApplicationFeatureContext context)
{
Features = context;
}

public void SetOrganization(int organizationId, FeaturesContext featuresContext, Organization organization)
public void SetOrganization(int organizationId, OrganizationFeaturesContext organizationFeaturesContext, Organization organization)
{
OrgId = organizationId;
OrganizationFeatures = featuresContext;
OrganizationFeatures = organizationFeaturesContext;
Organization = organization;
}
}
2 changes: 1 addition & 1 deletion src/AdminConsole/Middleware/CurrentContextMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private async Task InvokeCoreAsync(HttpContext httpContext,
return;
}

var featuresContext = FeaturesContext.FromDto(features);
var featuresContext = ApplicationFeatureContext.FromDto(features);

#pragma warning disable CS0618 // I am the one valid caller of this method
currentContext.SetFeatures(featuresContext);
Expand Down
8 changes: 4 additions & 4 deletions src/AdminConsole/Middleware/ICurrentContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ public interface ICurrentContext
string? ApiSecret { get; }
string? ApiKey { get; }
bool IsFrozen { get; }
FeaturesContext Features { get; }
ApplicationFeatureContext Features { get; }
Organization? Organization { get; }
int? OrgId { get; }
FeaturesContext OrganizationFeatures { get; }
OrganizationFeaturesContext OrganizationFeatures { get; }

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("There should only be one caller of this method, you are probably not it.")]
void SetApp(Application application);

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("There should only be one caller of this method, you are probably not it.")]
void SetFeatures(FeaturesContext context);
void SetFeatures(ApplicationFeatureContext context);

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("There should only be one caller of this method, you are probably not it.")]
void SetOrganization(int organizationId, FeaturesContext context, Organization organization);
void SetOrganization(int organizationId, OrganizationFeaturesContext context, Organization organization);
}
16 changes: 16 additions & 0 deletions src/AdminConsole/Models/ApplicationFeatureContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Passwordless.Common.Models.Apps;

namespace Passwordless.AdminConsole.Models;

public record ApplicationFeatureContext(
bool EventLoggingIsEnabled,
int EventLoggingRetentionPeriod,
DateTime? DeveloperLoggingEndsAt,
long? MaxUsers,
bool IsGenerateSignInTokenEndpointEnabled)
{
public static ApplicationFeatureContext FromDto(AppFeatureResponse dto)
{
return new ApplicationFeatureContext(dto.EventLoggingIsEnabled, dto.EventLoggingRetentionPeriod, dto.DeveloperLoggingEndsAt, dto.MaxUsers, dto.IsGenerateSignInTokenEndpointEnabled);
}
};
15 changes: 0 additions & 15 deletions src/AdminConsole/Models/FeaturesContext.cs

This file was deleted.

5 changes: 5 additions & 0 deletions src/AdminConsole/Models/OrganizationFeaturesContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Passwordless.AdminConsole.Models;

public record OrganizationFeaturesContext(
bool EventLoggingIsEnabled,
int EventLoggingRetentionPeriod);
18 changes: 18 additions & 0 deletions src/AdminConsole/Pages/App/Settings/Settings.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@
<partial name="_ApiKeys" model="Model.ApiKeysModel" />
}

<panel header="Settings">
<form method="post" asp-page-handler="Settings">
<h3>Manually Generated Verification Tokens</h3>
<p>
Manually generated verification tokens have many uses in authentication. A primary use would be issuing a "magic link" for a
user to log-in to their account in the event they've misplaced their passkey. Once the identity of the user has been
verified, you can send them a hyperlink that contains the token issued by <code>signin/generate-token</code>.
</p>

<label for="@SettingsModel.ManualVerificationTokenCheckboxId">
<input class="mr-1 my-4" id="@SettingsModel.ManualVerificationTokenCheckboxId" type="checkbox" asp-for="IsManualTokenGenerationEnabled" />
Enable Manually Generated Verification Tokens
</label>
<br>
<button id="btn-save-settings" class="btn-primary" type="submit">Save</button>
</form>
</panel>

<panel header="Delete Application">
@if (Model.PendingDelete)
{
Expand Down
60 changes: 54 additions & 6 deletions src/AdminConsole/Pages/App/Settings/Settings.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ namespace Passwordless.AdminConsole.Pages.App.Settings;

public class SettingsModel : BaseExtendedPageModel
{
public const string ManualVerificationTokenCheckboxId = "IsManualTokenGenerationEnabled";
private const string Unknown = "unknown";
private readonly ILogger<SettingsModel> _logger;
private readonly IDataService _dataService;
private readonly ICurrentContext _currentContext;
private readonly IApplicationService _appService;
private readonly ISharedBillingService _billingService;
private readonly IPasswordlessManagementClient _managementClient;
private readonly BillingOptions _billingOptions;

public SettingsModel(
Expand All @@ -36,6 +38,7 @@ public SettingsModel(
_currentContext = currentContext;
_appService = appService;
_billingService = billingService;
_managementClient = managementClient;
_billingOptions = billingOptions.Value;
ApiKeysModel = new ApiKeysModel(managementClient, currentContext, httpContextAccessor, eventLogger, logger);
}
Expand All @@ -54,6 +57,8 @@ public SettingsModel(

public ApiKeysModel ApiKeysModel { get; }

public bool IsManualTokenGenerationEnabled { get; private set; }

private async Task InitializeAsync()
{
Organization = await _dataService.GetOrganizationWithDataAsync();
Expand All @@ -63,6 +68,8 @@ private async Task InitializeAsync()

if (application == null) throw new InvalidOperationException("Application not found.");
Application = application;

IsManualTokenGenerationEnabled = _currentContext.Features.IsGenerateSignInTokenEndpointEnabled;
}

public async Task OnGet()
Expand Down Expand Up @@ -94,12 +101,21 @@ public async Task<IActionResult> OnPostDeleteAsync()
if (userName == Unknown || applicationId == Unknown)
{
_logger.LogError("Failed to delete application with name: {appName} and by user: {username}.", applicationId, userName);
return StatusCode(StatusCodes.Status500InternalServerError, new { Message = "Something unexpected happened. Please try again later." });
return RedirectToPage("/Error", new { Message = "Something unexpected happened." });
}

var response = await _appService.MarkApplicationForDeletionAsync(applicationId, userName);
try
{
var response = await _appService.MarkApplicationForDeletionAsync(applicationId, userName);

return response.IsDeleted ? RedirectToPage("/Organization/Overview") : RedirectToPage();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete application: {appName}.", applicationId);
return RedirectToPage("/Error", new { ex.Message });
}

return response.IsDeleted ? RedirectToPage("/Organization/Overview") : RedirectToPage();
}

/// <summary>
Expand All @@ -110,16 +126,21 @@ public async Task<IActionResult> OnPostCancelAsync()
{
var applicationId = _currentContext.AppId ?? Unknown;

if (applicationId == Unknown)
{
_logger.LogError("Failed to cancel application deletion for application: {appId}", applicationId);
return RedirectToPage("/Error", new { Message = "Something unexpected happened." });
}

try
{
await _appService.CancelDeletionForApplicationAsync(applicationId);

return RedirectToPage();
}
catch (Exception)
catch (Exception ex)
{
_logger.LogError("Failed to cancel application deletion for application: {appId}", applicationId);
return StatusCode(StatusCodes.Status500InternalServerError, new { Message = "Something unexpected occured. Please try again later." });
return RedirectToPage("/Error", new { ex.Message });
}
}

Expand Down Expand Up @@ -176,6 +197,33 @@ public async Task<IActionResult> OnPostDeleteApiKeyAsync()
}
}

public async Task<IActionResult> OnPostSettingsAsync()
{
if (string.IsNullOrWhiteSpace(_currentContext.AppId) || string.IsNullOrWhiteSpace(User.Identity?.Name))
{
return RedirectToPage("/Error", new { Message = "Something unexpected happened." });
}

try
{
if (Convert.ToBoolean(Request.Form[ManualVerificationTokenCheckboxId].FirstOrDefault()))
{
await _managementClient.EnabledManuallyGeneratedTokensAsync(_currentContext.AppId, User.Identity.Name);
}
else
{
await _managementClient.DisabledManuallyGeneratedTokensAsync(_currentContext.AppId, User.Identity.Name);
}

return RedirectToPage();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save ManuallyGeneratedToken setting for {appId}", _currentContext.AppId);
return RedirectToPage("/Error", new { ex.Message });
}
}

private void AddPlan(string plan)
{
var options = _billingOptions.Plans[plan];
Expand Down
2 changes: 1 addition & 1 deletion src/AdminConsole/Services/IOrganizationFeatureService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace Passwordless.AdminConsole.Services;

public interface IOrganizationFeatureService
{
FeaturesContext GetOrganizationFeatures(int orgId);
OrganizationFeaturesContext GetOrganizationFeatures(int orgId);
}
8 changes: 3 additions & 5 deletions src/AdminConsole/Services/OrganizationFeatureService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public OrganizationFeatureService(IDbContextFactory<TDbContext> dbContextFactory
_options = options.Value;
}

public FeaturesContext GetOrganizationFeatures(int orgId)
public OrganizationFeaturesContext GetOrganizationFeatures(int orgId)
{
using var db = _dbContextFactory.CreateDbContext();
var billingPlans = db.Applications
Expand All @@ -40,10 +40,8 @@ public FeaturesContext GetOrganizationFeatures(int orgId)
features = plan.Value.Features;
}

return new FeaturesContext(
return new OrganizationFeaturesContext(
features.EventLoggingIsEnabled,
features.EventLoggingRetentionPeriod,
null,
features.MaxUsers);
features.EventLoggingRetentionPeriod);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface IPasswordlessManagementClient
Task LockApiKeyAsync(string appId, string apiKeyId);
Task UnlockApiKeyAsync(string appId, string apiKeyId);
Task DeleteApiKeyAsync(string appId, string apiKeyId);
Task EnabledManuallyGeneratedTokensAsync(string appId, string performedBy);
Task DisabledManuallyGeneratedTokensAsync(string appId, string performedBy);
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,22 @@ public async Task DeleteApiKeyAsync(string appId, string apiKeyId)
var response = await _client.DeleteAsync($"admin/apps/{appId}/api-keys/{apiKeyId}");
response.EnsureSuccessStatusCode();
}

public async Task EnabledManuallyGeneratedTokensAsync(string appId, string performedBy)
{
var response = await _client.PostAsJsonAsync($"admin/apps/{appId}/sign-in-generate-token-endpoint/enable", new
{
PerformedBy = performedBy
});
response.EnsureSuccessStatusCode();
}

public async Task DisabledManuallyGeneratedTokensAsync(string appId, string performedBy)
{
var response = await _client.PostAsJsonAsync($"admin/apps/{appId}/sign-in-generate-token-endpoint/disable", new
{
PerformedBy = performedBy
});
response.EnsureSuccessStatusCode();
}
}

0 comments on commit 45677cb

Please sign in to comment.