Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Swimburger committed Sep 17, 2022
0 parents commit a2488fa
Show file tree
Hide file tree
Showing 10 changed files with 822 additions and 0 deletions.
479 changes: 479 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions Migrations/20220917022529_InitialCreate.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions Migrations/20220917022529_InitialCreate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace UrlShortener.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Urls",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Path = table.Column<string>(type: "TEXT", nullable: false),
Destination = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Urls", x => x.Id);
});

migrationBuilder.CreateIndex(
name: "IX_Urls_Path",
table: "Urls",
column: "Path",
unique: true);
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Urls");
}
}
}
43 changes: 43 additions & 0 deletions Migrations/UrlShortenerDbContextModelSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

#nullable disable

namespace UrlShortener.Migrations
{
[DbContext(typeof(UrlShortenerDbContext))]
partial class UrlShortenerDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");

modelBuilder.Entity("Url", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("Destination")
.IsRequired()
.HasColumnType("TEXT");

b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("Path")
.IsUnique();

b.ToTable("Urls");
});
#pragma warning restore 612, 618
}
}
}
104 changes: 104 additions & 0 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<UrlShortenerDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString($"UrlDb")));

var app = builder.Build();

app.UseDefaultFiles()
.UseStaticFiles();

string AbsoluteUrl(HttpRequest request, string path)
=> $"{request.Scheme}://{request.Host}{request.PathBase}{path}";

var pathRegex = new Regex(
"^[a-zA-Z0-9_]*$",
RegexOptions.None,
TimeSpan.FromMilliseconds(1)
);

app.MapPost("/api/urls", async (
HttpRequest request,
UrlShortenerDbContext dbContext
) =>
{
var jsonObject = await request.ReadFromJsonAsync<JsonObject>();
var path = jsonObject?["path"]?.GetValue<string?>()?.Trim('/');
var destination = jsonObject?["destination"]?.GetValue<string?>();

if (string.IsNullOrEmpty(path))
return Results.Problem("Path cannot be empty.");

if (path.Length > 10)
return Results.Problem("Path cannot be longer than 10 characters.");

if (pathRegex.IsMatch(path) == false)
return Results.Problem("Path can only contain alphanumeric characters and underscores.");

if (string.IsNullOrEmpty(destination))
return Results.Problem("Destination cannot be empty.");

if (!Uri.IsWellFormedUriString(destination, UriKind.Absolute))
return Results.Problem("Destination has to be a valid absolute URL.");

if (dbContext.Urls.Any(u => u.Path.Equals(path)))
return Results.Problem("Path is already in use.");

var url = new Url {Path = path, Destination = destination};
await dbContext.Urls.AddAsync(url);
await dbContext.SaveChangesAsync();

return Results.Created(
uri: new Uri(AbsoluteUrl(request, $"/api/urls/{url.Path}")),
value: new
{
Path = url.Path,
Destination = url.Destination,
Id = url.Id,
ShortenedUrl = AbsoluteUrl(request, $"/{url.Path}")
}
);
});

app.MapGet("/{path}", async (
string path,
UrlShortenerDbContext dbContext
) =>
{
if (string.IsNullOrEmpty(path) ||
path.Length > 10 ||
pathRegex.IsMatch(path) == false)
return Results.BadRequest();

var url = await dbContext.Urls.FirstOrDefaultAsync(u => u.Path == path);
if (url == null)
return Results.NotFound();

return Results.Redirect(url.Destination);
});

app.Run();

public sealed class UrlShortenerDbContext : DbContext
{
public DbSet<Url> Urls { get; set; }

public UrlShortenerDbContext(DbContextOptions<UrlShortenerDbContext> options)
: base(options)
{
}
}

[Index(nameof(Path), IsUnique = true)]
public sealed record Url
{
public Guid Id { get; set; }
public string Path { get; set; }
public string Destination { get; set; }
}
22 changes: 22 additions & 0 deletions Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5063",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7205;http://localhost:5063",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
17 changes: 17 additions & 0 deletions UrlShortener.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"UrlDb": "Data Source=urls.db"
}
}
9 changes: 9 additions & 0 deletions appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
54 changes: 54 additions & 0 deletions wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>URL Shortener</title>
</head>
<body>
<form>
<div>
<label for="destination">Destination URL</label>
<input type="text" id="destination" name="destination">
</div>
<div>
<label for="path">Short Path</label>
<input id="path" name="path">
</div>
<button type="submit">Shorten URL</button>
</form>
<div style="display: none">
Shortened URL created: <br>
<label for="result">Shortened URL</label>
<input readonly id="result">
</div>
<script>
const resultInput = document.getElementById('result');
const resultParent = resultInput.parentElement;
const form = document.getElementsByTagName('form')[0];
form.addEventListener('submit', async (event) => {
event.preventDefault();
const url = {
destination: form['destination'].value,
path: form['path'].value
};
fetch('/api/urls', {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(url)
})
.then(async response => {
const responseObject = await response.json()
if (!response.ok) {
console.error(responseObject)
return;
}

resultInput.value = responseObject.shortenedUrl;
resultParent.style.display = 'block';
});
});
</script>
</body>
</html>

0 comments on commit a2488fa

Please sign in to comment.