Initial commit: IronServices.Client SDK
Unified client for Iron Services APIs with: - Session-based authentication - Automatic token persistence - Access to IronLicensing, IronNotify, and IronTelemetry APIs - Generic HTTP methods for custom endpoints
This commit is contained in:
commit
d64072a334
|
|
@ -0,0 +1,6 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
*.DotSettings.user
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
#region Licensing Models
|
||||||
|
|
||||||
|
public class LicensingDashboardStats
|
||||||
|
{
|
||||||
|
public int TotalLicenses { get; set; }
|
||||||
|
public int ActiveLicenses { get; set; }
|
||||||
|
public int ExpiringThisMonth { get; set; }
|
||||||
|
public int NewThisMonth { get; set; }
|
||||||
|
public decimal MonthlyRevenue { get; set; }
|
||||||
|
public int TotalProducts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LicenseDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string LicenseKey { get; set; } = "";
|
||||||
|
public string Status { get; set; } = "";
|
||||||
|
public string? CustomerEmail { get; set; }
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
public string ProductName { get; set; } = "";
|
||||||
|
public string? ProductSlug { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
public DateTime? LastActivatedAt { get; set; }
|
||||||
|
public int MaxActivations { get; set; }
|
||||||
|
public int CurrentActivations { get; set; }
|
||||||
|
public string? SuspensionReason { get; set; }
|
||||||
|
public string? RevocationReason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ActivityItem
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string? EntityType { get; set; }
|
||||||
|
public Guid? EntityId { get; set; }
|
||||||
|
public string? UserEmail { get; set; }
|
||||||
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TicketDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Subject { get; set; } = "";
|
||||||
|
public string Status { get; set; } = "";
|
||||||
|
public string Priority { get; set; } = "";
|
||||||
|
public string? CustomerEmail { get; set; }
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public int MessageCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TicketDetailDto : TicketDto
|
||||||
|
{
|
||||||
|
public List<TicketMessageDto> Messages { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TicketMessageDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Content { get; set; } = "";
|
||||||
|
public string SenderType { get; set; } = "";
|
||||||
|
public string? SenderEmail { get; set; }
|
||||||
|
public string? SenderName { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateProfileRequest
|
||||||
|
{
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? CurrentPassword { get; set; }
|
||||||
|
public string? NewPassword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Notify Models
|
||||||
|
|
||||||
|
public class NotificationDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
public string Severity { get; set; } = "Info";
|
||||||
|
public string? AppSlug { get; set; }
|
||||||
|
public string? AppName { get; set; }
|
||||||
|
public string? ActionUrl { get; set; }
|
||||||
|
public string? EntityId { get; set; }
|
||||||
|
public bool IsRead { get; set; }
|
||||||
|
public bool IsAcknowledged { get; set; }
|
||||||
|
public bool IsResolved { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? ReadAt { get; set; }
|
||||||
|
public DateTime? AcknowledgedAt { get; set; }
|
||||||
|
public DateTime? ResolvedAt { get; set; }
|
||||||
|
public string? AcknowledgedBy { get; set; }
|
||||||
|
public string? ResolvedBy { get; set; }
|
||||||
|
public Dictionary<string, string>? Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OnCallStatus
|
||||||
|
{
|
||||||
|
public bool IsOnCall { get; set; }
|
||||||
|
public string? OnCallUserName { get; set; }
|
||||||
|
public string? OnCallUserEmail { get; set; }
|
||||||
|
public DateTime? OnCallUntil { get; set; }
|
||||||
|
public string? NextOnCallUserName { get; set; }
|
||||||
|
public DateTime? NextShiftStart { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScheduleSlot
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string UserName { get; set; } = "";
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime EndTime { get; set; }
|
||||||
|
public bool IsCurrent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TeamMember
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
public string? PhoneNumber { get; set; }
|
||||||
|
public string? Role { get; set; }
|
||||||
|
public Guid? TeamId { get; set; }
|
||||||
|
public string? TeamName { get; set; }
|
||||||
|
public bool IsOnCall { get; set; }
|
||||||
|
public int DeviceCount { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
// Alias for compatibility
|
||||||
|
public Guid UserId => Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppSubscription
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? WebhookUrl { get; set; }
|
||||||
|
public string? WebhookSecret { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public bool IsMuted { get; set; }
|
||||||
|
public DateTime? MutedUntil { get; set; }
|
||||||
|
public int UnreadCount { get; set; }
|
||||||
|
public int TotalNotifications { get; set; }
|
||||||
|
public DateTime? LastEventAt { get; set; }
|
||||||
|
public string SeverityFilter { get; set; } = "All";
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
// Aliases for compatibility
|
||||||
|
public string AppSlug => Slug;
|
||||||
|
public string AppName => Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationPreferences
|
||||||
|
{
|
||||||
|
public bool PushEnabled { get; set; } = true;
|
||||||
|
public bool SoundEnabled { get; set; } = true;
|
||||||
|
public bool VibrationEnabled { get; set; } = true;
|
||||||
|
public string MinimumSeverity { get; set; } = "Info";
|
||||||
|
public TimeSpan? QuietHoursStart { get; set; }
|
||||||
|
public TimeSpan? QuietHoursEnd { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Telemetry Models
|
||||||
|
|
||||||
|
public class TelemetryDashboardStats
|
||||||
|
{
|
||||||
|
public int TotalErrors { get; set; }
|
||||||
|
public int OpenIssues { get; set; }
|
||||||
|
public int ResolvedToday { get; set; }
|
||||||
|
public double ErrorRate { get; set; }
|
||||||
|
public int ActiveProjects { get; set; }
|
||||||
|
public List<ErrorTrend> ErrorTrends { get; set; } = [];
|
||||||
|
public List<TopIssue> TopIssues { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ErrorTrend
|
||||||
|
{
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TopIssue
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public int EventCount { get; set; }
|
||||||
|
public string ProjectName { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string? Culprit { get; set; }
|
||||||
|
public string Level { get; set; } = "error";
|
||||||
|
public string Severity { get; set; } = "error";
|
||||||
|
public string Status { get; set; } = "unresolved";
|
||||||
|
public int EventCount { get; set; }
|
||||||
|
public int UserCount { get; set; }
|
||||||
|
public DateTime FirstSeen { get; set; }
|
||||||
|
public DateTime LastSeen { get; set; }
|
||||||
|
public Guid? ProjectId { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public string? ProjectSlug { get; set; }
|
||||||
|
public bool IsIgnored { get; set; }
|
||||||
|
public Guid? AssignedToUserId { get; set; }
|
||||||
|
public string? AssignedTo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SignalDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string Level { get; set; } = "info";
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
public string? IssueId { get; set; }
|
||||||
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TelemetryProjectDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
public string? Platform { get; set; }
|
||||||
|
public string Dsn { get; set; } = "";
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public bool StackTraceGroupingEnabled { get; set; }
|
||||||
|
public bool JourneyTrackingEnabled { get; set; }
|
||||||
|
public bool SourceMapEnabled { get; set; }
|
||||||
|
public int SampleRate { get; set; }
|
||||||
|
public Guid? ProductId { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlertPreferences
|
||||||
|
{
|
||||||
|
public bool PushEnabled { get; set; } = true;
|
||||||
|
public bool EmailEnabled { get; set; } = true;
|
||||||
|
public string MinimumLevel { get; set; } = "error";
|
||||||
|
public int SpikeThreshold { get; set; } = 10;
|
||||||
|
public bool NotifyOnNewIssue { get; set; } = true;
|
||||||
|
public bool NotifyOnRegression { get; set; } = true;
|
||||||
|
public bool NotifyOnSpike { get; set; } = true;
|
||||||
|
public TimeSpan? QuietHoursStart { get; set; }
|
||||||
|
public TimeSpan? QuietHoursEnd { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Internal Response Types
|
||||||
|
|
||||||
|
internal class UnreadCountResponse
|
||||||
|
{
|
||||||
|
public int Count { get; set; }
|
||||||
|
public int UnreadCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for storing authentication tokens.
|
||||||
|
/// Implement this for platform-specific secure storage (e.g., MAUI SecureStorage, Windows Credential Manager).
|
||||||
|
/// </summary>
|
||||||
|
public interface ITokenStorage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Save the session token.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveTokenAsync(string token, DateTime? expiresAt);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the stored session token.
|
||||||
|
/// </summary>
|
||||||
|
Task<(string? Token, DateTime? ExpiresAt)> GetTokenAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear the stored token.
|
||||||
|
/// </summary>
|
||||||
|
Task ClearTokenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-memory token storage (not persisted across app restarts).
|
||||||
|
/// </summary>
|
||||||
|
public class InMemoryTokenStorage : ITokenStorage
|
||||||
|
{
|
||||||
|
private string? _token;
|
||||||
|
private DateTime? _expiresAt;
|
||||||
|
|
||||||
|
public Task SaveTokenAsync(string token, DateTime? expiresAt)
|
||||||
|
{
|
||||||
|
_token = token;
|
||||||
|
_expiresAt = expiresAt;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<(string? Token, DateTime? ExpiresAt)> GetTokenAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult((_token, _expiresAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ClearTokenAsync()
|
||||||
|
{
|
||||||
|
_token = null;
|
||||||
|
_expiresAt = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File-based token storage (for console apps, testing).
|
||||||
|
/// </summary>
|
||||||
|
public class FileTokenStorage : ITokenStorage
|
||||||
|
{
|
||||||
|
private readonly string _filePath;
|
||||||
|
|
||||||
|
public FileTokenStorage(string? filePath = null)
|
||||||
|
{
|
||||||
|
_filePath = filePath ?? Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"IronServices",
|
||||||
|
"session.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveTokenAsync(string token, DateTime? expiresAt)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(_filePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new { token, expiresAt = expiresAt?.ToString("O") };
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(data);
|
||||||
|
await File.WriteAllTextAsync(_filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string? Token, DateTime? ExpiresAt)> GetTokenAsync()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_filePath))
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await File.ReadAllTextAsync(_filePath);
|
||||||
|
var data = System.Text.Json.JsonSerializer.Deserialize<TokenData>(json);
|
||||||
|
|
||||||
|
DateTime? expiresAt = null;
|
||||||
|
if (!string.IsNullOrEmpty(data?.ExpiresAt))
|
||||||
|
{
|
||||||
|
expiresAt = DateTime.Parse(data.ExpiresAt, null, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (data?.Token, expiresAt);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ClearTokenAsync()
|
||||||
|
{
|
||||||
|
if (File.Exists(_filePath))
|
||||||
|
{
|
||||||
|
File.Delete(_filePath);
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TokenData
|
||||||
|
{
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public string? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<RootNamespace>IronServices.Client</RootNamespace>
|
||||||
|
<AssemblyName>IronServices.Client</AssemblyName>
|
||||||
|
<Description>Client library for IronServices API (IronLicensing, IronTelemetry and IronNotify)</Description>
|
||||||
|
<Authors>David H Friedel Jr</Authors>
|
||||||
|
<Company>MarketAlly</Company>
|
||||||
|
<PackageId>IronServices.Client</PackageId>
|
||||||
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,411 @@
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client for IronServices APIs with session-based authentication.
|
||||||
|
/// Handles login, token storage, and automatic bearer token attachment across multiple services.
|
||||||
|
/// </summary>
|
||||||
|
public class IronServicesClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ITokenStorage _tokenStorage;
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
private readonly IronServicesClientOptions _options;
|
||||||
|
private string? _sessionToken;
|
||||||
|
private DateTime? _expiresAt;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
// Lazy-initialized API services
|
||||||
|
private LicensingApi? _licensing;
|
||||||
|
private NotifyApi? _notify;
|
||||||
|
private TelemetryApi? _telemetry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IronLicensing API operations.
|
||||||
|
/// </summary>
|
||||||
|
public LicensingApi Licensing => _licensing ??= new LicensingApi(this, _options.LicensingUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IronNotify API operations.
|
||||||
|
/// </summary>
|
||||||
|
public NotifyApi Notify => _notify ??= new NotifyApi(this, _options.NotifyUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IronTelemetry API operations.
|
||||||
|
/// </summary>
|
||||||
|
public TelemetryApi Telemetry => _telemetry ??= new TelemetryApi(this, _options.TelemetryUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new IronServices client with multiple service URLs.
|
||||||
|
/// </summary>
|
||||||
|
public IronServicesClient(IronServicesClientOptions options, ITokenStorage? tokenStorage = null)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||||
|
|
||||||
|
_tokenStorage = tokenStorage ?? new InMemoryTokenStorage();
|
||||||
|
_jsonOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to restore session from storage
|
||||||
|
RestoreSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new IronServices client with a single base URL (legacy compatibility).
|
||||||
|
/// All services will use this URL.
|
||||||
|
/// </summary>
|
||||||
|
public IronServicesClient(string baseUrl, ITokenStorage? tokenStorage = null)
|
||||||
|
: this(new IronServicesClientOptions
|
||||||
|
{
|
||||||
|
LicensingUrl = baseUrl,
|
||||||
|
NotifyUrl = baseUrl,
|
||||||
|
TelemetryUrl = baseUrl
|
||||||
|
}, tokenStorage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the client has a valid session token.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAuthenticated => !string.IsNullOrEmpty(_sessionToken) &&
|
||||||
|
(_expiresAt == null || _expiresAt > DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current session expiration time (UTC).
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? SessionExpiresAt => _expiresAt;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current session token (for advanced scenarios).
|
||||||
|
/// </summary>
|
||||||
|
public string? SessionToken => _sessionToken;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Login with email and password.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LoginResult> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = new { email, password };
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/login");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, request, _jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await ReadErrorAsync(response, cancellationToken);
|
||||||
|
return new LoginResult { Success = false, Error = error };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<LoginResponse>(_jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
if (result == null || string.IsNullOrEmpty(result.SessionToken))
|
||||||
|
{
|
||||||
|
return new LoginResult { Success = false, Error = "Invalid response from server" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store session
|
||||||
|
await SetSessionAsync(result.SessionToken, result.ExpiresAt);
|
||||||
|
|
||||||
|
return new LoginResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
UserId = result.UserId,
|
||||||
|
Email = result.Email,
|
||||||
|
DisplayName = result.DisplayName,
|
||||||
|
Role = result.Role,
|
||||||
|
ExpiresAt = result.ExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a new account.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<RegisterResult> RegisterAsync(string email, string password, string? displayName = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = new { email, password, displayName };
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/register");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, request, _jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await ReadErrorAsync(response, cancellationToken);
|
||||||
|
return new RegisterResult { Success = false, Error = error };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<RegisterResponse>(_jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
return new RegisterResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
UserId = result?.User?.Id,
|
||||||
|
Email = result?.User?.Email,
|
||||||
|
ApiKey = result?.ApiKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logout and clear session.
|
||||||
|
/// </summary>
|
||||||
|
public async Task LogoutAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (IsAuthenticated)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/logout");
|
||||||
|
await _httpClient.PostAsync(url, null, cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore logout errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ClearSessionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current user profile.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserProfile?> GetProfileAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsureAuthenticated();
|
||||||
|
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/me");
|
||||||
|
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
await ClearSessionAsync();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<UserProfile>(_jsonOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify email with 6-digit code.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<VerifyEmailResult> VerifyEmailAsync(string email, string code, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = new { email, code };
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/verify-email");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, request, _jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await ReadErrorAsync(response, cancellationToken);
|
||||||
|
return new VerifyEmailResult { Success = false, Error = error };
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<VerifyEmailResponse>(_jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
if (result?.SessionToken != null)
|
||||||
|
{
|
||||||
|
await SetSessionAsync(result.SessionToken, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VerifyEmailResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = result?.Message ?? "Email verified"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request password reset email.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = new { email };
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/forgot-password");
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, request, _jsonOptions, cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset password with token.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ResetPasswordResult> ResetPasswordAsync(string token, string newPassword, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = new { token, newPassword };
|
||||||
|
var url = BuildUrl(_options.LicensingUrl, "api/v1/users/reset-password");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, request, _jsonOptions, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await ReadErrorAsync(response, cancellationToken);
|
||||||
|
return new ResetPasswordResult { Success = false, Error = error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResetPasswordResult { Success = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make an authenticated GET request to a specific URL.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<T?> GetAsync<T>(string fullUrl, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsureAuthenticated();
|
||||||
|
var response = await _httpClient.GetAsync(fullUrl, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
throw new HttpRequestException($"GET {fullUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}: {errorContent}");
|
||||||
|
}
|
||||||
|
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make an authenticated POST request to a specific URL.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TResponse?> PostAsync<TRequest, TResponse>(string fullUrl, TRequest data, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsureAuthenticated();
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(fullUrl, data, _jsonOptions, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
throw new HttpRequestException($"POST {fullUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}: {errorContent}");
|
||||||
|
}
|
||||||
|
return await response.Content.ReadFromJsonAsync<TResponse>(_jsonOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make an authenticated PUT request to a specific URL.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TResponse?> PutAsync<TRequest, TResponse>(string fullUrl, TRequest data, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsureAuthenticated();
|
||||||
|
var response = await _httpClient.PutAsJsonAsync(fullUrl, data, _jsonOptions, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
throw new HttpRequestException($"PUT {fullUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}: {errorContent}");
|
||||||
|
}
|
||||||
|
return await response.Content.ReadFromJsonAsync<TResponse>(_jsonOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make an authenticated DELETE request to a specific URL.
|
||||||
|
/// </summary>
|
||||||
|
public async Task DeleteAsync(string fullUrl, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsureAuthenticated();
|
||||||
|
var response = await _httpClient.DeleteAsync(fullUrl, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
throw new HttpRequestException($"DELETE {fullUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}: {errorContent}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build a full URL from a base URL and endpoint.
|
||||||
|
/// </summary>
|
||||||
|
internal static string BuildUrl(string baseUrl, string endpoint)
|
||||||
|
{
|
||||||
|
return baseUrl.TrimEnd('/') + "/" + endpoint.TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetSessionAsync(string token, DateTime? expiresAt)
|
||||||
|
{
|
||||||
|
_sessionToken = token;
|
||||||
|
_expiresAt = expiresAt;
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _sessionToken);
|
||||||
|
await _tokenStorage.SaveTokenAsync(_sessionToken, _expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreSession()
|
||||||
|
{
|
||||||
|
var (token, expiresAt) = _tokenStorage.GetTokenAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(token) && (expiresAt == null || expiresAt > DateTime.UtcNow))
|
||||||
|
{
|
||||||
|
_sessionToken = token;
|
||||||
|
_expiresAt = expiresAt;
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _sessionToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearSessionAsync()
|
||||||
|
{
|
||||||
|
_sessionToken = null;
|
||||||
|
_expiresAt = null;
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
await _tokenStorage.ClearTokenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureAuthenticated()
|
||||||
|
{
|
||||||
|
if (!IsAuthenticated)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Not authenticated. Call LoginAsync first.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ReadErrorAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var error = JsonSerializer.Deserialize<ErrorResponse>(content, _jsonOptions);
|
||||||
|
return error?.Error ?? $"Request failed with status {response.StatusCode}";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return $"Request failed with status {response.StatusCode}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_httpClient.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for IronServicesClient.
|
||||||
|
/// </summary>
|
||||||
|
public class IronServicesClientOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base URL for IronLicensing API (also used for auth/user operations).
|
||||||
|
/// Default: https://ironlicensing.com
|
||||||
|
/// </summary>
|
||||||
|
public string LicensingUrl { get; set; } = "https://ironlicensing.com";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base URL for IronNotify API.
|
||||||
|
/// Default: https://ironnotify.com
|
||||||
|
/// </summary>
|
||||||
|
public string NotifyUrl { get; set; } = "https://ironnotify.com";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base URL for IronTelemetry API.
|
||||||
|
/// Default: https://irontelemetry.com
|
||||||
|
/// </summary>
|
||||||
|
public string TelemetryUrl { get; set; } = "https://irontelemetry.com";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 IronServices
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API client for IronLicensing operations.
|
||||||
|
/// Access via IronServicesClient.Licensing
|
||||||
|
/// </summary>
|
||||||
|
public class LicensingApi
|
||||||
|
{
|
||||||
|
private readonly IronServicesClient _client;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
internal LicensingApi(IronServicesClient client, string baseUrl)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Url(string endpoint) => IronServicesClient.BuildUrl(_baseUrl, endpoint);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get dashboard statistics.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LicensingDashboardStats> GetDashboardStatsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<LicensingDashboardStats>(Url("api/v1/dashboard/stats"), ct)
|
||||||
|
?? new LicensingDashboardStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get recent activity from audit logs.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<ActivityItem>> GetRecentActivityAsync(int limit = 50, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<List<ActivityItem>>(Url($"api/v1/admin/audit?limit={limit}"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get licenses with optional filtering.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<LicenseDto>> GetLicensesAsync(string? status = null, Guid? productId = null, int page = 1, int limit = 50, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = new List<string> { $"page={page}", $"limit={limit}" };
|
||||||
|
if (!string.IsNullOrEmpty(status)) query.Add($"status={status}");
|
||||||
|
if (productId.HasValue) query.Add($"productId={productId}");
|
||||||
|
var queryString = "?" + string.Join("&", query);
|
||||||
|
|
||||||
|
return await _client.GetAsync<List<LicenseDto>>(Url($"api/v1/admin/licenses{queryString}"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific license by ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LicenseDto?> GetLicenseAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<LicenseDto>(Url($"api/v1/admin/licenses/{id}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new license.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LicenseDto?> CreateLicenseAsync(CreateLicenseRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<CreateLicenseRequest, LicenseDto>(Url("api/v1/admin/licenses"), request, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suspend a license.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LicenseDto?> SuspendLicenseAsync(Guid id, string? reason = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, LicenseDto>(Url($"api/v1/admin/licenses/{id}/suspend"), new { reason }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Revoke a license.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LicenseDto?> RevokeLicenseAsync(Guid id, string? reason = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, LicenseDto>(Url($"api/v1/admin/licenses/{id}/revoke"), new { reason }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reactivate a suspended license.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<LicenseDto?> ReactivateLicenseAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, LicenseDto>(Url($"api/v1/admin/licenses/{id}/reactivate"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resend activation email for a license.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ResendActivationEmailAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _client.PostAsync<object, object>(Url($"api/v1/admin/licenses/{id}/resend-activation"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update user profile.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateProfileAsync(UpdateProfileRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _client.PutAsync<UpdateProfileRequest, object>(Url("api/v1/profile"), request, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get tickets with optional status filter.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<TicketDto>> GetTicketsAsync(string? status = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = !string.IsNullOrEmpty(status) ? $"?status={status}" : "";
|
||||||
|
return await _client.GetAsync<List<TicketDto>>(Url($"api/v1/tickets{query}"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific ticket with messages.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TicketDetailDto?> GetTicketAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<TicketDetailDto>(Url($"api/v1/tickets/{id}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new ticket.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TicketDto?> CreateTicketAsync(string subject, string message, string? category = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TicketDto>(Url("api/v1/tickets"), new { subject, message, category }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reply to a ticket.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TicketMessageDto?> ReplyToTicketAsync(Guid ticketId, string content, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TicketMessageDto>(Url($"api/v1/tickets/{ticketId}/messages"), new { content }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close a ticket.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TicketDto?> CloseTicketAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TicketDto>(Url($"api/v1/tickets/{id}/close"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reopen a closed ticket.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TicketDto?> ReopenTicketAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TicketDto>(Url($"api/v1/tickets/{id}/reopen"), new { }, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateLicenseRequest
|
||||||
|
{
|
||||||
|
public Guid ProductId { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? TierId { get; set; }
|
||||||
|
public int? MaxActivations { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
#region API Response DTOs
|
||||||
|
|
||||||
|
internal class LoginResponse
|
||||||
|
{
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public bool EmailVerified { get; set; }
|
||||||
|
public string? SubscriptionTier { get; set; }
|
||||||
|
public string? Role { get; set; }
|
||||||
|
public string? SessionToken { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RegisterResponse
|
||||||
|
{
|
||||||
|
public UserInfo? User { get; set; }
|
||||||
|
public string? ApiKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UserInfo
|
||||||
|
{
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string? TenantId { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public bool EmailVerified { get; set; }
|
||||||
|
public string? Role { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class VerifyEmailResponse
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string? SessionToken { get; set; }
|
||||||
|
public UserInfo? User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ErrorResponse
|
||||||
|
{
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Result Types
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a login attempt.
|
||||||
|
/// </summary>
|
||||||
|
public class LoginResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? Role { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a registration attempt.
|
||||||
|
/// </summary>
|
||||||
|
public class RegisterResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? ApiKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of email verification.
|
||||||
|
/// </summary>
|
||||||
|
public class VerifyEmailResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of password reset.
|
||||||
|
/// </summary>
|
||||||
|
public class ResetPasswordResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User profile information.
|
||||||
|
/// </summary>
|
||||||
|
public class UserProfile
|
||||||
|
{
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public bool EmailVerified { get; set; }
|
||||||
|
public string? Role { get; set; }
|
||||||
|
public string? SubscriptionPlan { get; set; }
|
||||||
|
public DateTime? CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API client for IronNotify operations.
|
||||||
|
/// Access via IronServicesClient.Notify
|
||||||
|
/// </summary>
|
||||||
|
public class NotifyApi
|
||||||
|
{
|
||||||
|
private readonly IronServicesClient _client;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
internal NotifyApi(IronServicesClient client, string baseUrl)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Url(string endpoint) => IronServicesClient.BuildUrl(_baseUrl, endpoint);
|
||||||
|
|
||||||
|
#region Notifications
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get notifications for the current user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<NotificationDto>> GetNotificationsAsync(string? appSlug = null, bool unreadOnly = false, int page = 1, int pageSize = 50, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = new List<string> { $"page={page}", $"pageSize={pageSize}" };
|
||||||
|
if (unreadOnly) query.Add("isRead=false");
|
||||||
|
if (!string.IsNullOrEmpty(appSlug)) query.Add($"type={appSlug}");
|
||||||
|
var queryString = "?" + string.Join("&", query);
|
||||||
|
|
||||||
|
var result = await _client.GetAsync<NotificationsResponse>(Url($"api/v1/notifications{queryString}"), ct);
|
||||||
|
return result?.Notifications ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get unread notification count.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> GetUnreadCountAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = await _client.GetAsync<UnreadCountResponse>(Url("api/v1/notifications/unread-count"), ct);
|
||||||
|
return result?.Count ?? result?.UnreadCount ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark a notification as read.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MarkAsReadAsync(Guid notificationId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _client.PutAsync<object, object>(Url($"api/v1/notifications/{notificationId}/read"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark all notifications as read.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MarkAllAsReadAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _client.PutAsync<object, object>(Url("api/v1/notifications/read-all"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledge a notification (marks as read).
|
||||||
|
/// </summary>
|
||||||
|
public async Task AcknowledgeAsync(Guid notificationId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await MarkAsReadAsync(notificationId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific notification by ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<NotificationDto?> GetNotificationAsync(Guid notificationId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<NotificationDto>(Url($"api/v1/notifications/{notificationId}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve a notification.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ResolveAsync(Guid notificationId, string? resolution = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _client.PostAsync<object, object>(Url($"api/v1/notifications/{notificationId}/resolve"), new { resolution }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get notification preferences.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<NotificationPreferences?> GetNotificationPreferencesAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<NotificationPreferences>(Url("api/v1/notifications/preferences"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update notification preferences.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateNotificationPreferencesAsync(NotificationPreferences prefs, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _client.PutAsync<NotificationPreferences, object>(Url("api/v1/notifications/preferences"), prefs, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Apps
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all apps for the tenant.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<AppSubscription>> GetMyAppsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<List<AppSubscription>>(Url("api/v1/apps"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific app by ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<AppSubscription?> GetAppAsync(Guid appId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<AppSubscription>(Url($"api/v1/apps/{appId}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get an app by slug.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<AppSubscription?> GetAppBySlugAsync(string slug, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<AppSubscription>(Url($"api/v1/apps/by-slug/{slug}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Members & On-Call
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all team members.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<TeamMember>> GetTeamMembersAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<List<TeamMember>>(Url("api/v1/members"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current on-call member.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TeamMember?> GetOnCallMemberAsync(Guid? teamId = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = teamId.HasValue ? $"?teamId={teamId}" : "";
|
||||||
|
return await _client.GetAsync<TeamMember>(Url($"api/v1/members/on-call{query}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get on-call status for the current user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<OnCallStatus?> GetOnCallStatusAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var member = await GetOnCallMemberAsync(ct: ct);
|
||||||
|
if (member == null) return new OnCallStatus { IsOnCall = false };
|
||||||
|
|
||||||
|
return new OnCallStatus
|
||||||
|
{
|
||||||
|
IsOnCall = true,
|
||||||
|
OnCallUserName = member.Name,
|
||||||
|
OnCallUserEmail = member.Email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set on-call status for a member.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TeamMember?> SetOnCallAsync(Guid memberId, bool isOnCall, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TeamMember>(Url($"api/v1/members/{memberId}/on-call"), new { isOnCall }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Take on-call shift for current user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task TakeOnCallAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// This would need the current user's member ID - placeholder for now
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End on-call shift for current user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task EndOnCallAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// This would need the current user's member ID - placeholder for now
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request coverage from another team member.
|
||||||
|
/// </summary>
|
||||||
|
public async Task RequestCoverageAsync(Guid memberId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// Placeholder - would send notification to member
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get today's on-call schedule.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<ScheduleSlot>> GetScheduleAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// Placeholder - schedule endpoint not implemented in backend
|
||||||
|
return await Task.FromResult(new List<ScheduleSlot>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region App Muting
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mute notifications from an app.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MuteAppAsync(string appSlug, int? minutes = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// Placeholder - mute endpoint not implemented in backend
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unmute notifications from an app.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UnmuteAppAsync(string appSlug, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// Placeholder - unmute endpoint not implemented in backend
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Teams
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all teams.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<TeamInfo>> GetTeamsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<List<TeamInfo>>(Url("api/v1/teams"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific team.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TeamInfo?> GetTeamAsync(Guid teamId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<TeamInfo>(Url($"api/v1/teams/{teamId}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get members of a specific team.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<TeamMember>> GetTeamMembersAsync(Guid teamId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<List<TeamMember>>(Url($"api/v1/teams/{teamId}/members"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationsResponse
|
||||||
|
{
|
||||||
|
public List<NotificationDto> Notifications { get; set; } = [];
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
public int UnreadCount { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TeamInfo
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int MemberCount { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
# IronServices.Client
|
||||||
|
|
||||||
|
Unified client for Iron Services APIs with session-based authentication. Access IronLicensing, IronNotify, and IronTelemetry from a single authenticated client.
|
||||||
|
|
||||||
|
[](https://www.nuget.org/packages/IronServices.Client)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package IronServices.Client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using IronServices.Client;
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
var client = new IronServicesClient(new IronServicesClientOptions
|
||||||
|
{
|
||||||
|
LicensingUrl = "https://ironlicensing.com",
|
||||||
|
NotifyUrl = "https://ironnotify.com",
|
||||||
|
TelemetryUrl = "https://irontelemetry.com"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
var loginResult = await client.LoginAsync("user@example.com", "password");
|
||||||
|
|
||||||
|
if (loginResult.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Welcome, {loginResult.DisplayName}!");
|
||||||
|
|
||||||
|
// Access all Iron Services APIs
|
||||||
|
var licenses = await client.Licensing.GetLicensesAsync();
|
||||||
|
var events = await client.Notify.GetEventsAsync();
|
||||||
|
var errors = await client.Telemetry.GetErrorsAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await client.LoginAsync("user@example.com", "password");
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User ID: {result.UserId}");
|
||||||
|
Console.WriteLine($"Role: {result.Role}");
|
||||||
|
Console.WriteLine($"Expires: {result.ExpiresAt}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Login failed: {result.Error}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await client.RegisterAsync(
|
||||||
|
email: "newuser@example.com",
|
||||||
|
password: "securepassword",
|
||||||
|
displayName: "John Doe"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Registered! API Key: {result.ApiKey}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Verification
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await client.VerifyEmailAsync("user@example.com", "123456");
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Email verified!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Password Reset
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Request reset email
|
||||||
|
await client.RequestPasswordResetAsync("user@example.com");
|
||||||
|
|
||||||
|
// Reset with token from email
|
||||||
|
var result = await client.ResetPasswordAsync(token, "newpassword");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Check authentication status
|
||||||
|
if (client.IsAuthenticated)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Session expires: {client.SessionExpiresAt}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current user profile
|
||||||
|
var profile = await client.GetProfileAsync();
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
await client.LogoutAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Storage
|
||||||
|
|
||||||
|
Sessions are persisted automatically. Provide a custom storage for platform-specific secure storage:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SecureTokenStorage : ITokenStorage
|
||||||
|
{
|
||||||
|
public async Task SaveTokenAsync(string token, DateTime? expiresAt)
|
||||||
|
{
|
||||||
|
await SecureStorage.SetAsync("session_token", token);
|
||||||
|
if (expiresAt.HasValue)
|
||||||
|
await SecureStorage.SetAsync("session_expires", expiresAt.Value.ToString("O"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string? token, DateTime? expiresAt)> GetTokenAsync()
|
||||||
|
{
|
||||||
|
var token = await SecureStorage.GetAsync("session_token");
|
||||||
|
var expiresStr = await SecureStorage.GetAsync("session_expires");
|
||||||
|
DateTime? expires = expiresStr != null ? DateTime.Parse(expiresStr) : null;
|
||||||
|
return (token, expires);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearTokenAsync()
|
||||||
|
{
|
||||||
|
SecureStorage.Remove("session_token");
|
||||||
|
SecureStorage.Remove("session_expires");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use custom storage
|
||||||
|
var client = new IronServicesClient(options, new SecureTokenStorage());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service APIs
|
||||||
|
|
||||||
|
### IronLicensing
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Get all licenses
|
||||||
|
var licenses = await client.Licensing.GetLicensesAsync();
|
||||||
|
|
||||||
|
// Get license details
|
||||||
|
var license = await client.Licensing.GetLicenseAsync(licenseId);
|
||||||
|
|
||||||
|
// Create license
|
||||||
|
var newLicense = await client.Licensing.CreateLicenseAsync(new CreateLicenseRequest
|
||||||
|
{
|
||||||
|
TierId = tierId,
|
||||||
|
CustomerEmail = "customer@example.com"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### IronNotify
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Get events
|
||||||
|
var events = await client.Notify.GetEventsAsync();
|
||||||
|
|
||||||
|
// Get apps
|
||||||
|
var apps = await client.Notify.GetAppsAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### IronTelemetry
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Get errors
|
||||||
|
var errors = await client.Telemetry.GetErrorsAsync();
|
||||||
|
|
||||||
|
// Get projects
|
||||||
|
var projects = await client.Telemetry.GetProjectsAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new IronServicesClient(new IronServicesClientOptions
|
||||||
|
{
|
||||||
|
// Service URLs (defaults shown)
|
||||||
|
LicensingUrl = "https://ironlicensing.com",
|
||||||
|
NotifyUrl = "https://ironnotify.com",
|
||||||
|
TelemetryUrl = "https://irontelemetry.com"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
For local development:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new IronServicesClient(new IronServicesClientOptions
|
||||||
|
{
|
||||||
|
LicensingUrl = "https://localhost:5001",
|
||||||
|
NotifyUrl = "https://localhost:5003",
|
||||||
|
TelemetryUrl = "https://localhost:5005"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic API Methods
|
||||||
|
|
||||||
|
Make authenticated requests to any endpoint:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// GET
|
||||||
|
var data = await client.GetAsync<MyResponse>("https://ironlicensing.com/api/v1/custom");
|
||||||
|
|
||||||
|
// POST
|
||||||
|
var result = await client.PostAsync<MyRequest, MyResponse>(
|
||||||
|
"https://ironlicensing.com/api/v1/custom",
|
||||||
|
new MyRequest { /* ... */ }
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT
|
||||||
|
await client.PutAsync<MyRequest, MyResponse>(url, data);
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
await client.DeleteAsync(url);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [IronLicensing](https://www.ironlicensing.com)
|
||||||
|
- [IronNotify](https://www.ironnotify.com)
|
||||||
|
- [IronTelemetry](https://www.irontelemetry.com)
|
||||||
|
- [Documentation](https://www.ironlicensing.com/docs)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
namespace IronServices.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API client for IronTelemetry operations.
|
||||||
|
/// Access via IronServicesClient.Telemetry
|
||||||
|
/// </summary>
|
||||||
|
public class TelemetryApi
|
||||||
|
{
|
||||||
|
private readonly IronServicesClient _client;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
internal TelemetryApi(IronServicesClient client, string baseUrl)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Url(string endpoint) => IronServicesClient.BuildUrl(_baseUrl, endpoint);
|
||||||
|
|
||||||
|
#region Dashboard & Stats
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get issue statistics (serves as dashboard stats).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TelemetryDashboardStats> GetDashboardStatsAsync(Guid? projectId = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = projectId.HasValue ? $"?projectId={projectId}" : "";
|
||||||
|
return await _client.GetAsync<TelemetryDashboardStats>(Url($"api/v1/issues/stats{query}"), ct)
|
||||||
|
?? new TelemetryDashboardStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Issues
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get issues with optional filtering.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<IssueDto>> GetIssuesAsync(Guid? projectId = null, string? status = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = new List<string> { $"page={page}", $"pageSize={pageSize}" };
|
||||||
|
if (projectId.HasValue) query.Add($"projectId={projectId}");
|
||||||
|
if (!string.IsNullOrEmpty(status)) query.Add($"status={status}");
|
||||||
|
var queryString = "?" + string.Join("&", query);
|
||||||
|
|
||||||
|
var result = await _client.GetAsync<IssuesListResponse>(Url($"api/v1/issues{queryString}"), ct);
|
||||||
|
return result?.Items ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific issue by ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IssueDto?> GetIssueAsync(Guid issueId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<IssueDto>(Url($"api/v1/issues/{issueId}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledge an issue.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IssueDto?> AcknowledgeIssueAsync(Guid issueId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, IssueDto>(Url($"api/v1/issues/{issueId}/acknowledge"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve an issue.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IssueDto?> ResolveIssueAsync(Guid issueId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, IssueDto>(Url($"api/v1/issues/{issueId}/resolve"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ignore an issue.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IssueDto?> IgnoreIssueAsync(Guid issueId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, IssueDto>(Url($"api/v1/issues/{issueId}/ignore"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update an issue's status and other properties.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IssueDto?> UpdateIssueAsync(Guid issueId, UpdateIssueRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PutAsync<UpdateIssueRequest, IssueDto>(Url($"api/v1/issues/{issueId}"), request, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Signals
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get signals (telemetry notifications).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<SignalDto>> GetSignalsAsync(Guid? projectId = null, int page = 1, int pageSize = 50, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = new List<string> { $"page={page}", $"pageSize={pageSize}" };
|
||||||
|
if (projectId.HasValue) query.Add($"projectId={projectId}");
|
||||||
|
var queryString = "?" + string.Join("&", query);
|
||||||
|
|
||||||
|
var result = await _client.GetAsync<SignalListResponse>(Url($"api/v1/signals{queryString}"), ct);
|
||||||
|
return result?.Signals ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get signal statistics.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SignalStatsResponse?> GetSignalStatsAsync(Guid? projectId = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = projectId.HasValue ? $"?projectId={projectId}" : "";
|
||||||
|
return await _client.GetAsync<SignalStatsResponse>(Url($"api/v1/signals/stats{query}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dismiss a signal.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SignalDto?> DismissSignalAsync(Guid signalId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, SignalDto>(Url($"api/v1/signals/{signalId}/dismiss"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Projects
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all projects.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<TelemetryProjectDto>> GetProjectsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<List<TelemetryProjectDto>>(Url("api/v1/projects"), ct) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a specific project by ID.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TelemetryProjectDto?> GetProjectAsync(Guid projectId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.GetAsync<TelemetryProjectDto>(Url($"api/v1/projects/{projectId}"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new telemetry project.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TelemetryProjectDto?> CreateProjectAsync(string name, string platform = "DotNet", CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TelemetryProjectDto>(Url("api/v1/projects"), new { name, platform }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update a project.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TelemetryProjectDto?> UpdateProjectAsync(Guid projectId, UpdateProjectRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PutAsync<UpdateProjectRequest, TelemetryProjectDto>(Url($"api/v1/projects/{projectId}"), request, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regenerate project DSN.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TelemetryProjectDto?> RegenerateDsnAsync(Guid projectId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _client.PostAsync<object, TelemetryProjectDto>(Url($"api/v1/projects/{projectId}/regenerate-dsn"), new { }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssuesListResponse
|
||||||
|
{
|
||||||
|
public List<IssueDto> Items { get; set; } = [];
|
||||||
|
public int Total { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SignalListResponse
|
||||||
|
{
|
||||||
|
public List<SignalDto> Signals { get; set; } = [];
|
||||||
|
public int Total { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SignalStatsResponse
|
||||||
|
{
|
||||||
|
public int TotalSignals { get; set; }
|
||||||
|
public int SignalsLast24Hours { get; set; }
|
||||||
|
public int SignalsLast7Days { get; set; }
|
||||||
|
public int SentToNotify { get; set; }
|
||||||
|
public int PendingSignals { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateIssueRequest
|
||||||
|
{
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Severity { get; set; }
|
||||||
|
public Guid? AssignedToUserId { get; set; }
|
||||||
|
public bool? NotifyOnRegression { get; set; }
|
||||||
|
public bool? NotifyOnNewOccurrence { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateProjectRequest
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Platform { get; set; }
|
||||||
|
public bool? IsActive { get; set; }
|
||||||
|
public bool? StackTraceGroupingEnabled { get; set; }
|
||||||
|
public bool? JourneyTrackingEnabled { get; set; }
|
||||||
|
public bool? SourceMapEnabled { get; set; }
|
||||||
|
public int? SampleRate { get; set; }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue