:drunk: No idea what did AI did

This commit is contained in:
2025-07-06 19:46:59 +08:00
parent 14b79f16f4
commit 3391c08c04
40 changed files with 2484 additions and 112 deletions

View 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);
}
}
}

View 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);
}
}
}

View 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; }
}

View File

@ -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
};
}
}

View 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
}

View 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; }
}

View 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;
}
}

View 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;
}
}

View 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);
}