aboutsummaryrefslogtreecommitdiffstats
path: root/code/api/src/Services
diff options
context:
space:
mode:
Diffstat (limited to 'code/api/src/Services')
-rw-r--r--code/api/src/Services/EmailValidationService.cs67
-rw-r--r--code/api/src/Services/MailService.cs132
-rw-r--r--code/api/src/Services/PasswordResetService.cs101
-rw-r--r--code/api/src/Services/TenantService.cs61
-rw-r--r--code/api/src/Services/UserService.cs54
-rw-r--r--code/api/src/Services/VaultService.cs237
6 files changed, 652 insertions, 0 deletions
diff --git a/code/api/src/Services/EmailValidationService.cs b/code/api/src/Services/EmailValidationService.cs
new file mode 100644
index 0000000..5552c4f
--- /dev/null
+++ b/code/api/src/Services/EmailValidationService.cs
@@ -0,0 +1,67 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class EmailValidationService
+{
+ private readonly IStringLocalizer<SharedResources> _localizer;
+ private readonly MainAppDatabase _database;
+ private readonly MailService _mailService;
+ private readonly ILogger<EmailValidationService> _logger;
+ private readonly string EmailValidationUrl;
+
+ public EmailValidationService(IStringLocalizer<SharedResources> localizer, MainAppDatabase database, MailService mailService, ILogger<EmailValidationService> logger, VaultService vaultService) {
+ _localizer = localizer;
+ _database = database;
+ _mailService = mailService;
+ _logger = logger;
+ var configuration = vaultService.GetCurrentAppConfiguration();
+ EmailValidationUrl = configuration.CANONICAL_BACKEND_URL + "/_/validate";
+ }
+
+ public bool FulfillEmailValidationRequest(Guid id, Guid userId) {
+ var item = _database.ValidationEmails.FirstOrDefault(c => c.Id == id);
+ if (item == default) {
+ _logger.LogDebug("Did not find email validation request with id: {requestId}", id);
+ return false;
+ }
+
+ if (item.UserId != userId) {
+ _logger.LogInformation("An unknown user tried to validate the email validation request {requestId}", id);
+ return false;
+ }
+
+ var user = _database.Users.FirstOrDefault(c => c.Id == item.UserId);
+ if (user == default) {
+ _database.ValidationEmails.Remove(item);
+ _database.SaveChanges();
+ _logger.LogInformation("Deleting request {requestId} because user does not exist anymore", id);
+ return false;
+ }
+
+ user.EmailLastValidated = AppDateTime.UtcNow;
+ _database.ValidationEmails.Remove(item);
+ _database.Users.Update(user);
+ _database.SaveChanges();
+ _logger.LogInformation("Successfully validated the email for user {userId}", user.Id);
+ return true;
+ }
+
+ public async Task SendValidationEmailAsync(User user) {
+ var queueItem = new ValidationEmail() {
+ UserId = user.Id,
+ Id = Guid.NewGuid()
+ };
+ var email = new MailService.PostmarkEmail() {
+ To = user.Username,
+ Subject = _localizer["Greatoffice Email Validation"],
+ TextBody = _localizer["""
+Hello {0},
+
+Validate your email address by opening this link in a browser {1}
+""", user.DisplayName(true), EmailValidationUrl + "?id=" + queueItem.Id]
+ };
+ queueItem.EmailSentAt = AppDateTime.UtcNow;
+ _database.ValidationEmails.Add(queueItem);
+ await _database.SaveChangesAsync();
+ Task.Run(async () => await _mailService.SendMailAsync(email));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs
new file mode 100644
index 0000000..a6f7db4
--- /dev/null
+++ b/code/api/src/Services/MailService.cs
@@ -0,0 +1,132 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class MailService
+{
+ private readonly ILogger<MailService> _logger;
+ private static string _fromEmail;
+ private readonly HttpClient _httpClient;
+
+ public MailService(VaultService vaultService, ILogger<MailService> logger, HttpClient httpClient) {
+ var configuration = vaultService.GetCurrentAppConfiguration();
+ _fromEmail = configuration.EMAIL_FROM_ADDRESS;
+ _logger = logger;
+ httpClient.DefaultRequestHeaders.Add("X-Postmark-Server-Token", configuration.POSTMARK_TOKEN);
+ _httpClient = httpClient;
+ }
+
+ /// <summary>
+ /// Send an email.
+ /// </summary>
+ /// <param name="message"></param>
+ /// <exception cref="ArgumentException"></exception>
+ public async Task SendMailAsync(PostmarkEmail message) {
+ try {
+ if (message.MessageStream.IsNullOrWhiteSpace()) {
+ message.MessageStream = "outbound";
+ }
+
+ if (message.From.IsNullOrWhiteSpace() && _fromEmail.HasValue()) {
+ message.From = _fromEmail;
+ } else {
+ throw new ApplicationException("Not one from-email is available");
+ }
+
+ if (message.To.IsNullOrWhiteSpace()) {
+ throw new ArgumentNullException(nameof(message.To), "A recipient should be specified.");
+ }
+
+ if (!message.To.IsValidEmailAddress()) {
+ throw new ArgumentException(nameof(message.To), "To is not a valid email address");
+ }
+
+ if (message.HtmlBody.IsNullOrWhiteSpace() && message.TextBody.IsNullOrWhiteSpace()) {
+ throw new ArgumentNullException(nameof(message), "Both HtmlBody and TextBody is empty, nothing to send");
+ }
+#if DEBUG
+ _logger.LogInformation("Sending email: {0}", JsonSerializer.Serialize(message, new JsonSerializerOptions() {WriteIndented = true}));
+#endif
+ var response = await _httpClient.PostAsJsonAsync("https://api.postmarkapp.com/email", message);
+ _logger.LogInformation("Postmark returned with message: {0}", (await response.Content.ReadFromJsonAsync<PostmarkSendResponse>()).Message);
+ } catch (Exception e) {
+ _logger.LogError(e, "A silent exception occured while trying to send an email");
+ }
+ }
+
+ private class PostmarkSendResponse
+ {
+ /// <summary>
+ /// The Message ID returned from Postmark.
+ /// </summary>
+ public Guid MessageID { get; set; }
+
+ /// <summary>
+ /// The message from the API.
+ /// In the event of an error, this message may contain helpful text.
+ /// </summary>
+ public string Message { get; set; }
+
+ /// <summary>
+ /// The time the request was received by Postmark.
+ /// </summary>
+ public DateTime SubmittedAt { get; set; }
+
+ /// <summary>
+ /// The recipient of the submitted request.
+ /// </summary>
+ public string To { get; set; }
+
+ /// <summary>
+ /// The error code returned from Postmark.
+ /// This does not map to HTTP status codes.
+ /// </summary>
+ public int ErrorCode { get; set; }
+ }
+
+ public class PostmarkEmail
+ {
+ /// <summary>
+ /// The message subject line.
+ /// </summary>
+ public string Subject { get; set; }
+
+ /// <summary>
+ /// The message body, if the message contains
+ /// </summary>
+ public string HtmlBody { get; set; }
+
+ /// <summary>
+ /// The message body, if the message is plain text.
+ /// </summary>
+ public string TextBody { get; set; }
+
+ /// <summary>
+ /// The message stream used to send this message.
+ /// </summary>
+ public string MessageStream { get; set; }
+
+ /// <summary>
+ /// The sender's email address.
+ /// </summary>
+ public string From { get; set; }
+
+ /// <summary>
+ /// Any recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string To { get; set; }
+
+ /// <summary>
+ /// Any CC recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string Cc { get; set; }
+
+ /// <summary>
+ /// Any BCC recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string Bcc { get; set; }
+
+ /// <summary>
+ /// The email address to reply to. This is optional.
+ /// </summary>
+ public string ReplyTo { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs
new file mode 100644
index 0000000..4c00b81
--- /dev/null
+++ b/code/api/src/Services/PasswordResetService.cs
@@ -0,0 +1,101 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class PasswordResetService
+{
+ private readonly MainAppDatabase _database;
+ private readonly MailService _mailService;
+ private readonly AppConfiguration _configuration;
+ private readonly ILogger<PasswordResetService> _logger;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public PasswordResetService(
+ MainAppDatabase database,
+ VaultService vaultService,
+ ILogger<PasswordResetService> logger,
+ MailService mailService, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _configuration = vaultService.GetCurrentAppConfiguration();
+ _logger = logger;
+ _mailService = mailService;
+ _localizer = localizer;
+ }
+
+ public async Task<PasswordResetRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) {
+ var request = await _database.PasswordResetRequests
+ .Include(c => c.User)
+ .SingleOrDefaultAsync(c => c.Id == id, cancellationToken);
+ if (request == default) {
+ return default;
+ }
+
+ _logger.LogInformation($"Found password reset request for user: {request.User.Username}, expires at {request.ExpirationDate} (in {request.ExpirationDate.Subtract(AppDateTime.UtcNow).Minutes} minutes).");
+ return request;
+ }
+
+ public async Task<FulfillPasswordResetRequestResult> FulfillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) {
+ var request = await GetRequestAsync(id, cancellationToken);
+ if (request == default) return FulfillPasswordResetRequestResult.REQUEST_NOT_FOUND;
+ var user = _database.Users.FirstOrDefault(c => c.Id == request.User.Id);
+ if (user == default) return FulfillPasswordResetRequestResult.USER_NOT_FOUND;
+ user.HashAndSetPassword(newPassword);
+ _database.Users.Update(user);
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation($"Fullfilled password reset request for user: {request.User.Username}");
+ await DeleteRequestsForUserAsync(user.Id, cancellationToken);
+ return FulfillPasswordResetRequestResult.FULFILLED;
+ }
+
+ public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) {
+ await DeleteRequestsForUserAsync(user.Id, cancellationToken);
+ var request = new PasswordResetRequest(user);
+ _database.PasswordResetRequests.Add(request);
+ await _database.SaveChangesAsync(cancellationToken);
+ var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id);
+ var message = new MailService.PostmarkEmail() {
+ To = request.User.Username,
+ Subject = _localizer["Reset password - Greatoffice"],
+ TextBody = _localizer["""
+Hi {0},
+
+Go to the following link to set a new password.
+
+{1}/reset-password/{2}
+
+The link expires at {3}.
+If you did not request a password reset, no action is required.
+""", user.DisplayName(true), _configuration.CANONICAL_FRONTEND_URL, request.Id, zonedExpirationDate.ToString("yyyy-MM-dd hh:mm")]
+ };
+
+#pragma warning disable 4014
+ Task.Run(() => {
+#pragma warning restore 4014
+ _mailService.SendMailAsync(message);
+ _logger.LogInformation($"Added password reset request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(AppDateTime.UtcNow)}.");
+ },
+ cancellationToken);
+ }
+
+ public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) {
+ var requestsToRemove = _database.PasswordResetRequests.Where(c => c.UserId == userId).ToList();
+ if (!requestsToRemove.Any()) return;
+ _database.PasswordResetRequests.RemoveRange(requestsToRemove);
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation($"Deleted {requestsToRemove.Count} password reset requests for user: {userId}.");
+ }
+
+ public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) {
+ var deleteCount = 0;
+ foreach (var request in _database.PasswordResetRequests.Where(c => c.IsExpired)) {
+ if (!request.IsExpired) {
+ continue;
+ }
+
+ _database.PasswordResetRequests.Remove(request);
+ deleteCount++;
+ _logger.LogInformation($"Marking password reset request with id: {request.Id} for deletion, expiration date was {request.ExpirationDate}.");
+ }
+
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation($"Deleted {deleteCount} stale password reset requests.");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/TenantService.cs b/code/api/src/Services/TenantService.cs
new file mode 100644
index 0000000..0de6f53
--- /dev/null
+++ b/code/api/src/Services/TenantService.cs
@@ -0,0 +1,61 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class TenantService
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<TenantService> _logger;
+
+ public TenantService(MainAppDatabase database, ILogger<TenantService> logger) {
+ _database = database;
+ _logger = logger;
+ }
+
+ public Tenant CreateTenant(string name, Guid masterUserId, string email) {
+ var tenant = new Tenant() {
+ Name = name,
+ Slug = Slug.Generate(true, name),
+ MasterUserId = masterUserId,
+ ContactEmail = email
+ };
+
+ var masterUserExists = _database.Users.Any(c => c.Id == tenant.MasterUserId);
+ if (!masterUserExists) {
+ _logger.LogError("Tried to create a new tenant with a non-existent master user: {masterUserId}", masterUserId);
+ return default;
+ }
+
+ if (!email.IsValidEmailAddress()) {
+ _logger.LogError("Tried to create a new tenant with an invalid email {contactEmail}", email);
+ return default;
+ }
+
+ _database.Tenants.Add(tenant);
+ _database.SaveChanges();
+ return tenant;
+ }
+
+ public void AddUserToTenant(Guid userId, Guid tenantId) {
+ var tenant = _database.Tenants.FirstOrDefault(c => c.Id == tenantId);
+ if (tenant == default) {
+ _logger.LogError("Tried adding user {userId} to tenant {tenantId} but the tenant was not found", userId, tenantId);
+ return;
+ }
+
+ var user = _database.Users.FirstOrDefault(c => c.Id == userId);
+ if (user == default) {
+ _logger.LogError("Tried adding user {userId} to tenant {tenantId} but the user was not found", userId, tenantId);
+ return;
+ }
+
+ if (tenant.Users.Any(c => c.Id == user.Id)) {
+ _logger.LogDebug("User {userId} is already a part of tenant {tenantId}", userId, tenantId);
+ return;
+ }
+
+ tenant.Users.Add(user);
+ tenant.SetModified();
+ _database.Tenants.Update(tenant);
+ _database.SaveChanges();
+ _logger.LogInformation("Added user {userId} to tenant {tenantId}", userId, tenantId);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/UserService.cs b/code/api/src/Services/UserService.cs
new file mode 100644
index 0000000..4df8ded
--- /dev/null
+++ b/code/api/src/Services/UserService.cs
@@ -0,0 +1,54 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class UserService
+{
+ private readonly PasswordResetService _passwordResetService;
+ private readonly ILogger<UserService> _logger;
+ private readonly MainAppDatabase _database;
+
+ public UserService(PasswordResetService passwordResetService, ILogger<UserService> logger, MainAppDatabase database) {
+ _passwordResetService = passwordResetService;
+ _logger = logger;
+ _database = database;
+ }
+
+ public async Task LogInUserAsync(HttpContext httpContext, User user, bool persist = false, CancellationToken cancellationToken = default) {
+ var identity = new ClaimsIdentity(user.DefaultClaims(), CookieAuthenticationDefaults.AuthenticationScheme);
+ var principal = new ClaimsPrincipal(identity);
+ var authenticationProperties = new AuthenticationProperties {
+ AllowRefresh = true,
+ IssuedUtc = DateTimeOffset.UtcNow,
+ };
+
+ if (persist) {
+ authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(6);
+ authenticationProperties.IsPersistent = true;
+ }
+
+ await httpContext.SignInAsync(principal, authenticationProperties);
+ await _passwordResetService.DeleteRequestsForUserAsync(user.Id, cancellationToken);
+ _logger.LogInformation("Logged in user {userId}", user.Id);
+ }
+
+ public async Task LogOutUser(HttpContext httpContext, CancellationToken cancellationToken = default) {
+ await httpContext.SignOutAsync();
+ _logger.LogInformation("Logged out user {userId}", httpContext.User.FindFirst(AppClaims.USER_ID));
+ }
+
+ public async Task MarkUserAsDeleted(Guid userId, Guid actorId) {
+ var user = _database.Users.FirstOrDefault(c => c.Id == userId);
+ if (user == default) {
+ _logger.LogInformation("Tried to delete unknown user {userId}", userId);
+ return;
+ }
+
+ if (user.Username is "demo@greatoffice.app" or "tester@greatoffice.app") {
+ _logger.LogInformation("Not deleting user {userId}, because it's username is {username}", user.Id, user.Username);
+ return;
+ }
+
+ await _passwordResetService.DeleteRequestsForUserAsync(user.Id);
+ user.SetDeleted(actorId);
+ await _database.SaveChangesAsync();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs
new file mode 100644
index 0000000..cd2eecf
--- /dev/null
+++ b/code/api/src/Services/VaultService.cs
@@ -0,0 +1,237 @@
+using Microsoft.Extensions.Caching.Memory;
+
+namespace IOL.GreatOffice.Api.Services;
+
+public class VaultService
+{
+ private readonly HttpClient _client;
+ private readonly IMemoryCache _cache;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger<VaultService> _logger;
+ private int CACHE_TTL { get; set; }
+
+ public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) {
+ var token = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_TOKEN);
+ var vaultUrl = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_URL);
+ CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
+ if (token.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_TOKEN is empty");
+ if (vaultUrl.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_URL is empty");
+ client.DefaultRequestHeaders.Add("X-Vault-Token", token);
+ client.BaseAddress = new Uri(vaultUrl);
+ _client = client;
+ _cache = cache;
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ public async Task<T> GetSecretJsonAsync<T>(string path) {
+ var result = await _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY,
+ async cacheEntry => {
+ cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL);
+ var getSecretResponse = await _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path);
+ if (getSecretResponse == null) {
+ return default;
+ }
+
+ Log.Debug("Setting new vault cache, " + new {
+ PATH = path,
+ CACHE_TTL,
+ Data = JsonSerializer.Serialize(getSecretResponse.Data.Data)
+ });
+ return getSecretResponse.Data.Data ?? default;
+ });
+ return result;
+ }
+
+ public Task<T> RefreshAsync<T>(string path) {
+ _cache.Remove(AppConstants.VAULT_CACHE_KEY);
+ CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
+ return GetSecretJsonAsync<T>(path);
+ }
+
+ public async Task<RenewTokenResponse> RenewTokenAsync() {
+ var response = await _client.PostAsJsonAsync("v1/auth/token/renew-self", new {increment = "2h"});
+ if (response.IsSuccessStatusCode) {
+ return await response.Content.ReadFromJsonAsync<RenewTokenResponse>();
+ }
+
+ return default;
+ }
+
+ public async Task<TokenLookupResponse> LookupTokenAsync() {
+ var response = await _client.GetAsync("v1/auth/token/lookup-self");
+ if (response.IsSuccessStatusCode) {
+ return await response.Content.ReadFromJsonAsync<TokenLookupResponse>();
+ }
+
+ return default;
+ }
+
+ public AppConfiguration GetCurrentAppConfiguration() {
+#if DEBUG
+ var isInFlightMode = true;
+ if (isInFlightMode) {
+ return new AppConfiguration() {
+ EMAIL_FROM_ADDRESS = "heydev@greatoffice.life",
+ DB_HOST = "localhost",
+ DB_PORT = "5432",
+ DB_NAME = "greatoffice_ivar_dev",
+ DB_PASSWORD = "ivar123",
+ DB_USER = "postgres",
+ CANONICAL_FRONTEND_URL = "http://localhost:5173",
+ CANONICAL_BACKEND_URL = "http://localhost:5000",
+ POSTMARK_TOKEN = "b530c311-45c7-43e5-aa48-f2c992886e51",
+ QUARTZ_DB_HOST = "localhost",
+ QUARTZ_DB_PORT = "5432",
+ QUARTZ_DB_NAME = "greatoffice_quartz_ivar_dev",
+ QUARTZ_DB_PASSWORD = "ivar123",
+ QUARTZ_DB_USER = "postgres",
+ APP_CERT = "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"
+ };
+ }
+#endif
+
+ return GetSecretJsonAsync<AppConfiguration>(
+ _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET)
+ ).Result;
+ }
+
+ public async Task RefreshCurrentAppConfigurationAsync() {
+ await RefreshAsync<AppConfiguration>(_configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET));
+ }
+
+ public class RenewTokenResponse
+ {
+ public Guid RequestId { get; set; }
+ public string LeaseId { get; set; }
+ public bool Renewable { get; set; }
+ public long LeaseDuration { get; set; }
+ public object Data { get; set; }
+ public object WrapInfo { get; set; }
+ public List<string> Warnings { get; set; }
+ public Auth Auth { get; set; }
+ }
+
+ public class Auth
+ {
+ public string ClientToken { get; set; }
+ public string Accessor { get; set; }
+ public List<string> Policies { get; set; }
+ public List<string> TokenPolicies { get; set; }
+ public object Metadata { get; set; }
+ public long LeaseDuration { get; set; }
+ public bool Renewable { get; set; }
+ public string EntityId { get; set; }
+ public string TokenType { get; set; }
+ public bool Orphan { get; set; }
+ public object MfaRequirement { get; set; }
+ public long NumUses { get; set; }
+ }
+
+ public class GetSecretResponse<T>
+ {
+ public VaultSecret<T> Data { get; set; }
+ }
+
+ public class VaultSecret<T>
+ {
+ public T Data { get; set; }
+ public VaultSecretMetadata Metadata { get; set; }
+ }
+
+ public class VaultSecretMetadata
+ {
+ public DateTimeOffset CreatedTime { get; set; }
+ public object CustomMetadata { get; set; }
+ public string DeletionTime { get; set; }
+ public bool Destroyed { get; set; }
+ public long Version { get; set; }
+ }
+
+ public class TokenLookupResponse
+ {
+ [JsonPropertyName("request_id")]
+ public Guid RequestId { get; set; }
+
+ [JsonPropertyName("lease_id")]
+ public string LeaseId { get; set; }
+
+ [JsonPropertyName("renewable")]
+ public bool Renewable { get; set; }
+
+ [JsonPropertyName("lease_duration")]
+ public long LeaseDuration { get; set; }
+
+ [JsonPropertyName("data")]
+ public TokenLookupResponseData Data { get; set; }
+
+ [JsonPropertyName("wrap_info")]
+ public object WrapInfo { get; set; }
+
+ [JsonPropertyName("warnings")]
+ public object Warnings { get; set; }
+
+ [JsonPropertyName("auth")]
+ public object Auth { get; set; }
+ }
+
+ public class TokenLookupResponseData
+ {
+ [JsonPropertyName("accessor")]
+ public string Accessor { get; set; }
+
+ [JsonPropertyName("creation_time")]
+ public long CreationTime { get; set; }
+
+ [JsonPropertyName("creation_ttl")]
+ public long CreationTtl { get; set; }
+
+ [JsonPropertyName("display_name")]
+ public string DisplayName { get; set; }
+
+ [JsonPropertyName("entity_id")]
+ public string EntityId { get; set; }
+
+ [JsonPropertyName("expire_time")]
+ public DateTimeOffset ExpireTime { get; set; }
+
+ [JsonPropertyName("explicit_max_ttl")]
+ public long ExplicitMaxTtl { get; set; }
+
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ [JsonPropertyName("issue_time")]
+ public DateTimeOffset IssueTime { get; set; }
+
+ [JsonPropertyName("last_renewal")]
+ public DateTimeOffset LastRenewal { get; set; }
+
+ [JsonPropertyName("last_renewal_time")]
+ public long LastRenewalTime { get; set; }
+
+ [JsonPropertyName("meta")]
+ public object Meta { get; set; }
+
+ [JsonPropertyName("num_uses")]
+ public long NumUses { get; set; }
+
+ [JsonPropertyName("orphan")]
+ public bool Orphan { get; set; }
+
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ [JsonPropertyName("policies")]
+ public List<string> Policies { get; set; }
+
+ [JsonPropertyName("renewable")]
+ public bool Renewable { get; set; }
+
+ [JsonPropertyName("ttl")]
+ public long Ttl { get; set; }
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+ }
+} \ No newline at end of file