Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions examples/Mailtrap.Example.ApiTokens/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@
IList<ApiToken> apiTokens = await apiTokensResource.GetAll();
logger.LogInformation("Found {Count} API token(s).", apiTokens.Count);

// Create a new API token scoped to a specific inbox with Viewer access
// Create a new API token scoped to a specific inbox with Viewer access.
// ExpiresAt is optional: omit it for the server default expiration,
// use ApiTokenExpiration.Never for a token that never expires,
// or ApiTokenExpiration.At(...) for an explicit expiration date.
var createRequest = new CreateApiTokenRequest
{
Name = "Demo Viewer Token"
Name = "Demo Viewer Token",
ExpiresAt = ApiTokenExpiration.At(DateTimeOffset.UtcNow.AddMonths(6))
};
createRequest.Resources.Add(new ApiTokenAccessRequest(ResourceType.Inbox, inboxId, AccessLevel.Viewer));

Expand All @@ -62,15 +66,37 @@
tokenDetails.Name,
tokenDetails.CreatedBy);

// Reset the API token - expires the current token and returns a new one with the same permissions
// Reset the API token - retires the current token and creates a new one with the same permissions.
// The response carries the id of the NEW token, so the resource must be re-created from it
// before performing further operations.
// The parameterless overload keeps the server default expiration for the new token.
ApiTokenResetResponse resetResponse = await apiTokenResource.Reset();
logger.LogInformation(
"Reset API Token: Id={Id}, NewLast4={Last4}",
resetResponse.Id,
resetResponse.Last4Digits);
logger.LogInformation("New token value (store securely, returned only once): {Token}", resetResponse.Token);

// Delete the API token
// Re-point the resource at the new token
apiTokenResource = accountResource.ApiToken(resetResponse.Id);

// Reset the new token, this time requesting a replacement that never expires
ApiTokenResetResponse neverExpiringToken = await apiTokenResource.Reset(new ResetApiTokenRequest
{
ExpiresAt = ApiTokenExpiration.Never
});
logger.LogInformation(
"Reset API Token: Id={Id}, NewLast4={Last4}, ExpiresAt={ExpiresAt}",
neverExpiringToken.Id,
neverExpiringToken.Last4Digits,
neverExpiringToken.ExpiresAt);

// Re-point the resource at the never-expiring token
apiTokenResource = accountResource.ApiToken(neverExpiringToken.Id);

// Delete the active (never-expiring) API token.
// The retired tokens (the original and the first reset one) stop working after the server's
// short grace period and need no cleanup.
// The API token resource becomes invalid after deletion and should not be used anymore
await apiTokenResource.Delete();
logger.LogInformation("API Token Deleted.");
Expand Down
19 changes: 17 additions & 2 deletions src/Mailtrap.Abstractions/AccountAccesses/Models/Specifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ public sealed record Specifier
[JsonPropertyOrder(5)]
public string? Token { get; set; }

/// <summary>
/// Gets the token value with all but the last characters masked.
/// </summary>
///
/// <value>
/// Token value with all but the last characters masked.
/// </value>
///
/// <remarks>
/// Applicable to 'ApiToken' specifier type only.
/// </remarks>
[JsonPropertyName("masked_token")]
[JsonPropertyOrder(6)]
public string? MaskedToken { get; set; }
Comment thread
oshchyhol marked this conversation as resolved.

/// <summary>
/// Gets the token expiration date and time.
/// </summary>
Expand All @@ -91,7 +106,7 @@ public sealed record Specifier
/// Applicable to 'ApiToken' specifier type only.
/// </remarks>
[JsonPropertyName("expires_at")]
[JsonPropertyOrder(6)]
[JsonPropertyOrder(7)]
public DateTimeOffset? ExpiresAt { get; set; }

/// <summary>
Expand All @@ -107,6 +122,6 @@ public sealed record Specifier
/// Applicable to 'User' specifier type only.
/// </remarks>
[JsonPropertyName("two_factor_authentication_enabled")]
[JsonPropertyOrder(7)]
[JsonPropertyOrder(8)]
public bool? TwoFactorAuthEnabled { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Mailtrap.ApiTokens.Converters;


/// <summary>
/// Custom JSON converter to be used for <see cref="ApiTokenExpiration"/>.<br/>
/// Writes JSON <see langword="null"/> for <see cref="ApiTokenExpiration.Never"/>
/// and the ISO 8601 date-time string otherwise.
/// </summary>
internal sealed class ApiTokenExpirationJsonConverter : JsonConverter<ApiTokenExpiration>
{
public override bool HandleNull => true;


public override ApiTokenExpiration? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => ApiTokenExpiration.Never,
JsonTokenType.String => reader.TryGetDateTimeOffset(out var date)
? ApiTokenExpiration.At(date)
: throw new JsonException($"Cannot convert value '{reader.GetString()}' to {nameof(ApiTokenExpiration)}."),
_ => throw new JsonException($"Unexpected JSON token {reader.TokenType} for {nameof(ApiTokenExpiration)}.")
};
}

public override void Write(Utf8JsonWriter writer, ApiTokenExpiration value, JsonSerializerOptions options)
{
Ensure.NotNull(writer, nameof(writer));

// HandleNull is required to map JSON null to Never on read, and it also routes
// null references here on write, so this has to handle them rather than throw.
if (value is null || !value.Value.HasValue)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.Value.Value);
}
}
}
23 changes: 23 additions & 0 deletions src/Mailtrap.Abstractions/ApiTokens/IApiTokenResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,27 @@ public interface IApiTokenResource : IRestResource
/// the new token value — store it securely; it is only returned once.
/// </remarks>
public Task<ApiTokenResetResponse> Reset(CancellationToken cancellationToken = default);

