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",
|
"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": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "run-debug-android",
|
"label": "run-debug-android",
|
||||||
|
|
@ -20,21 +36,6 @@
|
||||||
"-p:AndroidAttachDebugger=true",
|
"-p:AndroidAttachDebugger=true",
|
||||||
"-p:AndroidSdbHostPort=55555",
|
"-p:AndroidSdbHostPort=55555",
|
||||||
"-p:AndroidSdbTargetPort=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",
|
"group": "build",
|
||||||
"isBuildCommand": true,
|
"isBuildCommand": true,
|
||||||
"isTestCommand": false,
|
"isTestCommand": false,
|
||||||
"problemMatcher": ["$msCompile"],
|
|
||||||
"isBackground": true
|
"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)
|
[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian)
|
||||||
pour la production des paquets `.deb`.
|
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
|
## [1.0.8-rc3] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@ namespace Yavsc
|
||||||
{
|
{
|
||||||
|
|
||||||
public const string APIPrefix = "api/v1";
|
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 = {
|
public static readonly Scope[] SiteScopes = {
|
||||||
new Scope { Id = "profile", Description = "Your profile informations" },
|
new Scope { Id = "profile", Description = "Your profile informations" },
|
||||||
new Scope { Id = "book" , Description ="Your booking interface"},
|
new Scope { Id = "book" , Description ="Your booking interface"},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Yavsc.Abstract.BlogSpot;
|
using Yavsc.Abstract.BlogSpot;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Access;
|
using Yavsc.Models.Access;
|
||||||
|
using Yavsc.Models.Blog;
|
||||||
using Yavsc.Tests.Shared;
|
using Yavsc.Tests.Shared;
|
||||||
using static Yavsc.Constants;
|
using static Yavsc.Constants;
|
||||||
|
|
||||||
|
|
@ -38,15 +40,16 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
||||||
{
|
{
|
||||||
private readonly BlogsWebServerFixture _fixture;
|
private readonly BlogsWebServerFixture _fixture;
|
||||||
|
|
||||||
|
|
||||||
public BlogAclApiTests(BlogsWebServerFixture fixture)
|
public BlogAclApiTests(BlogsWebServerFixture fixture)
|
||||||
{
|
{
|
||||||
_fixture = fixture;
|
_fixture = fixture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string BlogUrl()
|
||||||
|
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogSpotPath}";
|
||||||
private string BlogAclUrl()
|
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
|
/// <summary>Delete any ACL rows tied to the fixture's seeded
|
||||||
/// <c>(CircleId, BlogPostId)</c> pair. The shared SQLite store
|
/// <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
|
// owned by the caller. We seed the same shape pre-POST so the
|
||||||
// test reproduces the prod scenario end-to-end.
|
// test reproduces the prod scenario end-to-end.
|
||||||
CleanupAcl();
|
CleanupAcl();
|
||||||
using var http = NewClient("alice");
|
using var http = NewClient(_fixture.DefaultUserLogin);
|
||||||
|
|
||||||
var payload = new PostAccessControlRulePayload
|
var payload = new PostAccessControlRulePayload
|
||||||
{
|
{
|
||||||
|
|
@ -178,7 +181,7 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
|
||||||
[MemberData(nameof(BlogAclPayloadsForNever500))]
|
[MemberData(nameof(BlogAclPayloadsForNever500))]
|
||||||
public async Task PostCircleAuthorization_never_returns_500(PostAccessControlRulePayload payload)
|
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(
|
var response = await http.PostAsJsonAsync(
|
||||||
BlogAclUrl(), payload,
|
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.Net.Http.Json;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Blog;
|
using Yavsc.Models.Blog;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
|
|
@ -31,18 +30,6 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
||||||
_fixture = fixture;
|
_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
|
/// <summary>Reset the database and seed the
|
||||||
/// <c>tester</c> <see cref="ApplicationUser"/> row. Required
|
/// <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>
|
/// at <c>SaveChanges</c> and the controller returns 500.</summary>
|
||||||
private void ResetAndSeedDefaultUser()
|
private void ResetAndSeedDefaultUser()
|
||||||
{
|
{
|
||||||
ResetDatabase();
|
_fixture.ResetDatabase();
|
||||||
_fixture.SeedUser("tester");
|
_fixture.SeedUser("tester");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +98,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBlogs_returns_200_with_empty_list_when_no_posts()
|
public async Task GetBlogs_returns_200_with_empty_list_when_no_posts()
|
||||||
{
|
{
|
||||||
ResetDatabase();
|
_fixture.ResetDatabase();
|
||||||
using var http = NewClient();
|
using var http = NewClient();
|
||||||
|
|
||||||
var response = await http.GetAsync("/api/v1/blog",
|
var response = await http.GetAsync("/api/v1/blog",
|
||||||
|
|
@ -266,7 +253,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetBlog_returns_401_when_no_token_is_provided()
|
public async Task GetBlog_returns_401_when_no_token_is_provided()
|
||||||
{
|
{
|
||||||
ResetDatabase();
|
_fixture.ResetDatabase();
|
||||||
using var http = NewAnonymousClient();
|
using var http = NewAnonymousClient();
|
||||||
|
|
||||||
// No Authorization header → the JwtBearer middleware
|
// 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
|
// behaviour so a future change that, say, makes Title
|
||||||
// nullable in the model or drops [Required], triggers a
|
// nullable in the model or drops [Required], triggers a
|
||||||
// conscious update of the test (and probably of the VM).
|
// conscious update of the test (and probably of the VM).
|
||||||
ResetDatabase();
|
_fixture.ResetDatabase();
|
||||||
using var http = NewClient(subject: "tester");
|
using var http = NewClient(subject: "tester");
|
||||||
|
|
||||||
var draft = new BlogPost
|
var draft = new BlogPost
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
|
|
||||||
public long CircleId { get; private set; }
|
public long CircleId { get; private set; }
|
||||||
public long PostId { get; private set; }
|
public long PostId { get; private set; }
|
||||||
|
public string DefaultUserLogin { get => "alice"; }
|
||||||
|
|
||||||
// A single SqliteConnection held open at the static level,
|
// A single SqliteConnection held open at the static level,
|
||||||
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
|
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
|
||||||
|
|
@ -169,7 +170,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
// PermissionHandler ownership check sees a null
|
// PermissionHandler ownership check sees a null
|
||||||
// user id and rejects every PUT.
|
// user id and rejects every PUT.
|
||||||
options.MapInboundClaims = false;
|
options.MapInboundClaims = false;
|
||||||
options.TokenValidationParameters = new TokenValidationParameters
|
options.TokenValidationParameters
|
||||||
|
= new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
ValidIssuer = TestTokenIssuer.Issuer,
|
ValidIssuer = TestTokenIssuer.Issuer,
|
||||||
|
|
@ -263,6 +265,27 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
return app;
|
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()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
|
|
@ -310,7 +333,8 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
/// <param name="configure">Optional hook to fill in fields
|
/// <param name="configure">Optional hook to fill in fields
|
||||||
/// like <c>FullName</c> / <c>Avatar</c> / <c>EmailConfirmed</c>
|
/// like <c>FullName</c> / <c>Avatar</c> / <c>EmailConfirmed</c>
|
||||||
/// that downstream tests assert on.</param>
|
/// 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();
|
using var scope = Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
|
@ -351,20 +375,31 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
||||||
/// <summary>Create a circle owned by <paramref name="ownerId"/>
|
/// <summary>Create a circle owned by <paramref name="ownerId"/>
|
||||||
/// directly in the SQLite store and return its server-assigned
|
/// directly in the SQLite store and return its server-assigned
|
||||||
/// id.</summary>
|
/// 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();
|
using var scope = Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic };
|
var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic };
|
||||||
db.Circle.Add(circle);
|
db.Circle.Add(circle);
|
||||||
db.SaveChanges();
|
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;
|
return circle.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Create a blog post owned by <paramref name="authorId"/>
|
/// <summary>Create a blog post owned by <paramref name="authorId"/>
|
||||||
/// directly in the SQLite store and return its server-assigned
|
/// directly in the SQLite store and return its server-assigned
|
||||||
/// id.</summary>
|
/// id.</summary>
|
||||||
private long SeedBlogPost(string authorId, string title)
|
public long SeedBlogPost(string authorId, string title)
|
||||||
{
|
{
|
||||||
using var scope = Services.CreateScope();
|
using var scope = Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
{
|
{
|
||||||
[Authorize("BlogScope")]
|
[Authorize("BlogScope")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Route(APIPrefix + "/blog")]
|
[Route(APIPrefix + "/" + BlogSpotPath)]
|
||||||
public class BlogApiController : Controller
|
public class BlogApiController : Controller
|
||||||
{
|
{
|
||||||
private readonly BlogSpotService blogSpotService;
|
private readonly BlogSpotService blogSpotService;
|
||||||
|
|
@ -19,14 +19,14 @@ namespace Yavsc.Blogs.Controllers
|
||||||
this.blogSpotService = blogSpotService;
|
this.blogSpotService = blogSpotService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/BlogApi
|
// GET: api/v1/blogspot
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IEnumerable<IBlogPost>> GetBlogspot(int start = 0, int take = 25)
|
public async Task<IEnumerable<IBlogPost>> GetBlogspot(int start = 0, int take = 25)
|
||||||
{
|
{
|
||||||
return await blogSpotService.Index(User, null, start, take);
|
return await blogSpotService.Index(User, null, start, take);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/BlogApi/5
|
// GET: api/v1/blogspot/5
|
||||||
[HttpGet("{id}", Name = "GetBlog")]
|
[HttpGet("{id}", Name = "GetBlog")]
|
||||||
public async Task<IActionResult> GetBlog([FromRoute] long id)
|
public async Task<IActionResult> GetBlog([FromRoute] long id)
|
||||||
{
|
{
|
||||||
|
|
@ -43,7 +43,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(blog);
|
return Ok(blog.GetPayload());
|
||||||
}
|
}
|
||||||
catch (AuthorizationFailureException)
|
catch (AuthorizationFailureException)
|
||||||
{
|
{
|
||||||
|
|
@ -51,7 +51,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT: api/BlogApi/5
|
// PUT: api/v1/blogspot/5
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
public async Task<IActionResult> PutBlog(long id, [FromBody] Models.Blog.BlogPost blog)
|
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);
|
return new StatusCodeResult(StatusCodes.Status204NoContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/v1/blog
|
// POST: api/v1/blogspot
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog)
|
public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog)
|
||||||
{
|
{
|
||||||
|
|
@ -116,7 +116,8 @@ namespace Yavsc.Blogs.Controllers
|
||||||
: (IFormFileCollection)new FormFileCollection();
|
: (IFormFileCollection)new FormFileCollection();
|
||||||
var uid = User.GetUserId();
|
var uid = User.GetUserId();
|
||||||
var post = blogSpotService.Create(uid, blog, files);
|
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
|
// DELETE: api/BlogApi/5
|
||||||
|
|
@ -135,7 +136,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
await blogSpotService.Delete(User, id);
|
await blogSpotService.Delete(User, id);
|
||||||
return Ok(blog);
|
return Ok(blog.GetPayload());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using static Yavsc.Constants;
|
||||||
namespace Yavsc.Blogs.Controllers
|
namespace Yavsc.Blogs.Controllers
|
||||||
{
|
{
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Route(APIPrefix + "/blogtags")]
|
[Route(APIPrefix + "/" + BlogTagPath )]
|
||||||
public class BlogTagsApiController : Controller
|
public class BlogTagsApiController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ using static Yavsc.Constants;
|
||||||
namespace Yavsc.Blogs.Controllers
|
namespace Yavsc.Blogs.Controllers
|
||||||
{
|
{
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Route(APIPrefix +"/circle")]
|
[Route(APIPrefix +"/" + CirclePath)]
|
||||||
public class CircleApiController : Controller
|
public class CircleApiController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Yavsc.Blogs.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Route(APIPrefix + "/blogcomments")]
|
[Route(APIPrefix + "/" + CommentsPath)]
|
||||||
public class CommentsApiController : Controller
|
public class CommentsApiController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
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()
|
public string[] GetTags()
|
||||||
{
|
{
|
||||||
return Tags.Select(t => t.Tag.Name).ToArray();
|
return Tags?.Select(t => t.Tag.Name).ToArray() ?? Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[InverseProperty("Post")]
|
[InverseProperty("Post")]
|
||||||
|
|
@ -106,6 +106,7 @@ namespace Yavsc.Models.Blog
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool IsPublished { get; set; }
|
public bool IsPublished { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Explicit interface implementation of
|
/// Explicit interface implementation of
|
||||||
/// <see cref="IBlogPost.Author"/>. The underlying
|
/// <see cref="IBlogPost.Author"/>. The underlying
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Yavsc.Models.Relationship;
|
using Yavsc.Models.Relationship;
|
||||||
|
|
||||||
namespace Yavsc.Models.Blog
|
namespace Yavsc.Models.Blog
|
||||||
{
|
{
|
||||||
public partial class BlogTag
|
public partial class BlogTag
|
||||||
{
|
{
|
||||||
|
[JsonIgnore]
|
||||||
[ForeignKey("PostId")]
|
[ForeignKey("PostId")]
|
||||||
public virtual BlogPost Post { get; set; }
|
public virtual BlogPost Post { get; set; }
|
||||||
public long PostId { get; set; }
|
public long PostId { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
[ForeignKey("TagId")]
|
[ForeignKey("TagId")]
|
||||||
public virtual Tag Tag{ get; set; }
|
public virtual Tag Tag{ get; set; }
|
||||||
public long TagId { get; set; }
|
public long TagId { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,17 @@ namespace Yavsc.Models.Blog
|
||||||
|
|
||||||
[YaStringLength(1024)]
|
[YaStringLength(1024)]
|
||||||
public string Article { get; set; }
|
public string Article { get; set; }
|
||||||
|
|
||||||
[ForeignKeyAttribute(nameof(ReceiverId))][JsonIgnore]
|
[JsonIgnore]
|
||||||
|
[ForeignKeyAttribute(nameof(ReceiverId))]
|
||||||
public virtual BlogPost Post { get; set; }
|
public virtual BlogPost Post { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public long ReceiverId { get; set; }
|
public long ReceiverId { get; set; }
|
||||||
public bool Visible { get; set; }
|
public bool Visible { get; set; }
|
||||||
|
|
||||||
[ForeignKeyAttribute("AuthorId")][JsonIgnore]
|
[ForeignKeyAttribute("AuthorId")]
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ApplicationUser Author {
|
public virtual ApplicationUser Author {
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue