feat: integration cloudfile
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Documents;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.CloudFile;
|
||||
|
||||
public class CloudFileDocumentContentStore(
|
||||
ICloudFileHttpClient cf,
|
||||
ILogger<CloudFileDocumentContentStore> log) : IDocumentContentStore
|
||||
{
|
||||
private static string Bucket(Guid projectId) => $"mws-p-{projectId:N}";
|
||||
private static string Key(Guid documentId) => $"{documentId:N}.html";
|
||||
|
||||
public Task EnsureBucketAsync(Guid projectId, CancellationToken ct) =>
|
||||
cf.EnsureBucketAsync(Bucket(projectId), ct);
|
||||
|
||||
public async Task UploadAsync(Guid projectId, Guid documentId, string contentType, Stream body, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var meta = await cf.UploadAsync(Bucket(projectId), Key(documentId), contentType, body, ct);
|
||||
log.LogDebug("Uploaded document {DocumentId} to cloudfile: {Hash}", documentId, meta?.ContentHash);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
log.LogError(ex, "CloudFile upload failed for document {DocumentId}", documentId);
|
||||
throw new StorageUnavailableException("Cloud file upload failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await cf.DownloadAsync(Bucket(projectId), Key(documentId), ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
log.LogError(ex, "CloudFile download failed for document {DocumentId}", documentId);
|
||||
throw new StorageUnavailableException("Cloud file download failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await cf.ExistsAsync(Bucket(projectId), Key(documentId), ct);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid projectId, Guid documentId, CancellationToken ct)
|
||||
{
|
||||
var bucket = Bucket(projectId);
|
||||
var key = Key(documentId);
|
||||
log.LogInformation("CloudFile delete: bucket={Bucket}, key={Key}", bucket, key);
|
||||
try
|
||||
{
|
||||
await cf.DeleteAsync(bucket, key, ct);
|
||||
log.LogInformation("CloudFile delete OK for document {DocumentId}", documentId);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
log.LogWarning(ex, "CloudFile delete failed for document {DocumentId} (bucket={Bucket}, key={Key})", documentId, bucket, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.CloudFile;
|
||||
|
||||
public interface ICloudFileHttpClient
|
||||
{
|
||||
Task<CloudFileMetadata?> UploadAsync(string bucket, string key, string contentType, Stream body, CancellationToken ct);
|
||||
Task<Stream> DownloadAsync(string bucket, string key, CancellationToken ct);
|
||||
Task<bool> ExistsAsync(string bucket, string key, CancellationToken ct);
|
||||
Task DeleteAsync(string bucket, string key, CancellationToken ct);
|
||||
Task EnsureBucketAsync(string bucket, CancellationToken ct);
|
||||
}
|
||||
|
||||
public class CloudFileHttpClient(
|
||||
HttpClient http,
|
||||
IOptions<CloudFileOptions> opt,
|
||||
ILogger<CloudFileHttpClient> log) : ICloudFileHttpClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
private HttpRequestMessage Req(HttpMethod method, string url)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
request.Headers.Add("X-API-Key", opt.Value.ApiKey);
|
||||
return request;
|
||||
}
|
||||
|
||||
public async Task<CloudFileMetadata?> UploadAsync(string bucket, string key, string contentType, Stream body, CancellationToken ct)
|
||||
{
|
||||
using var form = new MultipartFormDataContent
|
||||
{
|
||||
{ new StreamContent(body), "file", key },
|
||||
{ new StringContent(bucket), "bucket" },
|
||||
{ new StringContent(key), "key" }
|
||||
};
|
||||
|
||||
var request = Req(HttpMethod.Post, "/api/files/upload");
|
||||
request.Content = form;
|
||||
|
||||
var resp = await http.SendAsync(request, ct);
|
||||
var responseText = await resp.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
log.LogWarning("CloudFile upload failed {Status}: {Body}", resp.StatusCode, responseText);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
var dto = JsonSerializer.Deserialize<CloudFileUploadResponse>(responseText, Json);
|
||||
return dto?.Metadata;
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadAsync(string bucket, string key, CancellationToken ct)
|
||||
{
|
||||
var request = Req(HttpMethod.Get, $"/api/files/download/{bucket}/{Uri.EscapeDataString(key)}");
|
||||
var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
|
||||
if (resp.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
return await resp.Content.ReadAsStreamAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string bucket, string key, CancellationToken ct)
|
||||
{
|
||||
var request = Req(HttpMethod.Get, $"/api/files/{bucket}/metadata/{Uri.EscapeDataString(key)}");
|
||||
var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string bucket, string key, CancellationToken ct)
|
||||
{
|
||||
var request = Req(HttpMethod.Delete, $"/api/files/{bucket}/{Uri.EscapeDataString(key)}");
|
||||
var resp = await http.SendAsync(request, ct);
|
||||
|
||||
if (resp.StatusCode != HttpStatusCode.NotFound)
|
||||
{
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureBucketAsync(string bucket, CancellationToken ct)
|
||||
{
|
||||
var request = Req(HttpMethod.Post, "/api/buckets");
|
||||
var json = JsonSerializer.Serialize(new { name = bucket }, Json);
|
||||
request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
var resp = await http.SendAsync(request, ct);
|
||||
var responseText = await resp.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!resp.IsSuccessStatusCode && resp.StatusCode != HttpStatusCode.BadRequest)
|
||||
{
|
||||
log.LogWarning("CloudFile ensureBucket failed {Status}: {Body}", resp.StatusCode, responseText);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CloudFileUploadResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public CloudFileMetadata? Metadata { get; set; }
|
||||
}
|
||||
|
||||
public class CloudFileMetadata
|
||||
{
|
||||
[JsonPropertyName("fileId")]
|
||||
public string FileId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("fileName")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("extension")]
|
||||
public string Extension { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("contentType")]
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonPropertyName("bucket")]
|
||||
public string Bucket { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("contentHash")]
|
||||
public string ContentHash { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("modifiedAt")]
|
||||
public DateTime ModifiedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace mws.backend.dotnet.infrastructure.CloudFile;
|
||||
|
||||
public class CloudFileOptions
|
||||
{
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string BucketPrefix { get; set; } = "mws-p-";
|
||||
public int HttpTimeoutSeconds { get; set; } = 30;
|
||||
}
|
||||
@@ -3,8 +3,10 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Documents;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.infrastructure.Authentication;
|
||||
using mws.backend.dotnet.infrastructure.CloudFile;
|
||||
using mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure;
|
||||
@@ -37,6 +39,17 @@ public static class DependencyInjection
|
||||
|
||||
services.AddScoped<IPermissionService, PermissionService>();
|
||||
|
||||
services.Configure<CloudFileOptions>(configuration.GetSection("CloudFile"));
|
||||
services.AddHttpClient<ICloudFileHttpClient, CloudFileHttpClient>((sp, http) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<CloudFileOptions>>().Value;
|
||||
#pragma warning disable S1075 // hardcoded URL — this is a configurable base address from appsettings
|
||||
http.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/");
|
||||
#pragma warning restore S1075
|
||||
http.Timeout = TimeSpan.FromSeconds(options.HttpTimeoutSeconds);
|
||||
});
|
||||
services.AddScoped<IDocumentContentStore, CloudFileDocumentContentStore>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260906043840_DropDocumentContent_AddStorageFields")]
|
||||
partial class DropDocumentContent_AddStorageFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<long?>("ContentSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("UpdatedContentAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("ProjectId", "ParentId");
|
||||
|
||||
b.ToTable("documents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.MasterData.MasterDataEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Group", "Value")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("master_data", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedBy");
|
||||
|
||||
b.HasIndex("UpdatedBy");
|
||||
|
||||
b.ToTable("projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("ProjectId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("project_members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMemberPermission", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("ProjectId", "UserId", "Screen");
|
||||
|
||||
b.ToTable("project_member_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("RoleId", "Screen");
|
||||
|
||||
b.ToTable("role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Users.UserRole", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("user_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Documents.Document", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.Project", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Users.User", "CreatedByUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedBy")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("mws.backend.dotnet.domain.Users.User", "UpdatedByUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UpdatedBy")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("CreatedByUser");
|
||||
|
||||
b.Navigation("UpdatedByUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Projects.Project", "Project")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("mws.backend.dotnet.domain.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMemberPermission", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Projects.ProjectMember", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Roles.Role", "Role")
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Users.User", "Assignee")
|
||||
.WithMany()
|
||||
.HasForeignKey("AssigneeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Assignee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Users.UserRole", b =>
|
||||
{
|
||||
b.HasOne("mws.backend.dotnet.domain.Roles.Role", "Role")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("mws.backend.dotnet.domain.Users.User", "User")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.Role", b =>
|
||||
{
|
||||
b.Navigation("Permissions");
|
||||
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("mws.backend.dotnet.domain.Users.User", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DropDocumentContent_AddStorageFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Content",
|
||||
table: "documents");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ContentHash",
|
||||
table: "documents",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "ContentSize",
|
||||
table: "documents",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "UpdatedContentAt",
|
||||
table: "documents",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentHash",
|
||||
table: "documents");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentSize",
|
||||
table: "documents");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedContentAt",
|
||||
table: "documents");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Content",
|
||||
table: "documents",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,12 @@ namespace mws.backend.dotnet.infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<long?>("ContentSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
@@ -59,6 +63,9 @@ namespace mws.backend.dotnet.infrastructure.Migrations
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("UpdatedContentAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
@@ -12,7 +12,9 @@ public class DocumentConfiguration : IEntityTypeConfiguration<Document>
|
||||
e.HasKey(d => d.Id);
|
||||
e.Property(d => d.Title).HasMaxLength(300).IsRequired();
|
||||
e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20);
|
||||
e.Property(d => d.Content).HasColumnType("text");
|
||||
e.Property(d => d.ContentHash).HasMaxLength(64);
|
||||
e.Property(d => d.ContentSize);
|
||||
e.Property(d => d.UpdatedContentAt);
|
||||
|
||||
e.HasIndex(d => new { d.ProjectId, d.ParentId });
|
||||
e.HasIndex(d => d.ParentId);
|
||||
|
||||
@@ -82,21 +82,18 @@ public static class DbSeeder
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
|
||||
Title = "Authentication", Type = DocumentType.Document,
|
||||
Content = "<h2>Authentication</h2><p>Phase 1 uses simple username/password with JWT.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
|
||||
Title = "User Management", Type = DocumentType.Document,
|
||||
Content = "<h2>User Management</h2><p>Users are created via seeding in Phase 1.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = design.Id,
|
||||
Title = "API Design", Type = DocumentType.Document,
|
||||
Content = "<h2>API Design</h2><p>REST API with ASP.NET Core Web API.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Reference in New Issue
Block a user