diff --git a/Components/MessageComponent.razor b/Components/MessageComponent.razor index 63822ec..23be409 100644 --- a/Components/MessageComponent.razor +++ b/Components/MessageComponent.razor @@ -1,16 +1,24 @@ @if (Message is not null) { -
+
@Message - +
+ @if (LinkToCopy is not null) { + + } + +
} @@ -19,6 +27,8 @@ public string? Message { get; set; } [Parameter] public AlertType Type { get; set; } = AlertType.Information; + [Parameter] + public string? LinkToCopy { get; set; } private string GetAlertTypeClass() => Type switch{ AlertType.Information => "alert-info", diff --git a/Components/Pages/Urls.razor b/Components/Pages/Urls.razor new file mode 100644 index 0000000..e1c110f --- /dev/null +++ b/Components/Pages/Urls.razor @@ -0,0 +1,195 @@ +@page "/urls" +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Extensions.Caching.Distributed +@using System.ComponentModel.DataAnnotations +@using System.Text.RegularExpressions +@using System.Web +@using Humanizer +@using Microsoft.AspNetCore.WebUtilities +@attribute [Authorize] + +@inject IDistributedCache Db +@inject IConfiguration Configuration +@inject NavigationManager Navigation + +Urls - Just Short It + +
+
+

+ Urls Administration +

+ + + + +

Inspect URL

+ +
+ +
+ +
+
+ +
+
+ + +
+ + + +

New URL

+ +
+ + + + + + + +
+ +
+
+ +
+ + + + +
+ +
+
+ +
+ + + @foreach (var expiration in Enum.GetValues()) { + + } + + + + +
+ + +
+
+
+ +@code { + [SupplyParameterFromForm(FormName = "inspect")] + public InspectModel Inspect { get; set; } = default!; + [SupplyParameterFromForm(FormName = "new")] + public NewModel New { get; set; } = default!; + + private string BaseUrl { get; set; } = null!; + private string? Message { get; set; } + private string? Link { get; set; } + private MessageComponent.AlertType MessageType { get; set; } + + protected override void OnInitialized() { + // ReSharper disable NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + Inspect ??= new InspectModel(); + New ??= new NewModel(); + // ReSharper restore NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + New.Id ??= GenerateNewId(); + + string url = Configuration.GetValue("BaseUrl") ?? throw new ApplicationException("BaseUrl not set"); + BaseUrl = new Uri(url, UriKind.Absolute).ToString(); + } + + private async Task Submit_Inspect(EditContext context) { + if (string.IsNullOrWhiteSpace(Inspect.Id)) return; + + if (await Db.GetAsync(Inspect.Id) is null) { + Message = "ID does not exist"; + MessageType = MessageComponent.AlertType.Error; + return; + } + + Navigation.NavigateTo(QueryHelpers.AddQueryString("/inspect", "Id", Inspect.Id)); + } + + private async Task Submit_New() { + if (string.IsNullOrWhiteSpace(New.Id)) return; + + string id = HttpUtility.UrlEncode(New.Id); + + if (await Db.GetAsync(id) is not null) { + Message = "This ID is already taken, sorry!"; + MessageType = MessageComponent.AlertType.Error; + return; + } + + if (Uri.TryCreate($"{BaseUrl}{id}", UriKind.Absolute, out var link) is false) { + Message = "This ID cannot be used in a URL, sorry!"; + MessageType = MessageComponent.AlertType.Error; + return; + } + + await Db.SetStringAsync(id, New.Url!, new DistributedCacheEntryOptions { + AbsoluteExpiration = DateTime.FromBinary(ToUnixTime(New.RedirectExpiration!.Value)) + }); + + Message = $"URL Generated! {link}"; + Link = link.ToString(); + MessageType = MessageComponent.AlertType.Success; + } + + private static string GenerateNewId() { + string base64Guid = Regex.Replace( + Convert.ToBase64String(Guid.NewGuid().ToByteArray()), + "[/+=]", ""); + return base64Guid[..6]; + } + + #region Models + + public sealed class InspectModel { + [Required(AllowEmptyStrings = false, ErrorMessage = "Id is required.")] + [MinLength(2, ErrorMessage = "Id needs to be at least 2 characters long.")] + [MaxLength(16, ErrorMessage = "Id needs to be at maximum 16 characters long.")] + public string? Id { get; set; } + } + public sealed class NewModel { + [Required(AllowEmptyStrings = false, ErrorMessage = "Id is required.")] + [MinLength(2, ErrorMessage = "Id needs to be at least 2 characters long.")] + [MaxLength(16, ErrorMessage = "Id needs to be at maximum 16 characters long.")] + public string? Id { get; set; } + [Required(AllowEmptyStrings = false, ErrorMessage = "Target is required.")] + [Url(ErrorMessage = "Target needs to be a valid URL.")] + public string? Url { get; set; } + [Required(ErrorMessage = "Expiration is required.")] + public Expiration? RedirectExpiration { get; set; } + } + + public enum Expiration { + OneDay, OneWeek, FourWeeks, OneMonth, OneYear, Never + } + + private static long ToUnixTime(Expiration expiration) => expiration switch{ + Expiration.OneDay => DateTime.UtcNow.AddDays(1).ToBinary(), + Expiration.OneWeek => DateTime.UtcNow.AddDays(7).ToBinary(), + Expiration.FourWeeks => DateTime.UtcNow.AddDays(4*7).ToBinary(), + Expiration.OneYear => DateTime.UtcNow.AddYears(1).ToBinary(), + Expiration.Never => DateTime.UtcNow.AddYears(1000).ToBinary(), + _ => throw new ArgumentOutOfRangeException(nameof(expiration), expiration, null)}; + + #endregion +} diff --git a/JustShortIt.csproj b/JustShortIt.csproj index d84de90..2a395cc 100644 --- a/JustShortIt.csproj +++ b/JustShortIt.csproj @@ -9,6 +9,7 @@ + diff --git a/Pages/Urls.cshtml b/Pages/Urls.cshtml deleted file mode 100644 index 4d1014b..0000000 --- a/Pages/Urls.cshtml +++ /dev/null @@ -1,96 +0,0 @@ -@page -@model JustShortIt.Pages.UrlsModel -@{ - ViewData["Title"] = "Urls"; -} - -
-
- @if (!string.IsNullOrEmpty(Model.Message)) { - -
- - - - - @Html.Raw(Model.Message) - - -
- } - -

Urls Administration

- -
-

Inspect URL

- -
- -
- -
-
- -
-
- - @ModelState["Inspect_Id"]?.Errors.FirstOrDefault()?.ErrorMessage -
- -
-

New URL

- -
- - - - - - - -
- -
-
- -
- -
- -
-
- -
- - - - @Html.DropDownListFor(m => m.Model!.ExpirationDate, new List { - new("1 Day", DateTime.UtcNow.AddDays(1).ToBinary().ToString()), - new("1 Week", DateTime.UtcNow.AddDays(7).ToBinary().ToString()), - new("4 Weeks", DateTime.UtcNow.AddDays(4 * 7).ToBinary().ToString()), - new("1 Year", DateTime.UtcNow.AddYears(1).ToBinary().ToString()), - new("Never", DateTime.UtcNow.AddYears(1000).ToBinary().ToString()) - }, "Select Expiration", new { - @class = "select select-bordered w-full" - }) -
- -
-
- -
- - -
-
-
- diff --git a/Pages/Urls.cshtml.cs b/Pages/Urls.cshtml.cs deleted file mode 100644 index 05b129a..0000000 --- a/Pages/Urls.cshtml.cs +++ /dev/null @@ -1,83 +0,0 @@ -using JustShortIt.Model; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using System.Text.RegularExpressions; -using System.Web; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Caching.Distributed; - -namespace JustShortIt.Pages; - -[Authorize] -public class UrlsModel : PageModel { - [BindProperty] - public UrlRedirect? Model { get; set; } - [BindProperty(Name="message")] - public string? Message { get; set; } - - private string BaseUrl { get; } - private IDistributedCache Db { get; } - - public UrlsModel(IConfiguration configuration, IDistributedCache db) { -#if DEBUG - BaseUrl = "https://localhost/"; -#else - string url = configuration.GetValue("BaseUrl") ?? throw new ApplicationException("BaseUrl not set"); - BaseUrl = new Uri(url, UriKind.Absolute).ToString(); -#endif - Db = db; - } - - public async Task OnPostInspectAsync() { - string? id = Request.Form["Inspect_Id"]; - if (id is null || string.IsNullOrEmpty(id)) { - ModelState.AddModelError("Inspect_Id", "ID is a required field"); - return Page(); - } - - if (await Db.GetAsync(id) is null) { - ModelState.AddModelError("Inspect_Id", "ID does not exist"); - return Page(); - } - - return LocalRedirect(QueryHelpers.AddQueryString("~/inspect", "Id", id)); - } - - public async Task OnPostNewAsync() { - if (!ModelState.IsValid) return Page(); - string id = HttpUtility.UrlEncode(Model.Id); - - if (await Db.GetAsync(id) is not null) { - Message = "This ID is already taken, sorry!"; - return Page(); - } - - if (Uri.TryCreate($"{BaseUrl}{id}", UriKind.Absolute, out Uri? link) is false) { - Message = "This ID cannot be used in a URL, sorry!"; - return Page(); - } - - await Db.SetStringAsync(id, Model.Target, new DistributedCacheEntryOptions { - AbsoluteExpiration = DateTime.FromBinary(long.Parse(Model.ExpirationDate)) - }); - - ModelState.Clear(); - ModelState.SetModelValue(nameof(UrlRedirect.Id), GenerateNewId(), GenerateNewId()); - - Message = $"URL Generated! {link}. " + - $""; - return OnGet(Message); - } - - public IActionResult OnGet(string message) { - Message = message; - Model = new UrlRedirect(GenerateNewId(), string.Empty, string.Empty); - return Page(); - } - - private static string GenerateNewId() { - string base64Guid = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""); ; - return base64Guid[..6]; - } -} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json index 770d3e9..fc62fe4 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -5,5 +5,6 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "BaseUrl": "http://localhost" }