Plumb a per-resource owner (Zitadel sub) through the sync contract without enforcing isolation client-side — the server stays the authority. - Dtos: add optional ownerId to RemoteList/RemoteTask/MirrorTask - JwtClaims: decode the sub claim from the access token (never throws) - OnlineSyncService: stamp ownerId on pushed lists + mirror; defensively skip pulled tasks owned by a different user (unowned tasks still sync, so single-user behavior is unchanged) - docs: contract documents ownerId + multi-user readiness Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
1.4 KiB
C#
50 lines
1.4 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ClaudeDo.Worker.Online;
|
|
|
|
/// <summary>
|
|
/// Minimal, dependency-free reader for a JWT access token's payload claims. Used to resolve the
|
|
/// current user's Zitadel subject (<c>sub</c>) so sync payloads can be stamped with an owner.
|
|
/// Never throws — returns null when the token is absent or cannot be parsed.
|
|
/// </summary>
|
|
public static class JwtClaims
|
|
{
|
|
/// <summary>
|
|
/// Returns the <c>sub</c> claim of the JWT, or null if the token is absent/unparseable or
|
|
/// carries no subject.
|
|
/// </summary>
|
|
public static string? GetSubject(string? jwt)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(jwt))
|
|
return null;
|
|
|
|
var parts = jwt.Split('.');
|
|
if (parts.Length < 2)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(Base64UrlDecode(parts[1]));
|
|
if (doc.RootElement.TryGetProperty("sub", out var sub) &&
|
|
sub.ValueKind == JsonValueKind.String)
|
|
return sub.GetString();
|
|
return null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static byte[] Base64UrlDecode(string input)
|
|
{
|
|
var s = input.Replace('-', '+').Replace('_', '/');
|
|
switch (s.Length % 4)
|
|
{
|
|
case 2: s += "=="; break;
|
|
case 3: s += "="; break;
|
|
}
|
|
return Convert.FromBase64String(s);
|
|
}
|
|
}
|