Skip to content

Commit

Permalink
Merge pull request #59 from the-urlist/hasher-singleton-service
Browse files Browse the repository at this point in the history
Move Hasher to singleton service
  • Loading branch information
jongalloway authored Feb 23, 2024
2 parents 708d2d8 + 165e05f commit 0eb4712
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 104 deletions.
3 changes: 1 addition & 2 deletions Api/Functions/CreateLinkBundle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace Api.Functions
{
public partial class CreateLinkBundle(CosmosClient cosmosClient)
public partial class CreateLinkBundle(CosmosClient cosmosClient, Hasher hasher)
{
protected const string CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789";
protected const string VANITY_REGEX = @"^([\w\d-])+(/([\w\d-])+)*$";
Expand Down Expand Up @@ -43,7 +43,6 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
if (clientPrincipal != null)
{
string username = clientPrincipal.UserDetails;
Hasher hasher = new();
linkBundle.UserId = hasher.HashString(username);
linkBundle.Provider = clientPrincipal.IdentityProvider;
}
Expand Down
3 changes: 1 addition & 2 deletions Api/Functions/DeleteLinkBundle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Api.Functions
{
public class DeleteLinkBundle(ILoggerFactory loggerFactory, CosmosClient cosmosClient)
public class DeleteLinkBundle(ILoggerFactory loggerFactory, CosmosClient cosmosClient, Hasher hasher)
{
private readonly ILogger _logger = loggerFactory.CreateLogger<DeleteLinkBundle>();

Expand All @@ -30,7 +30,6 @@ public async Task<HttpResponseData> Run(

if (result.Count != 0)
{
Hasher hasher = new();
var hashedUsername = hasher.HashString(principal.UserDetails);
if (hashedUsername != result.First().UserId || principal.IdentityProvider != result.First().Provider)
{
Expand Down
3 changes: 1 addition & 2 deletions Api/Functions/GetLinkBundlesForUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Api.Functions
{
public class GetLinkBundlesForUser(CosmosClient cosmosClient)
public class GetLinkBundlesForUser(CosmosClient cosmosClient, Hasher hasher)
{
[Function(nameof(GetLinkBundlesForUser))]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "user")] HttpRequestData req)
Expand All @@ -23,7 +23,6 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou

if (clientPrincipal != null)
{
Hasher hasher = new();
string username = hasher.HashString(clientPrincipal.UserDetails);
string provider = clientPrincipal.IdentityProvider;

Expand Down
3 changes: 1 addition & 2 deletions Api/Functions/UpdateLinkBundle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Api
{
public class UpdateLinkBundle(CosmosClient cosmosClient)
public class UpdateLinkBundle(CosmosClient cosmosClient, Hasher hasher)
{
[Function(nameof(UpdateLinkBundle))]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "links/{vanityUrl}")] HttpRequestData req,
Expand Down Expand Up @@ -45,7 +45,6 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou

if (result.Count != 0)
{
Hasher hasher = new();
var hashedUsername = hasher.HashString(principal.UserDetails);
if (hashedUsername != result.First().UserId || principal.IdentityProvider != result.First().Provider)
{
Expand Down
4 changes: 4 additions & 0 deletions Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Api;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand All @@ -23,6 +24,9 @@
context.Configuration["CosmosDb:Endpoint"],
context.Configuration["CosmosDb:Key"],
cosmosClientOptions));
services.AddSingleton<Hasher>(services => new Hasher(
context.Configuration["HASHER_KEY"],
context.Configuration["HASHER_SALT"]));
})
.ConfigureFunctionsWorkerDefaults()
.Build();
Expand Down
204 changes: 108 additions & 96 deletions Client/Shared/LinkBundleDetails.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,117 +7,129 @@
@inject NavigationManager NavigationManager

<div>
<div id="listDetails" class="addbar flex is-horizontally-centered">
<div class="container main">
<div class="columns">
<div class="column">
<label class="control-label" for="vanityUrl">Vanity Url</label>
<input id="vanityUrl" @bind="StateContainer.LinkBundle.VanityUrl" Class=@($"input is-large has-tooltip {validationErrorClass}")
title=" Optional: Enter a vanity url for this list (i.e. my-list becomes
<div id="listDetails" class="addbar flex is-horizontally-centered">
<div class="container main">
<div class="columns">
<div class="column">
<label class="control-label" for="vanityUrl">Vanity Url</label>
<input id="vanityUrl" @bind="StateContainer.LinkBundle.VanityUrl" Class=@($"input is-large has-tooltip {validationErrorClass}")
title=" Optional: Enter a vanity url for this list (i.e. my-list becomes
theurlist.com/my-list). If you leave this box blank, we'll generate a random vanity for you."
@oninput="ValidateVanityUrl"
disabled=@(IsPublished)/>
<p class=@($"has-text-danger is-font-weight-medium mt-2 { validationMessageClass }")>
@validationErrorMessage</p>
<p id="liveLink" v-if="listIsPublished">
<a :href="liveLink" target="_new">
</a>
</p>
@oninput="ValidateVanityUrl"
disabled=@(IsPublished) />
<p class=@($"has-text-danger is-font-weight-medium mt-2 { validationMessageClass }")>
@validationErrorMessage
</p>
<p id="liveLink" v-if="listIsPublished">
<a :href="liveLink" target="_new">
</a>
</p>
</div>
<div class="column">
<label class="control-label" for="description">Description</label>
<textarea rows="2" title="Optional: The description will show up as the title on your public list page."
class="textarea has-fixed-size" id="description" @bind="StateContainer.LinkBundle.Description"></textarea>
</div>
<div class="column is-narrow">
<label class="control-label is-hidden-mobile" for>&nbsp;</label>
<button type="submit" id="publishButton" disabled="@(!PublishEnabled(listIsValid))"
class="button is-primary is-large has-text-white has-text-weight-bold" @onclick="PublishLinkBundle">
Publish
</button>
</div>
</div>
</div>
<div class="column">
<label class="control-label" for="description">Description</label>
<textarea rows="2" title="Optional: The description will show up as the title on your public list page."
class="textarea has-fixed-size" id="description" @bind="StateContainer.LinkBundle.Description"></textarea>
</div>
<div class="column is-narrow">
<label class="control-label is-hidden-mobile" for>&nbsp;</label>
<button type="submit" id="publishButton" disabled="@(!PublishEnabled(listIsValid))"
class="button is-primary is-large has-text-white has-text-weight-bold" @onclick="PublishLinkBundle">
Publish
</button>
</div>
</div>
</div>
</div>
</div>

@code {

protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}

[Parameter]
public bool IsPublished { get; set; } = false;

private Debouncer debouncer = new Debouncer(300);
private bool listIsValid = true;
private string validationErrorMessage = "";
private string validationErrorClass => listIsValid ? "" : "invalid";
private string validationMessageClass => listIsValid ? "is-invisible" : "is-visible";

bool PublishEnabled(bool valid) {
if (valid && StateContainer.LinkBundle.Links.Count > 0) return true;
return false;
}

private void ValidateVanityUrl(ChangeEventArgs e)
{
var vanityUrl = e.Value?.ToString();
StateContainer.LinkBundle.VanityUrl = vanityUrl;
listIsValid = true;

if (String.IsNullOrEmpty(vanityUrl)) return;

// Create a ValidationContext based on the LinkBundle object
var context = new ValidationContext(StateContainer.LinkBundle, null, null);
// This list will hold the results of the validation
var results = new List<ValidationResult>();
// Perform the data annotations validation
listIsValid = Validator.TryValidateObject(StateContainer.LinkBundle, context, results, true);

// if the data annotations validation fails, no need to check if the vanity url is taken
if (!listIsValid) {
validationErrorMessage = results.FirstOrDefault()?.ErrorMessage ?? "";
return;
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}

debouncer.Debounce(async () =>
[Parameter]
public bool IsPublished { get; set; } = false;

private Debouncer debouncer = new Debouncer(300);
private bool listIsValid = true;
private string validationErrorMessage = "";
private string validationErrorClass => listIsValid ? "" : "invalid";
private string validationMessageClass => listIsValid ? "is-invisible" : "is-visible";

bool PublishEnabled(bool valid)
{
// the characters are valid, so now check to see if the vanity url is already in use
await VanityUrlIsTaken(vanityUrl);
StateHasChanged();
});
}

// calls the API to see if the vanity is stil available for use
private async Task<bool> VanityUrlIsTaken(string vanityUrl) {

var response = await Http.GetAsync($"api/links/{vanityUrl}");

if (response.StatusCode == System.Net.HttpStatusCode.OK)
if (valid && StateContainer.LinkBundle.Links.Count > 0) return true;
return false;
}

private void ValidateVanityUrl(ChangeEventArgs e)
{
listIsValid = false;
validationErrorMessage = "This vanity URL is already in use. Please choose another.";
return true;
var vanityUrl = e.Value?.ToString();
StateContainer.LinkBundle.VanityUrl = vanityUrl;
listIsValid = true;

if (String.IsNullOrEmpty(vanityUrl)) return;

// Create a ValidationContext based on the LinkBundle object
var context = new ValidationContext(StateContainer.LinkBundle, null, null);
// This list will hold the results of the validation
var results = new List<ValidationResult>();
// Perform the data annotations validation
listIsValid = Validator.TryValidateObject(StateContainer.LinkBundle, context, results, true);

// if the data annotations validation fails, no need to check if the vanity url is taken
if (!listIsValid)
{
validationErrorMessage = results.FirstOrDefault()?.ErrorMessage ?? "";
return;
}

debouncer.Debounce(async () =>
{
// the characters are valid, so now check to see if the vanity url is already in use
await VanityUrlIsTaken(vanityUrl);
StateHasChanged();
});
}

return false;
}
// calls the API to see if the vanity is stil available for use
private async Task<bool> VanityUrlIsTaken(string vanityUrl)
{

private async Task PublishLinkBundle()
{
// post the statecontainer.linkbundle to the /api/links endpoint and read the response into the linkbundle
var response = await Http.PostAsJsonAsync<LinkBundle>("api/links", StateContainer.LinkBundle);
var linkBundle = await response.Content.ReadFromJsonAsync<LinkBundle>();
var response = await Http.GetAsync($"api/links/{vanityUrl}");

if (linkBundle != null)
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
listIsValid = false;
validationErrorMessage = "This vanity URL is already in use. Please choose another.";
return true;
}

return false;
}

private async Task PublishLinkBundle()
{
StateContainer.LinkBundle = linkBundle;

// navigate to the public page
NavigationManager.NavigateTo($"/{linkBundle.VanityUrl}");
HttpResponseMessage response;

if (IsPublished)
{
response = await Http.PutAsJsonAsync<LinkBundle>($"api/links/{StateContainer.LinkBundle.VanityUrl}", StateContainer.LinkBundle);
}
else
{
response = await Http.PostAsJsonAsync<LinkBundle>("api/links", StateContainer.LinkBundle);
}
var linkBundle = await response.Content.ReadFromJsonAsync<LinkBundle>();

if (linkBundle != null)
{
StateContainer.LinkBundle = linkBundle;

// navigate to the public page
NavigationManager.NavigateTo($"/{linkBundle.VanityUrl}");
}
}
}
}

0 comments on commit 0eb4712

Please sign in to comment.