Compare commits
4 Commits
master
...
refactor/s
Author | SHA1 | Date | |
---|---|---|---|
7d1f096e87 | |||
3391c08c04 | |||
14b79f16f4 | |||
6a3d04af3d |
154
DysonNetwork.Common/Clients/FileReferenceServiceClient.cs
Normal file
154
DysonNetwork.Common/Clients/FileReferenceServiceClient.cs
Normal file
@ -0,0 +1,154 @@
|
||||
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 async Task DeleteResourceReferencesAsync(string resourceId, string? usage = null)
|
||||
{
|
||||
var url = $"api/filereferences/resource/{resourceId}";
|
||||
if (!string.IsNullOrEmpty(usage))
|
||||
{
|
||||
url += $"?usage={Uri.EscapeDataString(usage)}";
|
||||
}
|
||||
|
||||
var response = await _httpClient.DeleteAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<List<CloudFileReference>> GetFileReferencesAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/filereferences/file/{fileId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var references = await JsonSerializer.DeserializeAsync<List<CloudFileReference>>(stream, _jsonOptions);
|
||||
return references ?? new List<CloudFileReference>();
|
||||
}
|
||||
|
||||
public async Task<List<CloudFileReference>> GetResourceReferencesAsync(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<List<CloudFileReference>>(stream, _jsonOptions);
|
||||
return references ?? new List<CloudFileReference>();
|
||||
}
|
||||
|
||||
public async Task<bool> HasReferencesAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/filereferences/file/{fileId}/exists");
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task UpdateReferenceExpirationAsync(string referenceId, Instant? expiredAt)
|
||||
{
|
||||
var request = new { ExpiredAt = expiredAt };
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(request, _jsonOptions),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await _httpClient.PutAsync($"api/filereferences/{referenceId}/expiration", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
116
DysonNetwork.Common/Clients/FileServiceClient.cs
Normal file
116
DysonNetwork.Common/Clients/FileServiceClient.cs
Normal file
@ -0,0 +1,116 @@
|
||||
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> GetFileStreamAsync(string fileId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileId))
|
||||
throw new ArgumentNullException(nameof(fileId));
|
||||
|
||||
var response = await _httpClient.GetAsync($"api/files/{fileId}/download");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
if (stream == null)
|
||||
throw new InvalidOperationException("Failed to read file stream from response.");
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> UploadFileAsync(Stream fileStream, string fileName, string? contentType = null)
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
var fileContent = new StreamContent(fileStream);
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
|
||||
}
|
||||
content.Add(fileContent, "file", fileName);
|
||||
|
||||
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<CloudFile> ProcessImageAsync(Stream imageStream, string fileName, string? contentType = null)
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
var fileContent = new StreamContent(imageStream);
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
|
||||
}
|
||||
content.Add(fileContent, "image", fileName);
|
||||
|
||||
var response = await _httpClient.PostAsync("api/files/process-image", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
var file = await JsonSerializer.DeserializeAsync<CloudFile>(responseStream, _jsonOptions);
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task<string> GetFileUrl(string fileId, bool useCdn = false)
|
||||
{
|
||||
var url = $"api/files/{fileId}/url";
|
||||
if (useCdn)
|
||||
{
|
||||
url += "?useCdn=true";
|
||||
}
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
return result.Trim('"');
|
||||
}
|
||||
|
||||
public async Task DeleteFileAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"api/files/{fileId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
30
DysonNetwork.Common/DysonNetwork.Common.csproj
Normal file
30
DysonNetwork.Common/DysonNetwork.Common.csproj
Normal file
@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,6 +1,6 @@
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Common interface for cloud file entities that can be used in file operations.
|
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Common.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Common.Interfaces
|
||||
{
|
||||
public interface IFileReferenceServiceClient
|
||||
{
|
||||
Task<CloudFileReference> CreateReferenceAsync(
|
||||
string fileId,
|
||||
string usage,
|
||||
string resourceId,
|
||||
Instant? expiredAt = null,
|
||||
Duration? duration = null);
|
||||
|
||||
Task DeleteReferenceAsync(string referenceId);
|
||||
Task DeleteResourceReferencesAsync(string resourceId, string? usage = null);
|
||||
Task<List<CloudFileReference>> GetFileReferencesAsync(string fileId);
|
||||
Task<List<CloudFileReference>> GetResourceReferencesAsync(string resourceId, string? usage = null);
|
||||
Task<bool> HasReferencesAsync(string fileId);
|
||||
Task UpdateReferenceExpirationAsync(string referenceId, Instant? expiredAt);
|
||||
}
|
||||
}
|
17
DysonNetwork.Common/Interfaces/IFileServiceClient.cs
Normal file
17
DysonNetwork.Common/Interfaces/IFileServiceClient.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Common.Models;
|
||||
|
||||
namespace DysonNetwork.Common.Interfaces
|
||||
{
|
||||
public interface IFileServiceClient
|
||||
{
|
||||
Task<CloudFile> GetFileAsync(string fileId);
|
||||
Task<Stream> GetFileStreamAsync(string fileId);
|
||||
Task<CloudFile> UploadFileAsync(Stream fileStream, string fileName, string? contentType = null);
|
||||
Task DeleteFileAsync(string fileId);
|
||||
Task<CloudFile> ProcessImageAsync(Stream imageStream, string fileName, string? contentType = null);
|
||||
Task<string> GetFileUrl(string fileId, bool useCdn = false);
|
||||
}
|
||||
}
|
6
DysonNetwork.Common/Interfaces/IIdentifiedResource.cs
Normal file
6
DysonNetwork.Common/Interfaces/IIdentifiedResource.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Common.Interfaces;
|
||||
|
||||
public interface IIdentifiedResource
|
||||
{
|
||||
public string ResourceIdentifier { get; }
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum AbuseReportType
|
||||
{
|
@ -1,14 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Sphere.Wallet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using OtpNet;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum AccountStatus
|
||||
{
|
||||
PendingActivation,
|
||||
Active,
|
||||
Suspended,
|
||||
Banned,
|
||||
Deleted
|
||||
}
|
||||
|
||||
[Index(nameof(Name), IsUnique = true)]
|
||||
public class Account : ModelBase
|
||||
@ -26,13 +32,54 @@ public class Account : ModelBase
|
||||
|
||||
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>();
|
||||
[JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>();
|
||||
[JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>();
|
||||
[JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>();
|
||||
[JsonIgnore] public ICollection<AuthSession> Sessions { get; set; } = new List<AuthSession>();
|
||||
[JsonIgnore] public ICollection<AuthChallenge> Challenges { get; set; } = new List<AuthChallenge>();
|
||||
|
||||
[JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
|
||||
[JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>();
|
||||
|
||||
[JsonIgnore] public ICollection<Subscription> Subscriptions { get; set; } = new List<Subscription>();
|
||||
|
||||
public AccountStatus Status { get; set; } = AccountStatus.PendingActivation;
|
||||
|
||||
[NotMapped]
|
||||
public string? Email => GetPrimaryEmail();
|
||||
|
||||
public string? GetPrimaryEmail()
|
||||
{
|
||||
return Contacts
|
||||
.FirstOrDefault(c => c.Type == AccountContactType.Email && c.IsPrimary)
|
||||
?.Content;
|
||||
}
|
||||
|
||||
public void SetPrimaryEmail(string email)
|
||||
{
|
||||
// Remove primary flag from existing primary email if any
|
||||
foreach (var contact in Contacts.Where(c => c.Type == AccountContactType.Email && c.IsPrimary))
|
||||
{
|
||||
contact.IsPrimary = false;
|
||||
}
|
||||
|
||||
// Find or create the email contact
|
||||
var emailContact = Contacts.FirstOrDefault(c =>
|
||||
c.Type == AccountContactType.Email &&
|
||||
string.Equals(c.Content, email, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (emailContact == null)
|
||||
{
|
||||
emailContact = new AccountContact
|
||||
{
|
||||
Type = AccountContactType.Email,
|
||||
Content = email,
|
||||
IsPrimary = true
|
||||
};
|
||||
Contacts.Add(emailContact);
|
||||
}
|
||||
else
|
||||
{
|
||||
emailContact.IsPrimary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Leveling
|
||||
@ -131,11 +178,28 @@ public class AccountAuthFactor : ModelBase
|
||||
/// </summary>
|
||||
public int Trustworthy { get; set; } = 1;
|
||||
|
||||
[MaxLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsBackup { get; set; }
|
||||
public Instant? LastUsedAt { get; set; }
|
||||
public Instant? EnabledAt { get; set; }
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
public Instant? DisabledAt { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public Dictionary<string, object>? Metadata { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
|
||||
// Navigation property for related AuthSessions
|
||||
[JsonIgnore]
|
||||
public virtual ICollection<AuthSession>? Sessions { get; set; }
|
||||
|
||||
public AccountAuthFactor HashSecret(int cost = 12)
|
||||
{
|
||||
@ -177,20 +241,5 @@ public enum AccountAuthFactorType
|
||||
EmailCode,
|
||||
InAppCode,
|
||||
TimedCode,
|
||||
PinCode,
|
||||
PinCode
|
||||
}
|
||||
|
||||
public class AccountConnection : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(4096)] public string Provider { get; set; } = null!;
|
||||
[MaxLength(8192)] public string ProvidedIdentifier { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } = new();
|
||||
|
||||
[JsonIgnore] [MaxLength(4096)] public string? AccessToken { get; set; }
|
||||
[JsonIgnore] [MaxLength(4096)] public string? RefreshToken { get; set; }
|
||||
public Instant? LastUsedAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account Account { get; set; } = null!;
|
||||
}
|
125
DysonNetwork.Common/Models/AccountConnection.cs
Normal file
125
DysonNetwork.Common/Models/AccountConnection.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a connection between an account and an authentication provider
|
||||
/// </summary>
|
||||
public class AccountConnection : ModelBase
|
||||
{
|
||||
/// <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>
|
||||
/// Alias for ProvidedIdentifier for backward compatibility
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string ProviderId
|
||||
{
|
||||
get => ProvidedIdentifier;
|
||||
set => ProvidedIdentifier = value;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Raw profile data from the provider
|
||||
/// </summary>
|
||||
[Column(TypeName = "jsonb")]
|
||||
public JsonDocument? ProfileData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the connection was first established
|
||||
/// </summary>
|
||||
public Instant ConnectedAt { get; set; } = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata about the connection
|
||||
/// </summary>
|
||||
[Column(TypeName = "jsonb")]
|
||||
public JsonDocument? Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the connection is currently active
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
/// <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>
|
||||
/// Navigation property for the associated account
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(AccountId))]
|
||||
[JsonIgnore]
|
||||
public virtual Account? Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the connection's tokens and related metadata
|
||||
/// </summary>
|
||||
/// <param name="accessToken">The new access token</param>
|
||||
/// <param name="refreshToken">The new refresh token, if any</param>
|
||||
/// <param name="expiresAt">When the access token expires, if any</param>
|
||||
public void UpdateTokens(string? accessToken, string? refreshToken, Instant? expiresAt)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
|
||||
if (!string.IsNullOrEmpty(refreshToken))
|
||||
{
|
||||
RefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
if (expiresAt.HasValue)
|
||||
{
|
||||
ExpiresAt = expiresAt;
|
||||
}
|
||||
|
||||
LastUsedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
using NetTopologySuite.Geometries;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public abstract class ActionLogType
|
||||
{
|
@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Activity;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public interface IActivity
|
||||
{
|
@ -1,13 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using NodaTime;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class Session : ModelBase
|
||||
public class AuthSession : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string? Label { get; set; }
|
||||
@ -15,9 +14,9 @@ public class Session : ModelBase
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
public Guid ChallengeId { get; set; }
|
||||
public Challenge Challenge { get; set; } = null!;
|
||||
public AuthChallenge Challenge { get; set; } = null!;
|
||||
public Guid? AppId { get; set; }
|
||||
public CustomApp? App { get; set; }
|
||||
}
|
||||
@ -40,7 +39,7 @@ public enum ChallengePlatform
|
||||
Linux
|
||||
}
|
||||
|
||||
public class Challenge : ModelBase
|
||||
public class AuthChallenge : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
@ -49,9 +48,9 @@ public class Challenge : ModelBase
|
||||
public int FailedAttempts { get; set; }
|
||||
public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
|
||||
public ChallengeType Type { get; set; } = ChallengeType.Login;
|
||||
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = [];
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
|
||||
[MaxLength(128)] public string? IpAddress { get; set; }
|
||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||
[MaxLength(256)] public string? DeviceId { get; set; }
|
||||
@ -59,11 +58,33 @@ public class Challenge : ModelBase
|
||||
public Point? Location { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Models.Account Account { get; set; } = null!;
|
||||
|
||||
public Challenge Normalize()
|
||||
public AuthChallenge Normalize()
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
47
DysonNetwork.Common/Models/Auth/AuthFactorType.cs
Normal file
47
DysonNetwork.Common/Models/Auth/AuthFactorType.cs
Normal file
@ -0,0 +1,47 @@
|
||||
namespace DysonNetwork.Common.Models.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the different types of authentication factors that can be used for multi-factor authentication.
|
||||
/// </summary>
|
||||
public enum AuthFactorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Password-based authentication factor.
|
||||
/// </summary>
|
||||
Password = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Time-based One-Time Password (TOTP) authentication factor.
|
||||
/// </summary>
|
||||
Totp = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Email-based authentication factor.
|
||||
/// </summary>
|
||||
Email = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Phone/SMS-based authentication factor.
|
||||
/// </summary>
|
||||
Phone = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Security key (FIDO2/WebAuthn) authentication factor.
|
||||
/// </summary>
|
||||
SecurityKey = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Recovery code authentication factor.
|
||||
/// </summary>
|
||||
RecoveryCode = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Backup code authentication factor.
|
||||
/// </summary>
|
||||
BackupCode = 6,
|
||||
|
||||
/// <summary>
|
||||
/// OpenID Connect (OIDC) authentication factor.
|
||||
/// </summary>
|
||||
Oidc = 7
|
||||
}
|
47
DysonNetwork.Common/Models/Auth/Enums.cs
Normal file
47
DysonNetwork.Common/Models/Auth/Enums.cs
Normal file
@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class Badge : ModelBase
|
||||
{
|
||||
@ -16,7 +16,7 @@ public class Badge : ModelBase
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Models.Account Account { get; set; } = null!;
|
||||
|
||||
public BadgeReferenceObject ToReference()
|
||||
{
|
@ -1,10 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum ChatRoomType
|
||||
{
|
||||
@ -31,7 +31,7 @@ public class ChatRoom : ModelBase, IIdentifiedResource
|
||||
[JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>();
|
||||
|
||||
public Guid? RealmId { get; set; }
|
||||
public Realm.Realm? Realm { get; set; }
|
||||
public Common.Models.Realm? Realm { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
[JsonPropertyName("members")]
|
||||
@ -73,7 +73,7 @@ public class ChatMember : ModelBase
|
||||
public Guid ChatRoomId { get; set; }
|
||||
public ChatRoom ChatRoom { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
|
||||
[MaxLength(1024)] public string? Nick { get; set; }
|
||||
|
||||
@ -105,7 +105,7 @@ public class ChatMemberTransmissionObject : ModelBase
|
||||
public Guid Id { get; set; }
|
||||
public Guid ChatRoomId { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
|
||||
[MaxLength(1024)] public string? Nick { get; set; }
|
||||
|
@ -1,9 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class RemoteStorageConfig
|
||||
{
|
||||
@ -74,7 +74,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource
|
||||
[MaxLength(4096)]
|
||||
public string? StorageUrl { get; set; }
|
||||
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
public CloudFileReferenceObject ToReferenceObject()
|
@ -1,11 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Developer;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum CustomAppStatus
|
||||
{
|
||||
@ -33,7 +32,7 @@ public class CustomApp : ModelBase, IIdentifiedResource
|
||||
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
|
||||
public Guid PublisherId { get; set; }
|
||||
public Publisher.Publisher Developer { get; set; } = null!;
|
||||
public Publisher Developer { get; set; } = null!;
|
||||
|
||||
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
|
||||
}
|
@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum StatusAttitude
|
||||
{
|
||||
@ -23,7 +23,7 @@ public class Status : ModelBase
|
||||
public Instant? ClearedAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account Account { get; set; } = null!;
|
||||
public Models.Account Account { get; set; } = null!;
|
||||
}
|
||||
|
||||
public enum CheckInResultLevel
|
||||
@ -44,7 +44,7 @@ public class CheckInResult : ModelBase
|
||||
[Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account Account { get; set; } = null!;
|
||||
public Models.Account Account { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class FortuneTip
|
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; }
|
||||
}
|
14
DysonNetwork.Common/Models/LoginModels.cs
Normal file
14
DysonNetwork.Common/Models/LoginModels.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace DysonNetwork.Sphere.Models
|
||||
{
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string AccessToken { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum MagicSpellType
|
||||
{
|
||||
@ -26,5 +26,5 @@ public class MagicSpell : ModelBase
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
|
||||
|
||||
public Guid? AccountId { get; set; }
|
||||
public Account? Account { get; set; }
|
||||
public Models.Account? Account { get; set; }
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class Message : ModelBase, IIdentifiedResource
|
||||
{
|
33
DysonNetwork.Common/Models/ModelBase.cs
Normal file
33
DysonNetwork.Common/Models/ModelBase.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all entity models in the system.
|
||||
/// Provides common properties and functionality for tracking entity lifecycle.
|
||||
/// </summary>
|
||||
public abstract class ModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the entity.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the entity was created, in UTC.
|
||||
/// </summary>
|
||||
public Instant CreatedAt { get; set; } = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the entity was last updated, in UTC.
|
||||
/// </summary>
|
||||
public Instant UpdatedAt { get; set; } = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the entity was soft-deleted, in UTC.
|
||||
/// Null if the entity has not been deleted.
|
||||
/// </summary>
|
||||
public Instant? DeletedAt { get; set; }
|
||||
}
|
@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class Notification : ModelBase
|
||||
{
|
||||
@ -18,7 +18,7 @@ public class Notification : ModelBase
|
||||
public Instant? ViewedAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Models.Account Account { get; set; } = null!;
|
||||
}
|
||||
|
||||
public enum NotificationPushProvider
|
||||
@ -37,5 +37,5 @@ public class NotificationPushSubscription : ModelBase
|
||||
public Instant? LastUsedAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Models.Account Account { get; set; } = null!;
|
||||
}
|
115
DysonNetwork.Common/Models/OidcUserInfo.cs
Normal file
115
DysonNetwork.Common/Models/OidcUserInfo.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class WalletCurrency
|
||||
{
|
@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Permission;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
/// The permission node model provides the infrastructure of permission control in Dyson Network.
|
||||
/// It based on the ABAC permission model.
|
@ -1,12 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Activity;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using NodaTime;
|
||||
using NpgsqlTypes;
|
||||
|
||||
namespace DysonNetwork.Sphere.Post;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum PostType
|
||||
{
|
||||
@ -59,7 +59,7 @@ public class Post : ModelBase, IIdentifiedResource, IActivity
|
||||
[JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!;
|
||||
|
||||
public Guid PublisherId { get; set; }
|
||||
public Publisher.Publisher Publisher { get; set; } = null!;
|
||||
public Publisher Publisher { get; set; } = null!;
|
||||
|
||||
public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>();
|
||||
public ICollection<PostTag> Tags { get; set; } = new List<PostTag>();
|
||||
@ -71,9 +71,9 @@ public class Post : ModelBase, IIdentifiedResource, IActivity
|
||||
|
||||
public string ResourceIdentifier => $"post/{Id}";
|
||||
|
||||
public Activity.Activity ToActivity()
|
||||
public Activity ToActivity()
|
||||
{
|
||||
return new Activity.Activity()
|
||||
return new Activity()
|
||||
{
|
||||
CreatedAt = PublishedAt ?? CreatedAt,
|
||||
UpdatedAt = UpdatedAt,
|
||||
@ -109,7 +109,7 @@ public class PostCollection : ModelBase
|
||||
[MaxLength(256)] public string? Name { get; set; }
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
|
||||
public Publisher.Publisher Publisher { get; set; } = null!;
|
||||
public Publisher Publisher { get; set; } = null!;
|
||||
|
||||
public ICollection<Post> Posts { get; set; } = new List<Post>();
|
||||
}
|
||||
@ -130,5 +130,5 @@ public class PostReaction : ModelBase
|
||||
public Guid PostId { get; set; }
|
||||
[JsonIgnore] public Post Post { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Publisher;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum PublisherType
|
||||
{
|
||||
@ -30,9 +29,9 @@ public class Publisher : ModelBase, IIdentifiedResource
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
|
||||
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<Post.Post> Posts { get; set; } = new List<Post.Post>();
|
||||
[JsonIgnore] public ICollection<Post> Posts { get; set; } = new List<Post>();
|
||||
[JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>();
|
||||
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
|
||||
[JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = new List<PublisherFeature>();
|
||||
@ -41,9 +40,9 @@ public class Publisher : ModelBase, IIdentifiedResource
|
||||
public ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>();
|
||||
|
||||
public Guid? AccountId { get; set; }
|
||||
public Account.Account? Account { get; set; }
|
||||
public Account? Account { get; set; }
|
||||
public Guid? RealmId { get; set; }
|
||||
[JsonIgnore] public Realm.Realm? Realm { get; set; }
|
||||
[JsonIgnore] public Realm? Realm { get; set; }
|
||||
|
||||
public string ResourceIdentifier => $"publisher/{Id}";
|
||||
}
|
||||
@ -61,7 +60,7 @@ public class PublisherMember : ModelBase
|
||||
public Guid PublisherId { get; set; }
|
||||
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
|
||||
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
|
||||
public Instant? JoinedAt { get; set; }
|
||||
@ -81,7 +80,7 @@ public class PublisherSubscription : ModelBase
|
||||
public Guid PublisherId { get; set; }
|
||||
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
|
||||
public PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active;
|
||||
public int Tier { get; set; } = 0;
|
@ -1,12 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Realm;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
[Index(nameof(Slug), IsUnique = true)]
|
||||
public class Realm : ModelBase, IIdentifiedResource
|
||||
@ -25,14 +24,13 @@ public class Realm : ModelBase, IIdentifiedResource
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
|
||||
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
|
||||
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
|
||||
[JsonIgnore] public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||
|
||||
public string ResourceIdentifier => $"realm/{Id}";
|
||||
}
|
||||
@ -49,7 +47,7 @@ public class RealmMember : ModelBase
|
||||
public Guid RealmId { get; set; }
|
||||
public Realm Realm { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
|
||||
public int Role { get; set; } = RealmMemberRole.Normal;
|
||||
public Instant? JoinedAt { get; set; }
|
@ -1,12 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Chat.Realtime;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class RealtimeCall : ModelBase
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public enum RelationshipStatus : short
|
||||
{
|
||||
@ -12,9 +12,9 @@ public enum RelationshipStatus : short
|
||||
public class Relationship : ModelBase
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
public Account Account { get; set; } = null!;
|
||||
public Models.Account Account { get; set; } = null!;
|
||||
public Guid RelatedId { get; set; }
|
||||
public Account Related { get; set; } = null!;
|
||||
public Models.Account Related { get; set; } = null!;
|
||||
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public record class SubscriptionTypeData(
|
||||
string Identifier,
|
||||
@ -138,7 +138,7 @@ public class Subscription : ModelBase
|
||||
public Instant? RenewalAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
|
||||
[NotMapped]
|
||||
public bool IsAvailable
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The verification info of a resource
|
@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
namespace DysonNetwork.Common.Models;
|
||||
|
||||
public class Wallet : ModelBase
|
||||
{
|
||||
@ -10,7 +10,7 @@ public class Wallet : ModelBase
|
||||
public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
|
||||
}
|
||||
|
||||
public class WalletPocket : ModelBase
|
@ -4,7 +4,7 @@ using NodaTime;
|
||||
using NodaTime.Serialization.JsonNet;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a distributed lock that can be used to synchronize access across multiple processes
|
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);
|
||||
}
|
100
DysonNetwork.Common/Services/Permission/PermissionMiddleware.cs
Normal file
100
DysonNetwork.Common/Services/Permission/PermissionMiddleware.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
namespace DysonNetwork.Common.Services.Permission;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
|
||||
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
||||
{
|
||||
public string Area { get; set; } = area;
|
||||
public string Key { get; } = key;
|
||||
}
|
||||
|
||||
public class PermissionMiddleware<TDbContext> where TDbContext : DbContext
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public PermissionMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
|
||||
{
|
||||
_next = next;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext httpContext)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var permissionService = new PermissionService<TDbContext>(
|
||||
scope.ServiceProvider.GetRequiredService<TDbContext>(),
|
||||
scope.ServiceProvider.GetRequiredService<ICacheService>()
|
||||
);
|
||||
|
||||
var endpoint = httpContext.GetEndpoint();
|
||||
var attr = endpoint?.Metadata.OfType<RequiredPermissionAttribute>().FirstOrDefault();
|
||||
|
||||
if (attr != null)
|
||||
{
|
||||
if (httpContext.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUserId = httpContext.User.GetUserId();
|
||||
if (currentUserId == Guid.Empty)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Check for superuser from PassClient
|
||||
// if (currentUser.IsSuperuser)
|
||||
// {
|
||||
// await _next(httpContext);
|
||||
// return;
|
||||
// }
|
||||
|
||||
var actor = $"user:{currentUserId}";
|
||||
var hasPermission = await permissionService.HasPermissionAsync(actor, attr.Area, attr.Key);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync("Forbidden");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _next.Invoke(httpContext);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PermissionServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddPermissionService<TDbContext>(this IServiceCollection services)
|
||||
where TDbContext : DbContext
|
||||
{
|
||||
services.AddScoped<PermissionService<TDbContext>>(sp =>
|
||||
new PermissionService<TDbContext>(
|
||||
sp.GetRequiredService<TDbContext>(),
|
||||
sp.GetRequiredService<ICacheService>()
|
||||
));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UsePermissionMiddleware<TDbContext>(this IApplicationBuilder builder)
|
||||
where TDbContext : DbContext
|
||||
{
|
||||
return builder.UseMiddleware<PermissionMiddleware<TDbContext>>(builder.ApplicationServices);
|
||||
}
|
||||
}
|
203
DysonNetwork.Common/Services/Permission/PermissionService.cs
Normal file
203
DysonNetwork.Common/Services/Permission/PermissionService.cs
Normal file
@ -0,0 +1,203 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Common.Services;
|
||||
|
||||
namespace DysonNetwork.Common.Services.Permission;
|
||||
|
||||
public class PermissionService<TDbContext> where TDbContext : DbContext
|
||||
{
|
||||
private readonly TDbContext _db;
|
||||
private readonly ICacheService _cache;
|
||||
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
|
||||
|
||||
private const string PermCacheKeyPrefix = "perm:";
|
||||
private const string PermGroupCacheKeyPrefix = "perm-cg:";
|
||||
private const string PermissionGroupPrefix = "perm-g:";
|
||||
|
||||
public PermissionService(TDbContext db, ICacheService cache)
|
||||
{
|
||||
_db = db;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
private static string GetPermissionCacheKey(string actor, string area, string key) =>
|
||||
PermCacheKeyPrefix + actor + ":" + area + ":" + key;
|
||||
|
||||
private static string GetGroupsCacheKey(string actor) =>
|
||||
PermGroupCacheKeyPrefix + actor;
|
||||
|
||||
private static string GetPermissionGroupKey(string actor) =>
|
||||
PermissionGroupPrefix + actor;
|
||||
|
||||
public async Task<bool> HasPermissionAsync(string actor, string area, string key)
|
||||
{
|
||||
var value = await GetPermissionAsync<bool>(actor, area, key);
|
||||
return value;
|
||||
}
|
||||
|
||||
public async Task<T?> GetPermissionAsync<T>(string actor, string area, string key)
|
||||
{
|
||||
var cacheKey = GetPermissionCacheKey(actor, area, key);
|
||||
|
||||
var (hit, cachedValue) = await _cache.GetAsyncWithStatus<T>(cacheKey);
|
||||
if (hit)
|
||||
return cachedValue;
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var groupsKey = GetGroupsCacheKey(actor);
|
||||
|
||||
var groupsId = await _cache.GetAsync<List<Guid>>(groupsKey);
|
||||
if (groupsId == null)
|
||||
{
|
||||
groupsId = await _db.Set<PermissionGroupMember>()
|
||||
.Where(n => n.Actor == actor)
|
||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
||||
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
||||
.Select(e => e.GroupId)
|
||||
.ToListAsync();
|
||||
|
||||
await _cache.SetWithGroupsAsync(groupsKey, groupsId,
|
||||
[GetPermissionGroupKey(actor)],
|
||||
CacheExpiration);
|
||||
}
|
||||
|
||||
var permission = await _db.Set<PermissionNode>()
|
||||
.Where(n => (n.GroupId == null && n.Actor == actor) ||
|
||||
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
||||
.Where(n => n.Key == key && n.Area == area)
|
||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
||||
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var result = permission is not null ? DeserializePermissionValue<T>(permission.Value) : default;
|
||||
|
||||
await _cache.SetWithGroupsAsync(cacheKey, result,
|
||||
[GetPermissionGroupKey(actor)],
|
||||
CacheExpiration);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PermissionNode> AddPermissionNode<T>(
|
||||
string actor,
|
||||
string area,
|
||||
string key,
|
||||
T value,
|
||||
Instant? expiredAt = null,
|
||||
Instant? affectedAt = null
|
||||
)
|
||||
{
|
||||
if (value is null) throw new ArgumentNullException(nameof(value));
|
||||
|
||||
var node = new PermissionNode
|
||||
{
|
||||
Actor = actor,
|
||||
Key = key,
|
||||
Area = area,
|
||||
Value = SerializePermissionValue(value),
|
||||
ExpiredAt = expiredAt,
|
||||
AffectedAt = affectedAt
|
||||
};
|
||||
|
||||
_db.Set<PermissionNode>().Add(node);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Invalidate related caches
|
||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public async Task<PermissionNode> AddPermissionNodeToGroup<T>(
|
||||
PermissionGroup group,
|
||||
string actor,
|
||||
string area,
|
||||
string key,
|
||||
T value,
|
||||
Instant? expiredAt = null,
|
||||
Instant? affectedAt = null
|
||||
)
|
||||
{
|
||||
if (value is null) throw new ArgumentNullException(nameof(value));
|
||||
|
||||
var node = new PermissionNode
|
||||
{
|
||||
Actor = actor,
|
||||
Key = key,
|
||||
Area = area,
|
||||
Value = SerializePermissionValue(value),
|
||||
ExpiredAt = expiredAt,
|
||||
AffectedAt = affectedAt,
|
||||
Group = group,
|
||||
GroupId = group.Id
|
||||
};
|
||||
|
||||
_db.Set<PermissionNode>().Add(node);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Invalidate related caches
|
||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||
await _cache.RemoveAsync(GetGroupsCacheKey(actor));
|
||||
await _cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public async Task RemovePermissionNode(string actor, string area, string key)
|
||||
{
|
||||
var node = await _db.Set<PermissionNode>()
|
||||
.Where(n => n.Actor == actor && n.Area == area && n.Key == key)
|
||||
.FirstOrDefaultAsync();
|
||||
if (node is not null) _db.Set<PermissionNode>().Remove(node);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Invalidate cache
|
||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||
}
|
||||
|
||||
public async Task RemovePermissionNodeFromGroup(PermissionGroup group, string actor, string area, string key)
|
||||
{
|
||||
var node = await _db.Set<PermissionNode>()
|
||||
.Where(n => n.GroupId == group.Id)
|
||||
.Where(n => n.Actor == actor && n.Area == area && n.Key == key)
|
||||
.FirstOrDefaultAsync();
|
||||
if (node is null) return;
|
||||
_db.Set<PermissionNode>().Remove(node);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Invalidate caches
|
||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
||||
await _cache.RemoveAsync(GetGroupsCacheKey(actor));
|
||||
await _cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
|
||||
}
|
||||
|
||||
private async Task InvalidatePermissionCacheAsync(string actor, string area, string key)
|
||||
{
|
||||
var cacheKey = GetPermissionCacheKey(actor, area, key);
|
||||
await _cache.RemoveAsync(cacheKey);
|
||||
}
|
||||
|
||||
private static T? DeserializePermissionValue<T>(JsonDocument json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(json.RootElement.GetRawText());
|
||||
}
|
||||
|
||||
private static JsonDocument SerializePermissionValue<T>(T obj)
|
||||
{
|
||||
var str = JsonSerializer.Serialize(obj);
|
||||
return JsonDocument.Parse(str);
|
||||
}
|
||||
|
||||
public static PermissionNode NewPermissionNode<T>(string actor, string area, string key, T value)
|
||||
{
|
||||
return new PermissionNode
|
||||
{
|
||||
Actor = actor,
|
||||
Area = area,
|
||||
Key = key,
|
||||
Value = SerializePermissionValue(value),
|
||||
};
|
||||
}
|
||||
}
|
13
DysonNetwork.Drive/Attributes/RequiredPermissionAttribute.cs
Normal file
13
DysonNetwork.Drive/Attributes/RequiredPermissionAttribute.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace DysonNetwork.Drive.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public class RequiredPermissionAttribute : AuthorizeAttribute
|
||||
{
|
||||
public RequiredPermissionAttribute(string permission) : base(permission)
|
||||
{
|
||||
Policy = permission;
|
||||
}
|
||||
}
|
68
DysonNetwork.Drive/Auth/AuthService.cs
Normal file
68
DysonNetwork.Drive/Auth/AuthService.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System.Security.Claims;
|
||||
using DysonNetwork.Drive.Data;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text;
|
||||
|
||||
namespace DysonNetwork.Drive.Auth;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<string> GenerateJwtToken(Account account);
|
||||
Task<Account?> GetAuthenticatedAccountAsync(ClaimsPrincipal user);
|
||||
Task<Account?> GetAuthenticatedAccountAsync(HttpContext context);
|
||||
}
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly AppDatabase _db;
|
||||
|
||||
public AuthService(IConfiguration configuration, AppDatabase db)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public Task<string> GenerateJwtToken(Account account)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"] ?? throw new InvalidOperationException("JWT Secret not configured"));
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, account.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, account.Username),
|
||||
new Claim(ClaimTypes.Email, account.Email)
|
||||
}),
|
||||
Expires = DateTime.UtcNow.AddDays(7),
|
||||
SigningCredentials = new SigningCredentials(
|
||||
new SymmetricSecurityKey(key),
|
||||
SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return Task.FromResult(tokenHandler.WriteToken(token));
|
||||
}
|
||||
|
||||
public async Task<Account?> GetAuthenticatedAccountAsync(ClaimsPrincipal user)
|
||||
{
|
||||
var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||
return null;
|
||||
|
||||
return await _db.Set<Account>().FindAsync(userId);
|
||||
}
|
||||
|
||||
public async Task<Account?> GetAuthenticatedAccountAsync(HttpContext context)
|
||||
{
|
||||
return await GetAuthenticatedAccountAsync(context.User);
|
||||
}
|
||||
}
|
42
DysonNetwork.Drive/Auth/Session.cs
Normal file
42
DysonNetwork.Drive/Auth/Session.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Drive.Models;
|
||||
|
||||
namespace DysonNetwork.Drive.Auth;
|
||||
|
||||
public class Session
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
public Guid AccountId { get; set; }
|
||||
public virtual Account? Account { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(64)]
|
||||
public string Token { get; set; } = null!;
|
||||
|
||||
public string? UserAgent { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public DateTimeOffset LastActiveAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Additional metadata
|
||||
public string? DeviceInfo { get; set; }
|
||||
public string? LocationInfo { get; set; }
|
||||
|
||||
public void UpdateLastActive()
|
||||
{
|
||||
LastActiveAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
return ExpiresAt.HasValue && DateTimeOffset.UtcNow >= ExpiresAt.Value;
|
||||
}
|
||||
}
|
131
DysonNetwork.Drive/Clients/FileReferenceServiceClient.cs
Normal file
131
DysonNetwork.Drive/Clients/FileReferenceServiceClient.cs
Normal file
@ -0,0 +1,131 @@
|
||||
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.Drive.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 responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<CloudFileReference>(responseStream, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize reference response");
|
||||
}
|
||||
|
||||
public async Task DeleteReferenceAsync(string referenceId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"api/filereferences/{referenceId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task DeleteResourceReferencesAsync(string resourceId, string? usage = null)
|
||||
{
|
||||
var url = $"api/filereferences/resource/{Uri.EscapeDataString(resourceId)}";
|
||||
if (!string.IsNullOrEmpty(usage))
|
||||
{
|
||||
url += $"?usage={Uri.EscapeDataString(usage)}";
|
||||
}
|
||||
|
||||
var response = await _httpClient.DeleteAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<List<CloudFileReference>> GetFileReferencesAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/filereferences/file/{fileId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<List<CloudFileReference>>(responseStream, _jsonOptions)
|
||||
?? new List<CloudFileReference>();
|
||||
}
|
||||
|
||||
public async Task<List<CloudFileReference>> GetResourceReferencesAsync(string resourceId, string? usage = null)
|
||||
{
|
||||
var url = $"api/filereferences/resource/{Uri.EscapeDataString(resourceId)}";
|
||||
if (!string.IsNullOrEmpty(usage))
|
||||
{
|
||||
url += $"?usage={Uri.EscapeDataString(usage)}";
|
||||
}
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<List<CloudFileReference>>(responseStream, _jsonOptions)
|
||||
?? new List<CloudFileReference>();
|
||||
}
|
||||
|
||||
public async Task<bool> HasReferencesAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/filereferences/file/{fileId}/has-references");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<bool>(result, _jsonOptions);
|
||||
}
|
||||
|
||||
public async Task UpdateReferenceExpirationAsync(string referenceId, Instant? expiredAt)
|
||||
{
|
||||
var request = new { ExpiredAt = expiredAt };
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(request, _jsonOptions),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await _httpClient.PatchAsync($"api/filereferences/{referenceId}", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
99
DysonNetwork.Drive/Clients/FileServiceClient.cs
Normal file
99
DysonNetwork.Drive/Clients/FileServiceClient.cs
Normal file
@ -0,0 +1,99 @@
|
||||
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;
|
||||
|
||||
namespace DysonNetwork.Drive.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 { 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();
|
||||
return await JsonSerializer.DeserializeAsync<CloudFile>(stream, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize file response");
|
||||
}
|
||||
|
||||
public async Task<Stream> GetFileStreamAsync(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 = null)
|
||||
{
|
||||
using var content = new MultipartFormDataContent
|
||||
{
|
||||
{ new StreamContent(fileStream), "file", fileName }
|
||||
};
|
||||
|
||||
var response = await _httpClient.PostAsync("api/files/upload", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<CloudFile>(responseStream, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize upload response");
|
||||
}
|
||||
|
||||
public async Task DeleteFileAsync(string fileId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"api/files/{fileId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<CloudFile> ProcessImageAsync(Stream imageStream, string fileName, string? contentType = null)
|
||||
{
|
||||
using var content = new MultipartFormDataContent
|
||||
{
|
||||
{ new StreamContent(imageStream), "image", fileName }
|
||||
};
|
||||
|
||||
var response = await _httpClient.PostAsync("api/files/process-image", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<CloudFile>(responseStream, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize image processing response");
|
||||
}
|
||||
|
||||
public async Task<string> GetFileUrl(string fileId, bool useCdn = false)
|
||||
{
|
||||
var url = $"api/files/{fileId}/url";
|
||||
if (useCdn)
|
||||
{
|
||||
url += "?useCdn=true";
|
||||
}
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<string>(result, _jsonOptions) ?? string.Empty;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using DysonNetwork.Sphere;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public class CloudFileUnusedRecyclingJob(
|
||||
AppDatabase db,
|
222
DysonNetwork.Drive/Controllers/FileController.cs
Normal file
222
DysonNetwork.Drive/Controllers/FileController.cs
Normal file
@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Drive.Interfaces;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Drive.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/files")]
|
||||
[Authorize]
|
||||
public class FileController : ControllerBase
|
||||
{
|
||||
private readonly IFileService _fileService;
|
||||
private readonly ILogger<FileController> _logger;
|
||||
|
||||
public FileController(IFileService fileService, ILogger<FileController> logger)
|
||||
{
|
||||
_fileService = fileService ?? throw new ArgumentNullException(nameof(fileService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
[HttpGet("{fileId}")]
|
||||
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetFile(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = await _fileService.GetFileAsync(fileId, cancellationToken);
|
||||
var stream = await _fileService.DownloadFileAsync(fileId, cancellationToken);
|
||||
|
||||
return File(stream, file.MimeType, file.OriginalName);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "File not found: {FileId}", fileId);
|
||||
return NotFound(new { message = $"File with ID {fileId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving file: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while retrieving the file." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
[ProducesResponseType(typeof(CloudFile), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> UploadFile(IFormFile file, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new { message = "No file uploaded." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
var uploadedFile = await _fileService.UploadFileAsync(
|
||||
stream,
|
||||
file.FileName,
|
||||
file.ContentType,
|
||||
null,
|
||||
cancellationToken);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetFile),
|
||||
new { fileId = uploadedFile.Id },
|
||||
uploadedFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading file: {FileName}", file?.FileName);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while uploading the file." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{fileId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteFile(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _fileService.DeleteFileAsync(fileId, cancellationToken);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(new { message = $"File with ID {fileId} not found." });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting file: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while deleting the file." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{fileId}/metadata")]
|
||||
[ProducesResponseType(typeof(CloudFile), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetFileMetadata(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = await _fileService.GetFileAsync(fileId, cancellationToken);
|
||||
return Ok(file);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "File not found: {FileId}", fileId);
|
||||
return NotFound(new { message = $"File with ID {fileId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving file metadata: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while retrieving file metadata." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{fileId}/metadata")]
|
||||
[ProducesResponseType(typeof(CloudFile), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateFileMetadata(
|
||||
Guid fileId,
|
||||
[FromBody] Dictionary<string, string> metadata,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (metadata == null || metadata.Count == 0)
|
||||
{
|
||||
return BadRequest(new { message = "No metadata provided." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var updatedFile = await _fileService.UpdateFileMetadataAsync(fileId, metadata, cancellationToken);
|
||||
return Ok(updatedFile);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "File not found: {FileId}", fileId);
|
||||
return NotFound(new { message = $"File with ID {fileId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating file metadata: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while updating file metadata." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{fileId}/url")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetFileUrl(Guid fileId, [FromQuery] int? expiresInSeconds = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
TimeSpan? expiry = expiresInSeconds.HasValue
|
||||
? TimeSpan.FromSeconds(expiresInSeconds.Value)
|
||||
: null;
|
||||
|
||||
var url = await _fileService.GetFileUrlAsync(fileId, expiry, cancellationToken);
|
||||
return Ok(new { url });
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "File not found: {FileId}", fileId);
|
||||
return NotFound(new { message = $"File with ID {fileId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating file URL: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while generating the file URL." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{fileId}/thumbnail")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetFileThumbnail(
|
||||
Guid fileId,
|
||||
[FromQuery] int? width = null,
|
||||
[FromQuery] int? height = null,
|
||||
[FromQuery] int? expiresInSeconds = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
TimeSpan? expiry = expiresInSeconds.HasValue
|
||||
? TimeSpan.FromSeconds(expiresInSeconds.Value)
|
||||
: null;
|
||||
|
||||
var url = await _fileService.GetFileThumbnailUrlAsync(
|
||||
fileId,
|
||||
width,
|
||||
height,
|
||||
expiry,
|
||||
cancellationToken);
|
||||
|
||||
return Ok(new { url });
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "File not found: {FileId}", fileId);
|
||||
return NotFound(new { message = $"File with ID {fileId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating thumbnail URL: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while generating the thumbnail URL." });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
325
DysonNetwork.Drive/Controllers/FileReferenceController.cs
Normal file
325
DysonNetwork.Drive/Controllers/FileReferenceController.cs
Normal file
@ -0,0 +1,325 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Drive.Interfaces;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Drive.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/references")]
|
||||
[Authorize]
|
||||
public class FileReferenceController : ControllerBase
|
||||
{
|
||||
private readonly IFileReferenceService _referenceService;
|
||||
private readonly ILogger<FileReferenceController> _logger;
|
||||
|
||||
public FileReferenceController(
|
||||
IFileReferenceService referenceService,
|
||||
ILogger<FileReferenceController> logger)
|
||||
{
|
||||
_referenceService = referenceService ?? throw new ArgumentNullException(nameof(referenceService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CloudFileReference), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateReference([FromBody] CreateReferenceRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reference = await _referenceService.CreateReferenceAsync(
|
||||
request.FileId,
|
||||
request.ResourceId,
|
||||
request.ResourceType,
|
||||
request.ReferenceType,
|
||||
request.ReferenceId,
|
||||
request.ReferenceName,
|
||||
request.ReferenceMimeType,
|
||||
request.ReferenceSize,
|
||||
request.ReferenceUrl,
|
||||
request.ReferenceThumbnailUrl,
|
||||
request.ReferencePreviewUrl,
|
||||
request.ReferenceMetadata,
|
||||
request.Metadata,
|
||||
cancellationToken);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetReference),
|
||||
new { referenceId = reference.Id },
|
||||
reference);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating file reference for file {FileId}", request.FileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while creating the file reference." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{referenceId}")]
|
||||
[ProducesResponseType(typeof(CloudFileReference), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetReference(Guid referenceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reference = await _referenceService.GetReferenceAsync(referenceId, cancellationToken);
|
||||
return Ok(reference);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Reference not found: {ReferenceId}", referenceId);
|
||||
return NotFound(new { message = $"Reference with ID {referenceId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving reference: {ReferenceId}", referenceId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while retrieving the reference." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("file/{fileId}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CloudFileReference>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetReferencesForFile(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var references = await _referenceService.GetReferencesForFileAsync(fileId, cancellationToken);
|
||||
return Ok(references);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving references for file: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while retrieving references for the file." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("resource/{resourceType}/{resourceId}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CloudFileReference>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetReferencesForResource(
|
||||
string resourceType,
|
||||
string resourceId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var references = await _referenceService.GetReferencesForResourceAsync(resourceId, resourceType, cancellationToken);
|
||||
return Ok(references);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving references for resource: {ResourceType}/{ResourceId}", resourceType, resourceId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while retrieving references for the resource." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("type/{referenceType}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CloudFileReference>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetReferencesOfType(string referenceType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var references = await _referenceService.GetReferencesOfTypeAsync(referenceType, cancellationToken);
|
||||
return Ok(references);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving references of type: {ReferenceType}", referenceType);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while retrieving references of the specified type." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{referenceId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteReference(Guid referenceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _referenceService.DeleteReferenceAsync(referenceId, cancellationToken);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(new { message = $"Reference with ID {referenceId} not found." });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting reference: {ReferenceId}", referenceId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while deleting the reference." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("file/{fileId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> DeleteReferencesForFile(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await _referenceService.DeleteReferencesForFileAsync(fileId, cancellationToken);
|
||||
return Ok(new { count });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting references for file: {FileId}", fileId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while deleting references for the file." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("resource/{resourceType}/{resourceId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> DeleteReferencesForResource(
|
||||
string resourceType,
|
||||
string resourceId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await _referenceService.DeleteReferencesForResourceAsync(resourceId, resourceType, cancellationToken);
|
||||
return Ok(new { count });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting references for resource: {ResourceType}/{ResourceId}", resourceType, resourceId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while deleting references for the resource." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{referenceId}/metadata")]
|
||||
[ProducesResponseType(typeof(CloudFileReference), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateReferenceMetadata(
|
||||
Guid referenceId,
|
||||
[FromBody] Dictionary<string, object> metadata,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (metadata == null || metadata.Count == 0)
|
||||
{
|
||||
return BadRequest(new { message = "No metadata provided." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reference = await _referenceService.UpdateReferenceMetadataAsync(referenceId, metadata, cancellationToken);
|
||||
return Ok(reference);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Reference not found: {ReferenceId}", referenceId);
|
||||
return NotFound(new { message = $"Reference with ID {referenceId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating reference metadata: {ReferenceId}", referenceId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while updating the reference metadata." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{referenceId}/resource")]
|
||||
[ProducesResponseType(typeof(CloudFileReference), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateReferenceResource(
|
||||
Guid referenceId,
|
||||
[FromBody] UpdateReferenceResourceRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reference = await _referenceService.UpdateReferenceResourceAsync(
|
||||
referenceId,
|
||||
request.NewResourceId,
|
||||
request.NewResourceType,
|
||||
cancellationToken);
|
||||
|
||||
return Ok(reference);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Reference not found: {ReferenceId}", referenceId);
|
||||
return NotFound(new { message = $"Reference with ID {referenceId} not found." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating reference resource: {ReferenceId}", referenceId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while updating the reference resource." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("exists/{fileId}/{resourceType}/{resourceId}")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> HasReference(
|
||||
Guid fileId,
|
||||
string resourceType,
|
||||
string resourceId,
|
||||
[FromQuery] string? referenceType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exists = await _referenceService.HasReferenceAsync(
|
||||
fileId,
|
||||
resourceId,
|
||||
resourceType,
|
||||
referenceType,
|
||||
cancellationToken);
|
||||
|
||||
return Ok(new { exists });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error checking reference existence - File: {FileId}, Resource: {ResourceType}/{ResourceId}, ReferenceType: {ReferenceType}",
|
||||
fileId,
|
||||
resourceType,
|
||||
resourceId,
|
||||
referenceType);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "An error occurred while checking reference existence." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateReferenceRequest
|
||||
{
|
||||
public Guid FileId { get; set; }
|
||||
public string ResourceId { get; set; } = null!;
|
||||
public string ResourceType { get; set; } = null!;
|
||||
public string ReferenceType { get; set; } = null!;
|
||||
public string? ReferenceId { get; set; }
|
||||
public string? ReferenceName { get; set; }
|
||||
public string? ReferenceMimeType { get; set; }
|
||||
public long? ReferenceSize { get; set; }
|
||||
public string? ReferenceUrl { get; set; }
|
||||
public string? ReferenceThumbnailUrl { get; set; }
|
||||
public string? ReferencePreviewUrl { get; set; }
|
||||
public string? ReferenceMetadata { get; set; }
|
||||
public Dictionary<string, object>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateReferenceResourceRequest
|
||||
{
|
||||
public string NewResourceId { get; set; } = null!;
|
||||
public string NewResourceType { get; set; } = null!;
|
||||
}
|
||||
}
|
127
DysonNetwork.Drive/Data/AppDatabase.cs
Normal file
127
DysonNetwork.Drive/Data/AppDatabase.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using DysonNetwork.Drive.Extensions;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
|
||||
|
||||
namespace DysonNetwork.Drive.Data;
|
||||
|
||||
public class AppDatabase : DbContext, IDisposable
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public AppDatabase(DbContextOptions<AppDatabase> options, IConfiguration configuration)
|
||||
: base(options)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public DbSet<CloudFile> Files { get; set; } = null!;
|
||||
public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (!optionsBuilder.IsConfigured)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
_configuration.GetConnectionString("DefaultConnection"),
|
||||
o => o.UseNodaTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Apply snake_case naming convention for all entities
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
// Replace table names
|
||||
entity.SetTableName(entity.GetTableName()?.ToSnakeCase());
|
||||
|
||||
// Replace column names
|
||||
foreach (var property in entity.GetProperties())
|
||||
{
|
||||
property.SetColumnName(property.Name.ToSnakeCase());
|
||||
}
|
||||
|
||||
// Replace keys
|
||||
foreach (var key in entity.GetKeys())
|
||||
{
|
||||
key.SetName(key.GetName()?.ToSnakeCase());
|
||||
}
|
||||
|
||||
// Replace foreign keys
|
||||
foreach (var key in entity.GetForeignKeys())
|
||||
{
|
||||
key.SetConstraintName(key.GetConstraintName()?.ToSnakeCase());
|
||||
}
|
||||
|
||||
// Replace indexes
|
||||
foreach (var index in entity.GetIndexes())
|
||||
{
|
||||
index.SetDatabaseName(index.GetDatabaseName()?.ToSnakeCase());
|
||||
}
|
||||
}
|
||||
|
||||
// Configure CloudFile entity
|
||||
modelBuilder.Entity<CloudFile>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.StoragePath).IsUnique();
|
||||
entity.HasIndex(e => e.ContentHash);
|
||||
entity.HasIndex(e => e.UploadedById);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
|
||||
entity.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
entity.Property(e => e.Name).IsRequired();
|
||||
entity.Property(e => e.OriginalName).IsRequired();
|
||||
entity.Property(e => e.MimeType).IsRequired();
|
||||
entity.Property(e => e.StoragePath).IsRequired();
|
||||
|
||||
// Configure JSONB column for ExtendedMetadata
|
||||
entity.Property(e => e.ExtendedMetadata)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
// Configure relationships
|
||||
entity.HasMany(e => e.References)
|
||||
.WithOne(e => e.File)
|
||||
.HasForeignKey(e => e.FileId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Configure CloudFileReference entity
|
||||
modelBuilder.Entity<CloudFileReference>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => new { e.ResourceId, e.ResourceType, e.ReferenceType });
|
||||
entity.HasIndex(e => e.ReferenceId);
|
||||
|
||||
entity.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
entity.Property(e => e.ResourceId).IsRequired();
|
||||
entity.Property(e => e.ResourceType).IsRequired();
|
||||
entity.Property(e => e.ReferenceType).IsRequired();
|
||||
|
||||
// Configure JSONB column for Metadata
|
||||
entity.Property(e => e.Metadata)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
// Configure relationship with CloudFile
|
||||
entity.HasOne(e => e.File)
|
||||
.WithMany(e => e.References)
|
||||
.HasForeignKey(e => e.FileId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
35
DysonNetwork.Drive/DysonNetwork.Drive.csproj
Normal file
35
DysonNetwork.Drive/DysonNetwork.Drive.csproj
Normal file
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Minio" Version="6.0.5" />
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
|
||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
43
DysonNetwork.Drive/Extensions/StringExtensions.cs
Normal file
43
DysonNetwork.Drive/Extensions/StringExtensions.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DysonNetwork.Drive.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
private static readonly Regex _matchFirstCap = new(@"(.)([A-Z][a-z])");
|
||||
private static readonly Regex _matchAllCap = new(@"([a-z0-9])([A-Z])");
|
||||
|
||||
public static string ToSnakeCase(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return input;
|
||||
|
||||
// Handle the first character
|
||||
var result = new StringBuilder();
|
||||
result.Append(char.ToLowerInvariant(input[0]));
|
||||
|
||||
// Process the rest of the string
|
||||
for (int i = 1; i < input.Length; i++)
|
||||
{
|
||||
if (char.IsUpper(input[i]))
|
||||
{
|
||||
result.Append('_');
|
||||
result.Append(char.ToLowerInvariant(input[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any remaining uppercase letters with lowercase
|
||||
var output = result.ToString().ToLowerInvariant();
|
||||
|
||||
// Handle special cases (acronyms)
|
||||
output = _matchFirstCap.Replace(output, "$1_$2");
|
||||
output = _matchAllCap.Replace(output, "$1_$2");
|
||||
|
||||
return output.ToLowerInvariant();
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio.DataModel.Args;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
// Using fully qualified names to avoid ambiguity with DysonNetwork.Common.Models
|
||||
using DysonNetwork.Drive.Attributes;
|
||||
using DysonNetwork.Drive.Models;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
[ApiController]
|
||||
[Route("/files")]
|
||||
@ -79,7 +83,7 @@ public class FileController(
|
||||
}
|
||||
|
||||
[HttpGet("{id}/info")]
|
||||
public async Task<ActionResult<CloudFile>> GetFileInfo(string id)
|
||||
public async Task<ActionResult<Models.CloudFile>> GetFileInfo(string id)
|
||||
{
|
||||
var file = await db.Files.FindAsync(id);
|
||||
if (file is null) return NotFound();
|
||||
@ -91,7 +95,7 @@ public class FileController(
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeleteFile(string id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var file = await db.Files
|
||||
@ -110,7 +114,7 @@ public class FileController(
|
||||
|
||||
[HttpPost("/maintenance/migrateReferences")]
|
||||
[Authorize]
|
||||
[RequiredPermission("maintenance", "files.references")]
|
||||
[RequiredPermission("maintenance.files.references")]
|
||||
public async Task<ActionResult> MigrateFileReferences()
|
||||
{
|
||||
await rms.ScanAndMigrateReferences();
|
@ -1,8 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using DysonNetwork.Sphere;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
/// <summary>
|
||||
/// Job responsible for cleaning up expired file references
|
@ -1,7 +1,10 @@
|
||||
using DysonNetwork.Common.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Common.Models;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public class FileReferenceService(AppDatabase db, FileService fileService, ICacheService cache)
|
||||
{
|
@ -1,8 +1,10 @@
|
||||
using EFCore.BulkExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Sphere;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public class FileReferenceMigrationService(AppDatabase db)
|
||||
{
|
||||
@ -101,56 +103,7 @@ public class FileReferenceMigrationService(AppDatabase db)
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanProfiles()
|
||||
{
|
||||
var profiles = await db.AccountProfiles
|
||||
.Where(p => p.PictureId != null || p.BackgroundId != null)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
if (profile is { PictureId: not null, Picture: null })
|
||||
{
|
||||
var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == profile.PictureId);
|
||||
if (avatarFile != null)
|
||||
{
|
||||
// Create a reference for the avatar file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = avatarFile.Id,
|
||||
File = avatarFile,
|
||||
Usage = "profile.picture",
|
||||
ResourceId = profile.Id.ToString()
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
profile.Picture = avatarFile.ToReferenceObject();
|
||||
db.AccountProfiles.Update(profile);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for the banner if it exists
|
||||
if (profile is not { BackgroundId: not null, Background: null }) continue;
|
||||
var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == profile.BackgroundId);
|
||||
if (bannerFile == null) continue;
|
||||
{
|
||||
// Create a reference for the banner file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = bannerFile.Id,
|
||||
File = bannerFile,
|
||||
Usage = "profile.background",
|
||||
ResourceId = profile.Id.ToString()
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
profile.Background = bannerFile.ToReferenceObject();
|
||||
db.AccountProfiles.Update(profile);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
private async Task ScanChatRooms()
|
||||
{
|
@ -1,15 +1,21 @@
|
||||
using System.Globalization;
|
||||
using FFMpegCore;
|
||||
using System.Security.Cryptography;
|
||||
using AngleSharp.Text;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Common.Services;
|
||||
using DysonNetwork.Sphere;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio;
|
||||
using Minio.DataModel.Args;
|
||||
using NetVips;
|
||||
using NodaTime;
|
||||
using tusdotnet.Stores;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using AngleSharp.Text;
|
||||
using FFMpegCore;
|
||||
using NetVips;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public class FileService(
|
||||
AppDatabase db,
|
||||
@ -79,7 +85,7 @@ public class FileService(
|
||||
MimeType = contentType,
|
||||
Size = fileSize,
|
||||
Hash = hash,
|
||||
AccountId = account.Id
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Hash == hash);
|
@ -1,6 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public interface IFlushHandler<T>
|
||||
{
|
@ -1,8 +1,11 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
|
||||
using EFCore.BulkExtensions;
|
||||
using Quartz;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage.Handlers;
|
||||
namespace DysonNetwork.Drive.Handlers;
|
||||
|
||||
public class ActionLogFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<ActionLog>
|
||||
{
|
@ -1,15 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using DysonNetwork.Drive.Auth;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using DysonNetwork.Common.Models;
|
||||
using System;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage.Handlers;
|
||||
|
||||
public class LastActiveInfo
|
||||
{
|
||||
public Auth.Session Session { get; set; } = null!;
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
public Instant SeenAt { get; set; }
|
||||
}
|
||||
namespace DysonNetwork.Drive.Handlers;
|
||||
|
||||
public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<LastActiveInfo>
|
||||
{
|
||||
@ -20,18 +18,18 @@ public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHa
|
||||
|
||||
// Remove duplicates by grouping on (sessionId, accountId), taking the most recent SeenAt
|
||||
var distinctItems = items
|
||||
.GroupBy(x => (SessionId: x.Session.Id, AccountId: x.Account.Id))
|
||||
.GroupBy(x => (SessionId: x.SessionId, AccountId: x.AccountId))
|
||||
.Select(g => g.OrderByDescending(x => x.SeenAt).First())
|
||||
.ToList();
|
||||
|
||||
// Build dictionaries so we can match session/account IDs to their new "last seen" timestamps
|
||||
var sessionIdMap = distinctItems
|
||||
.GroupBy(x => x.Session.Id)
|
||||
.ToDictionary(g => g.Key, g => g.Last().SeenAt);
|
||||
.GroupBy(x => x.SessionId)
|
||||
.ToDictionary(g => Guid.Parse(g.Key), g => g.Last().SeenAt);
|
||||
|
||||
var accountIdMap = distinctItems
|
||||
.GroupBy(x => x.Account.Id)
|
||||
.ToDictionary(g => g.Key, g => g.Last().SeenAt);
|
||||
.GroupBy(x => x.AccountId)
|
||||
.ToDictionary(g => Guid.Parse(g.Key), g => g.Last().SeenAt);
|
||||
|
||||
// Update sessions using native EF Core ExecuteUpdateAsync
|
||||
foreach (var kvp in sessionIdMap)
|
||||
@ -51,7 +49,7 @@ public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHa
|
||||
}
|
||||
}
|
||||
|
||||
public class LastActiveFlushJob(FlushBufferService fbs, ActionLogFlushHandler hdl) : IJob
|
||||
public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler hdl) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
@ -1,10 +1,12 @@
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using EFCore.BulkExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage.Handlers;
|
||||
namespace DysonNetwork.Drive.Handlers;
|
||||
|
||||
public class MessageReadReceiptFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<MessageReadReceipt>
|
||||
{
|
@ -1,12 +1,15 @@
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using DysonNetwork.Drive.Services;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage.Handlers;
|
||||
namespace DysonNetwork.Drive.Handlers;
|
||||
|
||||
public class PostViewFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<Post.PostViewInfo>
|
||||
public class PostViewFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<PostViewInfo>
|
||||
{
|
||||
public async Task FlushAsync(IReadOnlyList<Post.PostViewInfo> items)
|
||||
public async Task FlushAsync(IReadOnlyList<PostViewInfo> items)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
40
DysonNetwork.Drive/Interfaces/IFileReferenceService.cs
Normal file
40
DysonNetwork.Drive/Interfaces/IFileReferenceService.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Drive.Models;
|
||||
|
||||
namespace DysonNetwork.Drive.Interfaces;
|
||||
|
||||
public interface IFileReferenceService
|
||||
{
|
||||
Task<CloudFileReference> CreateReferenceAsync(
|
||||
Guid fileId,
|
||||
string resourceId,
|
||||
string resourceType,
|
||||
string referenceType,
|
||||
string? referenceId = null,
|
||||
string? referenceName = null,
|
||||
string? referenceMimeType = null,
|
||||
long? referenceSize = null,
|
||||
string? referenceUrl = null,
|
||||
string? referenceThumbnailUrl = null,
|
||||
string? referencePreviewUrl = null,
|
||||
string? referenceMetadata = null,
|
||||
IDictionary<string, object>? metadata = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CloudFileReference> GetReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<CloudFileReference>> GetReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<CloudFileReference>> GetReferencesForResourceAsync(string resourceId, string resourceType, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<CloudFileReference>> GetReferencesOfTypeAsync(string referenceType, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteReferencesForResourceAsync(string resourceId, string resourceType, CancellationToken cancellationToken = default);
|
||||
Task<CloudFileReference> UpdateReferenceMetadataAsync(Guid referenceId, IDictionary<string, object> metadata, CancellationToken cancellationToken = default);
|
||||
Task<bool> ReferenceExistsAsync(Guid referenceId, CancellationToken cancellationToken = default);
|
||||
Task<bool> HasReferenceAsync(Guid fileId, string resourceId, string resourceType, string? referenceType = null, CancellationToken cancellationToken = default);
|
||||
Task<CloudFileReference> UpdateReferenceResourceAsync(Guid referenceId, string newResourceId, string newResourceType, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<CloudFile>> GetFilesForResourceAsync(string resourceId, string resourceType, string? referenceType = null, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<CloudFile>> GetFilesForReferenceTypeAsync(string referenceType, CancellationToken cancellationToken = default);
|
||||
}
|
27
DysonNetwork.Drive/Interfaces/IFileService.cs
Normal file
27
DysonNetwork.Drive/Interfaces/IFileService.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Drive.Models;
|
||||
|
||||
namespace DysonNetwork.Drive.Interfaces;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
Task<CloudFile> GetFileAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<Stream> DownloadFileAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<CloudFile> UploadFileAsync(Stream fileStream, string fileName, string contentType, IDictionary<string, string>? metadata = null, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteFileAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<CloudFile> UpdateFileMetadataAsync(Guid fileId, IDictionary<string, string> metadata, CancellationToken cancellationToken = default);
|
||||
Task<bool> FileExistsAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<string> GetFileUrlAsync(Guid fileId, TimeSpan? expiry = null, CancellationToken cancellationToken = default);
|
||||
Task<string> GetFileThumbnailUrlAsync(Guid fileId, int? width = null, int? height = null, TimeSpan? expiry = null, CancellationToken cancellationToken = default);
|
||||
Task<CloudFile> CopyFileAsync(Guid sourceFileId, string? newName = null, IDictionary<string, string>? newMetadata = null, CancellationToken cancellationToken = default);
|
||||
Task<CloudFile> MoveFileAsync(Guid sourceFileId, string? newName = null, IDictionary<string, string>? newMetadata = null, CancellationToken cancellationToken = default);
|
||||
Task<CloudFile> RenameFileAsync(Guid fileId, string newName, CancellationToken cancellationToken = default);
|
||||
Task<long> GetFileSizeAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<string> GetFileHashAsync(Guid fileId, CancellationToken cancellationToken = default);
|
||||
Task<Stream> GetFileThumbnailAsync(Guid fileId, int? width = null, int? height = null, CancellationToken cancellationToken = default);
|
||||
Task<CloudFile> SetFileVisibilityAsync(Guid fileId, bool isPublic, CancellationToken cancellationToken = default);
|
||||
}
|
39
DysonNetwork.Drive/Models/Account.cs
Normal file
39
DysonNetwork.Drive/Models/Account.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace DysonNetwork.Drive.Models;
|
||||
|
||||
public class Account : ModelBase
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Username { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool IsVerified { get; set; } = false;
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<CloudFile> Files { get; set; } = new List<CloudFile>();
|
||||
|
||||
// Timestamps
|
||||
public DateTimeOffset? LastLoginAt { get; set; }
|
||||
|
||||
// Methods
|
||||
public bool HasPermission(Permission permission)
|
||||
{
|
||||
// TODO: Implement actual permission checking logic
|
||||
return true;
|
||||
}
|
||||
}
|
67
DysonNetwork.Drive/Models/CloudFile.cs
Normal file
67
DysonNetwork.Drive/Models/CloudFile.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Models;
|
||||
|
||||
public class CloudFile : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[MaxLength(256)]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string OriginalName { get; set; } = null!;
|
||||
|
||||
[MaxLength(256)]
|
||||
public string MimeType { get; set; } = null!;
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string StoragePath { get; set; } = null!;
|
||||
|
||||
[MaxLength(64)]
|
||||
public string StorageProvider { get; set; } = "local";
|
||||
|
||||
[MaxLength(64)]
|
||||
public string? ContentHash { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? ThumbnailPath { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? PreviewPath { get; set; }
|
||||
|
||||
public int? Width { get; set; }
|
||||
public int? Height { get; set; }
|
||||
public float? Duration { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? Metadata { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public JsonDocument? ExtendedMetadata { get; set; }
|
||||
|
||||
public bool IsPublic { get; set; }
|
||||
public bool IsTemporary { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
public Instant? ExpiresAt { get; set; }
|
||||
public new Instant? DeletedAt { get; set; }
|
||||
|
||||
public Guid? UploadedById { get; set; }
|
||||
public string? UploadedByType { get; set; }
|
||||
|
||||
public ICollection<CloudFileReference> References { get; set; } = new List<CloudFileReference>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ExtendedMetadata?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
61
DysonNetwork.Drive/Models/CloudFileReference.cs
Normal file
61
DysonNetwork.Drive/Models/CloudFileReference.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Models;
|
||||
|
||||
public class CloudFileReference : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string ResourceId { get; set; } = null!;
|
||||
|
||||
[MaxLength(256)]
|
||||
public string ResourceType { get; set; } = null!;
|
||||
|
||||
[MaxLength(256)]
|
||||
public string ReferenceType { get; set; } = null!;
|
||||
|
||||
[MaxLength(256)]
|
||||
public string? ReferenceId { get; set; }
|
||||
|
||||
[MaxLength(256)]
|
||||
public string? ReferenceName { get; set; }
|
||||
|
||||
[MaxLength(256)]
|
||||
public string? ReferenceMimeType { get; set; }
|
||||
|
||||
public long? ReferenceSize { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? ReferenceUrl { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? ReferenceThumbnailUrl { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? ReferencePreviewUrl { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? ReferenceMetadata { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public JsonDocument? Metadata { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public Instant? ExpiresAt { get; set; }
|
||||
|
||||
public Guid FileId { get; set; }
|
||||
public virtual CloudFile File { get; set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Metadata?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
10
DysonNetwork.Drive/Models/ModelBase.cs
Normal file
10
DysonNetwork.Drive/Models/ModelBase.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace DysonNetwork.Drive.Models;
|
||||
|
||||
public abstract class ModelBase
|
||||
{
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? UpdatedAt { get; set; }
|
||||
public DateTimeOffset? DeletedAt { get; set; }
|
||||
}
|
19
DysonNetwork.Drive/Models/Permission.cs
Normal file
19
DysonNetwork.Drive/Models/Permission.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace DysonNetwork.Drive.Models;
|
||||
|
||||
public enum Permission
|
||||
{
|
||||
// File permissions
|
||||
File_Read,
|
||||
File_Write,
|
||||
File_Delete,
|
||||
File_Share,
|
||||
|
||||
// Admin permissions
|
||||
Admin_Access,
|
||||
Admin_ManageUsers,
|
||||
Admin_ManageFiles,
|
||||
|
||||
// Special permissions
|
||||
BypassRateLimit,
|
||||
BypassQuota
|
||||
}
|
50
DysonNetwork.Drive/Models/Post.cs
Normal file
50
DysonNetwork.Drive/Models/Post.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DysonNetwork.Drive.Models;
|
||||
|
||||
public class Post : ModelBase
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(1024)]
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public Guid AuthorId { get; set; }
|
||||
public virtual Account? Author { get; set; }
|
||||
|
||||
public bool IsPublished { get; set; } = false;
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<PostViewInfo> Views { get; set; } = new List<PostViewInfo>();
|
||||
public virtual ICollection<CloudFileReference> Attachments { get; set; } = new List<CloudFileReference>();
|
||||
}
|
||||
|
||||
public class PostViewInfo
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public Guid PostId { get; set; }
|
||||
public virtual Post? Post { get; set; }
|
||||
|
||||
public string? UserAgent { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public string? Referrer { get; set; }
|
||||
|
||||
public string? ViewerId { get; set; }
|
||||
|
||||
public DateTimeOffset ViewedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
// Additional metadata
|
||||
public string? CountryCode { get; set; }
|
||||
public string? DeviceType { get; set; }
|
||||
public string? Platform { get; set; }
|
||||
public string? Browser { get; set; }
|
||||
}
|
193
DysonNetwork.Drive/Program.cs
Normal file
193
DysonNetwork.Drive/Program.cs
Normal file
@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Drive.Data;
|
||||
using DysonNetwork.Drive.Extensions;
|
||||
using DysonNetwork.Drive.Interfaces;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using DysonNetwork.Drive.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
using Npgsql;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add configuration
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
});
|
||||
|
||||
// Add NodaTime
|
||||
builder.Services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
|
||||
// Add database context
|
||||
builder.Services.AddDbContext<AppDatabase>((serviceProvider, options) =>
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
throw new InvalidOperationException("Database connection string 'DefaultConnection' not found.");
|
||||
}
|
||||
|
||||
options.UseNpgsql(
|
||||
connectionString,
|
||||
npgsqlOptions =>
|
||||
{
|
||||
npgsqlOptions.UseNodaTime();
|
||||
npgsqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName);
|
||||
});
|
||||
|
||||
options.UseSnakeCaseNamingConvention();
|
||||
});
|
||||
|
||||
// Register services
|
||||
builder.Services.AddScoped<IFileService, FileService>();
|
||||
builder.Services.AddScoped<IFileReferenceService, FileReferenceService>();
|
||||
|
||||
// Configure CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// Configure JWT Authentication
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = configuration["Jwt:Authority"];
|
||||
options.Audience = configuration["Jwt:Audience"];
|
||||
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
|
||||
});
|
||||
|
||||
// Configure Swagger
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "DysonNetwork.Drive API",
|
||||
Version = "v1",
|
||||
Description = "API for managing files and file references in the Dyson Network",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Dyson Network Team",
|
||||
Email = "support@dyson.network"
|
||||
}
|
||||
});
|
||||
|
||||
// Include XML comments for API documentation
|
||||
var xmlFile = $"{typeof(Program).Assembly.GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
if (File.Exists(xmlPath))
|
||||
{
|
||||
c.IncludeXmlComments(xmlPath);
|
||||
}
|
||||
|
||||
// Configure JWT Bearer Authentication for Swagger
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
|
||||
Name = "Authorization",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
},
|
||||
Scheme = "oauth2",
|
||||
Name = "Bearer",
|
||||
In = ParameterLocation.Header,
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Configure HTTP client for external services
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// Add health checks
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddDbContextCheck<AppDatabase>();
|
||||
|
||||
// Add logging
|
||||
builder.Services.AddLogging(configure => configure.AddConsole().AddDebug());
|
||||
|
||||
// Build the application
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "DysonNetwork.Drive API v1");
|
||||
c.RoutePrefix = "swagger";
|
||||
});
|
||||
}
|
||||
|
||||
// Apply database migrations
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
try
|
||||
{
|
||||
var dbContext = services.GetRequiredService<AppDatabase>();
|
||||
if (dbContext.Database.IsNpgsql())
|
||||
{
|
||||
await dbContext.Database.MigrateAsync();
|
||||
app.Logger.LogInformation("Database migrations applied successfully.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogError(ex, "An error occurred while applying database migrations.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
app.Logger.LogInformation("Starting DysonNetwork.Drive application...");
|
||||
|
||||
await app.RunAsync();
|
13
DysonNetwork.Drive/Properties/launchSettings.json
Normal file
13
DysonNetwork.Drive/Properties/launchSettings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"profiles": {
|
||||
"DysonNetwork.Drive": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5073",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
321
DysonNetwork.Drive/Services/FileReferenceService.cs
Normal file
321
DysonNetwork.Drive/Services/FileReferenceService.cs
Normal file
@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Drive.Data;
|
||||
using DysonNetwork.Drive.Interfaces;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Services;
|
||||
|
||||
public class FileReferenceService : IFileReferenceService, IDisposable
|
||||
{
|
||||
private readonly AppDatabase _dbContext;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IClock _clock;
|
||||
private readonly ILogger<FileReferenceService> _logger;
|
||||
private bool _disposed = false;
|
||||
|
||||
public FileReferenceService(
|
||||
AppDatabase dbContext,
|
||||
IFileService fileService,
|
||||
IClock clock,
|
||||
ILogger<FileReferenceService> logger)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
_fileService = fileService ?? throw new ArgumentNullException(nameof(fileService));
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CloudFileReference> CreateReferenceAsync(
|
||||
Guid fileId,
|
||||
string resourceId,
|
||||
string resourceType,
|
||||
string referenceType,
|
||||
string? referenceId = null,
|
||||
string? referenceName = null,
|
||||
string? referenceMimeType = null,
|
||||
long? referenceSize = null,
|
||||
string? referenceUrl = null,
|
||||
string? referenceThumbnailUrl = null,
|
||||
string? referencePreviewUrl = null,
|
||||
string? referenceMetadata = null,
|
||||
IDictionary<string, object>? metadata = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Verify file exists
|
||||
var fileExists = await _fileService.FileExistsAsync(fileId, cancellationToken);
|
||||
if (!fileExists)
|
||||
{
|
||||
throw new FileNotFoundException($"File with ID {fileId} not found.");
|
||||
}
|
||||
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FileId = fileId,
|
||||
ResourceId = resourceId,
|
||||
ResourceType = resourceType,
|
||||
ReferenceType = referenceType,
|
||||
ReferenceId = referenceId,
|
||||
ReferenceName = referenceName,
|
||||
ReferenceMimeType = referenceMimeType,
|
||||
ReferenceSize = referenceSize,
|
||||
ReferenceUrl = referenceUrl,
|
||||
ReferenceThumbnailUrl = referenceThumbnailUrl,
|
||||
ReferencePreviewUrl = referencePreviewUrl,
|
||||
ReferenceMetadata = referenceMetadata,
|
||||
IsActive = true,
|
||||
CreatedAt = _clock.GetCurrentInstant().ToDateTimeOffset()
|
||||
};
|
||||
|
||||
if (metadata != null && metadata.Any())
|
||||
{
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
reference.Metadata = JsonDocument.Parse(JsonSerializer.Serialize(metadata, options));
|
||||
}
|
||||
|
||||
_dbContext.FileReferences.Add(reference);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created reference {ReferenceId} for file {FileId} to resource {ResourceType}/{ResourceId}",
|
||||
reference.Id, fileId, resourceType, resourceId);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<CloudFileReference> GetReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reference = await _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Include(r => r.File)
|
||||
.FirstOrDefaultAsync(r => r.Id == referenceId, cancellationToken);
|
||||
|
||||
if (reference == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Reference with ID {referenceId} not found.");
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CloudFileReference>> GetReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Where(r => r.FileId == fileId && r.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CloudFileReference>> GetReferencesForResourceAsync(
|
||||
string resourceId,
|
||||
string resourceType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Where(r => r.ResourceId == resourceId &&
|
||||
r.ResourceType == resourceType &&
|
||||
r.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CloudFileReference>> GetReferencesOfTypeAsync(
|
||||
string referenceType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Where(r => r.ReferenceType == referenceType && r.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reference = await _dbContext.FileReferences
|
||||
.FirstOrDefaultAsync(r => r.Id == referenceId, cancellationToken);
|
||||
|
||||
if (reference == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
reference.IsActive = false;
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Deleted reference {ReferenceId}", referenceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var references = await _dbContext.FileReferences
|
||||
.Where(r => r.FileId == fileId && r.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
reference.IsActive = false;
|
||||
}
|
||||
|
||||
var count = await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Deleted {Count} references for file {FileId}", count, fileId);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteReferencesForResourceAsync(
|
||||
string resourceId,
|
||||
string resourceType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var references = await _dbContext.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId &&
|
||||
r.ResourceType == resourceType &&
|
||||
r.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
reference.IsActive = false;
|
||||
}
|
||||
|
||||
var count = await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Deleted {Count} references for resource {ResourceType}/{ResourceId}",
|
||||
count, resourceType, resourceId);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task<CloudFileReference> UpdateReferenceMetadataAsync(
|
||||
Guid referenceId,
|
||||
IDictionary<string, object> metadata,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reference = await GetReferenceAsync(referenceId, cancellationToken);
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
reference.Metadata = JsonDocument.Parse(JsonSerializer.Serialize(metadata, options));
|
||||
reference.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset();
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Updated metadata for reference {ReferenceId}", referenceId);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<bool> ReferenceExistsAsync(Guid referenceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.AnyAsync(r => r.Id == referenceId && r.IsActive, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> HasReferenceAsync(
|
||||
Guid fileId,
|
||||
string resourceId,
|
||||
string resourceType,
|
||||
string? referenceType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Where(r => r.FileId == fileId &&
|
||||
r.ResourceId == resourceId &&
|
||||
r.ResourceType == resourceType &&
|
||||
r.IsActive);
|
||||
|
||||
if (!string.IsNullOrEmpty(referenceType))
|
||||
{
|
||||
query = query.Where(r => r.ReferenceType == referenceType);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<CloudFileReference> UpdateReferenceResourceAsync(
|
||||
Guid referenceId,
|
||||
string newResourceId,
|
||||
string newResourceType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reference = await GetReferenceAsync(referenceId, cancellationToken);
|
||||
|
||||
reference.ResourceId = newResourceId;
|
||||
reference.ResourceType = newResourceType;
|
||||
reference.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset();
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Updated reference {ReferenceId} to point to resource {ResourceType}/{ResourceId}",
|
||||
referenceId, newResourceType, newResourceId);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CloudFile>> GetFilesForResourceAsync(
|
||||
string resourceId,
|
||||
string resourceType,
|
||||
string? referenceType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Include(r => r.File)
|
||||
.Where(r => r.ResourceId == resourceId &&
|
||||
r.ResourceType == resourceType &&
|
||||
r.IsActive);
|
||||
|
||||
if (!string.IsNullOrEmpty(referenceType))
|
||||
{
|
||||
query = query.Where(r => r.ReferenceType == referenceType);
|
||||
}
|
||||
|
||||
var references = await query.ToListAsync(cancellationToken);
|
||||
return references.Select(r => r.File!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CloudFile>> GetFilesForReferenceTypeAsync(
|
||||
string referenceType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var references = await _dbContext.FileReferences
|
||||
.AsNoTracking()
|
||||
.Include(r => r.File)
|
||||
.Where(r => r.ReferenceType == referenceType && r.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return references.Select(r => r.File!);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_dbContext?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
301
DysonNetwork.Drive/Services/FileService.cs
Normal file
301
DysonNetwork.Drive/Services/FileService.cs
Normal file
@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Drive.Data;
|
||||
using DysonNetwork.Drive.Extensions;
|
||||
using DysonNetwork.Drive.Interfaces;
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Services;
|
||||
|
||||
public class FileService : IFileService, IDisposable
|
||||
{
|
||||
private readonly ILogger<FileService> _logger;
|
||||
private readonly AppDatabase _dbContext;
|
||||
private readonly IClock _clock;
|
||||
private bool _disposed = false;
|
||||
|
||||
public FileService(AppDatabase dbContext, IClock clock, ILogger<FileService> logger)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CloudFile> GetFileAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await _dbContext.Files
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(f => f.Id == fileId, cancellationToken);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
throw new FileNotFoundException($"File with ID {fileId} not found.");
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadFileAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await GetFileAsync(fileId, cancellationToken);
|
||||
|
||||
// In a real implementation, this would stream the file from storage (e.g., S3, local filesystem)
|
||||
// For now, we'll return a MemoryStream with a placeholder
|
||||
var placeholder = $"This is a placeholder for file {fileId} with name {file.Name}";
|
||||
var memoryStream = new MemoryStream();
|
||||
var writer = new StreamWriter(memoryStream);
|
||||
await writer.WriteAsync(placeholder);
|
||||
await writer.FlushAsync();
|
||||
memoryStream.Position = 0;
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> UploadFileAsync(
|
||||
Stream fileStream,
|
||||
string fileName,
|
||||
string contentType,
|
||||
IDictionary<string, string>? metadata = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (fileStream == null) throw new ArgumentNullException(nameof(fileStream));
|
||||
if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentNullException(nameof(fileName));
|
||||
if (string.IsNullOrWhiteSpace(contentType)) throw new ArgumentNullException(nameof(contentType));
|
||||
|
||||
// In a real implementation, this would upload to a storage service
|
||||
var now = _clock.GetCurrentInstant();
|
||||
var file = new CloudFile
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = Path.GetFileName(fileName),
|
||||
OriginalName = fileName,
|
||||
MimeType = contentType,
|
||||
Size = fileStream.Length,
|
||||
StoragePath = $"uploads/{now.ToUnixTimeMilliseconds()}/{Guid.NewGuid()}/{Path.GetFileName(fileName)}",
|
||||
StorageProvider = "local", // or "s3", "azure", etc.
|
||||
CreatedAt = now.ToDateTimeOffset(),
|
||||
IsPublic = false,
|
||||
IsTemporary = false,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
if (metadata != null)
|
||||
{
|
||||
file.Metadata = System.Text.Json.JsonSerializer.Serialize(metadata);
|
||||
}
|
||||
|
||||
_dbContext.Files.Add(file);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Uploaded file {FileId} with name {FileName}", file.Id, file.Name);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await _dbContext.Files.FindAsync(new object[] { fileId }, cancellationToken);
|
||||
if (file == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// In a real implementation, this would also delete the file from storage
|
||||
file.IsDeleted = true;
|
||||
file.DeletedAt = _clock.GetCurrentInstant();
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Soft-deleted file {FileId}", fileId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> UpdateFileMetadataAsync(Guid fileId, IDictionary<string, string> metadata, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await GetFileAsync(fileId, cancellationToken);
|
||||
|
||||
file.Metadata = System.Text.Json.JsonSerializer.Serialize(metadata);
|
||||
var now = _clock.GetCurrentInstant();
|
||||
file.UpdatedAt = new DateTimeOffset(now.ToDateTimeUtc(), TimeSpan.Zero);
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Updated metadata for file {FileId}", fileId);
|
||||
return file;
|
||||
}
|
||||
|
||||
public Task<bool> FileExistsAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _dbContext.Files
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.Id == fileId && !f.IsDeleted, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> GetFileUrlAsync(Guid fileId, TimeSpan? expiry = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// In a real implementation, this would generate a signed URL with the specified expiry
|
||||
return Task.FromResult($"https://storage.dyson.network/files/{fileId}");
|
||||
}
|
||||
|
||||
public Task<string> GetFileThumbnailUrlAsync(Guid fileId, int? width = null, int? height = null, TimeSpan? expiry = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// In a real implementation, this would generate a signed thumbnail URL
|
||||
var size = width.HasValue || height.HasValue
|
||||
? $"_{width ?? 0}x{height ?? 0}"
|
||||
: string.Empty;
|
||||
|
||||
return Task.FromResult($"https://storage.dyson.network/thumbnails/{fileId}{size}");
|
||||
}
|
||||
|
||||
public async Task<CloudFile> CopyFileAsync(Guid sourceFileId, string? newName = null, IDictionary<string, string>? newMetadata = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sourceFile = await GetFileAsync(sourceFileId, cancellationToken);
|
||||
|
||||
var newFile = new CloudFile
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = newName ?? sourceFile.Name,
|
||||
OriginalName = sourceFile.OriginalName,
|
||||
MimeType = sourceFile.MimeType,
|
||||
Size = sourceFile.Size,
|
||||
StoragePath = $"copies/{_clock.GetCurrentInstant().ToUnixTimeMilliseconds()}/{Guid.NewGuid()}/{sourceFile.Name}",
|
||||
StorageProvider = sourceFile.StorageProvider,
|
||||
ContentHash = sourceFile.ContentHash,
|
||||
ThumbnailPath = sourceFile.ThumbnailPath,
|
||||
PreviewPath = sourceFile.PreviewPath,
|
||||
Width = sourceFile.Width,
|
||||
Height = sourceFile.Height,
|
||||
Duration = sourceFile.Duration,
|
||||
Metadata = newMetadata != null
|
||||
? System.Text.Json.JsonSerializer.Serialize(newMetadata)
|
||||
: sourceFile.Metadata,
|
||||
IsPublic = sourceFile.IsPublic,
|
||||
IsTemporary = sourceFile.IsTemporary,
|
||||
IsDeleted = false,
|
||||
ExpiresAt = sourceFile.ExpiresAt,
|
||||
UploadedById = sourceFile.UploadedById,
|
||||
UploadedByType = sourceFile.UploadedByType,
|
||||
CreatedAt = _clock.GetCurrentInstant().ToDateTimeOffset(),
|
||||
UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset()
|
||||
};
|
||||
|
||||
_dbContext.Files.Add(newFile);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Copied file {SourceFileId} to {NewFileId}", sourceFileId, newFile.Id);
|
||||
|
||||
return newFile;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> MoveFileAsync(Guid sourceFileId, string? newName = null, IDictionary<string, string>? newMetadata = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sourceFile = await GetFileAsync(sourceFileId, cancellationToken);
|
||||
|
||||
// In a real implementation, this would move the file in storage
|
||||
var newPath = $"moved/{_clock.GetCurrentInstant().ToUnixTimeMilliseconds()}/{Guid.NewGuid()}/{newName ?? sourceFile.Name}";
|
||||
|
||||
sourceFile.Name = newName ?? sourceFile.Name;
|
||||
sourceFile.StoragePath = newPath;
|
||||
sourceFile.UpdatedAt = _clock.GetCurrentInstant();
|
||||
|
||||
if (newMetadata != null)
|
||||
{
|
||||
sourceFile.Metadata = System.Text.Json.JsonSerializer.Serialize(newMetadata);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Moved file {FileId} to {NewPath}", sourceFileId, newPath);
|
||||
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> RenameFileAsync(Guid fileId, string newName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await GetFileAsync(fileId, cancellationToken);
|
||||
|
||||
file.Name = newName;
|
||||
file.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset();
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Renamed file {FileId} to {NewName}", fileId, newName);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task<long> GetFileSizeAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await GetFileAsync(fileId, cancellationToken);
|
||||
return file.Size;
|
||||
}
|
||||
|
||||
public async Task<string> GetFileHashAsync(Guid fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await GetFileAsync(fileId, cancellationToken);
|
||||
|
||||
if (string.IsNullOrEmpty(file.ContentHash))
|
||||
{
|
||||
// In a real implementation, this would compute the hash of the file content
|
||||
file.ContentHash = Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..16];
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return file.ContentHash;
|
||||
}
|
||||
|
||||
public async Task<Stream> GetFileThumbnailAsync(Guid fileId, int? width = null, int? height = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// In a real implementation, this would generate or retrieve a thumbnail
|
||||
var placeholder = $"This is a thumbnail for file {fileId} with size {width ?? 0}x{height ?? 0}";
|
||||
var memoryStream = new MemoryStream();
|
||||
var writer = new StreamWriter(memoryStream);
|
||||
await writer.WriteAsync(placeholder);
|
||||
await writer.FlushAsync();
|
||||
memoryStream.Position = 0;
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
public async Task<CloudFile> SetFileVisibilityAsync(Guid fileId, bool isPublic, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await GetFileAsync(fileId, cancellationToken);
|
||||
|
||||
if (file.IsPublic != isPublic)
|
||||
{
|
||||
file.IsPublic = isPublic;
|
||||
file.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset();
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Set visibility of file {FileId} to {Visibility}", fileId, isPublic ? "public" : "private");
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_dbContext?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
13
DysonNetwork.Drive/Services/ICacheService.cs
Normal file
13
DysonNetwork.Drive/Services/ICacheService.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Drive.Services;
|
||||
|
||||
public interface ICacheService
|
||||
{
|
||||
Task<T?> GetAsync<T>(string key);
|
||||
Task SetAsync<T>(string key, T value, System.TimeSpan? expiry = null);
|
||||
Task RemoveAsync(string key);
|
||||
Task<bool> ExistsAsync(string key);
|
||||
Task<long> IncrementAsync(string key, long value = 1);
|
||||
Task<long> DecrementAsync(string key, long value = 1);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public abstract class TextSanitizer
|
||||
{
|
@ -1,14 +1,17 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using tusdotnet.Interfaces;
|
||||
using tusdotnet.Models;
|
||||
using tusdotnet.Models.Configuration;
|
||||
// Using fully qualified names to avoid ambiguity with DysonNetwork.Common.Models
|
||||
using DysonNetwork.Drive.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
public abstract class TusService
|
||||
{
|
17
DysonNetwork.Pass/Connection/DummyConnection.cs
Normal file
17
DysonNetwork.Pass/Connection/DummyConnection.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace DysonNetwork.Pass.Connection;
|
||||
|
||||
public class GeoIpService
|
||||
{
|
||||
// Dummy class
|
||||
}
|
||||
|
||||
public class WebSocketService
|
||||
{
|
||||
// Dummy class
|
||||
}
|
||||
|
||||
public class WebSocketPacket
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public object Data { get; set; } = null!;
|
||||
}
|
407
DysonNetwork.Pass/Data/PassDatabase.cs
Normal file
407
DysonNetwork.Pass/Data/PassDatabase.cs
Normal file
@ -0,0 +1,407 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Pass.Features.Auth.Models;
|
||||
// Permission types are now in DysonNetwork.Common.Models
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using Account = DysonNetwork.Common.Models.Account;
|
||||
using AccountConnection = DysonNetwork.Common.Models.AccountConnection;
|
||||
using AccountAuthFactor = DysonNetwork.Common.Models.AccountAuthFactor;
|
||||
using AuthSession = DysonNetwork.Pass.Features.Auth.Models.AuthSession;
|
||||
using AuthChallenge = DysonNetwork.Pass.Features.Auth.Models.AuthChallenge;
|
||||
|
||||
namespace DysonNetwork.Pass.Data;
|
||||
|
||||
public class PassDatabase(
|
||||
DbContextOptions<PassDatabase> options,
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<PermissionNode> PermissionNodes { get; set; }
|
||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
||||
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
|
||||
|
||||
public DbSet<MagicSpell> MagicSpells { get; set; }
|
||||
public DbSet<Account> Accounts { get; set; } = null!;
|
||||
public DbSet<AccountConnection> AccountConnections { get; set; } = null!;
|
||||
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; } = null!;
|
||||
public DbSet<Relationship> AccountRelationships { get; set; }
|
||||
public DbSet<Notification> Notifications { get; set; }
|
||||
public DbSet<Badge> Badges { get; set; }
|
||||
public DbSet<ActionLog> ActionLogs { get; set; }
|
||||
public DbSet<AbuseReport> AbuseReports { get; set; }
|
||||
|
||||
public DbSet<AuthSession> AuthSessions { get; set; }
|
||||
public DbSet<AuthChallenge> AuthChallenges { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
configuration.GetConnectionString("App"),
|
||||
opt => opt
|
||||
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||
.UseNetTopologySuite()
|
||||
.UseNodaTime()
|
||||
).UseSnakeCaseNamingConvention();
|
||||
|
||||
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||
{
|
||||
// Add any initial seeding logic here if needed for PassDatabase
|
||||
});
|
||||
|
||||
optionsBuilder.UseSeeding((context, _) => {});
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<PermissionGroupMember>()
|
||||
.HasKey(pg => new { pg.GroupId, pg.Actor });
|
||||
modelBuilder.Entity<PermissionGroupMember>()
|
||||
.HasOne(pg => pg.Group)
|
||||
.WithMany(g => g.Members)
|
||||
.HasForeignKey(pg => pg.GroupId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Relationship>()
|
||||
.HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId });
|
||||
modelBuilder.Entity<Relationship>()
|
||||
.HasOne(r => r.Account)
|
||||
.WithMany(a => a.OutgoingRelationships)
|
||||
.HasForeignKey(r => r.AccountId);
|
||||
modelBuilder.Entity<Relationship>()
|
||||
.HasOne(r => r.Related)
|
||||
.WithMany(a => a.IncomingRelationships)
|
||||
.HasForeignKey(r => r.RelatedId);
|
||||
|
||||
// Configure AuthSession
|
||||
modelBuilder.Entity<AuthSession>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
entity.Property(e => e.Label)
|
||||
.HasMaxLength(500);
|
||||
|
||||
entity.Property(e => e.LastGrantedAt)
|
||||
.IsRequired();
|
||||
|
||||
entity.Property(e => e.ExpiredAt)
|
||||
.IsRequired();
|
||||
|
||||
entity.Property(e => e.AccessToken)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
entity.Property(e => e.RefreshToken)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
entity.Property(e => e.IpAddress)
|
||||
.HasMaxLength(128);
|
||||
|
||||
entity.Property(e => e.UserAgent)
|
||||
.HasMaxLength(500);
|
||||
|
||||
entity.HasOne(s => s.Account)
|
||||
.WithMany(a => a.Sessions)
|
||||
.HasForeignKey(s => s.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(s => s.Challenge)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.ChallengeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.Property(e => e.Metadata)
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
// Configure AuthChallenge
|
||||
modelBuilder.Entity<AuthChallenge>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Type)
|
||||
.IsRequired()
|
||||
.HasConversion<string>();
|
||||
|
||||
entity.Property(e => e.Platform)
|
||||
.IsRequired()
|
||||
.HasConversion<string>();
|
||||
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
entity.Property(e => e.ExpiredAt);
|
||||
|
||||
entity.Property(e => e.StepRemain)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1);
|
||||
|
||||
entity.Property(e => e.StepTotal)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1);
|
||||
|
||||
entity.Property(e => e.FailedAttempts)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0);
|
||||
|
||||
entity.Property(e => e.IpAddress)
|
||||
.HasMaxLength(128);
|
||||
|
||||
entity.Property(e => e.UserAgent)
|
||||
.HasMaxLength(512);
|
||||
|
||||
entity.Property(e => e.DeviceId)
|
||||
.HasMaxLength(256);
|
||||
|
||||
entity.Property(e => e.Nonce)
|
||||
.HasMaxLength(1024);
|
||||
|
||||
entity.Property(e => e.BlacklistFactors)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
entity.Property(e => e.Audiences)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
entity.Property(e => e.Scopes)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
entity.HasOne<Account>()
|
||||
.WithMany(a => a.Challenges)
|
||||
.HasForeignKey(e => e.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.Ignore(e => e.Location); // Ignore Point type as it's not directly supported by EF Core
|
||||
});
|
||||
|
||||
// Configure AccountAuthFactor
|
||||
modelBuilder.Entity<AccountAuthFactor>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
entity.Property(e => e.FactorType)
|
||||
.IsRequired()
|
||||
.HasConversion<string>();
|
||||
|
||||
entity.Property(e => e.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
entity.Property(e => e.Description)
|
||||
.HasMaxLength(500);
|
||||
|
||||
entity.Property(e => e.Secret)
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024);
|
||||
|
||||
entity.Property(e => e.IsDefault)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false);
|
||||
|
||||
entity.Property(e => e.IsBackup)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false);
|
||||
|
||||
entity.Property(e => e.LastUsedAt);
|
||||
entity.Property(e => e.EnabledAt);
|
||||
entity.Property(e => e.DisabledAt);
|
||||
|
||||
entity.Property(e => e.Metadata)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
entity.HasOne(f => f.Account)
|
||||
.WithMany(a => a.AuthFactors)
|
||||
.HasForeignKey(f => f.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Remove the incorrect relationship configuration
|
||||
// The relationship is already defined in the AuthSession configuration
|
||||
});
|
||||
|
||||
// Configure Account
|
||||
modelBuilder.Entity<Account>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Email)
|
||||
.IsRequired()
|
||||
.HasMaxLength(256);
|
||||
|
||||
entity.Property(e => e.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(256);
|
||||
|
||||
entity.Property(e => e.Status)
|
||||
.HasMaxLength(32);
|
||||
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
entity.Property(e => e.UpdatedAt)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
// Configure AccountConnection
|
||||
modelBuilder.Entity<AccountConnection>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Provider)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
entity.Property(e => e.ProviderId)
|
||||
.IsRequired()
|
||||
.HasMaxLength(256);
|
||||
|
||||
entity.Property(e => e.DisplayName)
|
||||
.HasMaxLength(256);
|
||||
|
||||
entity.Property(e => e.AccessToken)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
entity.Property(e => e.RefreshToken)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
entity.Property(e => e.ExpiresAt);
|
||||
|
||||
entity.Property(e => e.ProfileData)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
entity.HasOne<Account>()
|
||||
.WithMany(a => a.Connections)
|
||||
.HasForeignKey(e => e.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue;
|
||||
var method = typeof(PassDatabase)
|
||||
.GetMethod(nameof(SetSoftDeleteFilter),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.MakeGenericMethod(entityType.ClrType);
|
||||
|
||||
method.Invoke(null, [modelBuilder]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
|
||||
where TEntity : ModelBase
|
||||
{
|
||||
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
||||
{
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
entry.Entity.CreatedAt = now;
|
||||
entry.Entity.UpdatedAt = now;
|
||||
break;
|
||||
case EntityState.Modified:
|
||||
entry.Entity.UpdatedAt = now;
|
||||
break;
|
||||
case EntityState.Deleted:
|
||||
entry.State = EntityState.Modified;
|
||||
entry.Entity.DeletedAt = now;
|
||||
break;
|
||||
case EntityState.Detached:
|
||||
case EntityState.Unchanged:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class PassDatabaseFactory : IDesignTimeDbContextFactory<PassDatabase>
|
||||
{
|
||||
public PassDatabase CreateDbContext(string[] args)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<PassDatabase>();
|
||||
return new PassDatabase(optionsBuilder.Options, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
public class PassDatabaseRecyclingJob(PassDatabase db, ILogger<PassDatabaseRecyclingJob> logger) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
logger.LogInformation("Cleaning up expired records...");
|
||||
|
||||
// Expired relationships
|
||||
var affectedRows = await db.AccountRelationships
|
||||
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
||||
.ExecuteDeleteAsync();
|
||||
logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows);
|
||||
|
||||
logger.LogInformation("Deleting soft-deleted records...");
|
||||
|
||||
var threshold = now - Duration.FromDays(7);
|
||||
|
||||
var entityTypes = db.Model.GetEntityTypes()
|
||||
.Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase))
|
||||
.Select(t => t.ClrType);
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
|
||||
.MakeGenericMethod(entityType).Invoke(db, null)!;
|
||||
var parameter = Expression.Parameter(entityType, "e");
|
||||
var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt));
|
||||
var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?)));
|
||||
var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?)));
|
||||
var finalCondition = Expression.AndAlso(notNull, condition);
|
||||
var lambda = Expression.Lambda(finalCondition, parameter);
|
||||
|
||||
var queryable = set.Provider.CreateQuery(
|
||||
Expression.Call(
|
||||
typeof(Queryable),
|
||||
"Where",
|
||||
[entityType],
|
||||
set.Expression,
|
||||
Expression.Quote(lambda)
|
||||
)
|
||||
);
|
||||
|
||||
var toListAsync = typeof(EntityFrameworkQueryableExtensions)
|
||||
.GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))!
|
||||
.MakeGenericMethod(entityType);
|
||||
|
||||
var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!;
|
||||
db.RemoveRange(items);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
33
DysonNetwork.Pass/Developer/Developer.cs
Normal file
33
DysonNetwork.Pass/Developer/Developer.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Pass.Developer;
|
||||
|
||||
public class CustomApp
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(256)] public string Name { get; set; } = null!;
|
||||
[MaxLength(4096)] public string Description { get; set; } = null!;
|
||||
[MaxLength(1024)] public string Homepage { get; set; } = null!;
|
||||
[MaxLength(1024)] public string CallbackUrl { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public OauthConfig? OauthConfig { get; set; }
|
||||
|
||||
public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
}
|
||||
|
||||
public class CustomAppSecret
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(4096)] public string Secret { get; set; } = null!;
|
||||
public bool IsOidc { get; set; } = false;
|
||||
public DateTime? ExpiredAt { get; set; }
|
||||
|
||||
public Guid AppId { get; set; }
|
||||
[JsonIgnore] public CustomApp App { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class OauthConfig
|
||||
{
|
||||
public List<string>? AllowedScopes { get; set; }
|
||||
}
|
49
DysonNetwork.Pass/DysonNetwork.Pass.csproj
Normal file
49
DysonNetwork.Pass/DysonNetwork.Pass.csproj
Normal file
@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
|
||||
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Features\Account\Controllers\" />
|
||||
<Folder Include="Features\Account\Services\" />
|
||||
<Folder Include="Features\Auth\Controllers\" />
|
||||
<Folder Include="Features\Auth\Models\" />
|
||||
<Folder Include="Features\Auth\Services\" />
|
||||
<Folder Include="Data\" />
|
||||
<Folder Include="Email\" />
|
||||
<Folder Include="Developer\" />
|
||||
<Folder Include="Localization\" />
|
||||
<Folder Include="Storage\" />
|
||||
<Folder Include="Storage\Handlers\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
6
DysonNetwork.Pass/DysonNetwork.Pass.http
Normal file
6
DysonNetwork.Pass/DysonNetwork.Pass.http
Normal file
@ -0,0 +1,6 @@
|
||||
@DysonNetwork.Pass_HostAddress = http://localhost:5048
|
||||
|
||||
GET {{DysonNetwork.Pass_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
6
DysonNetwork.Pass/Email/EmailModels.cs
Normal file
6
DysonNetwork.Pass/Email/EmailModels.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Pass.Email;
|
||||
|
||||
public class EmailModels
|
||||
{
|
||||
// Dummy class for EmailModels
|
||||
}
|
10
DysonNetwork.Pass/Email/EmailService.cs
Normal file
10
DysonNetwork.Pass/Email/EmailService.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace DysonNetwork.Pass.Email;
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
public Task SendTemplatedEmailAsync<TTemplate, TModel>(string recipientName, string recipientEmail, string subject, TModel model) where TTemplate : class where TModel : class
|
||||
{
|
||||
// Dummy implementation
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
6
DysonNetwork.Pass/Email/RazorViewRenderer.cs
Normal file
6
DysonNetwork.Pass/Email/RazorViewRenderer.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Pass.Email;
|
||||
|
||||
public class RazorViewRenderer
|
||||
{
|
||||
// Dummy class for RazorViewRenderer
|
||||
}
|
45
DysonNetwork.Pass/Extensions/OptionalQueryExtensions.cs
Normal file
45
DysonNetwork.Pass/Extensions/OptionalQueryExtensions.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Collections;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
||||
namespace DysonNetwork.Pass.Extensions;
|
||||
|
||||
public static class OptionalQueryExtensions
|
||||
{
|
||||
public static IQueryable<T> If<
|
||||
T
|
||||
>(
|
||||
this IQueryable<T> source,
|
||||
bool condition,
|
||||
Func<IQueryable<T>, IQueryable<T>> transform
|
||||
)
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
|
||||
public static IQueryable<T> If<
|
||||
T,
|
||||
TP
|
||||
>(
|
||||
this IIncludableQueryable<T, TP> source,
|
||||
bool condition,
|
||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
|
||||
public static IQueryable<T> If<
|
||||
T,
|
||||
TP
|
||||
>(
|
||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
||||
bool condition,
|
||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
}
|
@ -1,28 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using DysonNetwork.Pass.Data;
|
||||
using DysonNetwork.Pass.Features.Auth;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Pass.Features.Account.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using NodaTime.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Features.Account.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("/accounts")]
|
||||
public class AccountController(
|
||||
AppDatabase db,
|
||||
PassDatabase db,
|
||||
AuthService auth,
|
||||
AccountService accounts,
|
||||
AccountEventService events
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<Common.Models.Account>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<Account?>> GetByName(string name)
|
||||
public async Task<ActionResult<Common.Models.Account?>> GetByName(string name)
|
||||
{
|
||||
var account = await db.Accounts
|
||||
.Include(e => e.Badges)
|
||||
@ -73,9 +72,9 @@ public class AccountController(
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<Common.Models.Account>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<Account>> CreateAccount([FromBody] AccountCreateRequest request)
|
||||
public async Task<ActionResult<Common.Models.Account>> CreateAccount([FromBody] AccountCreateRequest request)
|
||||
{
|
||||
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
|
||||
|
||||
@ -163,7 +162,7 @@ public class AccountController(
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
||||
public async Task<List<Common.Models.Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return [];
|
@ -1,20 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Pass.Data;
|
||||
using DysonNetwork.Pass.Features.Account.Services;
|
||||
using DysonNetwork.Pass.Features.Auth;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Features.Account.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("/accounts/me")]
|
||||
public class AccountCurrentController(
|
||||
AppDatabase db,
|
||||
PassDatabase db,
|
||||
AccountService accounts,
|
||||
FileReferenceService fileRefService,
|
||||
AccountEventService events,
|
||||
@ -22,10 +22,10 @@ public class AccountCurrentController(
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<Account>> GetCurrentIdentity()
|
||||
[ProducesResponseType<Common.Models.Account>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<Common.Models.Account>> GetCurrentIdentity()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var account = await db.Accounts
|
||||
@ -46,7 +46,7 @@ public class AccountCurrentController(
|
||||
[HttpPatch]
|
||||
public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
|
||||
|
||||
@ -77,7 +77,7 @@ public class AccountCurrentController(
|
||||
[HttpPatch("profile")]
|
||||
public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var profile = await db.AccountProfiles
|
||||
@ -162,7 +162,7 @@ public class AccountCurrentController(
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> RequestDeleteAccount()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -179,7 +179,7 @@ public class AccountCurrentController(
|
||||
[HttpGet("statuses")]
|
||||
public async Task<ActionResult<Status>> GetCurrentStatus()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
var status = await events.GetStatus(currentUser.Id);
|
||||
return Ok(status);
|
||||
}
|
||||
@ -188,7 +188,7 @@ public class AccountCurrentController(
|
||||
[RequiredPermission("global", "accounts.statuses.update")]
|
||||
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var status = await db.AccountStatuses
|
||||
@ -212,10 +212,10 @@ public class AccountCurrentController(
|
||||
}
|
||||
|
||||
[HttpPost("statuses")]
|
||||
[RequiredPermission("global", "accounts.statuses.create")]
|
||||
[DysonNetwork.Common.Services.Permission.RequiredPermission("global", "accounts.statuses.create")]
|
||||
public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var status = new Status
|
||||
{
|
||||
@ -233,7 +233,7 @@ public class AccountCurrentController(
|
||||
[HttpDelete("me/statuses")]
|
||||
public async Task<ActionResult> DeleteStatus()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var status = await db.AccountStatuses
|
||||
@ -250,7 +250,7 @@ public class AccountCurrentController(
|
||||
[HttpGet("check-in")]
|
||||
public async Task<ActionResult<CheckInResult>> GetCheckInResult()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
@ -270,7 +270,7 @@ public class AccountCurrentController(
|
||||
[HttpPost("check-in")]
|
||||
public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
|
||||
if (!isAvailable)
|
||||
@ -297,7 +297,7 @@ public class AccountCurrentController(
|
||||
public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month,
|
||||
[FromQuery] int? year)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
||||
month ??= currentDate.Month;
|
||||
@ -318,7 +318,7 @@ public class AccountCurrentController(
|
||||
[FromQuery] int offset = 0
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var query = db.ActionLogs
|
||||
.Where(log => log.AccountId == currentUser.Id)
|
||||
@ -338,7 +338,7 @@ public class AccountCurrentController(
|
||||
[HttpGet("factors")]
|
||||
public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var factors = await db.AccountAuthFactors
|
||||
.Include(f => f.Account)
|
||||
@ -358,7 +358,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
||||
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
||||
|
||||
@ -370,7 +370,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var factor = await db.AccountAuthFactors
|
||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||
@ -392,7 +392,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var factor = await db.AccountAuthFactors
|
||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||
@ -414,7 +414,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var factor = await db.AccountAuthFactors
|
||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||
@ -445,7 +445,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser ||
|
||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||
|
||||
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
|
||||
@ -475,13 +475,13 @@ public class AccountCurrentController(
|
||||
|
||||
[HttpGet("sessions")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Session>>> GetSessions(
|
||||
public async Task<ActionResult<List<AuthSession>>> GetSessions(
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] int offset = 0
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser ||
|
||||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
||||
|
||||
var query = db.AuthSessions
|
||||
.Include(session => session.Account)
|
||||
@ -505,7 +505,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Session>> DeleteSession(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -522,7 +522,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Session>> DeleteCurrentSession()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser ||
|
||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||
|
||||
try
|
||||
@ -537,9 +537,9 @@ public class AccountCurrentController(
|
||||
}
|
||||
|
||||
[HttpPatch("sessions/{id:guid}/label")]
|
||||
public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string label)
|
||||
public async Task<ActionResult<AuthSession>> UpdateSessionLabel(Guid id, [FromBody] string label)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -553,9 +553,9 @@ public class AccountCurrentController(
|
||||
}
|
||||
|
||||
[HttpPatch("sessions/current/label")]
|
||||
public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string label)
|
||||
public async Task<ActionResult<AuthSession>> UpdateCurrentSessionLabel([FromBody] string label)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser ||
|
||||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||
|
||||
try
|
||||
@ -573,7 +573,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<AccountContact>>> GetContacts()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var contacts = await db.AccountContacts
|
||||
.Where(c => c.AccountId == currentUser.Id)
|
||||
@ -592,7 +592,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -609,7 +609,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountContact>> VerifyContact(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var contact = await db.AccountContacts
|
||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||
@ -631,7 +631,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var contact = await db.AccountContacts
|
||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||
@ -653,7 +653,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AccountContact>> DeleteContact(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var contact = await db.AccountContacts
|
||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||
@ -676,7 +676,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Badge>>> GetBadges()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var badges = await db.Badges
|
||||
.Where(b => b.AccountId == currentUser.Id)
|
||||
@ -688,7 +688,7 @@ public class AccountCurrentController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Badge>> ActivateBadge(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
@ -1,10 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Pass.Data;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/spells")]
|
||||
public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase
|
||||
public class MagicSpellController(PassDatabase db, MagicSpellService sp) : ControllerBase
|
||||
{
|
||||
[HttpPost("{spellId:guid}/resend")]
|
||||
public async Task<ActionResult> ResendMagicSpell(Guid spellId)
|
@ -1,23 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Pass.Features.Auth;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Features.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/notifications")]
|
||||
public class NotificationController(AppDatabase db, NotificationService nty) : ControllerBase
|
||||
public class NotificationController(PassDatabase db, NotificationService nty) : ControllerBase
|
||||
{
|
||||
[HttpGet("count")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<int>> CountUnreadNotifications()
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||
if (currentUserValue is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var count = await db.Notifications
|
||||
.Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null)
|
||||
@ -35,7 +35,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||
if (currentUserValue is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var totalCount = await db.Notifications
|
||||
.Where(s => s.AccountId == currentUser.Id)
|
||||
@ -67,7 +67,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Account;
|
||||
var currentUser = currentUserValue as Common.Models.Account;
|
||||
if (currentUser == null) return Unauthorized();
|
||||
var currentSession = currentSessionValue as Session;
|
||||
if (currentSession == null) return Unauthorized();
|
||||
@ -85,7 +85,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Account;
|
||||
var currentUser = currentUserValue as Common.Models.Account;
|
||||
if (currentUser == null) return Unauthorized();
|
||||
var currentSession = currentSessionValue as Session;
|
||||
if (currentSession == null) return Unauthorized();
|
||||
@ -140,7 +140,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
|
||||
[HttpPost("send")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "notifications.send")]
|
||||
[DysonNetwork.Common.Services.Permission.RequiredPermission("global", "notifications.send")]
|
||||
public async Task<ActionResult> SendNotification(
|
||||
[FromBody] NotificationWithAimRequest request,
|
||||
[FromQuery] bool save = false
|
@ -1,21 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Data;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Features.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/relationships")]
|
||||
public class RelationshipController(AppDatabase db, RelationshipService rels) : ControllerBase
|
||||
public class RelationshipController(PassDatabase db, RelationshipService rels) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var query = db.AccountRelationships.AsQueryable()
|
||||
@ -46,7 +48,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Relationship>>> ListSentRequests()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relationships = await db.AccountRelationships
|
||||
.Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending)
|
||||
@ -69,7 +71,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId,
|
||||
[FromBody] RelationshipRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
@ -92,7 +94,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId,
|
||||
[FromBody] RelationshipRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -113,7 +115,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> GetRelationship(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
var queries = db.AccountRelationships.AsQueryable()
|
||||
@ -133,7 +135,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
@ -158,7 +160,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeleteFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -175,7 +177,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
|
||||
if (relationship is null) return NotFound("Friend request was not found.");
|
||||
@ -195,7 +197,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
|
||||
if (relationship is null) return NotFound("Friend request was not found.");
|
||||
@ -215,7 +217,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> BlockUser(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
||||
@ -235,7 +237,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Relationship>> UnblockUser(Guid userId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Common.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(userId);
|
||||
if (relatedUser is null) return NotFound("Account was not found.");
|
@ -0,0 +1,20 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Common.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface IAccountEventService
|
||||
{
|
||||
void PurgeStatusCache(Guid userId);
|
||||
Task<Status> GetStatus(Guid userId);
|
||||
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds);
|
||||
Task<Status> CreateStatus(Models.Account user, Status status);
|
||||
Task ClearStatus(Models.Account user, Status status);
|
||||
Task<bool> CheckInDailyDoAskCaptcha(Models.Account user);
|
||||
Task<bool> CheckInDailyIsAvailable(Models.Account user);
|
||||
Task<CheckInResult> CheckInDaily(Models.Account user);
|
||||
Task<List<DailyEventResponse>> GetEventCalendar(Models.Account user, int month, int year = 0, bool replaceInvisible = false);
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
using System.Globalization;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Pass.Features.Auth;
|
||||
using DysonNetwork.Pass.Features.Auth.OpenId;
|
||||
using DysonNetwork.Common.Models;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface IAccountService
|
||||
{
|
||||
static void SetCultureInfo(Models.Account account) { }
|
||||
static void SetCultureInfo(string? languageCode) { }
|
||||
Task PurgeAccountCache(Models.Account account);
|
||||
Task<Models.Account?> LookupAccount(string probe);
|
||||
Task<Models.Account?> LookupAccountByConnection(string identifier, string provider);
|
||||
Task<int?> GetAccountLevel(Guid accountId);
|
||||
Task<Models.Account> CreateAccount(
|
||||
string name,
|
||||
string nick,
|
||||
string email,
|
||||
string? password,
|
||||
string language = "en-US",
|
||||
bool isEmailVerified = false,
|
||||
bool isActivated = false
|
||||
);
|
||||
Task<Models.Account> CreateAccount(OidcUserInfo userInfo);
|
||||
Task RequestAccountDeletion(Models.Account account);
|
||||
Task RequestPasswordReset(Models.Account account);
|
||||
Task<bool> CheckAuthFactorExists(Models.Account account, AccountAuthFactorType type);
|
||||
Task<AccountAuthFactor?> CreateAuthFactor(Models.Account account, AccountAuthFactorType type, string? secret);
|
||||
Task<AccountAuthFactor> EnableAuthFactor(AccountAuthFactor factor, string? code);
|
||||
Task<AccountAuthFactor> DisableAuthFactor(AccountAuthFactor factor);
|
||||
Task DeleteAuthFactor(AccountAuthFactor factor);
|
||||
Task SendFactorCode(Models.Account account, AccountAuthFactor factor, string? hint = null);
|
||||
Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code);
|
||||
Task<Session> UpdateSessionLabel(Models.Account account, Guid sessionId, string label);
|
||||
Task DeleteSession(Models.Account account, Guid sessionId);
|
||||
Task<AccountContact> CreateContactMethod(Models.Account account, AccountContactType type, string content);
|
||||
Task VerifyContactMethod(Models.Account account, AccountContact contact);
|
||||
Task<AccountContact> SetContactMethodPrimary(Models.Account account, AccountContact contact);
|
||||
Task DeleteContactMethod(Models.Account account, AccountContact contact);
|
||||
Task<Badge> GrantBadge(Models.Account account, Badge badge);
|
||||
Task RevokeBadge(Models.Account account, Guid badgeId);
|
||||
Task ActiveBadge(Models.Account account, Guid badgeId);
|
||||
Task EnsureAccountProfileCreated();
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface IAccountUsernameService
|
||||
{
|
||||
Task<string> GenerateUniqueUsernameAsync(string baseName);
|
||||
string SanitizeUsername(string username);
|
||||
Task<bool> IsUsernameExistsAsync(string username);
|
||||
Task<string> GenerateUsernameFromEmailAsync(string email);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using DysonNetwork.Pass.Connection;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface IActionLogService
|
||||
{
|
||||
void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta);
|
||||
void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, Models.Account? account = null);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using NodaTime;
|
||||
using DysonNetwork.Pass.Features.Account;
|
||||
using DysonNetwork.Common.Models;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface IMagicSpellService
|
||||
{
|
||||
Task<MagicSpell> CreateMagicSpell(
|
||||
Models.Account account,
|
||||
MagicSpellType type,
|
||||
Dictionary<string, object> meta,
|
||||
Instant? expiredAt = null,
|
||||
Instant? affectedAt = null,
|
||||
bool preventRepeat = false
|
||||
);
|
||||
Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false);
|
||||
Task ApplyMagicSpell(MagicSpell spell);
|
||||
Task ApplyPasswordReset(MagicSpell spell, string newPassword);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Common.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
Task UnsubscribePushNotifications(string deviceId);
|
||||
Task<NotificationPushSubscription> SubscribePushNotification(
|
||||
Models.Account account,
|
||||
NotificationPushProvider provider,
|
||||
string deviceId,
|
||||
string deviceToken
|
||||
);
|
||||
Task<Notification> SendNotification(
|
||||
Models.Account account,
|
||||
string topic,
|
||||
string? title = null,
|
||||
string? subtitle = null,
|
||||
string? content = null,
|
||||
Dictionary<string, object>? meta = null,
|
||||
string? actionUri = null,
|
||||
bool isSilent = false,
|
||||
bool save = true
|
||||
);
|
||||
Task DeliveryNotification(Notification notification);
|
||||
Task MarkNotificationsViewed(ICollection<Notification> notifications);
|
||||
Task BroadcastNotification(Notification notification, bool save = false);
|
||||
Task SendNotificationBatch(Notification notification, List<Models.Account> accounts, bool save = false);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using DysonNetwork.Common.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Features.Account.Interfaces;
|
||||
|
||||
public interface IRelationshipService
|
||||
{
|
||||
Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId);
|
||||
Task<Relationship?> GetRelationship(
|
||||
Guid accountId,
|
||||
Guid relatedId,
|
||||
RelationshipStatus? status = null,
|
||||
bool ignoreExpired = false
|
||||
);
|
||||
Task<Relationship> CreateRelationship(Models.Account sender, Models.Account target, RelationshipStatus status);
|
||||
Task<Relationship> BlockAccount(Models.Account sender, Models.Account target);
|
||||
Task<Relationship> UnblockAccount(Models.Account sender, Models.Account target);
|
||||
Task<Relationship> SendFriendRequest(Models.Account sender, Models.Account target);
|
||||
Task DeleteFriendRequest(Guid accountId, Guid relatedId);
|
||||
Task<Relationship> AcceptFriendRelationship(
|
||||
Relationship relationship,
|
||||
RelationshipStatus status = RelationshipStatus.Friends
|
||||
);
|
||||
Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status);
|
||||
Task<List<Guid>> ListAccountFriends(Models.Account account);
|
||||
Task<List<Guid>> ListAccountBlocked(Models.Account account);
|
||||
Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, RelationshipStatus status = RelationshipStatus.Friends);
|
||||
}
|
@ -1,24 +1,30 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Sphere.Activity;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Sphere.Wallet;
|
||||
using DysonNetwork.Common.Models;
|
||||
using DysonNetwork.Pass.Data;
|
||||
using DysonNetwork.Pass.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NodaTime;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Features.Account.Services;
|
||||
|
||||
public class AccountEventService(
|
||||
AppDatabase db,
|
||||
WebSocketService ws,
|
||||
ICacheService cache,
|
||||
PaymentService payment,
|
||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||
)
|
||||
public class AccountEventService
|
||||
{
|
||||
private readonly PassDatabase db;
|
||||
private readonly ICacheService cache;
|
||||
private readonly IStringLocalizer<Localization.AccountEventResource> localizer;
|
||||
|
||||
public AccountEventService(
|
||||
PassDatabase db,
|
||||
ICacheService cache,
|
||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||
)
|
||||
{
|
||||
this.db = db;
|
||||
this.cache = cache;
|
||||
this.localizer = localizer;
|
||||
}
|
||||
|
||||
private static readonly Random Random = new();
|
||||
private const string StatusCacheKey = "AccountStatus_";
|
||||
|
||||
@ -139,7 +145,7 @@ public class AccountEventService(
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<Status> CreateStatus(Account user, Status status)
|
||||
public async Task<Status> CreateStatus(Common.Models.Account user, Status status)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.AccountStatuses
|
||||
@ -152,7 +158,7 @@ public class AccountEventService(
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task ClearStatus(Account user, Status status)
|
||||
public async Task ClearStatus(Common.Models.Account user, Status status)
|
||||
{
|
||||
status.ClearedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
db.Update(status);
|
||||
@ -164,7 +170,7 @@ public class AccountEventService(
|
||||
private const string CaptchaCacheKey = "CheckInCaptcha_";
|
||||
private const int CaptchaProbabilityPercent = 20;
|
||||
|
||||
public async Task<bool> CheckInDailyDoAskCaptcha(Account user)
|
||||
public async Task<bool> CheckInDailyDoAskCaptcha(Common.Models.Account user)
|
||||
{
|
||||
var cacheKey = $"{CaptchaCacheKey}{user.Id}";
|
||||
var needsCaptcha = await cache.GetAsync<bool?>(cacheKey);
|
||||
@ -176,7 +182,7 @@ public class AccountEventService(
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckInDailyIsAvailable(Account user)
|
||||
public async Task<bool> CheckInDailyIsAvailable(Common.Models.Account user)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var lastCheckIn = await db.AccountCheckInResults
|
||||
@ -195,7 +201,7 @@ public class AccountEventService(
|
||||
|
||||
public const string CheckInLockKey = "CheckInLock_";
|
||||
|
||||
public async Task<CheckInResult> CheckInDaily(Account user)
|
||||
public async Task<CheckInResult> CheckInDaily(Common.Models.Account user)
|
||||
{
|
||||
var lockKey = $"{CheckInLockKey}{user.Id}";
|
||||
|
||||
@ -280,7 +286,7 @@ public class AccountEventService(
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<DailyEventResponse>> GetEventCalendar(Account user, int month, int year = 0,
|
||||
public async Task<List<DailyEventResponse>> GetEventCalendar(Common.Models.Account user, int month, int year = 0,
|
||||
bool replaceInvisible = false)
|
||||
{
|
||||
if (year == 0)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user