Untis-API von Server zu Client

This commit is contained in:
2026-08-24 22:00:26 +02:00
parent 0f10f754d0
commit 34a9fdf73b
21 changed files with 243 additions and 445 deletions
-171
View File
@@ -244,177 +244,6 @@ public static class Endpoints
});
}
// ── WebUntis (granulare Abrufe über benutzergebundene RAM-Sitzungen) ──────
public static void MapWebUntisEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/webuntis").RequireAuthorization();
group.MapGet("/connection", (ClaimsPrincipal user, WebUntisConnectionStore connections) =>
{
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return uid is null ? Results.Unauthorized() : Results.Ok(connections.GetStatus(uid));
});
group.MapPost("/connection", async (WebUntisConnectRequest request, ClaimsPrincipal user,
WebUntisConnectionStore connections, CancellationToken cancellationToken) =>
{
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (uid is null) return Results.Unauthorized();
if (string.IsNullOrWhiteSpace(request.School) || string.IsNullOrWhiteSpace(request.Username) ||
string.IsNullOrWhiteSpace(request.Password))
return Results.BadRequest("Schule, Benutzername und Passwort sind erforderlich.");
return await WebUntisResult(() => connections.ConnectAsync(uid, request, cancellationToken));
});
group.MapDelete("/connection", async (ClaimsPrincipal user, WebUntisConnectionStore connections) =>
{
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (uid is null) return Results.Unauthorized();
await connections.DisconnectAsync(uid);
return Results.NoContent();
});
group.MapGet("/schoolyears", (ClaimsPrincipal user, WebUntisConnectionStore connections,
WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback,
client => client.GetSchoolYearsAsync(cancellationToken)));
group.MapGet("/classes", ([FromQuery] int schoolyearId, ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback,
CancellationToken cancellationToken) => schoolyearId <= 0
? Task.FromResult<IResult>(Results.BadRequest("schoolyearId muss größer als 0 sein."))
: WithWebUntisClient(user, connections, fallback,
client => client.GetClassesAsync(schoolyearId, cancellationToken)));
group.MapGet("/teachers", (ClaimsPrincipal user, WebUntisConnectionStore connections,
WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback, client => client.GetTeachersAsync(cancellationToken)));
group.MapGet("/student-report", ([FromQuery] string? className, ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback,
client => client.GetStudentReportAsync(className, cancellationToken)));
group.MapGet("/holidays", (ClaimsPrincipal user, WebUntisConnectionStore connections,
WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback, client => client.GetHolidaysAsync(cancellationToken)));
group.MapGet("/timegrid", (ClaimsPrincipal user, WebUntisConnectionStore connections,
WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback, client => client.GetTimeGridAsync(cancellationToken)));
group.MapGet("/substitutions", ([FromQuery] int startDate, [FromQuery] int endDate,
[FromQuery] int? departmentId, ClaimsPrincipal user, WebUntisConnectionStore connections,
WebUntisClient fallback, CancellationToken cancellationToken) =>
{
if (!ValidDateRange(startDate, endDate, 31, out var error))
return Task.FromResult<IResult>(Results.BadRequest(error));
return WithWebUntisClient(user, connections, fallback,
client => client.GetSubstitutionsAsync(startDate, endDate, departmentId, cancellationToken));
});
group.MapGet("/timetable", ([FromQuery] string elementType, [FromQuery] int elementId,
[FromQuery] int startDate, [FromQuery] int endDate, ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback, CancellationToken cancellationToken) =>
{
if (!Enum.TryParse<UntisTimetableElementType>(elementType, true, out var parsedType) ||
!Enum.IsDefined(parsedType))
return Task.FromResult<IResult>(Results.BadRequest(
"elementType muss class, teacher, subject, room oder student sein."));
if (elementId <= 0)
return Task.FromResult<IResult>(Results.BadRequest("elementId muss größer als 0 sein."));
if (!ValidDateRange(startDate, endDate, 62, out var error))
return Task.FromResult<IResult>(Results.BadRequest(error));
return WithWebUntisClient(user, connections, fallback,
client => client.GetTimetableAsync(parsedType, elementId, startDate, endDate, cancellationToken));
});
group.MapGet("/students/{studentKey:int}/absences", (int studentKey, [FromQuery] int startDate,
[FromQuery] int endDate, ClaimsPrincipal user, WebUntisConnectionStore connections,
WebUntisClient fallback, CancellationToken cancellationToken) =>
{
if (studentKey <= 0)
return Task.FromResult<IResult>(Results.BadRequest("studentKey muss größer als 0 sein."));
if (!ValidDateRange(startDate, endDate, 400, out var error))
return Task.FromResult<IResult>(Results.BadRequest(error));
return WithWebUntisClient(user, connections, fallback,
client => client.GetStudentAbsencesAsync(studentKey, startDate, endDate, cancellationToken));
});
group.MapGet("/students/{studentId:int}/class-register-entries", (int studentId,
[FromQuery] int startDate, [FromQuery] int endDate, ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback, CancellationToken cancellationToken) =>
{
if (studentId <= 0)
return Task.FromResult<IResult>(Results.BadRequest("studentId muss größer als 0 sein."));
if (!ValidDateRange(startDate, endDate, 400, out var error))
return Task.FromResult<IResult>(Results.BadRequest(error));
return WithWebUntisClient(user, connections, fallback,
client => client.GetClassRegisterEntriesAsync(studentId, startDate, endDate, cancellationToken));
});
group.MapGet("/class-register/categories", (ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback,
client => client.GetClassRegisterCategoriesAsync(cancellationToken)));
group.MapGet("/class-register/category-groups", (ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback, CancellationToken cancellationToken) =>
WithWebUntisClient(user, connections, fallback,
client => client.GetClassRegisterCategoryGroupsAsync(cancellationToken)));
}
private static Task<IResult> WithWebUntisClient<T>(ClaimsPrincipal user,
WebUntisConnectionStore connections, WebUntisClient fallback, Func<WebUntisClient, Task<T>> operation)
{
var uid = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (uid is null) return Task.FromResult<IResult>(Results.Unauthorized());
return WebUntisResult(() => operation(connections.GetClient(uid) ?? fallback));
}
private static async Task<IResult> WebUntisResult<T>(Func<Task<T>> operation)
{
try { return Results.Ok(await operation()); }
catch (WebUntisConfigurationException exception)
{
return Results.Problem(exception.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
}
catch (WebUntisException exception)
{
return Results.Problem(exception.Message, statusCode: StatusCodes.Status502BadGateway);
}
catch (InvalidDataException exception)
{
return Results.Problem($"Der WebUntis-Report ist ungültig: {exception.Message}",
statusCode: StatusCodes.Status502BadGateway);
}
}
private static bool ValidDateRange(int startDate, int endDate, int maximumDays, out string error)
{
error = "";
if (!DateOnly.TryParseExact(startDate.ToString(CultureInfo.InvariantCulture), "yyyyMMdd",
CultureInfo.InvariantCulture, DateTimeStyles.None, out var start) ||
!DateOnly.TryParseExact(endDate.ToString(CultureInfo.InvariantCulture), "yyyyMMdd",
CultureInfo.InvariantCulture, DateTimeStyles.None, out var end))
{
error = "startDate und endDate müssen gültige Datumswerte im Format yyyyMMdd sein.";
return false;
}
if (end < start)
{
error = "endDate darf nicht vor startDate liegen.";
return false;
}
if (end.DayNumber - start.DayNumber + 1 > maximumDays)
{
error = $"Der Zeitraum darf höchstens {maximumDays} Tage umfassen.";
return false;
}
return true;
}
private static Dictionary<string, string[]> ValidateLocation(SchoolLocationRequest request)
{
var errors = new Dictionary<string, string[]>();