♻️ Extract the Storage service to DysonNetwork.Drive microservice
This commit is contained in:
@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.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="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,9 @@ public class AuthSession : ModelBase
|
|||||||
public Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Models.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||||
public Guid ChallengeId { get; set; }
|
public Guid ChallengeId { get; set; }
|
||||||
public AuthChallenge AuthChallenge { get; set; } = null!;
|
public AuthChallenge Challenge { get; set; } = null!;
|
||||||
public Guid? AppId { get; set; }
|
public Guid? AppId { get; set; }
|
||||||
public CustomApp? App { get; set; }
|
public CustomApp? App { get; set; }
|
||||||
}
|
}
|
||||||
|
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 Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Drive;
|
||||||
|
|
||||||
public class CloudFileUnusedRecyclingJob(
|
public class CloudFileUnusedRecyclingJob(
|
||||||
AppDatabase db,
|
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);
|
||||||
|
}
|
||||||
|
}
|
31
DysonNetwork.Drive/DysonNetwork.Drive.csproj
Normal file
31
DysonNetwork.Drive/DysonNetwork.Drive.csproj
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.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.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="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>
|
||||||
|
</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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Minio.DataModel.Args;
|
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]
|
[ApiController]
|
||||||
[Route("/files")]
|
[Route("/files")]
|
||||||
@ -79,7 +83,7 @@ public class FileController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/info")]
|
[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);
|
var file = await db.Files.FindAsync(id);
|
||||||
if (file is null) return NotFound();
|
if (file is null) return NotFound();
|
||||||
@ -91,7 +95,7 @@ public class FileController(
|
|||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
public async Task<ActionResult> DeleteFile(string 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 userId = currentUser.Id;
|
||||||
|
|
||||||
var file = await db.Files
|
var file = await db.Files
|
||||||
@ -110,7 +114,7 @@ public class FileController(
|
|||||||
|
|
||||||
[HttpPost("/maintenance/migrateReferences")]
|
[HttpPost("/maintenance/migrateReferences")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "files.references")]
|
[RequiredPermission("maintenance.files.references")]
|
||||||
public async Task<ActionResult> MigrateFileReferences()
|
public async Task<ActionResult> MigrateFileReferences()
|
||||||
{
|
{
|
||||||
await rms.ScanAndMigrateReferences();
|
await rms.ScanAndMigrateReferences();
|
@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Drive;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Job responsible for cleaning up expired file references
|
/// Job responsible for cleaning up expired file references
|
@ -1,8 +1,10 @@
|
|||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
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)
|
public class FileReferenceService(AppDatabase db, FileService fileService, ICacheService cache)
|
||||||
{
|
{
|
@ -1,8 +1,10 @@
|
|||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Drive;
|
||||||
|
|
||||||
public class FileReferenceMigrationService(AppDatabase db)
|
public class FileReferenceMigrationService(AppDatabase db)
|
||||||
{
|
{
|
@ -1,16 +1,21 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using FFMpegCore;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using AngleSharp.Text;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Minio;
|
using Minio;
|
||||||
using Minio.DataModel.Args;
|
using Minio.DataModel.Args;
|
||||||
using NetVips;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using tusdotnet.Stores;
|
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(
|
public class FileService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Drive;
|
||||||
|
|
||||||
public interface IFlushHandler<T>
|
public interface IFlushHandler<T>
|
||||||
{
|
{
|
@ -1,8 +1,11 @@
|
|||||||
|
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Quartz;
|
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>
|
public class ActionLogFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<ActionLog>
|
||||||
{
|
{
|
@ -1,13 +1,16 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
using DysonNetwork.Drive.Auth;
|
||||||
|
using DysonNetwork.Drive.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage.Handlers;
|
namespace DysonNetwork.Drive.Handlers;
|
||||||
|
|
||||||
public class LastActiveInfo
|
public class LastActiveInfo
|
||||||
{
|
{
|
||||||
public Auth.Session Session { get; set; } = null!;
|
public Session Session { get; set; } = null!;
|
||||||
public Account.Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
public Instant SeenAt { get; set; }
|
public Instant SeenAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +54,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)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
{
|
{
|
@ -1,11 +1,12 @@
|
|||||||
using DysonNetwork.Common.Models;
|
|
||||||
using DysonNetwork.Sphere.Chat;
|
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
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>
|
public class MessageReadReceiptFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<MessageReadReceipt>
|
||||||
{
|
{
|
@ -1,13 +1,15 @@
|
|||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Drive.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
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();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
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.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Drive;
|
||||||
|
|
||||||
public abstract class TextSanitizer
|
public abstract class TextSanitizer
|
||||||
{
|
{
|
@ -1,14 +1,17 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using tusdotnet.Interfaces;
|
using tusdotnet.Interfaces;
|
||||||
using tusdotnet.Models;
|
using tusdotnet.Models;
|
||||||
using tusdotnet.Models.Configuration;
|
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
|
public abstract class TusService
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -44,6 +44,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.csproj" />
|
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -46,7 +46,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPatch]
|
[HttpPatch]
|
||||||
public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
|
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);
|
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPatch("profile")]
|
[HttpPatch("profile")]
|
||||||
public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request)
|
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 userId = currentUser.Id;
|
||||||
|
|
||||||
var profile = await db.AccountProfiles
|
var profile = await db.AccountProfiles
|
||||||
@ -162,7 +162,7 @@ public class AccountCurrentController(
|
|||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
public async Task<ActionResult> RequestDeleteAccount()
|
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
|
try
|
||||||
{
|
{
|
||||||
@ -179,7 +179,7 @@ public class AccountCurrentController(
|
|||||||
[HttpGet("statuses")]
|
[HttpGet("statuses")]
|
||||||
public async Task<ActionResult<Status>> GetCurrentStatus()
|
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);
|
var status = await events.GetStatus(currentUser.Id);
|
||||||
return Ok(status);
|
return Ok(status);
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ public class AccountCurrentController(
|
|||||||
[RequiredPermission("global", "accounts.statuses.update")]
|
[RequiredPermission("global", "accounts.statuses.update")]
|
||||||
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
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 now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var status = await db.AccountStatuses
|
||||||
@ -212,10 +212,10 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("statuses")]
|
[HttpPost("statuses")]
|
||||||
[RequiredPermission("global", "accounts.statuses.create")]
|
[DysonNetwork.Sphere.Permission.RequiredPermission("global", "accounts.statuses.create")]
|
||||||
public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request)
|
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
|
var status = new Status
|
||||||
{
|
{
|
||||||
@ -233,7 +233,7 @@ public class AccountCurrentController(
|
|||||||
[HttpDelete("me/statuses")]
|
[HttpDelete("me/statuses")]
|
||||||
public async Task<ActionResult> DeleteStatus()
|
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 now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var status = await db.AccountStatuses
|
||||||
@ -250,7 +250,7 @@ public class AccountCurrentController(
|
|||||||
[HttpGet("check-in")]
|
[HttpGet("check-in")]
|
||||||
public async Task<ActionResult<CheckInResult>> GetCheckInResult()
|
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 userId = currentUser.Id;
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
@ -270,7 +270,7 @@ public class AccountCurrentController(
|
|||||||
[HttpPost("check-in")]
|
[HttpPost("check-in")]
|
||||||
public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken)
|
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);
|
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
|
||||||
if (!isAvailable)
|
if (!isAvailable)
|
||||||
@ -297,7 +297,7 @@ public class AccountCurrentController(
|
|||||||
public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month,
|
public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month,
|
||||||
[FromQuery] int? year)
|
[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;
|
var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
||||||
month ??= currentDate.Month;
|
month ??= currentDate.Month;
|
||||||
@ -318,7 +318,7 @@ public class AccountCurrentController(
|
|||||||
[FromQuery] int offset = 0
|
[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
|
var query = db.ActionLogs
|
||||||
.Where(log => log.AccountId == currentUser.Id)
|
.Where(log => log.AccountId == currentUser.Id)
|
||||||
@ -338,7 +338,7 @@ public class AccountCurrentController(
|
|||||||
[HttpGet("factors")]
|
[HttpGet("factors")]
|
||||||
public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors()
|
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
|
var factors = await db.AccountAuthFactors
|
||||||
.Include(f => f.Account)
|
.Include(f => f.Account)
|
||||||
@ -358,7 +358,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request)
|
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))
|
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
||||||
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
||||||
|
|
||||||
@ -370,7 +370,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code)
|
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
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
@ -392,7 +392,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id)
|
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
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
@ -414,7 +414,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id)
|
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
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
@ -445,7 +445,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices()
|
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();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
|
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
|
||||||
@ -475,13 +475,13 @@ public class AccountCurrentController(
|
|||||||
|
|
||||||
[HttpGet("sessions")]
|
[HttpGet("sessions")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<Session>>> GetSessions(
|
public async Task<ActionResult<List<AuthSession>>> GetSessions(
|
||||||
[FromQuery] int take = 20,
|
[FromQuery] int take = 20,
|
||||||
[FromQuery] int offset = 0
|
[FromQuery] int offset = 0
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
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();
|
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
||||||
|
|
||||||
var query = db.AuthSessions
|
var query = db.AuthSessions
|
||||||
.Include(session => session.Account)
|
.Include(session => session.Account)
|
||||||
@ -505,7 +505,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Session>> DeleteSession(Guid id)
|
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
|
try
|
||||||
{
|
{
|
||||||
@ -522,7 +522,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Session>> DeleteCurrentSession()
|
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();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -537,9 +537,9 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("sessions/{id:guid}/label")]
|
[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
|
try
|
||||||
{
|
{
|
||||||
@ -553,9 +553,9 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("sessions/current/label")]
|
[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();
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -573,7 +573,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<AccountContact>>> GetContacts()
|
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
|
var contacts = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id)
|
.Where(c => c.AccountId == currentUser.Id)
|
||||||
@ -592,7 +592,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request)
|
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
|
try
|
||||||
{
|
{
|
||||||
@ -609,7 +609,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> VerifyContact(Guid id)
|
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
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||||
@ -631,7 +631,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id)
|
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
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||||
@ -653,7 +653,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<AccountContact>> DeleteContact(Guid id)
|
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
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
|
||||||
@ -676,7 +676,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<Badge>>> GetBadges()
|
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
|
var badges = await db.Badges
|
||||||
.Where(b => b.AccountId == currentUser.Id)
|
.Where(b => b.AccountId == currentUser.Id)
|
||||||
@ -688,7 +688,7 @@ public class AccountCurrentController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Badge>> ActivateBadge(Guid id)
|
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
|
try
|
||||||
{
|
{
|
||||||
|
@ -140,7 +140,7 @@ public class NotificationController(PassDatabase db, NotificationService nty) :
|
|||||||
|
|
||||||
[HttpPost("send")]
|
[HttpPost("send")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "notifications.send")]
|
[DysonNetwork.Sphere.Permission.RequiredPermission("global", "notifications.send")]
|
||||||
public async Task<ActionResult> SendNotification(
|
public async Task<ActionResult> SendNotification(
|
||||||
[FromBody] NotificationWithAimRequest request,
|
[FromBody] NotificationWithAimRequest request,
|
||||||
[FromQuery] bool save = false
|
[FromQuery] bool save = false
|
||||||
|
@ -3,7 +3,7 @@ using DysonNetwork.Pass.Features.Auth;
|
|||||||
using DysonNetwork.Pass.Features.Auth.OpenId;
|
using DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
using DysonNetwork.Pass.Email;
|
using DysonNetwork.Pass.Email;
|
||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Pass.Storage;
|
using DysonNetwork.Pass.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -3,7 +3,7 @@ using DysonNetwork.Pass.Storage;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Account;
|
namespace DysonNetwork.Pass.Features.Account;
|
||||||
|
|
||||||
|
@ -218,7 +218,7 @@ public class AuthController(
|
|||||||
if (session is not null)
|
if (session is not null)
|
||||||
return BadRequest("Session already exists for this challenge.");
|
return BadRequest("Session already exists for this challenge.");
|
||||||
|
|
||||||
session = new Session
|
var session = new AuthSession
|
||||||
{
|
{
|
||||||
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||||
|
@ -3,11 +3,14 @@ using System.Security.Cryptography;
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using DysonNetwork.Pass.Features.Account;
|
using DysonNetwork.Pass.Features.Account;
|
||||||
using DysonNetwork.Pass.Features.Auth.OidcProvider.Services;
|
using DysonNetwork.Pass.Features.Auth.OidcProvider.Services;
|
||||||
using DysonNetwork.Pass.Storage;
|
using DysonNetwork.Common.Services;
|
||||||
using DysonNetwork.Pass.Storage.Handlers;
|
using DysonNetwork.Drive.Handlers;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SystemClock = NodaTime.SystemClock;
|
using NodaTime;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
using DysonNetwork.Drive;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.Services;
|
namespace DysonNetwork.Pass.Features.Auth.Services;
|
||||||
|
|
||||||
@ -57,14 +60,14 @@ public class DysonTokenAuthHandler(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = NodaTime.SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
// Validate token and extract session ID
|
// Validate token and extract session ID
|
||||||
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
||||||
return AuthenticateResult.Fail("Invalid token.");
|
return AuthenticateResult.Fail("Invalid token.");
|
||||||
|
|
||||||
// Try to get session from cache first
|
// Try to get session from cache first
|
||||||
var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
|
var session = await cache.GetAsync<AuthSession>($"{AuthCachePrefix}{sessionId}");
|
||||||
|
|
||||||
// If not in cache, load from database
|
// If not in cache, load from database
|
||||||
if (session is null)
|
if (session is null)
|
||||||
@ -126,7 +129,7 @@ public class DysonTokenAuthHandler(
|
|||||||
{
|
{
|
||||||
Account = session.Account,
|
Account = session.Account,
|
||||||
Session = session,
|
Session = session,
|
||||||
SeenAt = SystemClock.Instance.GetCurrentInstant(),
|
SeenAt = NodaTime.SystemClock.Instance.GetCurrentInstant(),
|
||||||
};
|
};
|
||||||
fbs.Enqueue(lastInfo);
|
fbs.Enqueue(lastInfo);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ using DysonNetwork.Pass.Storage;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using DysonNetwork.Pass.Data;
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth;
|
namespace DysonNetwork.Pass.Features.Auth;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth;
|
namespace DysonNetwork.Pass.Features.Auth;
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ public class CompactTokenService(IConfiguration config)
|
|||||||
private readonly string _privateKeyPath = config["AuthToken:PrivateKeyPath"]
|
private readonly string _privateKeyPath = config["AuthToken:PrivateKeyPath"]
|
||||||
?? throw new InvalidOperationException("AuthToken:PrivateKeyPath configuration is missing");
|
?? throw new InvalidOperationException("AuthToken:PrivateKeyPath configuration is missing");
|
||||||
|
|
||||||
public string CreateToken(Session session)
|
public string CreateToken(AuthSession session)
|
||||||
{
|
{
|
||||||
// Load the private key for signing
|
// Load the private key for signing
|
||||||
var privateKeyPem = File.ReadAllText(_privateKeyPath);
|
var privateKeyPem = File.ReadAllText(_privateKeyPath);
|
||||||
|
@ -5,6 +5,8 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
@ -14,11 +16,12 @@ namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
|||||||
public class AppleOidcService(
|
public class AppleOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
PassDatabase db,
|
PassDatabase passDb,
|
||||||
|
AppDatabase sphereDb,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
: OidcService(configuration, httpClientFactory, db, auth, cache)
|
: OidcService(configuration, httpClientFactory, passDb, sphereDb, auth, cache)
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration = configuration;
|
private readonly IConfiguration _configuration = configuration;
|
||||||
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Pass.Storage;
|
using DysonNetwork.Common.Services;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
public class DiscordOidcService(
|
public class DiscordOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
PassDatabase db,
|
PassDatabase passDb,
|
||||||
|
AppDatabase sphereDb,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
: OidcService(configuration, httpClientFactory, db, auth, cache)
|
: OidcService(configuration, httpClientFactory, passDb, sphereDb, auth, cache)
|
||||||
{
|
{
|
||||||
public override string ProviderName => "Discord";
|
public override string ProviderName => "Discord";
|
||||||
protected override string DiscoveryEndpoint => ""; // Discord doesn't have a standard OIDC discovery endpoint
|
protected override string DiscoveryEndpoint => ""; // Discord doesn't have a standard OIDC discovery endpoint
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Pass.Storage;
|
using DysonNetwork.Common.Services;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
public class GitHubOidcService(
|
public class GitHubOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
PassDatabase db,
|
PassDatabase passDb,
|
||||||
|
AppDatabase sphereDb,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
: OidcService(configuration, httpClientFactory, db, auth, cache)
|
: OidcService(configuration, httpClientFactory, passDb, sphereDb, auth, cache)
|
||||||
{
|
{
|
||||||
public override string ProviderName => "GitHub";
|
public override string ProviderName => "GitHub";
|
||||||
protected override string DiscoveryEndpoint => ""; // GitHub doesn't have a standard OIDC discovery endpoint
|
protected override string DiscoveryEndpoint => ""; // GitHub doesn't have a standard OIDC discovery endpoint
|
||||||
@ -77,7 +80,7 @@ public class GitHubOidcService(
|
|||||||
var client = HttpClientFactory.CreateClient();
|
var client = HttpClientFactory.CreateClient();
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user");
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user");
|
||||||
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
request.Headers.Add("User-Agent", "DysonNetwork.Sphere");
|
request.Headers.Add("User-Agent", "DysonNetwork.Drive");
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@ -109,7 +112,7 @@ public class GitHubOidcService(
|
|||||||
var client = HttpClientFactory.CreateClient();
|
var client = HttpClientFactory.CreateClient();
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails");
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails");
|
||||||
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
request.Headers.Add("User-Agent", "DysonNetwork.Sphere");
|
request.Headers.Add("User-Agent", "DysonNetwork.Drive");
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
if (!response.IsSuccessStatusCode) return null;
|
if (!response.IsSuccessStatusCode) return null;
|
||||||
|
@ -4,17 +4,20 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
public class GoogleOidcService(
|
public class GoogleOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
PassDatabase db,
|
PassDatabase passDb,
|
||||||
|
AppDatabase sphereDb,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
: OidcService(configuration, httpClientFactory, db, auth, cache)
|
: OidcService(configuration, httpClientFactory, passDb, sphereDb, auth, cache)
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
||||||
|
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Pass.Storage;
|
using DysonNetwork.Common.Services;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
public class MicrosoftOidcService(
|
public class MicrosoftOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
PassDatabase db,
|
PassDatabase passDb,
|
||||||
|
AppDatabase sphereDb,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
: OidcService(configuration, httpClientFactory, db, auth, cache)
|
: OidcService(configuration, httpClientFactory, passDb, sphereDb, auth, cache)
|
||||||
{
|
{
|
||||||
public override string ProviderName => "Microsoft";
|
public override string ProviderName => "Microsoft";
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using DysonNetwork.Common.Services;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
using DysonNetwork.Sphere;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
@ -10,7 +12,8 @@ namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
|||||||
[Route("/auth/login")]
|
[Route("/auth/login")]
|
||||||
public class OidcController(
|
public class OidcController(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
PassDatabase db,
|
PassDatabase passDb,
|
||||||
|
AppDatabase sphereDb,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
@ -31,7 +34,7 @@ public class OidcController(
|
|||||||
var oidcService = GetOidcService(provider);
|
var oidcService = GetOidcService(provider);
|
||||||
|
|
||||||
// If the user is already authenticated, treat as an account connection request
|
// If the user is already authenticated, treat as an account connection request
|
||||||
if (HttpContext.Items["CurrentUser"] is Models.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is Account currentUser)
|
||||||
{
|
{
|
||||||
var state = Guid.NewGuid().ToString();
|
var state = Guid.NewGuid().ToString();
|
||||||
var nonce = Guid.NewGuid().ToString();
|
var nonce = Guid.NewGuid().ToString();
|
||||||
@ -67,7 +70,7 @@ public class OidcController(
|
|||||||
/// Handles Apple authentication directly from mobile apps
|
/// Handles Apple authentication directly from mobile apps
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("apple/mobile")]
|
[HttpPost("apple/mobile")]
|
||||||
public async Task<ActionResult<Challenge>> AppleMobileLogin(
|
public async Task<ActionResult<AuthChallenge>> AppleMobileSignIn(
|
||||||
[FromBody] AppleMobileSignInRequest request)
|
[FromBody] AppleMobileSignInRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -124,7 +127,7 @@ public class OidcController(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Models.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
|
private async Task<Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(userInfo.Email))
|
if (string.IsNullOrEmpty(userInfo.Email))
|
||||||
throw new ArgumentException("Email is required for account creation");
|
throw new ArgumentException("Email is required for account creation");
|
||||||
@ -134,15 +137,16 @@ public class OidcController(
|
|||||||
if (existingAccount != null)
|
if (existingAccount != null)
|
||||||
{
|
{
|
||||||
// Check if this provider connection already exists
|
// Check if this provider connection already exists
|
||||||
var existingConnection = await db.AccountConnections
|
var existingConnection = await passDb.AccountConnections
|
||||||
.FirstOrDefaultAsync(c => c.AccountId == existingAccount.Id &&
|
.FirstOrDefaultAsync(c => c.Provider == provider &&
|
||||||
c.Provider == provider &&
|
c.ProvidedIdentifier == userInfo.UserId &&
|
||||||
c.ProvidedIdentifier == userInfo.UserId);
|
c.AccountId == existingAccount.Id
|
||||||
|
);
|
||||||
|
|
||||||
// If no connection exists, create one
|
// If no connection exists, create one
|
||||||
if (existingConnection != null)
|
if (existingConnection != null)
|
||||||
{
|
{
|
||||||
await db.AccountConnections
|
await passDb.AccountConnections
|
||||||
.Where(c => c.AccountId == existingAccount.Id &&
|
.Where(c => c.AccountId == existingAccount.Id &&
|
||||||
c.Provider == provider &&
|
c.Provider == provider &&
|
||||||
c.ProvidedIdentifier == userInfo.UserId)
|
c.ProvidedIdentifier == userInfo.UserId)
|
||||||
@ -164,8 +168,8 @@ public class OidcController(
|
|||||||
Meta = userInfo.ToMetadata()
|
Meta = userInfo.ToMetadata()
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.AccountConnections.AddAsync(connection);
|
await passDb.AccountConnections.AddAsync(connection);
|
||||||
await db.SaveChangesAsync();
|
await passDb.SaveChangesAsync();
|
||||||
|
|
||||||
return existingAccount;
|
return existingAccount;
|
||||||
}
|
}
|
||||||
@ -185,8 +189,8 @@ public class OidcController(
|
|||||||
Meta = userInfo.ToMetadata()
|
Meta = userInfo.ToMetadata()
|
||||||
};
|
};
|
||||||
|
|
||||||
db.AccountConnections.Add(newConnection);
|
await passDb.AccountConnections.Add(newConnection);
|
||||||
await db.SaveChangesAsync();
|
await passDb.SaveChangesAsync();
|
||||||
|
|
||||||
return newAccount;
|
return newAccount;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ using DysonNetwork.Common.Services;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
using DysonNetwork.Pass.Data;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
namespace DysonNetwork.Pass.Features.Auth.OpenId;
|
||||||
|
|
||||||
@ -21,7 +23,7 @@ public abstract class OidcService(
|
|||||||
{
|
{
|
||||||
protected readonly IConfiguration Configuration = configuration;
|
protected readonly IConfiguration Configuration = configuration;
|
||||||
protected readonly IHttpClientFactory HttpClientFactory = httpClientFactory;
|
protected readonly IHttpClientFactory HttpClientFactory = httpClientFactory;
|
||||||
protected readonly AppDatabase Db = db;
|
protected readonly PassDatabase Db = db;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the unique identifier for this provider
|
/// Gets the unique identifier for this provider
|
||||||
|
@ -14,7 +14,7 @@ using NodaTime.Serialization.SystemTextJson;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using DysonNetwork.Pass.Email;
|
using DysonNetwork.Pass.Email;
|
||||||
using DysonNetwork.Pass.Developer;
|
using DysonNetwork.Pass.Developer;
|
||||||
using DysonNetwork.Pass.Features.Account.DysonNetwork.Pass.Features.Account;
|
using DysonNetwork.Pass.Features.Account;
|
||||||
using DysonNetwork.Pass.Features.Account.Services;
|
using DysonNetwork.Pass.Features.Account.Services;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
@ -1,180 +0,0 @@
|
|||||||
using DysonNetwork.Common.Models;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using DysonNetwork.Sphere.Sticker;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere;
|
|
||||||
|
|
||||||
public class AppDatabase(
|
|
||||||
DbContextOptions<AppDatabase> options,
|
|
||||||
IConfiguration configuration
|
|
||||||
) : DbContext(options)
|
|
||||||
{
|
|
||||||
public DbSet<CloudFile> Files { get; set; }
|
|
||||||
public DbSet<CloudFileReference> FileReferences { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Common.Models.Publisher> Publishers { get; set; }
|
|
||||||
|
|
||||||
public DbSet<PublisherFeature> PublisherFeatures { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Common.Models.Post> Posts { get; set; }
|
|
||||||
public DbSet<PostReaction> PostReactions { get; set; }
|
|
||||||
public DbSet<PostTag> PostTags { get; set; }
|
|
||||||
public DbSet<PostCategory> PostCategories { get; set; }
|
|
||||||
public DbSet<PostCollection> PostCollections { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Common.Models.Realm> Realms { get; set; }
|
|
||||||
public DbSet<RealmMember> RealmMembers { get; set; }
|
|
||||||
public DbSet<Tag> Tags { get; set; }
|
|
||||||
public DbSet<RealmTag> RealmTags { get; set; }
|
|
||||||
|
|
||||||
public DbSet<ChatRoom> ChatRooms { get; set; }
|
|
||||||
public DbSet<ChatMember> ChatMembers { get; set; }
|
|
||||||
public DbSet<Message> ChatMessages { get; set; }
|
|
||||||
public DbSet<RealtimeCall> ChatRealtimeCall { get; set; }
|
|
||||||
public DbSet<MessageReaction> ChatReactions { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Sticker.Sticker> Stickers { get; set; }
|
|
||||||
public DbSet<StickerPack> StickerPacks { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Common.Models.Wallet> Wallets { get; set; }
|
|
||||||
public DbSet<WalletPocket> WalletPockets { get; set; }
|
|
||||||
public DbSet<Order> PaymentOrders { get; set; }
|
|
||||||
public DbSet<Transaction> PaymentTransactions { get; set; }
|
|
||||||
|
|
||||||
public DbSet<CustomApp> CustomApps { get; set; }
|
|
||||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Subscription> WalletSubscriptions { get; set; }
|
|
||||||
public DbSet<Coupon> WalletCoupons { get; set; }
|
|
||||||
public DbSet<Connection.WebReader.WebArticle> WebArticles { get; set; }
|
|
||||||
public DbSet<Connection.WebReader.WebFeed> WebFeeds { 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();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity<PublisherMember>()
|
|
||||||
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
|
|
||||||
modelBuilder.Entity<PublisherMember>()
|
|
||||||
.HasOne(pm => pm.Publisher)
|
|
||||||
.WithMany(p => p.Members)
|
|
||||||
.HasForeignKey(pm => pm.PublisherId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Common.Models.Post>()
|
|
||||||
.HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content })
|
|
||||||
.HasIndex(p => p.SearchVector)
|
|
||||||
.HasMethod("GIN");
|
|
||||||
|
|
||||||
modelBuilder.Entity<CustomAppSecret>()
|
|
||||||
.HasIndex(s => s.Secret)
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
modelBuilder.Entity<CustomApp>()
|
|
||||||
.HasMany(c => c.Secrets)
|
|
||||||
.WithOne(s => s.App)
|
|
||||||
.HasForeignKey(s => s.AppId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Common.Models.Post>()
|
|
||||||
.HasOne(p => p.RepliedPost)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(p => p.RepliedPostId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
modelBuilder.Entity<Common.Models.Post>()
|
|
||||||
.HasOne(p => p.ForwardedPost)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(p => p.ForwardedPostId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
modelBuilder.Entity<Common.Models.Post>()
|
|
||||||
.HasMany(p => p.Tags)
|
|
||||||
.WithMany(t => t.Posts)
|
|
||||||
.UsingEntity(j => j.ToTable("post_tag_links"));
|
|
||||||
modelBuilder.Entity<Common.Models.Post>()
|
|
||||||
.HasMany(p => p.Categories)
|
|
||||||
.WithMany(c => c.Posts)
|
|
||||||
.UsingEntity(j => j.ToTable("post_category_links"));
|
|
||||||
modelBuilder.Entity<Common.Models.Post>()
|
|
||||||
.HasMany(p => p.Collections)
|
|
||||||
.WithMany(c => c.Posts)
|
|
||||||
.UsingEntity(j => j.ToTable("post_collection_links"));
|
|
||||||
|
|
||||||
modelBuilder.Entity<ChatMember>()
|
|
||||||
.HasKey(pm => new { pm.Id });
|
|
||||||
modelBuilder.Entity<ChatMember>()
|
|
||||||
.HasAlternateKey(pm => new { pm.ChatRoomId, pm.AccountId });
|
|
||||||
|
|
||||||
|
|
||||||
modelBuilder.Entity<Message>()
|
|
||||||
.HasOne(m => m.ForwardedMessage)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(m => m.ForwardedMessageId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
modelBuilder.Entity<Message>()
|
|
||||||
.HasOne(m => m.RepliedMessage)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(m => m.RepliedMessageId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
modelBuilder.Entity<Connection.WebReader.WebFeed>()
|
|
||||||
.HasIndex(f => f.Url)
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
modelBuilder.Entity<Connection.WebReader.WebArticle>()
|
|
||||||
.HasIndex(a => a.Url)
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
@ -16,7 +17,7 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
[Route("/chat")]
|
[Route("/chat")]
|
||||||
public class ChatRoomController(
|
public class ChatRoomController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
FileReferenceService fileRefService,
|
IFileReferenceServiceClient fileRefService,
|
||||||
ChatRoomService crs,
|
ChatRoomService crs,
|
||||||
RealmService rs,
|
RealmService rs,
|
||||||
ActionLogService als,
|
ActionLogService als,
|
||||||
@ -272,12 +273,12 @@ public class ChatRoomController(
|
|||||||
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
|
||||||
// Remove old references for pictures
|
// Remove old references for pictures
|
||||||
await fileRefService.DeleteResourceReferencesAsync(chatRoom.ResourceIdentifier, "chat.room.picture");
|
await fileRefService.DeleteResourceReferencesAsync(chatRoom.ResourceIdentifier, "chat-room.picture");
|
||||||
|
|
||||||
// Add a new reference
|
// Add a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
picture.Id,
|
picture.Id.ToString(),
|
||||||
"chat.room.picture",
|
"chat-room.picture",
|
||||||
chatRoom.ResourceIdentifier
|
chatRoom.ResourceIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -290,12 +291,12 @@ public class ChatRoomController(
|
|||||||
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
|
|
||||||
// Remove old references for backgrounds
|
// Remove old references for backgrounds
|
||||||
await fileRefService.DeleteResourceReferencesAsync(chatRoom.ResourceIdentifier, "chat.room.background");
|
await fileRefService.DeleteResourceReferencesAsync(chatRoom.ResourceIdentifier, "chat-room.background");
|
||||||
|
|
||||||
// Add a new reference
|
// Add a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
background.Id,
|
background.Id.ToString(),
|
||||||
"chat.room.background",
|
"chat-room.background",
|
||||||
chatRoom.ResourceIdentifier
|
chatRoom.ResourceIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DysonNetwork.Pass.Features.Account;
|
using DysonNetwork.Pass.Features.Account;
|
||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Sphere.Connection;
|
||||||
@ -11,7 +12,7 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
|
|
||||||
public partial class ChatService(
|
public partial class ChatService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
FileReferenceService fileRefService,
|
IFileReferenceServiceClient fileRefService,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
IRealtimeService realtime,
|
IRealtimeService realtime,
|
||||||
ILogger<ChatService> logger
|
ILogger<ChatService> logger
|
||||||
@ -162,10 +163,9 @@ public partial class ChatService(
|
|||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
file.Id,
|
file.Id.ToString(),
|
||||||
ChatFileUsageIdentifier,
|
ChatFileUsageIdentifier,
|
||||||
messageResourceId,
|
messageResourceId
|
||||||
duration: Duration.FromDays(30)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
59
DysonNetwork.Sphere/Data/AppDatabase.cs
Normal file
59
DysonNetwork.Sphere/Data/AppDatabase.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using DysonNetwork.Common.Models;
|
||||||
|
using DysonNetwork.Sphere.Realm;
|
||||||
|
using DysonNetwork.Sphere.Sticker;
|
||||||
|
using DysonNetwork.Sphere.Connection;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Data;
|
||||||
|
|
||||||
|
public class AppDatabase : DbContext
|
||||||
|
{
|
||||||
|
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!;
|
||||||
|
public DbSet<Common.Models.Publisher> Publishers { get; set; } = null!;
|
||||||
|
public DbSet<PublisherFeature> PublisherFeatures { get; set; } = null!;
|
||||||
|
public DbSet<Common.Models.Post> Posts { get; set; } = null!;
|
||||||
|
public DbSet<PostReaction> PostReactions { get; set; } = null!;
|
||||||
|
public DbSet<PostTag> PostTags { get; set; } = null!;
|
||||||
|
public DbSet<PostCategory> PostCategories { get; set; } = null!;
|
||||||
|
public DbSet<PostCollection> PostCollections { get; set; } = null!;
|
||||||
|
public DbSet<Common.Models.Realm> Realms { get; set; } = null!;
|
||||||
|
public DbSet<RealmMember> RealmMembers { get; set; } = null!;
|
||||||
|
public DbSet<Tag> Tags { get; set; } = null!;
|
||||||
|
public DbSet<RealmTag> RealmTags { get; set; } = null!;
|
||||||
|
public DbSet<ChatRoom> ChatRooms { get; set; } = null!;
|
||||||
|
public DbSet<ChatMember> ChatMembers { get; set; } = null!;
|
||||||
|
public DbSet<Message> ChatMessages { get; set; } = null!;
|
||||||
|
public DbSet<RealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
||||||
|
public DbSet<MessageReaction> ChatReactions { get; set; } = null!;
|
||||||
|
public DbSet<Sticker.Sticker> Stickers { get; set; } = null!;
|
||||||
|
public DbSet<StickerPack> StickerPacks { get; set; } = null!;
|
||||||
|
public DbSet<Common.Models.Wallet> Wallets { get; set; } = null!;
|
||||||
|
public DbSet<WalletPocket> WalletPockets { get; set; } = null!;
|
||||||
|
public DbSet<Order> PaymentOrders { get; set; } = null!;
|
||||||
|
public DbSet<Transaction> PaymentTransactions { get; set; } = null!;
|
||||||
|
public DbSet<CustomApp> CustomApps { get; set; } = null!;
|
||||||
|
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||||
|
public DbSet<Subscription> WalletSubscriptions { get; set; } = null!;
|
||||||
|
// TODO: Fix Connection type - no Connection class found in DysonNetwork.Sphere.Connection
|
||||||
|
// public DbSet<Connection> Connections { get; set; } = null!;
|
||||||
|
// public DbSet<Connection> Followers { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// Configure the database schema and relationships here
|
||||||
|
// This will be moved from the original AppDatabase class
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Developer;
|
namespace DysonNetwork.Sphere.Developer;
|
||||||
|
|
||||||
public class CustomAppService(AppDatabase db, FileReferenceService fileRefService)
|
public class CustomAppService(AppDatabase db, IFileReferenceServiceClient fileRefService)
|
||||||
{
|
{
|
||||||
public async Task<CustomApp?> CreateAppAsync(
|
public async Task<CustomApp?> CreateAppAsync(
|
||||||
Publisher.Publisher pub,
|
Publisher.Publisher pub,
|
||||||
@ -32,7 +33,7 @@ public class CustomAppService(AppDatabase db, FileReferenceService fileRefServic
|
|||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
picture.Id,
|
picture.Id.ToString(),
|
||||||
"custom-apps.picture",
|
"custom-apps.picture",
|
||||||
app.ResourceIdentifier
|
app.ResourceIdentifier
|
||||||
);
|
);
|
||||||
@ -101,7 +102,7 @@ public class CustomAppService(AppDatabase db, FileReferenceService fileRefServic
|
|||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
picture.Id,
|
picture.Id.ToString(),
|
||||||
"custom-apps.picture",
|
"custom-apps.picture",
|
||||||
app.ResourceIdentifier
|
app.ResourceIdentifier
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -39,10 +39,10 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Minio" Version="6.0.4" />
|
<PackageReference Include="Minio" Version="6.0.5" />
|
||||||
<PackageReference Include="NetVips" Version="3.0.1" />
|
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.16.1" />
|
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.0" />
|
||||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.16.1" />
|
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||||
@ -76,10 +76,10 @@
|
|||||||
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />
|
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||||
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
|
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
|
||||||
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.6" />
|
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.6" />
|
||||||
<PackageReference Include="tusdotnet" Version="2.8.1" />
|
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -89,16 +89,21 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Migrations\" />
|
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
|
||||||
<Folder Include="Discovery\" />
|
|
||||||
<Folder Include="Services\PassClient\" />
|
|
||||||
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Migrations\" />
|
||||||
|
<Folder Include="Discovery\" />
|
||||||
|
<Folder Include="Services\PassClient\" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Common\DysonNetwork.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="Resources\SharedResource.resx">
|
<EmbeddedResource Update="Resources\SharedResource.resx">
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
using DysonNetwork.Sphere.Connection.WebReader;
|
using DysonNetwork.Sphere.Connection.WebReader;
|
||||||
@ -13,7 +14,7 @@ namespace DysonNetwork.Sphere.Post;
|
|||||||
|
|
||||||
public partial class PostService(
|
public partial class PostService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
FileReferenceService fileRefService,
|
IFileReferenceServiceClient fileRefService,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
IServiceScopeFactory factory,
|
IServiceScopeFactory factory,
|
||||||
FlushBufferService flushBuffer,
|
FlushBufferService flushBuffer,
|
||||||
@ -135,9 +136,9 @@ public partial class PostService(
|
|||||||
foreach (var file in post.Attachments)
|
foreach (var file in post.Attachments)
|
||||||
{
|
{
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
file.Id,
|
file.Id.ToString(),
|
||||||
PostFileUsageIdentifier,
|
PostFileUsageIdentifier,
|
||||||
postResourceId
|
post.ResourceIdentifier
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,12 +219,18 @@ public partial class PostService(
|
|||||||
{
|
{
|
||||||
var postResourceId = $"post:{post.Id}";
|
var postResourceId = $"post:{post.Id}";
|
||||||
|
|
||||||
// Update resource references using the new file list
|
// Delete existing references for this resource and usage
|
||||||
await fileRefService.UpdateResourceFilesAsync(
|
await fileRefService.DeleteResourceReferencesAsync(post.ResourceIdentifier, PostFileUsageIdentifier);
|
||||||
postResourceId,
|
|
||||||
attachments,
|
// Create new references for each file
|
||||||
PostFileUsageIdentifier
|
foreach (var fileId in attachments)
|
||||||
|
{
|
||||||
|
await fileRefService.CreateReferenceAsync(
|
||||||
|
fileId.ToString(),
|
||||||
|
PostFileUsageIdentifier,
|
||||||
|
post.ResourceIdentifier
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update post attachments by getting files from database
|
// Update post attachments by getting files from database
|
||||||
var files = await db.Files
|
var files = await db.Files
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Realm;
|
using DysonNetwork.Sphere.Realm;
|
||||||
@ -15,7 +16,7 @@ namespace DysonNetwork.Sphere.Publisher;
|
|||||||
public class PublisherController(
|
public class PublisherController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PublisherService ps,
|
PublisherService ps,
|
||||||
FileReferenceService fileRefService,
|
IFileReferenceServiceClient fileRefService,
|
||||||
ActionLogService als)
|
ActionLogService als)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
@ -362,7 +363,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
picture.Id,
|
picture.Id.ToString(),
|
||||||
"publisher.picture",
|
"publisher.picture",
|
||||||
publisher.ResourceIdentifier
|
publisher.ResourceIdentifier
|
||||||
);
|
);
|
||||||
@ -384,7 +385,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
background.Id,
|
background.Id.ToString(),
|
||||||
"publisher.background",
|
"publisher.background",
|
||||||
publisher.ResourceIdentifier
|
publisher.ResourceIdentifier
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
@ -8,7 +9,7 @@ using NodaTime;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Publisher;
|
namespace DysonNetwork.Sphere.Publisher;
|
||||||
|
|
||||||
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
public class PublisherService(AppDatabase db, IFileReferenceServiceClient fileRefService, ICacheService cache)
|
||||||
{
|
{
|
||||||
public async Task<Publisher?> GetPublisherByName(string name)
|
public async Task<Publisher?> GetPublisherByName(string name)
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
using DysonNetwork.Common.Models;
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -13,7 +14,7 @@ namespace DysonNetwork.Sphere.Realm;
|
|||||||
public class RealmController(
|
public class RealmController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RealmService rs,
|
RealmService rs,
|
||||||
FileReferenceService fileRefService,
|
IFileReferenceServiceClient fileRefService,
|
||||||
RelationshipService rels,
|
RelationshipService rels,
|
||||||
ActionLogService als,
|
ActionLogService als,
|
||||||
AccountEventService aes
|
AccountEventService aes
|
||||||
@ -424,7 +425,7 @@ public class RealmController(
|
|||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
picture.Id,
|
picture.Id.ToString(),
|
||||||
"realm.picture",
|
"realm.picture",
|
||||||
realm.ResourceIdentifier
|
realm.ResourceIdentifier
|
||||||
);
|
);
|
||||||
@ -445,7 +446,7 @@ public class RealmController(
|
|||||||
|
|
||||||
// Create a new reference
|
// Create a new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
background.Id,
|
background.Id.ToString(),
|
||||||
"realm.background",
|
"realm.background",
|
||||||
realm.ResourceIdentifier
|
realm.ResourceIdentifier
|
||||||
);
|
);
|
||||||
|
@ -27,6 +27,11 @@ using DysonNetwork.Sphere.Discovery;
|
|||||||
using DysonNetwork.Sphere.Safety;
|
using DysonNetwork.Sphere.Safety;
|
||||||
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||||
using tusdotnet.Stores;
|
using tusdotnet.Stores;
|
||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
|
using DysonNetwork.Drive.Clients;
|
||||||
|
using DysonNetwork.Sphere.Data;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
|
|
||||||
@ -36,7 +41,13 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
|
|
||||||
services.AddDbContext<AppDatabase>();
|
services.AddDbContext<AppDatabase>(options =>
|
||||||
|
options.UseNpgsql(
|
||||||
|
configuration.GetConnectionString("DefaultConnection"),
|
||||||
|
o => o.UseNodaTime()
|
||||||
|
)
|
||||||
|
.UseSnakeCaseNamingConvention()
|
||||||
|
);
|
||||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||||
{
|
{
|
||||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
||||||
@ -49,6 +60,19 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddHttpClient<PassClient>();
|
services.AddHttpClient<PassClient>();
|
||||||
services.AddScoped<PassClient>();
|
services.AddScoped<PassClient>();
|
||||||
|
|
||||||
|
// Register HTTP clients for Drive microservice
|
||||||
|
services.AddHttpClient<IFileServiceClient, FileServiceClient>(client =>
|
||||||
|
{
|
||||||
|
var baseUrl = configuration["DriveService:BaseUrl"] ?? throw new InvalidOperationException("DriveService:BaseUrl is not configured");
|
||||||
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddHttpClient<IFileReferenceServiceClient, FileReferenceServiceClient>(client =>
|
||||||
|
{
|
||||||
|
var baseUrl = configuration["DriveService:BaseUrl"] ?? throw new InvalidOperationException("DriveService:BaseUrl is not configured");
|
||||||
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
// Register OIDC services
|
// Register OIDC services
|
||||||
|
|
||||||
|
|
||||||
@ -181,7 +205,6 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
|
|
||||||
services.AddScoped<FileService>();
|
services.AddScoped<FileService>();
|
||||||
services.AddScoped<FileReferenceService>();
|
|
||||||
services.AddScoped<FileReferenceMigrationService>();
|
services.AddScoped<FileReferenceMigrationService>();
|
||||||
services.AddScoped<PublisherService>();
|
services.AddScoped<PublisherService>();
|
||||||
services.AddScoped<PublisherSubscriptionService>();
|
services.AddScoped<PublisherSubscriptionService>();
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
using DysonNetwork.Common.Interfaces;
|
||||||
|
using DysonNetwork.Common.Models;
|
||||||
using DysonNetwork.Common.Services;
|
using DysonNetwork.Common.Services;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Sticker;
|
namespace DysonNetwork.Sphere.Sticker;
|
||||||
|
|
||||||
public class StickerService(AppDatabase db, FileService fs, FileReferenceService fileRefService, ICacheService cache)
|
public class StickerService(AppDatabase db, IFileReferenceServiceClient fileRefService, ICacheService cache)
|
||||||
{
|
{
|
||||||
public const string StickerFileUsageIdentifier = "sticker";
|
public const string StickerFileUsageIdentifier = "sticker";
|
||||||
|
|
||||||
@ -19,9 +22,9 @@ public class StickerService(AppDatabase db, FileService fs, FileReferenceService
|
|||||||
|
|
||||||
var stickerResourceId = $"sticker:{sticker.Id}";
|
var stickerResourceId = $"sticker:{sticker.Id}";
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
sticker.Image.Id,
|
fileId: sticker.Image.Id.ToString(),
|
||||||
StickerFileUsageIdentifier,
|
usage: StickerFileUsageIdentifier,
|
||||||
stickerResourceId
|
resourceId: stickerResourceId
|
||||||
);
|
);
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
@ -34,20 +37,23 @@ public class StickerService(AppDatabase db, FileService fs, FileReferenceService
|
|||||||
var stickerResourceId = $"sticker:{sticker.Id}";
|
var stickerResourceId = $"sticker:{sticker.Id}";
|
||||||
|
|
||||||
// Delete old references
|
// Delete old references
|
||||||
var oldRefs =
|
var oldRefs = await fileRefService.GetResourceReferencesAsync(
|
||||||
await fileRefService.GetResourceReferencesAsync(stickerResourceId, StickerFileUsageIdentifier);
|
resourceId: stickerResourceId,
|
||||||
|
usage: StickerFileUsageIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
foreach (var oldRef in oldRefs)
|
foreach (var oldRef in oldRefs)
|
||||||
{
|
{
|
||||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
await fileRefService.DeleteReferenceAsync(oldRef.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
sticker.Image = newImage.ToReferenceObject();
|
sticker.Image = newImage.ToReferenceObject();
|
||||||
|
|
||||||
// Create new reference
|
// Create new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
await fileRefService.CreateReferenceAsync(
|
||||||
newImage.Id,
|
fileId: newImage.Id.ToString(),
|
||||||
StickerFileUsageIdentifier,
|
usage: StickerFileUsageIdentifier,
|
||||||
stickerResourceId
|
resourceId: stickerResourceId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,5 +111,8 @@
|
|||||||
"KnownProxies": [
|
"KnownProxies": [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
"::1"
|
"::1"
|
||||||
]
|
],
|
||||||
|
"DriveService": {
|
||||||
|
"BaseUrl": "http://localhost:5073"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user