Wetterdienst
This commit is contained in:
@@ -149,6 +149,123 @@ public static class Endpoints
|
||||
});
|
||||
}
|
||||
|
||||
// ── Schulstandort und DWD-Wetter ─────────────────────────────────────────
|
||||
|
||||
public static void MapSchoolWeatherEndpoints(this WebApplication app)
|
||||
{
|
||||
var g = app.MapGroup("/api/school").RequireAuthorization();
|
||||
|
||||
g.MapGet("/location", (ClaimsPrincipal user, SchoolWeatherStore store) =>
|
||||
{
|
||||
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (uid is null) return Results.Unauthorized();
|
||||
var location = store.GetLocation(uid);
|
||||
return location is null
|
||||
? Results.NotFound("Noch kein Schulstandort hinterlegt.")
|
||||
: Results.Ok(location);
|
||||
});
|
||||
|
||||
g.MapPut("/location", async ([FromBody] SchoolLocationRequest request,
|
||||
ClaimsPrincipal user, SchoolWeatherStore store, IGeocoder geocoder,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (uid is null) return Results.Unauthorized();
|
||||
|
||||
var errors = ValidateLocation(request);
|
||||
if (errors.Count > 0) return Results.ValidationProblem(errors);
|
||||
|
||||
var existing = store.GetLocation(uid);
|
||||
if (existing is not null && SameLocation(existing, request))
|
||||
return Results.Ok(existing);
|
||||
|
||||
GeocodingResult? result;
|
||||
try { result = await geocoder.GeocodeAsync(request, cancellationToken); }
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return Results.Problem("Der Geocoding-Dienst ist momentan nicht erreichbar.",
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Results.Problem("Die Adressprüfung hat zu lange gedauert.",
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
if (result is null)
|
||||
return Results.UnprocessableEntity(new
|
||||
{
|
||||
detail = "Die Schuladresse konnte nicht eindeutig gefunden werden. Bitte Schreibweise, PLZ und Ort prüfen.",
|
||||
});
|
||||
|
||||
var profile = new SchoolLocationProfile
|
||||
{
|
||||
SchoolName = request.SchoolName.Trim(),
|
||||
Street = request.Street.Trim(),
|
||||
PostalCode = request.PostalCode.Trim(),
|
||||
City = request.City.Trim(),
|
||||
State = request.State,
|
||||
Latitude = result.Latitude,
|
||||
Longitude = result.Longitude,
|
||||
ResolvedAddress = result.DisplayName,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
store.SaveLocation(uid, profile);
|
||||
return Results.Ok(profile);
|
||||
});
|
||||
|
||||
g.MapGet("/weather", async (ClaimsPrincipal user, SchoolWeatherStore store,
|
||||
DwdWeatherService weather, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (uid is null) return Results.Unauthorized();
|
||||
var location = store.GetLocation(uid);
|
||||
if (location is null) return Results.NotFound("Noch kein Schulstandort hinterlegt.");
|
||||
|
||||
var cached = store.GetWeather(uid);
|
||||
if (cached is not null && DateTime.UtcNow - cached.RetrievedAt.ToUniversalTime() < TimeSpan.FromMinutes(5))
|
||||
return Results.Ok(cached);
|
||||
try
|
||||
{
|
||||
var current = await weather.GetAsync(location, cancellationToken);
|
||||
store.SaveWeather(uid, current);
|
||||
return Results.Ok(current);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or InvalidDataException or InvalidOperationException)
|
||||
{
|
||||
if (cached is not null)
|
||||
{
|
||||
cached.IsStale = true;
|
||||
return Results.Ok(cached);
|
||||
}
|
||||
return Results.Problem("Der DWD ist momentan nicht erreichbar und es liegt noch kein Wetter-Cache vor.",
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Dictionary<string, string[]> ValidateLocation(SchoolLocationRequest request)
|
||||
{
|
||||
var errors = new Dictionary<string, string[]>();
|
||||
if (string.IsNullOrWhiteSpace(request.SchoolName))
|
||||
errors[nameof(request.SchoolName)] = ["Bitte den Namen der Schule angeben."];
|
||||
if (string.IsNullOrWhiteSpace(request.Street))
|
||||
errors[nameof(request.Street)] = ["Bitte Straße und Hausnummer angeben."];
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(request.PostalCode?.Trim() ?? "", @"^\d{5}$"))
|
||||
errors[nameof(request.PostalCode)] = ["Bitte eine fünfstellige deutsche PLZ angeben."];
|
||||
if (string.IsNullOrWhiteSpace(request.City))
|
||||
errors[nameof(request.City)] = ["Bitte den Ort angeben."];
|
||||
if (!Enum.IsDefined(request.State))
|
||||
errors[nameof(request.State)] = ["Das Bundesland ist ungültig."];
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static bool SameLocation(SchoolLocationProfile existing, SchoolLocationRequest request) =>
|
||||
string.Equals(existing.SchoolName, request.SchoolName.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(existing.Street, request.Street.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(existing.PostalCode, request.PostalCode.Trim(), StringComparison.Ordinal) &&
|
||||
string.Equals(existing.City, request.City.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
existing.State == request.State;
|
||||
|
||||
// ── JWT ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private static string Jwt(string userId, string secret)
|
||||
|
||||
Reference in New Issue
Block a user