skydesportensdag/Program.cs

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;
}
}