Vorarbeit: WebUntis-API

This commit is contained in:
2026-08-24 12:14:36 +02:00
parent a30aab0fa5
commit 5841d96c5b
10 changed files with 1377 additions and 0 deletions
+127
View File
@@ -1,6 +1,7 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Globalization;
using LehrerApp.Core.Models;
using LehrerApp.Sync.Models;
using Microsoft.AspNetCore.Mvc;
@@ -243,6 +244,132 @@ public static class Endpoints
});
}
// ── WebUntis (granulare, zustandslose Abrufe) ─────────────────────────────
public static void MapWebUntisEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/webuntis").RequireAuthorization();
group.MapGet("/schoolyears", (WebUntisClient client, CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetSchoolYearsAsync(cancellationToken)));
group.MapGet("/classes", ([FromQuery] int schoolyearId, WebUntisClient client,
CancellationToken cancellationToken) => schoolyearId <= 0
? Task.FromResult<IResult>(Results.BadRequest("schoolyearId muss größer als 0 sein."))
: WebUntisResult(() => client.GetClassesAsync(schoolyearId, cancellationToken)));
group.MapGet("/teachers", (WebUntisClient client, CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetTeachersAsync(cancellationToken)));
group.MapGet("/student-report", ([FromQuery] string? className, WebUntisClient client,
CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetStudentReportAsync(className, cancellationToken)));
group.MapGet("/holidays", (WebUntisClient client, CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetHolidaysAsync(cancellationToken)));
group.MapGet("/timegrid", (WebUntisClient client, CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetTimeGridAsync(cancellationToken)));
group.MapGet("/substitutions", ([FromQuery] int startDate, [FromQuery] int endDate,
[FromQuery] int? departmentId, WebUntisClient client, CancellationToken cancellationToken) =>
{
if (!ValidDateRange(startDate, endDate, 31, out var error))
return Task.FromResult<IResult>(Results.BadRequest(error));
return WebUntisResult(() =>
client.GetSubstitutionsAsync(startDate, endDate, departmentId, cancellationToken));
});
group.MapGet("/timetable", ([FromQuery] string elementType, [FromQuery] int elementId,
[FromQuery] int startDate, [FromQuery] int endDate, WebUntisClient client,
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 WebUntisResult(() =>
client.GetTimetableAsync(parsedType, elementId, startDate, endDate, cancellationToken));
});
group.MapGet("/students/{studentKey:int}/absences", (int studentKey, [FromQuery] int startDate,
[FromQuery] int endDate, WebUntisClient client, 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 WebUntisResult(() =>
client.GetStudentAbsencesAsync(studentKey, startDate, endDate, cancellationToken));
});
group.MapGet("/students/{studentId:int}/class-register-entries", (int studentId,
[FromQuery] int startDate, [FromQuery] int endDate, WebUntisClient client,
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 WebUntisResult(() =>
client.GetClassRegisterEntriesAsync(studentId, startDate, endDate, cancellationToken));
});
group.MapGet("/class-register/categories", (WebUntisClient client,
CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetClassRegisterCategoriesAsync(cancellationToken)));
group.MapGet("/class-register/category-groups", (WebUntisClient client,
CancellationToken cancellationToken) =>
WebUntisResult(() => client.GetClassRegisterCategoryGroupsAsync(cancellationToken)));
}
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[]>();