Files
AutopilotMonitor/src/Shared/AutopilotMonitor.Shared/Models/PushApiModels.cs
T
Oliver Kieselbach caa6952ae7 Push: a Global Admin's platform device also receives the alerts of the owner's home tenant (D-334)
A receiver holds one pairing, so a Global Admin whose phone is paired in the platform scope
could not get their own tenant's enrollment alerts without replacing that pairing. The push
transport now resolves a tenant channel's recipients as the tenant's Active devices plus the
Active platform devices whose OwnerHomeTenantId is that tenant, filtered by the platform
eligibility rule and deduplicated by endpoint; no other tenant reaches a platform device, there
is no opt-in and no setting. The home tenant's device list shows such a row with
PushDeviceDto.Scope = "platform" (manifest and wire types regenerated); the panel marks it with
a Platform badge and no actions — it is managed under the platform's device list, and the
tenant routes do not know the id. Tests: fan-out incl. a foreign home tenant, a former Global
Admin and a shared endpoint; the list in the home tenant, the owner's and an operator's view,
and the refusing tenant routes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 23:49:40 +02:00

157 lines
7.5 KiB
C#

using System;
using System.Collections.Generic;
namespace AutopilotMonitor.Shared.Models
{
// Web Push pairing + devices (Functions/Push). Declaration order == wire order.
/// <summary>POST push/pairings · POST global/push/pairings — a fresh pairing code for the caller's own person.</summary>
public class CreatePairingResponse : IApiResponse
{
/// <summary>Handle for the PC-side status poll and the confirm/reject calls.</summary>
public string PairingId { get; set; } = default!;
/// <summary>The 11-character code to type into the receiver app.</summary>
public string Code { get; set; } = default!;
/// <summary>The same code as the pairing link (QR payload): …/push/pair/#p=&lt;code&gt;.</summary>
public string Url { get; set; } = default!;
public DateTime ExpiresUtc { get; set; }
}
/// <summary>GET push/pairings/{pairingId} — what the receiver has done with the code so far.</summary>
public class PairingStatusResponse : IApiResponse
{
/// <summary>Pending · Redeemed (a device waits for confirmation) · Confirmed · Rejected · Expired.</summary>
public string Status { get; set; } = default!;
/// <summary>The redeeming device, present from Redeemed on.</summary>
public PairingDeviceDto? Device { get; set; }
}
public class PairingDeviceDto
{
public string DeviceId { get; set; } = default!;
public string Label { get; set; } = default!;
public string Platform { get; set; } = default!;
public DateTime RedeemedUtc { get; set; }
}
/// <summary>POST push/pairings/{pairingId}/confirm — the device is Active from now on.</summary>
public class ConfirmPairingResponse : IApiResponse
{
public string DeviceId { get; set; } = default!;
}
/// <summary>POST push/pair/begin — the receiver validates its code before asking for notification permission.</summary>
public class BeginPairRequest : IApiRequest
{
public string Code { get; set; } = default!;
}
/// <summary>The VAPID application server key the receiver must subscribe with, and its identity.</summary>
public class BeginPairResponse : IApiResponse
{
public string Kid { get; set; } = default!;
/// <summary>Uncompressed P-256 point, base64url — pushManager.subscribe({applicationServerKey}).</summary>
public string VapidPublicKey { get; set; } = default!;
}
/// <summary>POST push/pair — the receiver hands over its subscription; the device is created Pending.</summary>
public class RedeemPairRequest : IApiRequest
{
public string Code { get; set; } = default!;
public string Kid { get; set; } = default!;
public string Endpoint { get; set; } = default!;
public string P256dh { get; set; } = default!;
public string Auth { get; set; } = default!;
public string? Label { get; set; }
public string? Platform { get; set; }
public string? AppVersion { get; set; }
}
public class RedeemPairResponse : IApiResponse
{
/// <summary>"{scope}.{deviceId}.{secret}" — the receiver's only credential; header X-Push-Device-Token.</summary>
public string DeviceToken { get; set; } = default!;
public string DeviceId { get; set; } = default!;
/// <summary>Always Pending here; the PC confirms.</summary>
public string Status { get; set; } = default!;
/// <summary>Suggested interval for the receiver's status poll while Pending.</summary>
public int PollSeconds { get; set; }
}
/// <summary>GET/PUT push/device — the receiver's own view of its registration.</summary>
public class PushDeviceStatusResponse : IApiResponse
{
public string DeviceId { get; set; } = default!;
/// <summary>Pending · Active · Paused · Stale (a revoked device is gone: the token answers 401).</summary>
public string Status { get; set; } = default!;
public string Label { get; set; } = default!;
public string Platform { get; set; } = default!;
public string Kid { get; set; } = default!;
public DateTime PairedUtc { get; set; }
public DateTime? ConfirmedUtc { get; set; }
public DateTime? LastDeliveredUtc { get; set; }
/// <summary>The owner's UPN — this is the owner's own device.</summary>
public string OwnerUpn { get; set; } = default!;
/// <summary>Tenant display name, or "Platform operator" for the platform scope.</summary>
public string ScopeName { get; set; } = default!;
/// <summary>
/// The platform's current VAPID key id. Differs from <see cref="Kid"/> after a key rotation:
/// the receiver then subscribes again with <see cref="ActiveVapidPublicKey"/> and PUTs the
/// new subscription with this kid — no re-pairing. Empty while the channel is unconfigured.
/// </summary>
public string ActiveKid { get; set; } = default!;
/// <summary>Uncompressed P-256 point, base64url, of the current key (what to subscribe with).</summary>
public string ActiveVapidPublicKey { get; set; } = default!;
}
/// <summary>PUT push/device — a changed or re-created subscription (same device, same owner).</summary>
public class ResubscribeRequest : IApiRequest
{
public string Endpoint { get; set; } = default!;
public string P256dh { get; set; } = default!;
public string Auth { get; set; } = default!;
public string Kid { get; set; } = default!;
}
/// <summary>GET push/devices · GET global/push/devices — own devices (everyone) plus every device of the scope (admins).</summary>
public class PushDeviceListResponse : IApiResponse
{
public IReadOnlyList<PushDeviceDto> Devices { get; set; } = default!;
}
public class PushDeviceDto
{
public string DeviceId { get; set; } = default!;
public string Label { get; set; } = default!;
public string Platform { get; set; } = default!;
public string Status { get; set; } = default!;
public string Kid { get; set; } = default!;
public DateTime PairedUtc { get; set; }
public DateTime? ConfirmedUtc { get; set; }
public DateTime? LastDeliveredUtc { get; set; }
public DateTime? LastOpenedUtc { get; set; }
public string OwnerUpn { get; set; } = default!;
/// <summary>True for the caller's own devices.</summary>
public bool IsOwn { get; set; }
/// <summary>
/// "tenant": paired in this scope. "platform": a Global Administrator's platform device, listed
/// in their home tenant because it receives this tenant's alerts without a second pairing
/// (D-334); the tenant routes do not act on it — it is managed under the platform's device list.
/// </summary>
public string Scope { get; set; } = "tenant";
}
/// <summary>GET/PUT/DELETE sessions/{sessionId}/watch — "notify my devices when this session ends".</summary>
public class SessionWatchResponse : IApiResponse
{
public bool Watching { get; set; }
public DateTime? ExpiresUtc { get; set; }
/// <summary>
/// Whether this caller can watch at all: an enabled Push channel in one of the caller's own
/// scopes (home tenant; the platform for a Global Admin) where the caller holds an Admin or
/// Operator role. False hides the portal's button instead of letting a click fail.
/// </summary>
public bool Available { get; set; }
}
}