Fixes ACL backend support
This commit is contained in:
parent
0034a311f2
commit
e62adedc17
14 changed files with 205 additions and 56 deletions
32
.vscode/tasks.json
vendored
32
.vscode/tasks.json
vendored
|
|
@ -1,5 +1,21 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"isRoot": true,
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "dotnet",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"source": "dotnet",
|
||||
"pattern": {
|
||||
"regexp": "^\\s+(.*)\\((\\d+):(\\d+)\\):\\s+(error|warning)\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"label": "run-debug-android",
|
||||
|
|
@ -20,21 +36,6 @@
|
|||
"-p:AndroidAttachDebugger=true",
|
||||
"-p:AndroidSdbHostPort=55555",
|
||||
"-p:AndroidSdbTargetPort=55555"
|
||||
],
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "dotnet",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"source": "dotnet",
|
||||
"pattern": {
|
||||
"regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -45,7 +46,6 @@
|
|||
"group": "build",
|
||||
"isBuildCommand": true,
|
||||
"isTestCommand": false,
|
||||
"problemMatcher": ["$msCompile"],
|
||||
"isBackground": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -16,6 +16,16 @@ Cette convention est partagée avec le dépôt
|
|||
[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian)
|
||||
pour la production des paquets `.deb`.
|
||||
|
||||
## [1.0.8-rc4] - unstable
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
* [TODO] bug loading a blog post from PostIt, ACL come along with and don't need any "Refresh" button.
|
||||
|
||||
## [1.0.8-rc3] - unstable
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ namespace Yavsc
|
|||
{
|
||||
|
||||
public const string APIPrefix = "api/v1";
|
||||
|
||||
public const string BlogSpotPath = "blogspot";
|
||||
public const string BlogAclPath = "blogacl";
|
||||
public const string BlogTagPath = "blogtag";
|
||||
public const string CirclePath = "circle";
|
||||
public const string CommentsPath = "blogcomments";
|
||||
|
||||
public static readonly Scope[] SiteScopes = {
|
||||
new Scope { Id = "profile", Description = "Your profile informations" },
|
||||
new Scope { Id = "book" , Description ="Your booking interface"},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Yavsc.Abstract.BlogSpot;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Access;
|
||||
using Yavsc.Models.Blog;
|
||||
using Yavsc.Tests.Shared;
|
||||
using static Yavsc.Constants;
|
||||
|
||||
|
|
@ -38,15 +40,16 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
{
|
||||
private readonly BlogsWebServerFixture _fixture;
|
||||
|
||||
|
||||
public BlogAclApiTests(BlogsWebServerFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
|
||||
private string BlogUrl()
|
||||
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogSpotPath}";
|
||||
private string BlogAclUrl()
|
||||
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/blogacl";
|
||||
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogAclPath}";
|
||||
|
||||
/// <summary>Delete any ACL rows tied to the fixture's seeded
|
||||
/// <c>(CircleId, BlogPostId)</c> pair. The shared SQLite store
|
||||
|
|
@ -120,7 +123,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
// owned by the caller. We seed the same shape pre-POST so the
|
||||
// test reproduces the prod scenario end-to-end.
|
||||
CleanupAcl();
|
||||
using var http = NewClient("alice");
|
||||
using var http = NewClient(_fixture.DefaultUserLogin);
|
||||
|
||||
var payload = new PostAccessControlRulePayload
|
||||
{
|
||||
|
|
@ -178,7 +181,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[MemberData(nameof(BlogAclPayloadsForNever500))]
|
||||
public async Task PostCircleAuthorization_never_returns_500(PostAccessControlRulePayload payload)
|
||||
{
|
||||
using var http = NewClient("alice");
|
||||
using var http = NewClient(_fixture.DefaultUserLogin);
|
||||
|
||||
var response = await http.PostAsJsonAsync(
|
||||
BlogAclUrl(), payload,
|
||||
|
|
@ -216,4 +219,83 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostBlog_with_ACL_creates_a_post_and_Get_returns_it_in_the_list()
|
||||
{
|
||||
CleanupAcl();
|
||||
_fixture.SeedUser(_fixture.DefaultUserLogin);
|
||||
_fixture.SeedUser("tester");
|
||||
_fixture.SeedCircle(_fixture.DefaultUserLogin, "test",
|
||||
false,
|
||||
new String[]
|
||||
{
|
||||
_fixture.DefaultUserLogin,
|
||||
"tester"
|
||||
});
|
||||
using var http = NewClient(_fixture.DefaultUserLogin );
|
||||
|
||||
// Create a minimal BlogPost. The server assigns Id, so we
|
||||
// send 0 + an explicit AuthorId; the production
|
||||
// BlogSpotService.Create() tolerates that.
|
||||
var draft = new BlogPost
|
||||
{
|
||||
Id = 0,
|
||||
Title = "Premier billet",
|
||||
AuthorId = "tester",
|
||||
Article = "Contenu de test.",
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
ACL = new List<CircleAuthorizationToBlogPost>(
|
||||
new CircleAuthorizationToBlogPost[]
|
||||
{
|
||||
new CircleAuthorizationToBlogPost
|
||||
{
|
||||
CircleId = _fixture.CircleId,
|
||||
BlogPostId = _fixture.PostId
|
||||
}
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
var postResponse = await http.PostAsJsonAsync(
|
||||
BlogUrl(),
|
||||
draft,
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
|
||||
|
||||
// The POST returns the server-issued post (with a real Id).
|
||||
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>(
|
||||
TestContext.Current.CancellationToken
|
||||
);
|
||||
Assert.NotNull(created);
|
||||
Assert.NotEqual(0, created!.Id);
|
||||
Assert.Equal(draft.Title, created.Title);
|
||||
|
||||
// The list should now contain exactly one entry.
|
||||
var listResponse = await http.GetAsync(
|
||||
BlogUrl(),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
|
||||
|
||||
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync(
|
||||
TestContext.Current.CancellationToken
|
||||
));
|
||||
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
|
||||
Assert.Equal(2, doc.RootElement.GetArrayLength());
|
||||
Assert.Equal(created.Id, doc.RootElement[0].GetProperty("id").GetInt64());
|
||||
|
||||
// detail should return the same post, with ACL and tags.
|
||||
var detailResponse = await http.GetAsync(
|
||||
$"{BlogUrl()}/{created.Id}",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode);
|
||||
using var detailDoc = JsonDocument.Parse(await detailResponse.Content.ReadAsStringAsync(
|
||||
TestContext.Current.CancellationToken
|
||||
));
|
||||
Assert.Equal(JsonValueKind.Object, detailDoc.RootElement.ValueKind);
|
||||
Assert.Equal(created.Id, detailDoc.RootElement.GetProperty("id").GetInt64());
|
||||
Assert.Equal(1, detailDoc.RootElement.GetProperty("acl").GetArrayLength());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System.Net;
|
|||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Blog;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
|
@ -31,18 +30,6 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Reset the in-memory database to a known empty state.
|
||||
/// <c>UseInMemoryDatabase</c> shares its store across the
|
||||
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
|
||||
/// so without a per-test reset the test order would leak
|
||||
/// state between tests.</summary>
|
||||
private void ResetDatabase()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureDeleted();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
/// <summary>Reset the database and seed the
|
||||
/// <c>tester</c> <see cref="ApplicationUser"/> row. Required
|
||||
|
|
@ -55,7 +42,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
/// at <c>SaveChanges</c> and the controller returns 500.</summary>
|
||||
private void ResetAndSeedDefaultUser()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
_fixture.SeedUser("tester");
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +98,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task GetBlogs_returns_200_with_empty_list_when_no_posts()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
using var http = NewClient();
|
||||
|
||||
var response = await http.GetAsync("/api/v1/blog",
|
||||
|
|
@ -266,7 +253,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task GetBlog_returns_401_when_no_token_is_provided()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
using var http = NewAnonymousClient();
|
||||
|
||||
// No Authorization header → the JwtBearer middleware
|
||||
|
|
@ -443,7 +430,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
// behaviour so a future change that, say, makes Title
|
||||
// nullable in the model or drops [Required], triggers a
|
||||
// conscious update of the test (and probably of the VM).
|
||||
ResetDatabase();
|
||||
_fixture.ResetDatabase();
|
||||
using var http = NewClient(subject: "tester");
|
||||
|
||||
var draft = new BlogPost
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
|
||||
public long CircleId { get; private set; }
|
||||
public long PostId { get; private set; }
|
||||
public string DefaultUserLogin { get => "alice"; }
|
||||
|
||||
// A single SqliteConnection held open at the static level,
|
||||
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
|
||||
|
|
@ -169,7 +170,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
// PermissionHandler ownership check sees a null
|
||||
// user id and rejects every PUT.
|
||||
options.MapInboundClaims = false;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
options.TokenValidationParameters
|
||||
= new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = TestTokenIssuer.Issuer,
|
||||
|
|
@ -263,6 +265,27 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
await Task.CompletedTask;
|
||||
return app;
|
||||
}
|
||||
/// <summary>Reset the in-memory database to a known empty state.
|
||||
/// <c>UseInMemoryDatabase</c> shares its store across the
|
||||
/// lifetime of the <see cref="BlogsWebServerFixture"/> instance,
|
||||
/// so without a per-test reset the test order would leak
|
||||
/// state between tests.</summary>
|
||||
public void ResetDatabase()
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureDeleted();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
public void CleanupAcl()
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.CircleAuthorizationToBlogPost
|
||||
.Where(a => a.CircleId == CircleId
|
||||
&& a.BlogPostId == PostId)
|
||||
.ExecuteDelete();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
|
|
@ -310,7 +333,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
/// <param name="configure">Optional hook to fill in fields
|
||||
/// like <c>FullName</c> / <c>Avatar</c> / <c>EmailConfirmed</c>
|
||||
/// that downstream tests assert on.</param>
|
||||
public ApplicationUser SeedUser(string userName, Action<ApplicationUser>? configure = null)
|
||||
public ApplicationUser SeedUser(string userName,
|
||||
Action<ApplicationUser>? configure = null)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
|
@ -351,20 +375,31 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
/// <summary>Create a circle owned by <paramref name="ownerId"/>
|
||||
/// directly in the SQLite store and return its server-assigned
|
||||
/// id.</summary>
|
||||
private long SeedCircle(string ownerId, string name, bool isPublic = false)
|
||||
public long SeedCircle(string ownerId, string name, bool isPublic = false,
|
||||
ICollection<String> members = null
|
||||
)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic };
|
||||
db.Circle.Add(circle);
|
||||
db.SaveChanges();
|
||||
if (members != null && members.Count > 0)
|
||||
{
|
||||
foreach (String memberId in members)
|
||||
{
|
||||
var member = new CircleMember { CircleId = circle.Id, MemberId = memberId };
|
||||
db.CircleMembers.Add(member);
|
||||
}
|
||||
db.SaveChanges();
|
||||
}
|
||||
return circle.Id;
|
||||
}
|
||||
|
||||
/// <summary>Create a blog post owned by <paramref name="authorId"/>
|
||||
/// directly in the SQLite store and return its server-assigned
|
||||
/// id.</summary>
|
||||
private long SeedBlogPost(string authorId, string title)
|
||||
public long SeedBlogPost(string authorId, string title)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
{
|
||||
[Authorize("BlogScope")]
|
||||
[Produces("application/json")]
|
||||
[Route(APIPrefix + "/blog")]
|
||||
[Route(APIPrefix + "/" + BlogSpotPath)]
|
||||
public class BlogApiController : Controller
|
||||
{
|
||||
private readonly BlogSpotService blogSpotService;
|
||||
|
|
@ -19,14 +19,14 @@ namespace Yavsc.Blogs.Controllers
|
|||
this.blogSpotService = blogSpotService;
|
||||
}
|
||||
|
||||
// GET: api/BlogApi
|
||||
// GET: api/v1/blogspot
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<IBlogPost>> GetBlogspot(int start = 0, int take = 25)
|
||||
{
|
||||
return await blogSpotService.Index(User, null, start, take);
|
||||
}
|
||||
|
||||
// GET: api/BlogApi/5
|
||||
// GET: api/v1/blogspot/5
|
||||
[HttpGet("{id}", Name = "GetBlog")]
|
||||
public async Task<IActionResult> GetBlog([FromRoute] long id)
|
||||
{
|
||||
|
|
@ -43,7 +43,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(blog);
|
||||
return Ok(blog.GetPayload());
|
||||
}
|
||||
catch (AuthorizationFailureException)
|
||||
{
|
||||
|
|
@ -51,7 +51,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
// PUT: api/BlogApi/5
|
||||
// PUT: api/v1/blogspot/5
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> PutBlog(long id, [FromBody] Models.Blog.BlogPost blog)
|
||||
{
|
||||
|
|
@ -83,7 +83,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
return new StatusCodeResult(StatusCodes.Status204NoContent);
|
||||
}
|
||||
|
||||
// POST: api/v1/blog
|
||||
// POST: api/v1/blogspot
|
||||
[HttpPost]
|
||||
public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog)
|
||||
{
|
||||
|
|
@ -116,7 +116,8 @@ namespace Yavsc.Blogs.Controllers
|
|||
: (IFormFileCollection)new FormFileCollection();
|
||||
var uid = User.GetUserId();
|
||||
var post = blogSpotService.Create(uid, blog, files);
|
||||
return CreatedAtRoute("GetBlog", new { id = post.Id }, post);
|
||||
return CreatedAtRoute("GetBlog", new { id = post.Id },
|
||||
post.GetPayload());
|
||||
}
|
||||
|
||||
// DELETE: api/BlogApi/5
|
||||
|
|
@ -135,7 +136,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
}
|
||||
|
||||
await blogSpotService.Delete(User, id);
|
||||
return Ok(blog);
|
||||
return Ok(blog.GetPayload());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using static Yavsc.Constants;
|
|||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
[Produces("application/json")]
|
||||
[Route(APIPrefix + "/blogtags")]
|
||||
[Route(APIPrefix + "/" + BlogTagPath )]
|
||||
public class BlogTagsApiController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using static Yavsc.Constants;
|
|||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
[Produces("application/json")]
|
||||
[Route(APIPrefix +"/circle")]
|
||||
[Route(APIPrefix +"/" + CirclePath)]
|
||||
public class CircleApiController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Yavsc.Blogs.Controllers
|
|||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(APIPrefix + "/blogcomments")]
|
||||
[Route(APIPrefix + "/" + CommentsPath)]
|
||||
public class CommentsApiController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
|
|
|||
21
src/Yavsc.Server/Helpers/PayloadHelpers.cs
Normal file
21
src/Yavsc.Server/Helpers/PayloadHelpers.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Yavsc.Models.Blog;
|
||||
|
||||
public static class PayloadHelpers
|
||||
{
|
||||
public static object GetPayload(this BlogPost post)
|
||||
{
|
||||
return new
|
||||
{
|
||||
post.Id,
|
||||
post.Title,
|
||||
post.Article,
|
||||
post.DateCreated,
|
||||
post.UserCreated,
|
||||
post.DateModified,
|
||||
post.UserModified,
|
||||
post.AuthorId,
|
||||
ACL = post.GetACL(),
|
||||
Tags = post.GetTags()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ namespace Yavsc.Models.Blog
|
|||
|
||||
public string[] GetTags()
|
||||
{
|
||||
return Tags.Select(t => t.Tag.Name).ToArray();
|
||||
return Tags?.Select(t => t.Tag.Name).ToArray() ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
[InverseProperty("Post")]
|
||||
|
|
@ -106,6 +106,7 @@ namespace Yavsc.Models.Blog
|
|||
[NotMapped]
|
||||
public bool IsPublished { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
/// Explicit interface implementation of
|
||||
/// <see cref="IBlogPost.Author"/>. The underlying
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Yavsc.Models.Relationship;
|
||||
|
||||
namespace Yavsc.Models.Blog
|
||||
{
|
||||
public partial class BlogTag
|
||||
{
|
||||
[JsonIgnore]
|
||||
[ForeignKey("PostId")]
|
||||
public virtual BlogPost Post { get; set; }
|
||||
public long PostId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[ForeignKey("TagId")]
|
||||
public virtual Tag Tag{ get; set; }
|
||||
public long TagId { get; set; }
|
||||
|
|
|
|||
|
|
@ -13,15 +13,17 @@ namespace Yavsc.Models.Blog
|
|||
|
||||
[YaStringLength(1024)]
|
||||
public string Article { get; set; }
|
||||
|
||||
[ForeignKeyAttribute(nameof(ReceiverId))][JsonIgnore]
|
||||
|
||||
[JsonIgnore]
|
||||
[ForeignKeyAttribute(nameof(ReceiverId))]
|
||||
public virtual BlogPost Post { get; set; }
|
||||
|
||||
[Required]
|
||||
public long ReceiverId { get; set; }
|
||||
public bool Visible { get; set; }
|
||||
|
||||
[ForeignKeyAttribute("AuthorId")][JsonIgnore]
|
||||
[ForeignKeyAttribute("AuthorId")]
|
||||
[JsonIgnore]
|
||||
public virtual ApplicationUser Author {
|
||||
get; set;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue