:drunk: No idea what did AI did
This commit is contained in:
98
DysonNetwork.Common/Clients/FileReferenceServiceClient.cs
Normal file
98
DysonNetwork.Common/Clients/FileReferenceServiceClient.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
|
||||
namespace DysonNetwork.Common.Clients
|
||||
{
|
||||
public class FileReferenceServiceClient : IFileReferenceServiceClient, IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<FileReferenceServiceClient> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public FileReferenceServiceClient(HttpClient httpClient, ILogger<FileReferenceServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions()
|
||||
.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
_jsonOptions.PropertyNameCaseInsensitive = true;
|
||||
}
|
||||
|
||||
public async Task<CloudFileReference> CreateReferenceAsync(
|
||||
string fileId,
|
||||
string usage,
|
||||
string resourceId,
|
||||
Instant? expiredAt = null,
|
||||
Duration? duration = null)
|
||||
{
|
||||
var request = new
|
||||
{
|
||||
FileId = fileId,
|
||||
Usage = usage,
|
||||
ResourceId = resourceId,
|
||||
ExpiredAt = expiredAt,
|
||||
Duration = duration
|
||||
};
|
||||
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(request, _jsonOptions),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync("api/filereferences", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var reference = await JsonSerializer.DeserializeAsync<CloudFileReference>(stream, _jsonOptions);
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<CloudFileReference> GetReferenceAsync(string referenceId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/filereferences/{referenceId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var reference = await JsonSerializer.DeserializeAsync<CloudFileReference>(stream, _jsonOptions);
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CloudFileReference>> GetReferencesForResourceAsync(string resourceId, string? usage = null)
|
||||
{
|
||||
var url = $"api/filereferences/resource/{resourceId}";
|
||||
if (!string.IsNullOrEmpty(usage))
|
||||
{
|
||||
url += $"?usage={Uri.EscapeDataString(usage)}";
|
||||
}
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var references = await JsonSerializer.DeserializeAsync<IEnumerable<CloudFileReference>>(stream, _jsonOptions);
|
||||
return references ?? Array.Empty<CloudFileReference>();
|
||||
}
|
||||
|
||||
public async Task DeleteReferenceAsync(string referenceId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"api/filereferences/{referenceId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
77
DysonNetwork.Common/Clients/FileServiceClient.cs
Normal file
77
DysonNetwork.Common/Clients/FileServiceClient.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
|
||||
namespace DysonNetwork.Common.Clients
|
||||
{
|
||||
public class FileServiceClient : IFileServiceClient, IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<FileServiceClient> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public FileServiceClient(HttpClient httpClient, ILogger<FileServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
_jsonOptions.PropertyNameCaseInsensitive = true;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> GetFileAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/files/{fileId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var file = await JsonSerializer.DeserializeAsync<CloudFile>(stream, _jsonOptions);
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadFileAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/files/{fileId}/download");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsStreamAsync();
|
||||
}
|
||||
|
||||
public async Task<CloudFile> UploadFileAsync(Stream fileStream, string fileName, string contentType, string? folderId = null)
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
var fileContent = new StreamContent(fileStream);
|
||||
content.Add(fileContent, "file", fileName);
|
||||
|
||||
if (!string.IsNullOrEmpty(folderId))
|
||||
{
|
||||
content.Add(new StringContent(folderId), "folderId");
|
||||
}
|
||||
|
||||
var response = await _httpClient.PostAsync("api/files/upload", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
var file = await JsonSerializer.DeserializeAsync<CloudFile>(responseStream, _jsonOptions);
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task DeleteFileAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"api/files/{fileId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
81
DysonNetwork.Common/Models/AccountConnection.cs
Normal file
81
DysonNetwork.Common/Models/AccountConnection.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a connection between an account and an authentication provider
|
||||
/// </summary>
|
||||
public class AccountConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the connection
|
||||
/// </summary>
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The account ID this connection is associated with
|
||||
/// </summary>
|
||||
public string? AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider (e.g., "google", "github")
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string Provider { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier for the user from the provider
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string ProvidedIdentifier { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the connection
|
||||
/// </summary>
|
||||
[MaxLength(100)]
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth access token from the provider
|
||||
/// </summary>
|
||||
public string? AccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth refresh token from the provider (if available)
|
||||
/// </summary>
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the access token expires (if available)
|
||||
/// </summary>
|
||||
public Instant? ExpiresAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the connection was first established
|
||||
/// </summary>
|
||||
public Instant CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the connection was last used
|
||||
/// </summary>
|
||||
public Instant? LastUsedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata about the connection
|
||||
/// </summary>
|
||||
[Column(TypeName = "jsonb")]
|
||||
public Dictionary<string, object>? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property for the associated account
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(AccountId))]
|
||||
public virtual Account? Account { get; set; }
|
||||
}
|
@ -65,4 +65,26 @@ public class AuthChallenge : ModelBase
|
||||
if (StepRemain == 0 && BlacklistFactors.Count == 0) StepRemain = StepTotal;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthTokens
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
public int ExpiresIn { get; set; }
|
||||
public string TokenType { get; set; } = "Bearer";
|
||||
public string? Scope { get; set; }
|
||||
public string? IdToken { get; set; }
|
||||
|
||||
public static AuthTokens Create(string accessToken, string refreshToken, int expiresIn, string? scope = null, string? idToken = null)
|
||||
{
|
||||
return new AuthTokens
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
ExpiresIn = expiresIn,
|
||||
Scope = scope,
|
||||
IdToken = idToken
|
||||
};
|
||||
}
|
||||
}
|
74
DysonNetwork.Common/Models/Auth/Enums.cs
Normal file
74
DysonNetwork.Common/Models/Auth/Enums.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Common.Models.Auth;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AuthChallengeType
|
||||
{
|
||||
// Authentication challenges
|
||||
Password = 0,
|
||||
EmailCode = 1,
|
||||
PhoneCode = 2,
|
||||
Totp = 3,
|
||||
WebAuthn = 4,
|
||||
RecoveryCode = 5,
|
||||
|
||||
// Authorization challenges
|
||||
Consent = 10,
|
||||
TwoFactor = 11,
|
||||
|
||||
// Account recovery challenges
|
||||
ResetPassword = 20,
|
||||
VerifyEmail = 21,
|
||||
VerifyPhone = 22,
|
||||
|
||||
// Security challenges
|
||||
Reauthentication = 30,
|
||||
DeviceVerification = 31,
|
||||
|
||||
// Custom challenges
|
||||
Custom = 100
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AuthChallengePlatform
|
||||
{
|
||||
Web = 0,
|
||||
Ios = 1,
|
||||
Android = 2,
|
||||
Desktop = 3,
|
||||
Api = 4,
|
||||
Cli = 5,
|
||||
Sdk = 6,
|
||||
|
||||
// Special platforms
|
||||
System = 100,
|
||||
Unknown = 999
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AuthFactorType
|
||||
{
|
||||
Password = 0,
|
||||
EmailCode = 1,
|
||||
PhoneCode = 2,
|
||||
Totp = 3,
|
||||
WebAuthn = 4,
|
||||
RecoveryCode = 5,
|
||||
|
||||
// Social and federation
|
||||
Google = 10,
|
||||
Apple = 11,
|
||||
Microsoft = 12,
|
||||
Facebook = 13,
|
||||
Twitter = 14,
|
||||
Github = 15,
|
||||
|
||||
// Enterprise
|
||||
Saml = 50,
|
||||
Oidc = 51,
|
||||
Ldap = 52,
|
||||
|
||||
// Custom factor types
|
||||
Custom = 100
|
||||
}
|
10
DysonNetwork.Common/Models/LastActiveInfo.cs
Normal file
10
DysonNetwork.Common/Models/LastActiveInfo.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class LastActiveInfo
|
||||
{
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public string AccountId { get; set; } = string.Empty;
|
||||
public Instant SeenAt { get; set; }
|
||||
}
|
114
DysonNetwork.Common/Models/OidcUserInfo.cs
Normal file
114
DysonNetwork.Common/Models/OidcUserInfo.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents user information from an OIDC provider
|
||||
/// </summary>
|
||||
public class OidcUserInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for the user from the OIDC provider
|
||||
/// </summary>
|
||||
public string? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's email address
|
||||
/// </summary>
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user's email has been verified by the OIDC provider
|
||||
/// </summary>
|
||||
public bool EmailVerified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's given name (first name)
|
||||
/// </summary>
|
||||
public string? GivenName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's family name (last name)
|
||||
/// </summary>
|
||||
public string? FamilyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's full name
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's preferred username
|
||||
/// </summary>
|
||||
public string? PreferredUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the user's profile picture
|
||||
/// </summary>
|
||||
public string? Picture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OIDC provider name (e.g., "google", "github")
|
||||
/// </summary>
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth access token from the provider
|
||||
/// </summary>
|
||||
public string? AccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth refresh token from the provider (if available)
|
||||
/// </summary>
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the access token expires (if available)
|
||||
/// </summary>
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional claims from the ID token or user info endpoint
|
||||
/// </summary>
|
||||
public Dictionary<string, object>? Claims { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the user info to a metadata dictionary for storage
|
||||
/// </summary>
|
||||
public Dictionary<string, object> ToMetadata()
|
||||
{
|
||||
var metadata = new Dictionary<string, object>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(UserId))
|
||||
metadata["user_id"] = UserId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Email))
|
||||
metadata["email"] = Email;
|
||||
|
||||
metadata["email_verified"] = EmailVerified;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(GivenName))
|
||||
metadata["given_name"] = GivenName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FamilyName))
|
||||
metadata["family_name"] = FamilyName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
metadata["name"] = Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(PreferredUsername))
|
||||
metadata["preferred_username"] = PreferredUsername;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Picture))
|
||||
metadata["picture"] = Picture;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Provider))
|
||||
metadata["provider"] = Provider;
|
||||
|
||||
if (ExpiresAt.HasValue)
|
||||
metadata["expires_at"] = ExpiresAt.Value;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
64
DysonNetwork.Common/Services/FlushBufferService.cs
Normal file
64
DysonNetwork.Common/Services/FlushBufferService.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Common.Services;
|
||||
|
||||
public class FlushBufferService
|
||||
{
|
||||
private readonly Dictionary<Type, object> _buffers = new();
|
||||
private readonly object _lockObject = new();
|
||||
|
||||
private ConcurrentQueue<T> GetOrCreateBuffer<T>()
|
||||
{
|
||||
var type = typeof(T);
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_buffers.TryGetValue(type, out var buffer))
|
||||
{
|
||||
buffer = new ConcurrentQueue<T>();
|
||||
_buffers[type] = buffer;
|
||||
}
|
||||
return (ConcurrentQueue<T>)buffer;
|
||||
}
|
||||
}
|
||||
|
||||
public void Enqueue<T>(T item)
|
||||
{
|
||||
var buffer = GetOrCreateBuffer<T>();
|
||||
buffer.Enqueue(item);
|
||||
}
|
||||
|
||||
public async Task FlushAsync<T>(IFlushHandler<T> handler)
|
||||
{
|
||||
var buffer = GetOrCreateBuffer<T>();
|
||||
var workingQueue = new List<T>();
|
||||
|
||||
while (buffer.TryDequeue(out var item))
|
||||
{
|
||||
workingQueue.Add(item);
|
||||
}
|
||||
|
||||
if (workingQueue.Count == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await handler.FlushAsync(workingQueue);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If flush fails, re-queue the items
|
||||
foreach (var item in workingQueue)
|
||||
buffer.Enqueue(item);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetPendingCount<T>()
|
||||
{
|
||||
var buffer = GetOrCreateBuffer<T>();
|
||||
return buffer.Count;
|
||||
}
|
||||
}
|
9
DysonNetwork.Common/Services/IFlushHandler.cs
Normal file
9
DysonNetwork.Common/Services/IFlushHandler.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Common.Services;
|
||||
|
||||
public interface IFlushHandler<T>
|
||||
{
|
||||
Task FlushAsync(IReadOnlyList<T> items);
|
||||
}
|
Reference in New Issue
Block a user