/// <summary>
/// Reset the API token represented by this resource, specifying an expiration for the new token.
/// </summary>
///
/// <param name="request">
/// Request with the optional expiration for the new token.
/// </param>
///
/// <param name="cancellationToken">
/// Token to control operation cancellation.
/// </param>
///
/// <returns>
/// New API token details, including the full token value.
/// </returns>
///
/// <remarks>
/// Expires the requested token and creates a new token with the same permissions.
/// The old token stops working after a short grace period. The response includes
/// the new token value – store it securely; it is only returned once.
/// </remarks>
public Task<ApiTokenResetResponse> Reset(ResetApiTokenRequest request, CancellationToken cancellationToken = default);
Comment thread
oshchyhol marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace Mailtrap.ApiTokens.Models;


/// <summary>
/// Represents an explicit API token expiration, serialized as an ISO 8601 date-time.<br/>
/// Leave the request property unset to omit the value and get the server default
/// (a 1-year default is being rolled out).<br/>
/// Use <see cref="Never"/> for a token that never expires.<br/>
/// Past or more-than-5-years-ahead values are rejected with 422.
/// </summary>
[JsonConverter(typeof(ApiTokenExpirationJsonConverter))]
public sealed record ApiTokenExpiration
{
/// <summary>
/// Gets the expiration for a token that never expires.
/// </summary>
///
/// <value>
/// Expiration for a token that never expires. Serialized as JSON <see langword="null"/>.
/// </value>
public static ApiTokenExpiration Never { get; } = new((DateTimeOffset?)null);


/// <summary>
/// Creates an expiration at the provided date and time.
/// </summary>
///
/// <param name="value">
/// Date and time when the token expires.
/// </param>
///
/// <returns>
/// Expiration at the provided date and time. Serialized as an ISO 8601 date-time string.
/// </returns>
public static ApiTokenExpiration At(DateTimeOffset value) => new(value);


/// <summary>
/// Gets the expiration date and time,
/// or <see langword="null"/> for a token that never expires.
/// </summary>
///
/// <value>
/// Expiration date and time, or <see langword="null"/> for a token that never expires.
/// </value>
internal DateTimeOffset? Value { get; }


private ApiTokenExpiration(DateTimeOffset? value)
{
Value = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ public sealed record CreateApiTokenRequest : IValidatable
[JsonRequired]
public string Name { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the optional token expiration.
/// </summary>
///
/// <value>
/// Optional token expiration as an ISO 8601 date-time.<br/>
/// Omit for the server default (a 1-year default is being rolled out).<br/>
/// Use <see cref="ApiTokenExpiration.Never"/> for a token that never expires.<br/>
/// Past or more-than-5-years-ahead values are rejected with 422.
/// </value>
[JsonPropertyName("expires_at")]
[JsonPropertyOrder(2)]
public ApiTokenExpiration? ExpiresAt { get; set; }

/// <summary>
/// Gets the resource accesses to grant to the API token.
/// </summary>
Expand All @@ -26,7 +40,7 @@ public sealed record CreateApiTokenRequest : IValidatable
/// Collection of resource accesses.
/// </value>
[JsonPropertyName("resources")]
[JsonPropertyOrder(2)]
[JsonPropertyOrder(3)]
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
public IList<ApiTokenAccessRequest> Resources { get; } = [];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Mailtrap.ApiTokens.Requests;


/// <summary>
/// Request to reset an API token.
/// </summary>
public sealed record ResetApiTokenRequest
{
/// <summary>
/// Gets or sets the optional expiration for the new token.
/// </summary>
///
/// <value>
/// Optional token expiration as an ISO 8601 date-time.<br/>
/// Omit for the server default (a 1-year default is being rolled out).<br/>
/// Use <see cref="ApiTokenExpiration.Never"/> for a token that never expires.<br/>
/// Past or more-than-5-years-ahead values are rejected with 422.
/// </value>
[JsonPropertyName("expires_at")]
[JsonPropertyOrder(1)]
public ApiTokenExpiration? ExpiresAt { get; set; }
}
1 change: 1 addition & 0 deletions src/Mailtrap.Abstractions/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
global using Mailtrap.Accounts;
global using Mailtrap.Accounts.Models;
global using Mailtrap.ApiTokens;
global using Mailtrap.ApiTokens.Converters;
global using Mailtrap.ApiTokens.Models;
global using Mailtrap.ApiTokens.Requests;
global using Mailtrap.ApiTokens.Responses;
Expand Down
14 changes: 14 additions & 0 deletions src/Mailtrap/ApiTokens/ApiTokenResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,18 @@ public async Task<ApiTokenResetResponse> Reset(CancellationToken cancellationTok

return result;
}

public async Task<ApiTokenResetResponse> Reset(ResetApiTokenRequest request, CancellationToken cancellationToken = default)
{
Ensure.NotNull(request, nameof(request));

var uri = ResourceUri.Append(ResetSegment);

var result = await RestResourceCommandFactory
.CreatePost<ResetApiTokenRequest, ApiTokenResetResponse>(uri, request)
.Execute(cancellationToken)
.ConfigureAwait(false);

return result;
}
}
Loading