250 lines
8.6 KiB
C#
250 lines
8.6 KiB
C#
using System.Net;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Services.AddAuthentication("Basic")
|
|
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
|
|
builder.Services.AddAuthorization();
|
|
builder.Services.AddSingleton<Database>();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseDefaultFiles();
|
|
app.UseStaticFiles();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
await app.Services.GetRequiredService<Database>().InitializeAsync();
|
|
|
|
app.MapPost("/api/visitors", async (VisitorInput input, Database database) =>
|
|
{
|
|
var errors = Validate(input);
|
|
if (errors.Count > 0)
|
|
{
|
|
return Results.ValidationProblem(errors);
|
|
}
|
|
|
|
await using var connection = await database.OpenConnectionAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
insert into visitors (name, street_address, postal_code, city, birthday, email)
|
|
values ($name, $street, $postal, $city, $birthday, $email)
|
|
""";
|
|
command.Parameters.AddWithValue("$name", input.Name.Trim());
|
|
command.Parameters.AddWithValue("$street", input.StreetAddress.Trim());
|
|
command.Parameters.AddWithValue("$postal", input.PostalCode.Trim());
|
|
command.Parameters.AddWithValue("$city", input.City.Trim());
|
|
command.Parameters.AddWithValue("$birthday", DateOnly.Parse(input.Birthday).ToString("yyyy-MM-dd"));
|
|
command.Parameters.AddWithValue("$email", input.Email.Trim().ToLowerInvariant());
|
|
await command.ExecuteNonQueryAsync();
|
|
|
|
return Results.Created("/api/visitors", null);
|
|
});
|
|
|
|
app.MapGet("/api/visitors", async (Database database) =>
|
|
{
|
|
var visitors = new List<VisitorListItem>();
|
|
await using var connection = await database.OpenConnectionAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
select id, name, street_address, postal_code, city, birthday, email, created_at
|
|
from visitors
|
|
order by created_at desc, id desc
|
|
""";
|
|
await using var reader = await command.ExecuteReaderAsync();
|
|
|
|
while (await reader.ReadAsync())
|
|
{
|
|
visitors.Add(new VisitorListItem(
|
|
reader.GetInt64(0),
|
|
reader.GetString(1),
|
|
reader.GetString(2),
|
|
reader.GetString(3),
|
|
reader.GetString(4),
|
|
DateOnly.Parse(reader.GetString(5)),
|
|
reader.GetString(6),
|
|
DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc)
|
|
));
|
|
}
|
|
|
|
return Results.Ok(visitors);
|
|
}).RequireAuthorization();
|
|
|
|
app.MapDelete("/api/visitors/{id:long}", async (long id, Database database) =>
|
|
{
|
|
await using var connection = await database.OpenConnectionAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = "delete from visitors where id = $id";
|
|
command.Parameters.AddWithValue("$id", id);
|
|
var deleted = await command.ExecuteNonQueryAsync();
|
|
return deleted == 0 ? Results.NotFound() : Results.NoContent();
|
|
}).RequireAuthorization();
|
|
|
|
app.MapGet("/admin", (IWebHostEnvironment environment) =>
|
|
Results.File(Path.Combine(environment.WebRootPath, "admin.html"), "text/html"));
|
|
|
|
app.MapFallbackToFile("index.html");
|
|
|
|
app.Run();
|
|
|
|
static Dictionary<string, string[]> Validate(VisitorInput input)
|
|
{
|
|
var errors = new Dictionary<string, string[]>();
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Name))
|
|
{
|
|
errors["name"] = ["Navn mangler."];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.StreetAddress))
|
|
{
|
|
errors["streetAddress"] = ["Vejnavn og husnummer mangler."];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.PostalCode) || input.PostalCode.Trim().Length != 4 || !input.PostalCode.All(char.IsDigit))
|
|
{
|
|
errors["postalCode"] = ["Postnummer skal være 4 cifre."];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.City))
|
|
{
|
|
errors["city"] = ["Postby mangler."];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Birthday) || !DateOnly.TryParse(input.Birthday, out _))
|
|
{
|
|
errors["birthday"] = ["Fødselsdag mangler eller er ugyldig."];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Email) || !input.Email.Contains('@') || !input.Email.Contains('.'))
|
|
{
|
|
errors["email"] = ["Email mangler eller er ugyldig."];
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
public sealed record VisitorInput(string Name, string StreetAddress, string PostalCode, string City, string Birthday, string Email);
|
|
|
|
public sealed record VisitorListItem(long Id, string Name, string StreetAddress, string PostalCode, string City, DateOnly Birthday, string Email, DateTime CreatedAt);
|
|
|
|
public sealed class Database
|
|
{
|
|
private readonly string _connectionString;
|
|
|
|
public Database(IConfiguration configuration, IWebHostEnvironment environment)
|
|
{
|
|
var path = configuration["Database:Path"] ?? "data/visitors.db";
|
|
var fullPath = Path.GetFullPath(path, environment.ContentRootPath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
|
_connectionString = new SqliteConnectionStringBuilder { DataSource = fullPath }.ToString();
|
|
}
|
|
|
|
public async Task<SqliteConnection> OpenConnectionAsync()
|
|
{
|
|
var connection = new SqliteConnection(_connectionString);
|
|
try
|
|
{
|
|
await connection.OpenAsync();
|
|
return connection;
|
|
}
|
|
catch
|
|
{
|
|
await connection.DisposeAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await using var connection = await OpenConnectionAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
create table if not exists visitors (
|
|
id integer primary key autoincrement,
|
|
name text not null,
|
|
street_address text not null,
|
|
postal_code text not null,
|
|
city text not null,
|
|
birthday text not null,
|
|
email text not null,
|
|
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
);
|
|
|
|
create index if not exists visitors_created_at_idx on visitors (created_at desc);
|
|
""";
|
|
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
}
|
|
|
|
public sealed class BasicAuthenticationHandler(
|
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
|
ILoggerFactory logger,
|
|
UrlEncoder encoder,
|
|
IConfiguration configuration)
|
|
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
|
{
|
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
{
|
|
var expectedUser = configuration["Admin:Username"] ?? "admin";
|
|
var expectedPassword = configuration["Admin:Password"];
|
|
|
|
if (string.IsNullOrWhiteSpace(expectedPassword))
|
|
{
|
|
return Task.FromResult(AuthenticateResult.Fail("Admin:Password mangler."));
|
|
}
|
|
|
|
if (!Request.Headers.TryGetValue("Authorization", out var authorizationHeaders))
|
|
{
|
|
return Task.FromResult(AuthenticateResult.NoResult());
|
|
}
|
|
|
|
var authorization = authorizationHeaders.ToString();
|
|
if (!authorization.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return Task.FromResult(AuthenticateResult.NoResult());
|
|
}
|
|
|
|
try
|
|
{
|
|
var encoded = authorization["Basic ".Length..].Trim();
|
|
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
|
|
var separator = decoded.IndexOf(':');
|
|
if (separator < 0)
|
|
{
|
|
return Task.FromResult(AuthenticateResult.Fail("Ugyldige credentials."));
|
|
}
|
|
|
|
var username = decoded[..separator];
|
|
var password = decoded[(separator + 1)..];
|
|
|
|
if (username != expectedUser || password != expectedPassword)
|
|
{
|
|
return Task.FromResult(AuthenticateResult.Fail("Forkert login."));
|
|
}
|
|
|
|
var identity = new ClaimsIdentity([new Claim(ClaimTypes.Name, username)], Scheme.Name);
|
|
var principal = new ClaimsPrincipal(identity);
|
|
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)));
|
|
}
|
|
catch
|
|
{
|
|
return Task.FromResult(AuthenticateResult.Fail("Ugyldige credentials."));
|
|
}
|
|
}
|
|
|
|
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
|
{
|
|
Response.Headers.WWWAuthenticate = "Basic realm=\"Skydesportens Dag\"";
|
|
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|