chore: remove API v1
This commit is contained in:
parent
1e86c2d6c4
commit
7aaad288e6
@ -1,30 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public static class AuthExt
|
||||
{
|
||||
public static SystemId CurrentSystem(this ClaimsPrincipal user)
|
||||
{
|
||||
var claim = user.FindFirst(PKClaims.SystemId);
|
||||
if (claim == null) throw new ArgumentException("User is unauthorized");
|
||||
|
||||
if (int.TryParse(claim.Value, out var id))
|
||||
return new SystemId(id);
|
||||
throw new ArgumentException("User has non-integer system ID claim");
|
||||
}
|
||||
|
||||
public static LookupContext ContextFor(this ClaimsPrincipal user, PKSystem system)
|
||||
{
|
||||
if (!user.Identity.IsAuthenticated) return LookupContext.API;
|
||||
return system.Id == user.CurrentSystem() ? LookupContext.ByOwner : LookupContext.API;
|
||||
}
|
||||
|
||||
public static LookupContext ContextFor(this ClaimsPrincipal user, PKMember member)
|
||||
{
|
||||
if (!user.Identity.IsAuthenticated) return LookupContext.API;
|
||||
return member.System == user.CurrentSystem() ? LookupContext.ByOwner : LookupContext.API;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class PKClaims
|
||||
{
|
||||
public const string SystemId = "PluralKit:SystemId";
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class SystemTokenAuthenticationHandler: AuthenticationHandler<SystemTokenAuthenticationHandler.Opts>
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
|
||||
public SystemTokenAuthenticationHandler(IOptionsMonitor<Opts> options, ILoggerFactory logger,
|
||||
UrlEncoder encoder, ISystemClock clock, IDatabase db) : base(options,
|
||||
logger, encoder, clock)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.ContainsKey("Authorization"))
|
||||
return AuthenticateResult.NoResult();
|
||||
|
||||
var token = Request.Headers["Authorization"].FirstOrDefault();
|
||||
// todo: move this to ModelRepository
|
||||
var systemId = await _db.Execute(c =>
|
||||
c.QuerySingleOrDefaultAsync<SystemId?>("select id from systems where token = @token",
|
||||
new { token }));
|
||||
if (systemId == null) return AuthenticateResult.Fail("Invalid system token");
|
||||
|
||||
var claims = new[] { new Claim(PKClaims.SystemId, systemId.Value.Value.ToString()) };
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
ticket.Properties.IsPersistent = false;
|
||||
ticket.Properties.AllowRefresh = false;
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
|
||||
public class Opts: AuthenticationSchemeOptions { }
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class MemberOwnerHandler: AuthorizationHandler<OwnSystemRequirement, PKMember>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
OwnSystemRequirement requirement, PKMember resource)
|
||||
{
|
||||
if (!context.User.Identity.IsAuthenticated) return Task.CompletedTask;
|
||||
if (resource.System == context.User.CurrentSystem())
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class MemberPrivacyHandler: AuthorizationHandler<PrivacyRequirement<PKMember>, PKMember>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
PrivacyRequirement<PKMember> requirement, PKMember resource)
|
||||
{
|
||||
var level = requirement.Mapper(resource);
|
||||
var ctx = context.User.ContextFor(resource);
|
||||
if (level.CanAccess(ctx))
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class OwnSystemRequirement: IAuthorizationRequirement { }
|
@ -1,15 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class PrivacyRequirement<T>: IAuthorizationRequirement
|
||||
{
|
||||
public readonly Func<T, PrivacyLevel> Mapper;
|
||||
|
||||
public PrivacyRequirement(Func<T, PrivacyLevel> mapper)
|
||||
{
|
||||
Mapper = mapper;
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class SystemOwnerHandler: AuthorizationHandler<OwnSystemRequirement, PKSystem>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
OwnSystemRequirement requirement, PKSystem resource)
|
||||
{
|
||||
if (!context.User.Identity.IsAuthenticated) return Task.CompletedTask;
|
||||
if (resource.Id == context.User.CurrentSystem())
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class SystemPrivacyHandler: AuthorizationHandler<PrivacyRequirement<PKSystem>, PKSystem>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
PrivacyRequirement<PKSystem> requirement, PKSystem resource)
|
||||
{
|
||||
var level = requirement.Mapper(resource);
|
||||
var ctx = context.User.ContextFor(resource);
|
||||
if (level.CanAccess(ctx))
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ public class PrivateController: PKControllerBase
|
||||
var data = new JObject();
|
||||
data.Add("privacy", inner);
|
||||
|
||||
var patch = MemberPatch.FromJSON(data, APIVersion.V2);
|
||||
var patch = MemberPatch.FromJSON(data);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
@ -135,7 +135,7 @@ public class PrivateController: PKControllerBase
|
||||
|
||||
var o = new JObject();
|
||||
|
||||
o.Add("system", system.ToJson(LookupContext.ByOwner, APIVersion.V2));
|
||||
o.Add("system", system.ToJson(LookupContext.ByOwner));
|
||||
o.Add("user", user);
|
||||
o.Add("token", system.Token);
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/a")]
|
||||
public class AccountController: ControllerBase
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public AccountController(IDatabase db, ModelRepository repo)
|
||||
{
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
[HttpGet("{aid}")]
|
||||
public async Task<ActionResult<JObject>> GetSystemByAccount(ulong aid)
|
||||
{
|
||||
var system = await _repo.GetSystemByAccount(aid);
|
||||
if (system == null)
|
||||
return NotFound("Account not found.");
|
||||
|
||||
return Ok(system.ToJson(User.ContextFor(system)));
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/m")]
|
||||
public class MemberController: ControllerBase
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly IAuthorizationService _auth;
|
||||
|
||||
public MemberController(IAuthorizationService auth, IDatabase db, ModelRepository repo)
|
||||
{
|
||||
_auth = auth;
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
[HttpGet("{hid}")]
|
||||
public async Task<ActionResult<JObject>> GetMember(string hid)
|
||||
{
|
||||
var member = await _repo.GetMemberByHid(hid);
|
||||
if (member == null) return NotFound("Member not found.");
|
||||
|
||||
return Ok(member.ToJson(User.ContextFor(member), true));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties)
|
||||
{
|
||||
if (!properties.ContainsKey("name"))
|
||||
return BadRequest("Member name must be specified.");
|
||||
|
||||
var systemId = User.CurrentSystem();
|
||||
var config = await _repo.GetSystemConfig(systemId);
|
||||
|
||||
await using var conn = await _db.Obtain();
|
||||
|
||||
// Enforce per-system member limit
|
||||
var memberCount = await conn.QuerySingleAsync<int>("select count(*) from members where system = @System",
|
||||
new { System = systemId });
|
||||
var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
if (memberCount >= memberLimit)
|
||||
return BadRequest($"Member limit reached ({memberCount} / {memberLimit}).");
|
||||
|
||||
await using var tx = await conn.BeginTransactionAsync();
|
||||
var member = await _repo.CreateMember(systemId, properties.Value<string>("name"), conn);
|
||||
|
||||
var patch = MemberPatch.FromJSON(properties);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
|
||||
var err = patch.Errors[0];
|
||||
if (err is FieldTooLongError)
|
||||
return BadRequest($"Field {err.Key} is too long "
|
||||
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
|
||||
if (err.Text != null)
|
||||
return BadRequest(err.Text);
|
||||
return BadRequest($"Field {err.Key} is invalid.");
|
||||
}
|
||||
|
||||
member = await _repo.UpdateMember(member.Id, patch, conn);
|
||||
await tx.CommitAsync();
|
||||
return Ok(member.ToJson(User.ContextFor(member), true));
|
||||
}
|
||||
|
||||
[HttpPatch("{hid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> PatchMember(string hid, [FromBody] JObject changes)
|
||||
{
|
||||
var member = await _repo.GetMemberByHid(hid);
|
||||
if (member == null) return NotFound("Member not found.");
|
||||
|
||||
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
|
||||
if (!res.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, $"Member '{hid}' is not part of your system.");
|
||||
|
||||
var patch = MemberPatch.FromJSON(changes);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
{
|
||||
var err = patch.Errors[0];
|
||||
if (err is FieldTooLongError)
|
||||
return BadRequest($"Field {err.Key} is too long "
|
||||
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
|
||||
if (err.Text != null)
|
||||
return BadRequest(err.Text);
|
||||
return BadRequest($"Field {err.Key} is invalid.");
|
||||
}
|
||||
|
||||
var newMember = await _repo.UpdateMember(member.Id, patch);
|
||||
return Ok(newMember.ToJson(User.ContextFor(newMember), true));
|
||||
}
|
||||
|
||||
[HttpDelete("{hid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeleteMember(string hid)
|
||||
{
|
||||
var member = await _repo.GetMemberByHid(hid);
|
||||
if (member == null) return NotFound("Member not found.");
|
||||
|
||||
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
|
||||
if (!res.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, $"Member '{hid}' is not part of your system.");
|
||||
|
||||
await _repo.DeleteMember(member.Id);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1")]
|
||||
public class MessageController: ControllerBase
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public MessageController(ModelRepository repo, IDatabase db)
|
||||
{
|
||||
_repo = repo;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet("msg/{mid}")]
|
||||
public async Task<ActionResult<JObject>> GetMessage(ulong mid)
|
||||
{
|
||||
var msg = await _db.Execute(c => _repo.GetMessage(c, mid));
|
||||
if (msg == null) return NotFound("Message not found.");
|
||||
|
||||
var ctx = msg.System == null ? LookupContext.ByNonOwner : User.ContextFor(msg.System);
|
||||
return msg.ToJson(ctx, APIVersion.V1);
|
||||
}
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public struct SwitchesReturn
|
||||
{
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
|
||||
}
|
||||
|
||||
public struct FrontersReturn
|
||||
{
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<JObject> Members { get; set; }
|
||||
}
|
||||
|
||||
public struct PostSwitchParams
|
||||
{
|
||||
public Instant? Timestamp { get; set; }
|
||||
public ICollection<string> Members { get; set; }
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/s")]
|
||||
public class SystemController: ControllerBase
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly IAuthorizationService _auth;
|
||||
|
||||
public SystemController(IDatabase db, IAuthorizationService auth, ModelRepository repo)
|
||||
{
|
||||
_db = db;
|
||||
_auth = auth;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> GetOwnSystem()
|
||||
{
|
||||
var system = await _repo.GetSystem(User.CurrentSystem());
|
||||
return system.ToJson(User.ContextFor(system));
|
||||
}
|
||||
|
||||
[HttpGet("{hid}")]
|
||||
public async Task<ActionResult<JObject>> GetSystem(string hid)
|
||||
{
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null) return NotFound("System not found.");
|
||||
return Ok(system.ToJson(User.ContextFor(system)));
|
||||
}
|
||||
|
||||
[HttpGet("{hid}/members")]
|
||||
public async Task<ActionResult<IEnumerable<JObject>>> GetMembers(string hid)
|
||||
{
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null)
|
||||
return NotFound("System not found.");
|
||||
|
||||
if (!system.MemberListPrivacy.CanAccess(User.ContextFor(system)))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view member list.");
|
||||
|
||||
var members = _repo.GetSystemMembers(system.Id);
|
||||
return Ok(await members
|
||||
.Where(m => m.MemberVisibility.CanAccess(User.ContextFor(system)))
|
||||
.Select(m => m.ToJson(User.ContextFor(system), needsLegacyProxyTags: true))
|
||||
.ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{hid}/switches")]
|
||||
public async Task<ActionResult<IEnumerable<SwitchesReturn>>> GetSwitches(
|
||||
string hid, [FromQuery(Name = "before")] Instant? before)
|
||||
{
|
||||
if (before == null) before = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null) return NotFound("System not found.");
|
||||
|
||||
var auth = await _auth.AuthorizeAsync(User, system, "ViewFrontHistory");
|
||||
if (!auth.Succeeded)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view front history.");
|
||||
|
||||
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturn>(
|
||||
@"select *, array(
|
||||
select members.hid from switch_members, members
|
||||
where switch_members.switch = switches.id and members.id = switch_members.member
|
||||
) as members from switches
|
||||
where switches.system = @System and switches.timestamp < @Before
|
||||
order by switches.timestamp desc
|
||||
limit 100;",
|
||||
new { System = system.Id, Before = before }
|
||||
));
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("{hid}/fronters")]
|
||||
public async Task<ActionResult<FrontersReturn>> GetFronters(string hid)
|
||||
{
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null) return NotFound("System not found.");
|
||||
|
||||
var auth = await _auth.AuthorizeAsync(User, system, "ViewFront");
|
||||
if (!auth.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view fronter.");
|
||||
|
||||
var sw = await _repo.GetLatestSwitch(system.Id);
|
||||
if (sw == null) return NotFound("System has no registered switches.");
|
||||
|
||||
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
return Ok(new FrontersReturn
|
||||
{
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(User.ContextFor(system), true)).ToListAsync()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> EditSystem([FromBody] JObject changes)
|
||||
{
|
||||
var system = await _repo.GetSystem(User.CurrentSystem());
|
||||
|
||||
var patch = SystemPatch.FromJSON(changes);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
{
|
||||
var err = patch.Errors[0];
|
||||
if (err is FieldTooLongError)
|
||||
return BadRequest($"Field {err.Key} is too long "
|
||||
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
|
||||
|
||||
return BadRequest($"Field {err.Key} is invalid.");
|
||||
}
|
||||
|
||||
system = await _repo.UpdateSystem(system!.Id, patch);
|
||||
return Ok(system.ToJson(User.ContextFor(system)));
|
||||
}
|
||||
|
||||
[HttpPost("switches")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PostSwitch([FromBody] PostSwitchParams param)
|
||||
{
|
||||
if (param.Members.Distinct().Count() != param.Members.Count)
|
||||
return BadRequest("Duplicate members in member list.");
|
||||
|
||||
await using var conn = await _db.Obtain();
|
||||
|
||||
// We get the current switch, if it exists
|
||||
var latestSwitch = await _repo.GetLatestSwitch(User.CurrentSystem());
|
||||
if (latestSwitch != null)
|
||||
{
|
||||
var latestSwitchMembers = _repo.GetSwitchMembers(conn, latestSwitch.Id);
|
||||
|
||||
// Bail if this switch is identical to the latest one
|
||||
if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(param.Members.ToAsyncEnumerable()))
|
||||
return BadRequest("New members identical to existing fronters.");
|
||||
}
|
||||
|
||||
// Resolve member objects for all given IDs
|
||||
var membersList =
|
||||
(await conn.QueryAsync<PKMember>("select * from members where hid = any(@Hids)",
|
||||
new { Hids = param.Members })).ToList();
|
||||
|
||||
foreach (var member in membersList)
|
||||
if (member.System != User.CurrentSystem())
|
||||
return BadRequest($"Cannot switch to member '{member.Hid}' not in system.");
|
||||
|
||||
// membersList is in DB order, and we want it in actual input order
|
||||
// so we go through a dict and map the original input appropriately
|
||||
var membersDict = membersList.ToDictionary(m => m.Hid);
|
||||
|
||||
var membersInOrder = new List<PKMember>();
|
||||
// We do this without .Select() since we want to have the early return bail if it doesn't find the member
|
||||
foreach (var givenMemberId in param.Members)
|
||||
{
|
||||
if (!membersDict.TryGetValue(givenMemberId, out var member))
|
||||
return BadRequest($"Member '{givenMemberId}' not found.");
|
||||
membersInOrder.Add(member);
|
||||
}
|
||||
|
||||
// Finally, log the switch (yay!)
|
||||
await _repo.AddSwitch(conn, User.CurrentSystem(), membersInOrder.Select(m => m.Id).ToList());
|
||||
return NoContent();
|
||||
}
|
||||
}
|
@ -97,6 +97,6 @@ public class DiscordControllerV2: PKControllerBase
|
||||
throw Errors.MessageNotFound;
|
||||
|
||||
var ctx = msg.System == null ? LookupContext.ByNonOwner : ContextFor(msg.System);
|
||||
return msg.ToJson(ctx, APIVersion.V2);
|
||||
return msg.ToJson(ctx);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ public class GroupControllerV2: PKControllerBase
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (!system.GroupListPrivacy.CanAccess(User.ContextFor(system)))
|
||||
if (!system.GroupListPrivacy.CanAccess(ContextFor(system)))
|
||||
throw Errors.UnauthorizedGroupList;
|
||||
|
||||
var groups = _repo.GetSystemGroups(system.Id);
|
||||
|
@ -29,7 +29,7 @@ public class GroupMemberControllerV2: PKControllerBase
|
||||
var o = new JArray();
|
||||
|
||||
await foreach (var member in members)
|
||||
o.Add(member.ToJson(ctx, v: APIVersion.V2));
|
||||
o.Add(member.ToJson(ctx));
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ public class MemberControllerV2: PKControllerBase
|
||||
var members = _repo.GetSystemMembers(system.Id);
|
||||
return Ok(await members
|
||||
.Where(m => m.MemberVisibility.CanAccess(ctx))
|
||||
.Select(m => m.ToJson(ctx, v: APIVersion.V2))
|
||||
.Select(m => m.ToJson(ctx))
|
||||
.ToListAsync());
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ public class MemberControllerV2: PKControllerBase
|
||||
if (memberCount >= memberLimit)
|
||||
throw Errors.MemberLimitReached;
|
||||
|
||||
var patch = MemberPatch.FromJSON(data, APIVersion.V2);
|
||||
var patch = MemberPatch.FromJSON(data);
|
||||
patch.AssertIsValid();
|
||||
if (!patch.Name.IsPresent)
|
||||
patch.Errors.Add(new ValidationError("name", "Key 'name' is required when creating new member."));
|
||||
@ -64,7 +64,7 @@ public class MemberControllerV2: PKControllerBase
|
||||
|
||||
await tx.CommitAsync();
|
||||
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
|
||||
[HttpGet("members/{memberRef}")]
|
||||
@ -76,7 +76,7 @@ public class MemberControllerV2: PKControllerBase
|
||||
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
||||
return Ok(member.ToJson(ContextFor(member), systemStr: system.Hid, v: APIVersion.V2));
|
||||
return Ok(member.ToJson(ContextFor(member), systemStr: system.Hid));
|
||||
}
|
||||
|
||||
[HttpPatch("members/{memberRef}")]
|
||||
@ -89,14 +89,14 @@ public class MemberControllerV2: PKControllerBase
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var patch = MemberPatch.FromJSON(data, APIVersion.V2);
|
||||
var patch = MemberPatch.FromJSON(data);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
var newMember = await _repo.UpdateMember(member.Id, patch);
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
|
||||
[HttpDelete("members/{memberRef}")]
|
||||
|
@ -70,7 +70,7 @@ public class SwitchControllerV2: PKControllerBase
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(),
|
||||
Members = await members.Select(m => m.ToJson(ctx)).ToListAsync(),
|
||||
Uuid = sw.Uuid,
|
||||
});
|
||||
}
|
||||
@ -124,7 +124,7 @@ public class SwitchControllerV2: PKControllerBase
|
||||
{
|
||||
Uuid = newSwitch.Uuid,
|
||||
Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)),
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner)),
|
||||
});
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ public class SwitchControllerV2: PKControllerBase
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync()
|
||||
Members = await members.Select(m => m.ToJson(ctx)).ToListAsync()
|
||||
});
|
||||
}
|
||||
|
||||
@ -190,7 +190,7 @@ public class SwitchControllerV2: PKControllerBase
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2))
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner))
|
||||
});
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ public class SwitchControllerV2: PKControllerBase
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2))
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner))
|
||||
});
|
||||
}
|
||||
|
||||
@ -262,3 +262,9 @@ public class SwitchControllerV2: PKControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public struct PostSwitchParams
|
||||
{
|
||||
public Instant? Timestamp { get; set; }
|
||||
public ICollection<string> Members { get; set; }
|
||||
}
|
@ -17,7 +17,7 @@ public class SystemControllerV2: PKControllerBase
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null) throw Errors.SystemNotFound;
|
||||
return Ok(system.ToJson(ContextFor(system), APIVersion.V2));
|
||||
return Ok(system.ToJson(ContextFor(system)));
|
||||
}
|
||||
|
||||
[HttpPatch("{systemRef}")]
|
||||
@ -27,14 +27,14 @@ public class SystemControllerV2: PKControllerBase
|
||||
if (system == null) throw Errors.SystemNotFound;
|
||||
if (ContextFor(system) != LookupContext.ByOwner)
|
||||
throw Errors.GenericMissingPermissions;
|
||||
var patch = SystemPatch.FromJSON(data, APIVersion.V2);
|
||||
var patch = SystemPatch.FromJSON(data);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
var newSystem = await _repo.UpdateSystem(system.Id, patch);
|
||||
return Ok(newSystem.ToJson(LookupContext.ByOwner, APIVersion.V2));
|
||||
return Ok(newSystem.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
|
||||
[HttpGet("{systemRef}/settings")]
|
||||
|
@ -28,29 +28,6 @@ public class Startup
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddAuthentication("SystemToken")
|
||||
.AddScheme<SystemTokenAuthenticationHandler.Opts,
|
||||
SystemTokenAuthenticationHandler>("SystemToken", null);
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("EditSystem",
|
||||
p => p.RequireAuthenticatedUser().AddRequirements(new OwnSystemRequirement()));
|
||||
options.AddPolicy("EditMember",
|
||||
p => p.RequireAuthenticatedUser().AddRequirements(new OwnSystemRequirement()));
|
||||
|
||||
options.AddPolicy("ViewMembers",
|
||||
p => p.AddRequirements(new PrivacyRequirement<PKSystem>(s => s.MemberListPrivacy)));
|
||||
options.AddPolicy("ViewFront",
|
||||
p => p.AddRequirements(new PrivacyRequirement<PKSystem>(s => s.FrontPrivacy)));
|
||||
options.AddPolicy("ViewFrontHistory",
|
||||
p => p.AddRequirements(new PrivacyRequirement<PKSystem>(s => s.FrontHistoryPrivacy)));
|
||||
});
|
||||
services.AddSingleton<IAuthenticationHandler, SystemTokenAuthenticationHandler>();
|
||||
services.AddSingleton<IAuthorizationHandler, MemberOwnerHandler>();
|
||||
services.AddSingleton<IAuthorizationHandler, SystemOwnerHandler>();
|
||||
services.AddSingleton<IAuthorizationHandler, SystemPrivacyHandler>();
|
||||
|
||||
services.AddControllers()
|
||||
// sorry MS, this just does *more*
|
||||
.AddNewtonsoftJson(opts =>
|
||||
|
@ -152,7 +152,7 @@ public class DispatchService
|
||||
data.Event = DispatchEvent.CREATE_MESSAGE;
|
||||
data.SigningToken = system.WebhookToken;
|
||||
data.SystemId = system.Uuid.ToString();
|
||||
data.EventData = fullMessage.ToJson(LookupContext.ByOwner, APIVersion.V2);
|
||||
data.EventData = fullMessage.ToJson(LookupContext.ByOwner);
|
||||
|
||||
_logger.Debug("Dispatching webhook for message create (system {SystemId})", system.Id);
|
||||
await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody());
|
||||
|
@ -1,3 +0,0 @@
|
||||
namespace PluralKit.Core;
|
||||
|
||||
public enum APIVersion { V1, V2 }
|
@ -105,24 +105,21 @@ public static class PKMemberExt
|
||||
member.MetadataPrivacy.Get(ctx, member.MessageCount);
|
||||
|
||||
public static JObject ToJson(this PKMember member, LookupContext ctx, bool needsLegacyProxyTags = false,
|
||||
string systemStr = null, APIVersion v = APIVersion.V1)
|
||||
string systemStr = null)
|
||||
{
|
||||
var includePrivacy = ctx == LookupContext.ByOwner;
|
||||
|
||||
var o = new JObject();
|
||||
o.Add("id", member.Hid);
|
||||
|
||||
if (v == APIVersion.V2)
|
||||
{
|
||||
o.Add("uuid", member.Uuid.ToString());
|
||||
if (systemStr != null)
|
||||
o.Add("system", systemStr);
|
||||
}
|
||||
|
||||
o.Add("name", member.NameFor(ctx));
|
||||
|
||||
// o.Add("color", member.ColorPrivacy.CanAccess(ctx) ? member.Color : null);
|
||||
o.Add("display_name", member.NamePrivacy.CanAccess(ctx) ? member.DisplayName : null);
|
||||
// o.Add("color", member.ColorPrivacy.CanAccess(ctx) ? member.Color : null);
|
||||
o.Add("color", member.Color);
|
||||
o.Add("birthday", member.BirthdayFor(ctx)?.FormatExport());
|
||||
o.Add("pronouns", member.PronounsFor(ctx));
|
||||
@ -137,32 +134,6 @@ public static class PKMemberExt
|
||||
tagArray.Add(new JObject { { "prefix", tag.Prefix }, { "suffix", tag.Suffix } });
|
||||
o.Add("proxy_tags", tagArray);
|
||||
|
||||
switch (v)
|
||||
{
|
||||
case APIVersion.V1:
|
||||
{
|
||||
o.Add("privacy", includePrivacy ? member.MemberVisibility.LevelName() : null);
|
||||
|
||||
o.Add("visibility", includePrivacy ? member.MemberVisibility.LevelName() : null);
|
||||
o.Add("name_privacy", includePrivacy ? member.NamePrivacy.LevelName() : null);
|
||||
o.Add("description_privacy", includePrivacy ? member.DescriptionPrivacy.LevelName() : null);
|
||||
o.Add("birthday_privacy", includePrivacy ? member.BirthdayPrivacy.LevelName() : null);
|
||||
o.Add("pronoun_privacy", includePrivacy ? member.PronounPrivacy.LevelName() : null);
|
||||
o.Add("avatar_privacy", includePrivacy ? member.AvatarPrivacy.LevelName() : null);
|
||||
// o.Add("color_privacy", ctx == LookupContext.ByOwner ? (member.ColorPrivacy.LevelName()) : null);
|
||||
o.Add("metadata_privacy", includePrivacy ? member.MetadataPrivacy.LevelName() : null);
|
||||
|
||||
if (member.ProxyTags.Count > 0 && needsLegacyProxyTags)
|
||||
{
|
||||
// Legacy compatibility only, TODO: remove at some point
|
||||
o.Add("prefix", member.ProxyTags?.FirstOrDefault().Prefix);
|
||||
o.Add("suffix", member.ProxyTags?.FirstOrDefault().Suffix);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case APIVersion.V2:
|
||||
{
|
||||
if (includePrivacy)
|
||||
{
|
||||
var p = new JObject();
|
||||
@ -182,10 +153,6 @@ public static class PKMemberExt
|
||||
o.Add("privacy", null);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ public class FullMessage
|
||||
public PKMember? Member;
|
||||
public PKSystem? System;
|
||||
|
||||
public JObject ToJson(LookupContext ctx, APIVersion v)
|
||||
public JObject ToJson(LookupContext ctx)
|
||||
{
|
||||
var o = new JObject();
|
||||
|
||||
@ -30,8 +30,8 @@ public class FullMessage
|
||||
o.Add("sender", Message.Sender.ToString());
|
||||
o.Add("channel", Message.Channel.ToString());
|
||||
o.Add("guild", Message.Guild?.ToString());
|
||||
o.Add("system", System?.ToJson(ctx, v));
|
||||
o.Add("member", Member?.ToJson(ctx, v: v));
|
||||
o.Add("system", System?.ToJson(ctx));
|
||||
o.Add("member", Member?.ToJson(ctx));
|
||||
|
||||
return o;
|
||||
}
|
||||
|
@ -59,17 +59,15 @@ public static class PKSystemExt
|
||||
public static string DescriptionFor(this PKSystem system, LookupContext ctx) =>
|
||||
system.DescriptionPrivacy.Get(ctx, system.Description);
|
||||
|
||||
public static JObject ToJson(this PKSystem system, LookupContext ctx, APIVersion v = APIVersion.V1)
|
||||
public static JObject ToJson(this PKSystem system, LookupContext ctx)
|
||||
{
|
||||
var o = new JObject();
|
||||
o.Add("id", system.Hid);
|
||||
if (v == APIVersion.V2)
|
||||
o.Add("uuid", system.Uuid.ToString());
|
||||
|
||||
o.Add("name", system.Name);
|
||||
o.Add("description", system.DescriptionFor(ctx));
|
||||
o.Add("tag", system.Tag);
|
||||
if (v == APIVersion.V2)
|
||||
o.Add("pronouns", system.PronounPrivacy.Get(ctx, system.Pronouns));
|
||||
|
||||
o.Add("avatar_url", system.AvatarUrl.TryGetCleanCdnUrl());
|
||||
@ -77,28 +75,6 @@ public static class PKSystemExt
|
||||
o.Add("color", system.Color);
|
||||
o.Add("created", system.Created.FormatExport());
|
||||
|
||||
switch (v)
|
||||
{
|
||||
case APIVersion.V1:
|
||||
{
|
||||
// this property was moved to SystemConfig
|
||||
// see notice in /api/legacy docs
|
||||
o.Add("tz", "UTC");
|
||||
|
||||
o.Add("description_privacy",
|
||||
ctx == LookupContext.ByOwner ? system.DescriptionPrivacy.ToJsonString() : null);
|
||||
o.Add("member_list_privacy",
|
||||
ctx == LookupContext.ByOwner ? system.MemberListPrivacy.ToJsonString() : null);
|
||||
o.Add("group_list_privacy",
|
||||
ctx == LookupContext.ByOwner ? system.GroupListPrivacy.ToJsonString() : null);
|
||||
o.Add("front_privacy", ctx == LookupContext.ByOwner ? system.FrontPrivacy.ToJsonString() : null);
|
||||
o.Add("front_history_privacy",
|
||||
ctx == LookupContext.ByOwner ? system.FrontHistoryPrivacy.ToJsonString() : null);
|
||||
|
||||
break;
|
||||
}
|
||||
case APIVersion.V2:
|
||||
{
|
||||
if (ctx == LookupContext.ByOwner)
|
||||
{
|
||||
// todo: should this be moved to a different JSON model?
|
||||
@ -121,10 +97,6 @@ public static class PKSystemExt
|
||||
o.Add("privacy", null);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
@ -79,7 +79,7 @@ public class MemberPatch: PatchObject
|
||||
|
||||
#nullable disable
|
||||
|
||||
public static MemberPatch FromJSON(JObject o, APIVersion v = APIVersion.V1)
|
||||
public static MemberPatch FromJSON(JObject o, bool isImport = false)
|
||||
{
|
||||
var patch = new MemberPatch();
|
||||
|
||||
@ -109,35 +109,12 @@ public class MemberPatch: PatchObject
|
||||
if (o.ContainsKey("description")) patch.Description = o.Value<string>("description").NullIfEmpty();
|
||||
if (o.ContainsKey("keep_proxy")) patch.KeepProxy = o.Value<bool>("keep_proxy");
|
||||
|
||||
switch (v)
|
||||
{
|
||||
case APIVersion.V1:
|
||||
if (isImport)
|
||||
{
|
||||
// legacy: used in old export files and APIv1
|
||||
if (o.ContainsKey("prefix") || o.ContainsKey("suffix") && !o.ContainsKey("proxy_tags"))
|
||||
patch.ProxyTags = new[] { new ProxyTag(o.Value<string>("prefix"), o.Value<string>("suffix")) };
|
||||
else if (o.ContainsKey("proxy_tags"))
|
||||
patch.ProxyTags = o.Value<JArray>("proxy_tags")
|
||||
.OfType<JObject>().Select(o =>
|
||||
new ProxyTag(o.Value<string>("prefix"), o.Value<string>("suffix")))
|
||||
.Where(p => p.Valid)
|
||||
.ToArray();
|
||||
|
||||
if (o.ContainsKey("privacy"))
|
||||
{
|
||||
var plevel = patch.ParsePrivacy(o, "privacy");
|
||||
|
||||
patch.Visibility = plevel;
|
||||
patch.NamePrivacy = plevel;
|
||||
patch.AvatarPrivacy = plevel;
|
||||
patch.DescriptionPrivacy = plevel;
|
||||
patch.BirthdayPrivacy = plevel;
|
||||
patch.PronounPrivacy = plevel;
|
||||
// member.ColorPrivacy = plevel;
|
||||
patch.MetadataPrivacy = plevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (o.ContainsKey("visibility")) patch.Visibility = patch.ParsePrivacy(o, "visibility");
|
||||
if (o.ContainsKey("name_privacy")) patch.NamePrivacy = patch.ParsePrivacy(o, "name_privacy");
|
||||
if (o.ContainsKey("description_privacy"))
|
||||
@ -153,10 +130,6 @@ public class MemberPatch: PatchObject
|
||||
patch.MetadataPrivacy = patch.ParsePrivacy(o, "metadata_privacy");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case APIVersion.V2:
|
||||
{
|
||||
if (o.ContainsKey("proxy_tags"))
|
||||
patch.ProxyTags = o.Value<JArray>("proxy_tags")
|
||||
.OfType<JObject>().Select(o =>
|
||||
@ -190,10 +163,6 @@ public class MemberPatch: PatchObject
|
||||
patch.MetadataPrivacy = patch.ParsePrivacy(privacy, "metadata_privacy");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ public class SystemPatch: PatchObject
|
||||
|
||||
#nullable disable
|
||||
|
||||
public static SystemPatch FromJSON(JObject o, APIVersion v = APIVersion.V1)
|
||||
public static SystemPatch FromJSON(JObject o, bool isImport = false)
|
||||
{
|
||||
var patch = new SystemPatch();
|
||||
if (o.ContainsKey("name")) patch.Name = o.Value<string>("name").NullIfEmpty();
|
||||
@ -78,9 +78,7 @@ public class SystemPatch: PatchObject
|
||||
if (o.ContainsKey("banner")) patch.BannerImage = o.Value<string>("banner").NullIfEmpty();
|
||||
if (o.ContainsKey("color")) patch.Color = o.Value<string>("color").NullIfEmpty();
|
||||
|
||||
switch (v)
|
||||
{
|
||||
case APIVersion.V1:
|
||||
if (isImport)
|
||||
{
|
||||
if (o.ContainsKey("description_privacy"))
|
||||
patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy");
|
||||
@ -89,11 +87,8 @@ public class SystemPatch: PatchObject
|
||||
if (o.ContainsKey("front_privacy")) patch.FrontPrivacy = patch.ParsePrivacy(o, "front_privacy");
|
||||
if (o.ContainsKey("front_history_privacy"))
|
||||
patch.FrontHistoryPrivacy = patch.ParsePrivacy(o, "front_history_privacy");
|
||||
|
||||
break;
|
||||
}
|
||||
case APIVersion.V2:
|
||||
{
|
||||
|
||||
if (o.ContainsKey("privacy") && o["privacy"].Type != JTokenType.Null)
|
||||
{
|
||||
var privacy = o.Value<JObject>("privacy");
|
||||
@ -117,10 +112,6 @@ public class SystemPatch: PatchObject
|
||||
patch.FrontHistoryPrivacy = patch.ParsePrivacy(privacy, "front_history_privacy");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ public partial class BulkImporter
|
||||
{
|
||||
private async Task<ImportResultNew> ImportPluralKit(JObject importFile)
|
||||
{
|
||||
var patch = SystemPatch.FromJSON(importFile);
|
||||
var patch = SystemPatch.FromJSON(importFile, isImport: true);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
@ -100,7 +100,7 @@ public partial class BulkImporter
|
||||
referenceName, _system.Id, isNewMember
|
||||
);
|
||||
|
||||
var patch = MemberPatch.FromJSON(member);
|
||||
var patch = MemberPatch.FromJSON(member, isImport: true);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
|
Loading…
Reference in New Issue
Block a user