Initial
This commit is contained in:
147
LehrerApp.Api/Endpoints/Endpoints.cs
Normal file
147
LehrerApp.Api/Endpoints/Endpoints.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using LehrerApp.Sync.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace LehrerApp.Api;
|
||||
|
||||
public static class Endpoints
|
||||
{
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public static void MapAuthEndpoints(this WebApplication app, string jwtSecret)
|
||||
{
|
||||
app.MapPost("/api/auth/login", (LoginRequest req) =>
|
||||
{
|
||||
// TODO: Passwort-Hash gegen DB prüfen
|
||||
if (string.IsNullOrWhiteSpace(req.Username) ||
|
||||
string.IsNullOrWhiteSpace(req.Password))
|
||||
return Results.Unauthorized();
|
||||
|
||||
var token = GenerateJwt(req.Username, jwtSecret);
|
||||
return Results.Ok(new { token, userId = req.Username });
|
||||
});
|
||||
|
||||
app.MapPost("/api/auth/register", (RegisterRequest req) =>
|
||||
{
|
||||
// TODO: User anlegen, Passwort hashen (BCrypt)
|
||||
if (string.IsNullOrWhiteSpace(req.Username) ||
|
||||
req.Password.Length < 12)
|
||||
return Results.BadRequest(
|
||||
"Passwort muss mindestens 12 Zeichen haben.");
|
||||
|
||||
var token = GenerateJwt(req.Username, jwtSecret);
|
||||
return Results.Ok(new { token, userId = req.Username });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sync ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public static void MapSyncEndpoints(this WebApplication app)
|
||||
{
|
||||
var sync = app.MapGroup("/api/sync").RequireAuthorization();
|
||||
|
||||
sync.MapPost("/push", (
|
||||
[FromBody] List<SyncEvent> events,
|
||||
ClaimsPrincipal user,
|
||||
EventStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
var result = store.Push(userId, events);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
sync.MapGet("/pull", (
|
||||
[FromQuery] long since,
|
||||
[FromQuery] string deviceId,
|
||||
ClaimsPrincipal user,
|
||||
EventStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
var result = store.Pull(userId, since, deviceId);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
sync.MapGet("/status", (ClaimsPrincipal user) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
return Results.Ok(new { userId, timestamp = DateTime.UtcNow });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Snapshot ──────────────────────────────────────────────────────────────
|
||||
|
||||
public static void MapSnapshotEndpoints(this WebApplication app)
|
||||
{
|
||||
var snap = app.MapGroup("/api/snapshot").RequireAuthorization();
|
||||
|
||||
// Sender: Snapshot hochladen → Einmal-Code erhalten
|
||||
snap.MapPost("/upload", (
|
||||
[FromBody] SnapshotUploadRequest request,
|
||||
ClaimsPrincipal user,
|
||||
SnapshotStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.EncryptedPayload))
|
||||
return Results.BadRequest("Kein Payload.");
|
||||
|
||||
var result = store.Store(userId, request);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
// Empfänger: Snapshot per Code abrufen (Einmal-Verwendung)
|
||||
snap.MapGet("/{code}", (
|
||||
string code,
|
||||
ClaimsPrincipal user,
|
||||
SnapshotStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
var result = store.Retrieve(userId, code);
|
||||
if (result is null)
|
||||
return Results.NotFound(
|
||||
"Snapshot nicht gefunden, abgelaufen oder bereits verwendet.");
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
}
|
||||
|
||||
// ── JWT ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private static string GenerateJwt(string userId, string secret)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(secret));
|
||||
var creds = new SigningCredentials(
|
||||
key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
claims:
|
||||
[
|
||||
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||
new Claim(JwtRegisteredClaimNames.Jti,
|
||||
Guid.NewGuid().ToString()),
|
||||
],
|
||||
expires: DateTime.UtcNow.AddDays(30),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
public record LoginRequest(string Username, string Password);
|
||||
public record RegisterRequest(string Username, string Password, string DisplayName);
|
||||
|
||||
// Ergänzung – wird unten in der bestehenden Datei angefügt
|
||||
// In Program.cs: app.MapReadableSnapshotEndpoints(); und app.MapPlainSyncEndpoints();
|
||||
115
LehrerApp.Api/Endpoints/ReadableSnapshotEndpoints.cs
Normal file
115
LehrerApp.Api/Endpoints/ReadableSnapshotEndpoints.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.Security.Claims;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Sync.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LehrerApp.Api;
|
||||
|
||||
public static class ReadableSnapshotEndpoints
|
||||
{
|
||||
// ── Readable Snapshot ─────────────────────────────────────────────────────
|
||||
|
||||
public static void MapReadableSnapshotEndpoints(this WebApplication app)
|
||||
{
|
||||
var snap = app.MapGroup("/api/snapshot/readable")
|
||||
.RequireAuthorization();
|
||||
|
||||
// Desktop → Server: aktuellen Snapshot hochladen
|
||||
snap.MapPost("/", (
|
||||
[FromBody] ReadableSnapshot snapshot,
|
||||
ClaimsPrincipal user,
|
||||
ReadableSnapshotStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
snapshot.ExportedAt = DateTime.UtcNow;
|
||||
store.Store(userId, snapshot);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
exportedAt = snapshot.ExportedAt,
|
||||
studentCount = snapshot.Meta.StudentCount,
|
||||
groupCount = snapshot.Meta.GroupCount,
|
||||
});
|
||||
});
|
||||
|
||||
// WebApp → Server: Snapshot laden
|
||||
snap.MapGet("/", (
|
||||
ClaimsPrincipal user,
|
||||
ReadableSnapshotStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
var snapshot = store.Load(userId);
|
||||
if (snapshot is null)
|
||||
return Results.NotFound(
|
||||
"Kein Snapshot vorhanden. " +
|
||||
"Bitte zuerst den Desktop-Client mit dem Server verbinden.");
|
||||
|
||||
return Results.Ok(snapshot);
|
||||
});
|
||||
|
||||
// Nur Metadaten – für Freshness-Check der WebApp
|
||||
snap.MapGet("/meta", (
|
||||
ClaimsPrincipal user,
|
||||
ReadableSnapshotStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
var meta = store.LoadMeta(userId);
|
||||
if (meta is null) return Results.NotFound();
|
||||
|
||||
return Results.Ok(meta);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Plain Sync (WebApp → EventStore) ─────────────────────────────────────
|
||||
|
||||
public static void MapPlainSyncEndpoints(this WebApplication app)
|
||||
{
|
||||
var sync = app.MapGroup("/api/sync/plain")
|
||||
.RequireAuthorization();
|
||||
|
||||
// WebApp schreibt Events (Klartext) – werden beim Desktop-Pull abgeholt
|
||||
sync.MapPost("/push", (
|
||||
[FromBody] List<PlainSyncEvent> events,
|
||||
ClaimsPrincipal user,
|
||||
PlainEventStore store) =>
|
||||
{
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId is null) return Results.Unauthorized();
|
||||
|
||||
// Nur erlaubte EntityTypes für WebApp-Schreibzugriff
|
||||
var allowed = new HashSet<string>
|
||||
{
|
||||
"Grade", "ExamResult", "WorkTask", "Lesson"
|
||||
};
|
||||
|
||||
var rejected = events
|
||||
.Where(e => !allowed.Contains(e.EntityType))
|
||||
.Select(e => e.EventId)
|
||||
.ToList();
|
||||
|
||||
var permitted = events
|
||||
.Where(e => allowed.Contains(e.EntityType))
|
||||
.ToList();
|
||||
|
||||
PlainPushResponse result;
|
||||
if (permitted.Count > 0)
|
||||
result = store.Push(userId, permitted);
|
||||
else
|
||||
result = new PlainPushResponse { Success = true };
|
||||
|
||||
// Abgelehnte Events mit Grund zurückmelden
|
||||
result = result with
|
||||
{
|
||||
RejectedEventIds = [.. result.RejectedEventIds, .. rejected],
|
||||
};
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user