mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 13:06:50 +00:00
feat: upgrade to .NET 6, refactor everything
This commit is contained in:
parent
d28e99ba43
commit
1918c56937
314 changed files with 27954 additions and 27966 deletions
|
|
@ -1,6 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
|
@ -8,60 +5,58 @@ using NodaTime;
|
|||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public static class APIJsonExt
|
||||
{
|
||||
public static class APIJsonExt
|
||||
public static JArray ToJSON(this IEnumerable<PKShardInfo> shards)
|
||||
{
|
||||
public static JArray ToJSON(this IEnumerable<PKShardInfo> shards)
|
||||
var o = new JArray();
|
||||
|
||||
foreach (var shard in shards)
|
||||
{
|
||||
var o = new JArray();
|
||||
var s = new JObject();
|
||||
s.Add("id", shard.Id);
|
||||
|
||||
foreach (var shard in shards)
|
||||
{
|
||||
var s = new JObject();
|
||||
s.Add("id", shard.Id);
|
||||
if (shard.Status == PKShardInfo.ShardStatus.Down)
|
||||
s.Add("status", "down");
|
||||
else
|
||||
s.Add("status", "up");
|
||||
|
||||
if (shard.Status == PKShardInfo.ShardStatus.Down)
|
||||
s.Add("status", "down");
|
||||
else
|
||||
s.Add("status", "up");
|
||||
s.Add("ping", shard.Ping);
|
||||
s.Add("last_heartbeat", shard.LastHeartbeat.ToString());
|
||||
s.Add("last_connection", shard.LastConnection.ToString());
|
||||
|
||||
s.Add("ping", shard.Ping);
|
||||
s.Add("last_heartbeat", shard.LastHeartbeat.ToString());
|
||||
s.Add("last_connection", shard.LastConnection.ToString());
|
||||
|
||||
o.Add(s);
|
||||
}
|
||||
|
||||
return o;
|
||||
o.Add(s);
|
||||
}
|
||||
|
||||
public static JObject ToJson(this ModelRepository.Counts counts)
|
||||
{
|
||||
var o = new JObject();
|
||||
|
||||
o.Add("system_count", counts.SystemCount);
|
||||
o.Add("member_count", counts.MemberCount);
|
||||
o.Add("group_count", counts.GroupCount);
|
||||
o.Add("switch_count", counts.SwitchCount);
|
||||
o.Add("message_count", counts.MessageCount);
|
||||
|
||||
return o;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
public struct FrontersReturnNew
|
||||
public static JObject ToJson(this ModelRepository.Counts counts)
|
||||
{
|
||||
[JsonProperty("id")] public Guid Uuid { get; set; }
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<JObject> Members { get; set; }
|
||||
}
|
||||
var o = new JObject();
|
||||
|
||||
public struct SwitchesReturnNew
|
||||
{
|
||||
[JsonProperty("id")] public Guid Uuid { get; set; }
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
|
||||
}
|
||||
o.Add("system_count", counts.SystemCount);
|
||||
o.Add("member_count", counts.MemberCount);
|
||||
o.Add("group_count", counts.GroupCount);
|
||||
o.Add("switch_count", counts.SwitchCount);
|
||||
o.Add("message_count", counts.MessageCount);
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
public struct FrontersReturnNew
|
||||
{
|
||||
[JsonProperty("id")] public Guid Uuid { get; set; }
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<JObject> Members { get; set; }
|
||||
}
|
||||
|
||||
public struct SwitchesReturnNew
|
||||
{
|
||||
[JsonProperty("id")] public Guid Uuid { get; set; }
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class ApiConfig
|
||||
{
|
||||
public class ApiConfig
|
||||
{
|
||||
public int Port { get; set; } = 5000;
|
||||
}
|
||||
public int Port { get; set; } = 5000;
|
||||
}
|
||||
|
|
@ -1,32 +1,30 @@
|
|||
using System;
|
||||
using System.Security.Claims;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public static class AuthExt
|
||||
{
|
||||
public static class AuthExt
|
||||
public static SystemId CurrentSystem(this ClaimsPrincipal user)
|
||||
{
|
||||
public static SystemId CurrentSystem(this ClaimsPrincipal user)
|
||||
{
|
||||
var claim = user.FindFirst(PKClaims.SystemId);
|
||||
if (claim == null) throw new ArgumentException("User is unauthorized");
|
||||
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");
|
||||
}
|
||||
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, 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;
|
||||
}
|
||||
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,7 +1,6 @@
|
|||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class PKClaims
|
||||
{
|
||||
public class PKClaims
|
||||
{
|
||||
public const string SystemId = "PluralKit:SystemId";
|
||||
}
|
||||
public const string SystemId = "PluralKit:SystemId";
|
||||
}
|
||||
|
|
@ -1,50 +1,46 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class SystemTokenAuthenticationHandler: AuthenticationHandler<SystemTokenAuthenticationHandler.Opts>
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
|
||||
}
|
||||
_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,20 +1,17 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class MemberOwnerHandler: AuthorizationHandler<OwnSystemRequirement, PKMember>
|
||||
{
|
||||
public class MemberOwnerHandler: AuthorizationHandler<OwnSystemRequirement, PKMember>
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
OwnSystemRequirement requirement, PKMember resource)
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (!context.User.Identity.IsAuthenticated) return Task.CompletedTask;
|
||||
if (resource.System == context.User.CurrentSystem())
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,18 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class MemberPrivacyHandler: AuthorizationHandler<PrivacyRequirement<PKMember>, PKMember>
|
||||
{
|
||||
public class MemberPrivacyHandler: AuthorizationHandler<PrivacyRequirement<PKMember>, PKMember>
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
PrivacyRequirement<PKMember> requirement, PKMember resource)
|
||||
{
|
||||
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;
|
||||
}
|
||||
var level = requirement.Mapper(resource);
|
||||
var ctx = context.User.ContextFor(resource);
|
||||
if (level.CanAccess(ctx))
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace PluralKit.API
|
||||
{
|
||||
public class OwnSystemRequirement: IAuthorizationRequirement { }
|
||||
}
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class OwnSystemRequirement: IAuthorizationRequirement { }
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
using System;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
{
|
||||
public class PrivacyRequirement<T>: IAuthorizationRequirement
|
||||
{
|
||||
public readonly Func<T, PrivacyLevel> Mapper;
|
||||
namespace PluralKit.API;
|
||||
|
||||
public PrivacyRequirement(Func<T, PrivacyLevel> mapper)
|
||||
{
|
||||
Mapper = mapper;
|
||||
}
|
||||
public class PrivacyRequirement<T>: IAuthorizationRequirement
|
||||
{
|
||||
public readonly Func<T, PrivacyLevel> Mapper;
|
||||
|
||||
public PrivacyRequirement(Func<T, PrivacyLevel> mapper)
|
||||
{
|
||||
Mapper = mapper;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,17 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class SystemOwnerHandler: AuthorizationHandler<OwnSystemRequirement, PKSystem>
|
||||
{
|
||||
public class SystemOwnerHandler: AuthorizationHandler<OwnSystemRequirement, PKSystem>
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
OwnSystemRequirement requirement, PKSystem resource)
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (!context.User.Identity.IsAuthenticated) return Task.CompletedTask;
|
||||
if (resource.Id == context.User.CurrentSystem())
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,18 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class SystemPrivacyHandler: AuthorizationHandler<PrivacyRequirement<PKSystem>, PKSystem>
|
||||
{
|
||||
public class SystemPrivacyHandler: AuthorizationHandler<PrivacyRequirement<PKSystem>, PKSystem>
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
PrivacyRequirement<PKSystem> requirement, PKSystem resource)
|
||||
{
|
||||
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;
|
||||
}
|
||||
var level = requirement.Mapper(resource);
|
||||
var ctx = context.User.ContextFor(resource);
|
||||
if (level.CanAccess(ctx))
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +1,92 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class PKControllerBase: ControllerBase
|
||||
{
|
||||
public class PKControllerBase: ControllerBase
|
||||
private readonly Guid _requestId = Guid.NewGuid();
|
||||
private readonly Regex _shortIdRegex = new("^[a-z]{5}$");
|
||||
private readonly Regex _snowflakeRegex = new("^[0-9]{17,19}$");
|
||||
|
||||
protected readonly ApiConfig _config;
|
||||
protected readonly IDatabase _db;
|
||||
protected readonly ModelRepository _repo;
|
||||
protected readonly DispatchService _dispatch;
|
||||
|
||||
public PKControllerBase(IServiceProvider svc)
|
||||
{
|
||||
private readonly Guid _requestId = Guid.NewGuid();
|
||||
private readonly Regex _shortIdRegex = new Regex("^[a-z]{5}$");
|
||||
private readonly Regex _snowflakeRegex = new Regex("^[0-9]{17,19}$");
|
||||
_config = svc.GetRequiredService<ApiConfig>();
|
||||
_db = svc.GetRequiredService<IDatabase>();
|
||||
_repo = svc.GetRequiredService<ModelRepository>();
|
||||
_dispatch = svc.GetRequiredService<DispatchService>();
|
||||
}
|
||||
|
||||
protected readonly ApiConfig _config;
|
||||
protected readonly IDatabase _db;
|
||||
protected readonly ModelRepository _repo;
|
||||
protected readonly DispatchService _dispatch;
|
||||
|
||||
public PKControllerBase(IServiceProvider svc)
|
||||
{
|
||||
_config = svc.GetRequiredService<ApiConfig>();
|
||||
_db = svc.GetRequiredService<IDatabase>();
|
||||
_repo = svc.GetRequiredService<ModelRepository>();
|
||||
_dispatch = svc.GetRequiredService<DispatchService>();
|
||||
}
|
||||
|
||||
protected Task<PKSystem?> ResolveSystem(string systemRef)
|
||||
{
|
||||
if (systemRef == "@me")
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null)
|
||||
throw Errors.GenericAuthError;
|
||||
return _repo.GetSystem((SystemId)systemId);
|
||||
}
|
||||
|
||||
if (Guid.TryParse(systemRef, out var guid))
|
||||
return _repo.GetSystemByGuid(guid);
|
||||
|
||||
if (_snowflakeRegex.IsMatch(systemRef))
|
||||
return _repo.GetSystemByAccount(ulong.Parse(systemRef));
|
||||
|
||||
if (_shortIdRegex.IsMatch(systemRef))
|
||||
return _repo.GetSystemByHid(systemRef);
|
||||
|
||||
return Task.FromResult<PKSystem?>(null);
|
||||
}
|
||||
|
||||
protected Task<PKMember?> ResolveMember(string memberRef)
|
||||
{
|
||||
if (Guid.TryParse(memberRef, out var guid))
|
||||
return _repo.GetMemberByGuid(guid);
|
||||
|
||||
if (_shortIdRegex.IsMatch(memberRef))
|
||||
return _repo.GetMemberByHid(memberRef);
|
||||
|
||||
return Task.FromResult<PKMember?>(null);
|
||||
}
|
||||
|
||||
protected Task<PKGroup?> ResolveGroup(string groupRef)
|
||||
{
|
||||
if (Guid.TryParse(groupRef, out var guid))
|
||||
return _repo.GetGroupByGuid(guid);
|
||||
|
||||
if (_shortIdRegex.IsMatch(groupRef))
|
||||
return _repo.GetGroupByHid(groupRef);
|
||||
|
||||
return Task.FromResult<PKGroup?>(null);
|
||||
}
|
||||
|
||||
protected LookupContext ContextFor(PKSystem system)
|
||||
protected Task<PKSystem?> ResolveSystem(string systemRef)
|
||||
{
|
||||
if (systemRef == "@me")
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null) return LookupContext.ByNonOwner;
|
||||
return ((SystemId)systemId) == system.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||
if (systemId == null)
|
||||
throw Errors.GenericAuthError;
|
||||
return _repo.GetSystem((SystemId)systemId);
|
||||
}
|
||||
|
||||
protected LookupContext ContextFor(PKMember member)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null) return LookupContext.ByNonOwner;
|
||||
return ((SystemId)systemId) == member.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||
}
|
||||
if (Guid.TryParse(systemRef, out var guid))
|
||||
return _repo.GetSystemByGuid(guid);
|
||||
|
||||
protected LookupContext ContextFor(PKGroup group)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null) return LookupContext.ByNonOwner;
|
||||
return ((SystemId)systemId) == group.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||
}
|
||||
if (_snowflakeRegex.IsMatch(systemRef))
|
||||
return _repo.GetSystemByAccount(ulong.Parse(systemRef));
|
||||
|
||||
if (_shortIdRegex.IsMatch(systemRef))
|
||||
return _repo.GetSystemByHid(systemRef);
|
||||
|
||||
return Task.FromResult<PKSystem?>(null);
|
||||
}
|
||||
|
||||
protected Task<PKMember?> ResolveMember(string memberRef)
|
||||
{
|
||||
if (Guid.TryParse(memberRef, out var guid))
|
||||
return _repo.GetMemberByGuid(guid);
|
||||
|
||||
if (_shortIdRegex.IsMatch(memberRef))
|
||||
return _repo.GetMemberByHid(memberRef);
|
||||
|
||||
return Task.FromResult<PKMember?>(null);
|
||||
}
|
||||
|
||||
protected Task<PKGroup?> ResolveGroup(string groupRef)
|
||||
{
|
||||
if (Guid.TryParse(groupRef, out var guid))
|
||||
return _repo.GetGroupByGuid(guid);
|
||||
|
||||
if (_shortIdRegex.IsMatch(groupRef))
|
||||
return _repo.GetGroupByHid(groupRef);
|
||||
|
||||
return Task.FromResult<PKGroup?>(null);
|
||||
}
|
||||
|
||||
protected LookupContext ContextFor(PKSystem system)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null) return LookupContext.ByNonOwner;
|
||||
return (SystemId)systemId == system.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||
}
|
||||
|
||||
protected LookupContext ContextFor(PKMember member)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null) return LookupContext.ByNonOwner;
|
||||
return (SystemId)systemId == member.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||
}
|
||||
|
||||
protected LookupContext ContextFor(PKGroup group)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("SystemId", out var systemId);
|
||||
if (systemId == null) return LookupContext.ByNonOwner;
|
||||
return (SystemId)systemId == group.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,32 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/a")]
|
||||
public class AccountController: ControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/a")]
|
||||
public class AccountController: ControllerBase
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public AccountController(IDatabase db, ModelRepository repo)
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
public AccountController(IDatabase db, ModelRepository repo)
|
||||
{
|
||||
_db = db;
|
||||
_repo = 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.");
|
||||
[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)));
|
||||
}
|
||||
return Ok(system.ToJson(User.ContextFor(system)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
|
@ -10,117 +7,115 @@ using Newtonsoft.Json.Linq;
|
|||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/m")]
|
||||
public class MemberController: ControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/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)
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly IAuthorizationService _auth;
|
||||
_auth = auth;
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public MemberController(IAuthorizationService auth, IDatabase db, ModelRepository 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 systemData = await _repo.GetSystem(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 = systemData?.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)
|
||||
{
|
||||
_auth = auth;
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
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.");
|
||||
}
|
||||
|
||||
[HttpGet("{hid}")]
|
||||
public async Task<ActionResult<JObject>> GetMember(string hid)
|
||||
{
|
||||
var member = await _repo.GetMemberByHid(hid);
|
||||
if (member == null) return NotFound("Member not found.");
|
||||
member = await _repo.UpdateMember(member.Id, patch, conn);
|
||||
await tx.CommitAsync();
|
||||
return Ok(member.ToJson(User.ContextFor(member), true));
|
||||
}
|
||||
|
||||
return Ok(member.ToJson(User.ContextFor(member), needsLegacyProxyTags: 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 Unauthorized($"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.");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties)
|
||||
{
|
||||
if (!properties.ContainsKey("name"))
|
||||
return BadRequest("Member name must be specified.");
|
||||
var newMember = await _repo.UpdateMember(member.Id, patch);
|
||||
return Ok(newMember.ToJson(User.ContextFor(newMember), true));
|
||||
}
|
||||
|
||||
var systemId = User.CurrentSystem();
|
||||
var systemData = await _repo.GetSystem(systemId);
|
||||
[HttpDelete("{hid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeleteMember(string hid)
|
||||
{
|
||||
var member = await _repo.GetMemberByHid(hid);
|
||||
if (member == null) return NotFound("Member not found.");
|
||||
|
||||
await using var conn = await _db.Obtain();
|
||||
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
|
||||
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
|
||||
|
||||
// Enforce per-system member limit
|
||||
var memberCount = await conn.QuerySingleAsync<int>("select count(*) from members where system = @System", new { System = systemId });
|
||||
var memberLimit = systemData?.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}).");
|
||||
else if (err.Text != null)
|
||||
return BadRequest(err.Text);
|
||||
else
|
||||
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), needsLegacyProxyTags: 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 Unauthorized($"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}).");
|
||||
else if (err.Text != null)
|
||||
return BadRequest(err.Text);
|
||||
else
|
||||
return BadRequest($"Field {err.Key} is invalid.");
|
||||
}
|
||||
|
||||
var newMember = await _repo.UpdateMember(member.Id, patch);
|
||||
return Ok(newMember.ToJson(User.ContextFor(newMember), needsLegacyProxyTags: 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 Unauthorized($"Member '{hid}' is not part of your system.");
|
||||
|
||||
await _repo.DeleteMember(member.Id);
|
||||
return Ok();
|
||||
}
|
||||
await _repo.DeleteMember(member.Id);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,31 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/msg")]
|
||||
public class MessageController: ControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/msg")]
|
||||
public class MessageController: ControllerBase
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public MessageController(ModelRepository repo, IDatabase db)
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
_repo = repo;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public MessageController(ModelRepository repo, IDatabase db)
|
||||
{
|
||||
_repo = repo;
|
||||
_db = db;
|
||||
}
|
||||
[HttpGet("{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.");
|
||||
|
||||
[HttpGet("{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.");
|
||||
|
||||
return msg.ToJson(User.ContextFor(msg.System), APIVersion.V1);
|
||||
}
|
||||
return msg.ToJson(User.ContextFor(msg.System), APIVersion.V1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class MetaController: ControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class MetaController: ControllerBase
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public MetaController(IDatabase db, ModelRepository repo)
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
public MetaController(IDatabase db, ModelRepository repo)
|
||||
{
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
[HttpGet("meta")]
|
||||
public async Task<ActionResult<JObject>> GetMeta()
|
||||
{
|
||||
await using var conn = await _db.Obtain();
|
||||
var shards = await _repo.GetShards();
|
||||
[HttpGet("meta")]
|
||||
public async Task<ActionResult<JObject>> GetMeta()
|
||||
{
|
||||
await using var conn = await _db.Obtain();
|
||||
var shards = await _repo.GetShards();
|
||||
|
||||
var o = new JObject();
|
||||
o.Add("shards", shards.ToJSON());
|
||||
o.Add("version", BuildInfoService.Version);
|
||||
var o = new JObject();
|
||||
o.Add("shards", shards.ToJSON());
|
||||
o.Add("version", BuildInfoService.Version);
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
return Ok(o);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -16,183 +10,189 @@ using NodaTime;
|
|||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public struct SwitchesReturn
|
||||
{
|
||||
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]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/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)
|
||||
{
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
|
||||
_db = db;
|
||||
_auth = auth;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public struct FrontersReturn
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> GetOwnSystem()
|
||||
{
|
||||
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||
[JsonProperty("members")] public IEnumerable<JObject> Members { get; set; }
|
||||
var system = await _repo.GetSystem(User.CurrentSystem());
|
||||
return system.ToJson(User.ContextFor(system));
|
||||
}
|
||||
|
||||
public struct PostSwitchParams
|
||||
[HttpGet("{hid}")]
|
||||
public async Task<ActionResult<JObject>> GetSystem(string hid)
|
||||
{
|
||||
public Instant? Timestamp { get; set; }
|
||||
public ICollection<string> Members { get; set; }
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null) return NotFound("System not found.");
|
||||
return Ok(system.ToJson(User.ContextFor(system)));
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("v{version:apiVersion}/s")]
|
||||
public class SystemController: ControllerBase
|
||||
[HttpGet("{hid}/members")]
|
||||
public async Task<ActionResult<IEnumerable<JObject>>> GetMembers(string hid)
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly IAuthorizationService _auth;
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null)
|
||||
return NotFound("System not found.");
|
||||
|
||||
public SystemController(IDatabase db, IAuthorizationService auth, ModelRepository repo)
|
||||
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
|
||||
{
|
||||
_db = db;
|
||||
_auth = auth;
|
||||
_repo = repo;
|
||||
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.");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JObject>> GetOwnSystem()
|
||||
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 system = await _repo.GetSystem(User.CurrentSystem());
|
||||
return system.ToJson(User.ContextFor(system));
|
||||
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.");
|
||||
}
|
||||
|
||||
[HttpGet("{hid}")]
|
||||
public async Task<ActionResult<JObject>> GetSystem(string hid)
|
||||
// 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)
|
||||
{
|
||||
var system = await _repo.GetSystemByHid(hid);
|
||||
if (system == null) return NotFound("System not found.");
|
||||
return Ok(system.ToJson(User.ContextFor(system)));
|
||||
if (!membersDict.TryGetValue(givenMemberId, out var member))
|
||||
return BadRequest($"Member '{givenMemberId}' not found.");
|
||||
membersInOrder.Add(member);
|
||||
}
|
||||
|
||||
[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), needsLegacyProxyTags: 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();
|
||||
}
|
||||
// Finally, log the switch (yay!)
|
||||
await _repo.AddSwitch(conn, User.CurrentSystem(), membersInOrder.Select(m => m.Id).ToList());
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +1,134 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class DiscordControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class DiscordControllerV2: PKControllerBase
|
||||
public DiscordControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
|
||||
[HttpGet("systems/@me/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> SystemGuildGet(ulong guild_id)
|
||||
{
|
||||
public DiscordControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var system = await ResolveSystem("@me");
|
||||
var settings = await _repo.GetSystemGuild(guild_id, system.Id, false);
|
||||
if (settings == null)
|
||||
throw Errors.SystemGuildNotFound;
|
||||
|
||||
PKMember member = null;
|
||||
if (settings.AutoproxyMember != null)
|
||||
member = await _repo.GetMember(settings.AutoproxyMember.Value);
|
||||
|
||||
[HttpGet("systems/@me/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> SystemGuildGet(ulong guild_id)
|
||||
return Ok(settings.ToJson(member?.Hid));
|
||||
}
|
||||
|
||||
[HttpPatch("systems/@me/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> DoSystemGuildPatch(ulong guild_id, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var settings = await _repo.GetSystemGuild(guild_id, system.Id, false);
|
||||
if (settings == null)
|
||||
throw Errors.SystemGuildNotFound;
|
||||
|
||||
MemberId? memberId = null;
|
||||
if (data.ContainsKey("autoproxy_member"))
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var settings = await _repo.GetSystemGuild(guild_id, system.Id, defaultInsert: false);
|
||||
if (settings == null)
|
||||
throw Errors.SystemGuildNotFound;
|
||||
|
||||
PKMember member = null;
|
||||
if (settings.AutoproxyMember != null)
|
||||
member = await _repo.GetMember(settings.AutoproxyMember.Value);
|
||||
|
||||
return Ok(settings.ToJson(member?.Hid));
|
||||
}
|
||||
|
||||
[HttpPatch("systems/@me/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> DoSystemGuildPatch(ulong guild_id, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var settings = await _repo.GetSystemGuild(guild_id, system.Id, defaultInsert: false);
|
||||
if (settings == null)
|
||||
throw Errors.SystemGuildNotFound;
|
||||
|
||||
MemberId? memberId = null;
|
||||
if (data.ContainsKey("autoproxy_member"))
|
||||
if (data["autoproxy_member"].Type != JTokenType.Null)
|
||||
{
|
||||
if (data["autoproxy_member"].Type != JTokenType.Null)
|
||||
{
|
||||
var member = await ResolveMember(data.Value<string>("autoproxy_member"));
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
var member = await ResolveMember(data.Value<string>("autoproxy_member"));
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
|
||||
memberId = member.Id;
|
||||
}
|
||||
memberId = member.Id;
|
||||
}
|
||||
else
|
||||
memberId = settings.AutoproxyMember;
|
||||
}
|
||||
else
|
||||
{
|
||||
memberId = settings.AutoproxyMember;
|
||||
}
|
||||
|
||||
var patch = SystemGuildPatch.FromJson(data, memberId);
|
||||
var patch = SystemGuildPatch.FromJson(data, memberId);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
// this is less than great, but at least it's legible
|
||||
if (patch.AutoproxyMember.Value == null)
|
||||
if (patch.AutoproxyMode.IsPresent)
|
||||
{
|
||||
if (patch.AutoproxyMode.Value == AutoproxyMode.Member)
|
||||
throw Errors.MissingAutoproxyMember;
|
||||
}
|
||||
else if (settings.AutoproxyMode == AutoproxyMode.Member)
|
||||
// this is less than great, but at least it's legible
|
||||
if (patch.AutoproxyMember.Value == null)
|
||||
if (patch.AutoproxyMode.IsPresent)
|
||||
{
|
||||
if (patch.AutoproxyMode.Value == AutoproxyMode.Member)
|
||||
throw Errors.MissingAutoproxyMember;
|
||||
}
|
||||
else if (settings.AutoproxyMode == AutoproxyMode.Member)
|
||||
{
|
||||
throw Errors.MissingAutoproxyMember;
|
||||
}
|
||||
|
||||
var newSettings = await _repo.UpdateSystemGuild(system.Id, guild_id, patch);
|
||||
var newSettings = await _repo.UpdateSystemGuild(system.Id, guild_id, patch);
|
||||
|
||||
PKMember? newMember = null;
|
||||
if (newSettings.AutoproxyMember != null)
|
||||
newMember = await _repo.GetMember(newSettings.AutoproxyMember.Value);
|
||||
return Ok(newSettings.ToJson(newMember?.Hid));
|
||||
}
|
||||
PKMember? newMember = null;
|
||||
if (newSettings.AutoproxyMember != null)
|
||||
newMember = await _repo.GetMember(newSettings.AutoproxyMember.Value);
|
||||
return Ok(newSettings.ToJson(newMember?.Hid));
|
||||
}
|
||||
|
||||
[HttpGet("members/{memberRef}/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> MemberGuildGet(string memberRef, ulong guild_id)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
[HttpGet("members/{memberRef}/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> MemberGuildGet(string memberRef, ulong guild_id)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var settings = await _repo.GetMemberGuild(guild_id, member.Id, defaultInsert: false);
|
||||
if (settings == null)
|
||||
throw Errors.MemberGuildNotFound;
|
||||
var settings = await _repo.GetMemberGuild(guild_id, member.Id, false);
|
||||
if (settings == null)
|
||||
throw Errors.MemberGuildNotFound;
|
||||
|
||||
return Ok(settings.ToJson());
|
||||
}
|
||||
return Ok(settings.ToJson());
|
||||
}
|
||||
|
||||
[HttpPatch("members/{memberRef}/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> DoMemberGuildPatch(string memberRef, ulong guild_id, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
[HttpPatch("members/{memberRef}/guilds/{guild_id}")]
|
||||
public async Task<IActionResult> DoMemberGuildPatch(string memberRef, ulong guild_id, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var settings = await _repo.GetMemberGuild(guild_id, member.Id, defaultInsert: false);
|
||||
if (settings == null)
|
||||
throw Errors.MemberGuildNotFound;
|
||||
var settings = await _repo.GetMemberGuild(guild_id, member.Id, false);
|
||||
if (settings == null)
|
||||
throw Errors.MemberGuildNotFound;
|
||||
|
||||
var patch = MemberGuildPatch.FromJson(data);
|
||||
var patch = MemberGuildPatch.FromJson(data);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
var newSettings = await _repo.UpdateMemberGuild(member.Id, guild_id, patch);
|
||||
return Ok(newSettings.ToJson());
|
||||
}
|
||||
var newSettings = await _repo.UpdateMemberGuild(member.Id, guild_id, patch);
|
||||
return Ok(newSettings.ToJson());
|
||||
}
|
||||
|
||||
[HttpGet("messages/{messageId}")]
|
||||
public async Task<ActionResult<JObject>> MessageGet(ulong messageId)
|
||||
{
|
||||
var msg = await _db.Execute(c => _repo.GetMessage(c, messageId));
|
||||
if (msg == null)
|
||||
throw Errors.MessageNotFound;
|
||||
[HttpGet("messages/{messageId}")]
|
||||
public async Task<ActionResult<JObject>> MessageGet(ulong messageId)
|
||||
{
|
||||
var msg = await _db.Execute(c => _repo.GetMessage(c, messageId));
|
||||
if (msg == null)
|
||||
throw Errors.MessageNotFound;
|
||||
|
||||
var ctx = this.ContextFor(msg.System);
|
||||
return msg.ToJson(ctx, APIVersion.V2);
|
||||
}
|
||||
var ctx = ContextFor(msg.System);
|
||||
return msg.ToJson(ctx, APIVersion.V2);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +1,135 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class GroupControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class GroupControllerV2: PKControllerBase
|
||||
public GroupControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
[HttpGet("systems/{systemRef}/groups")]
|
||||
public async Task<IActionResult> GetSystemGroups(string systemRef, [FromQuery] bool with_members)
|
||||
{
|
||||
public GroupControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
[HttpGet("systems/{systemRef}/groups")]
|
||||
public async Task<IActionResult> GetSystemGroups(string systemRef, [FromQuery] bool with_members)
|
||||
var ctx = ContextFor(system);
|
||||
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (!system.GroupListPrivacy.CanAccess(User.ContextFor(system)))
|
||||
throw Errors.UnauthorizedGroupList;
|
||||
|
||||
var groups = _repo.GetSystemGroups(system.Id);
|
||||
|
||||
var j_groups = await groups
|
||||
.Where(g => g.Visibility.CanAccess(ctx))
|
||||
.Select(g => g.ToJson(ctx, needsMembersArray: with_members))
|
||||
.ToListAsync();
|
||||
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (with_members && j_groups.Count > 0)
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
var q = await _repo.GetGroupMemberInfo(await groups.Select(x => x.Id).ToListAsync());
|
||||
|
||||
var ctx = this.ContextFor(system);
|
||||
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (!system.GroupListPrivacy.CanAccess(User.ContextFor(system)))
|
||||
throw Errors.UnauthorizedGroupList;
|
||||
|
||||
var groups = _repo.GetSystemGroups(system.Id);
|
||||
|
||||
var j_groups = await groups
|
||||
.Where(g => g.Visibility.CanAccess(ctx))
|
||||
.Select(g => g.ToJson(ctx, needsMembersArray: with_members))
|
||||
.ToListAsync();
|
||||
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (with_members && j_groups.Count > 0)
|
||||
{
|
||||
var q = await _repo.GetGroupMemberInfo(await groups.Select(x => x.Id).ToListAsync());
|
||||
|
||||
foreach (var row in q)
|
||||
if (row.MemberVisibility.CanAccess(ctx))
|
||||
((JArray)j_groups.Find(x => x.Value<string>("id") == row.Group)["members"]).Add(row.MemberUuid);
|
||||
}
|
||||
|
||||
return Ok(j_groups);
|
||||
foreach (var row in q)
|
||||
if (row.MemberVisibility.CanAccess(ctx))
|
||||
((JArray)j_groups.Find(x => x.Value<string>("id") == row.Group)["members"]).Add(row.MemberUuid);
|
||||
}
|
||||
|
||||
[HttpPost("groups")]
|
||||
public async Task<IActionResult> GroupCreate([FromBody] JObject data)
|
||||
return Ok(j_groups);
|
||||
}
|
||||
|
||||
[HttpPost("groups")]
|
||||
public async Task<IActionResult> GroupCreate([FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
// Check group cap
|
||||
var existingGroupCount = await _repo.GetSystemGroupCount(system.Id);
|
||||
var groupLimit = system.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
if (existingGroupCount >= groupLimit)
|
||||
throw Errors.GroupLimitReached;
|
||||
|
||||
var patch = GroupPatch.FromJson(data);
|
||||
patch.AssertIsValid();
|
||||
if (!patch.Name.IsPresent)
|
||||
patch.Errors.Add(new ValidationError("name", "Key 'name' is required when creating new group."));
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
using var conn = await _db.Obtain();
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
|
||||
var newGroup = await _repo.CreateGroup(system.Id, patch.Name.Value, conn);
|
||||
newGroup = await _repo.UpdateGroup(newGroup.Id, patch, conn);
|
||||
|
||||
_ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData()
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
Event = DispatchEvent.CREATE_GROUP,
|
||||
EventData = patch.ToJson(),
|
||||
});
|
||||
|
||||
// Check group cap
|
||||
var existingGroupCount = await _repo.GetSystemGroupCount(system.Id);
|
||||
var groupLimit = system.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
if (existingGroupCount >= groupLimit)
|
||||
throw Errors.GroupLimitReached;
|
||||
await tx.CommitAsync();
|
||||
|
||||
var patch = GroupPatch.FromJson(data);
|
||||
patch.AssertIsValid();
|
||||
if (!patch.Name.IsPresent)
|
||||
patch.Errors.Add(new ValidationError("name", $"Key 'name' is required when creating new group."));
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
|
||||
using var conn = await _db.Obtain();
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
[HttpGet("groups/{groupRef}")]
|
||||
public async Task<IActionResult> GroupGet(string groupRef)
|
||||
{
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
|
||||
var newGroup = await _repo.CreateGroup(system.Id, patch.Name.Value, conn);
|
||||
newGroup = await _repo.UpdateGroup(newGroup.Id, patch, conn);
|
||||
var system = await _repo.GetSystem(group.System);
|
||||
|
||||
return Ok(group.ToJson(ContextFor(group), system.Hid));
|
||||
}
|
||||
|
||||
_ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData()
|
||||
{
|
||||
Event = DispatchEvent.CREATE_GROUP,
|
||||
EventData = patch.ToJson(),
|
||||
});
|
||||
[HttpPatch("groups/{groupRef}")]
|
||||
public async Task<IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
var patch = GroupPatch.FromJson(data);
|
||||
|
||||
await tx.CommitAsync();
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
var newGroup = await _repo.UpdateGroup(group.Id, patch);
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
|
||||
[HttpGet("groups/{groupRef}")]
|
||||
public async Task<IActionResult> GroupGet(string groupRef)
|
||||
{
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
[HttpDelete("groups/{groupRef}")]
|
||||
public async Task<IActionResult> GroupDelete(string groupRef)
|
||||
{
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
|
||||
var system = await _repo.GetSystem(group.System);
|
||||
var system = await ResolveSystem("@me");
|
||||
if (system.Id != group.System)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
return Ok(group.ToJson(this.ContextFor(group), systemStr: system.Hid));
|
||||
}
|
||||
await _repo.DeleteGroup(group.Id);
|
||||
|
||||
[HttpPatch("groups/{groupRef}")]
|
||||
public async Task<IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
var patch = GroupPatch.FromJson(data);
|
||||
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
var newGroup = await _repo.UpdateGroup(group.Id, patch);
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner));
|
||||
}
|
||||
|
||||
[HttpDelete("groups/{groupRef}")]
|
||||
public async Task<IActionResult> GroupDelete(string groupRef)
|
||||
{
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
if (system.Id != group.System)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
await _repo.DeleteGroup(group.Id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,281 +1,272 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class GroupMemberControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class GroupMemberControllerV2: PKControllerBase
|
||||
public GroupMemberControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
[HttpGet("groups/{groupRef}/members")]
|
||||
public async Task<IActionResult> GetGroupMembers(string groupRef)
|
||||
{
|
||||
public GroupMemberControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
|
||||
[HttpGet("groups/{groupRef}/members")]
|
||||
public async Task<IActionResult> GetGroupMembers(string groupRef)
|
||||
var ctx = ContextFor(group);
|
||||
|
||||
if (!group.ListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedGroupMemberList;
|
||||
|
||||
var members = _repo.GetGroupMembers(group.Id).Where(m => m.MemberVisibility.CanAccess(ctx));
|
||||
|
||||
var o = new JArray();
|
||||
|
||||
await foreach (var member in members)
|
||||
o.Add(member.ToJson(ctx, v: APIVersion.V2));
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
|
||||
[HttpPost("groups/{groupRef}/members/add")]
|
||||
public async Task<IActionResult> AddGroupMembers(string groupRef, [FromBody] JArray memberRefs)
|
||||
{
|
||||
if (memberRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
var members = new List<MemberId>();
|
||||
|
||||
foreach (var JmemberRef in memberRefs)
|
||||
{
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
var member = await ResolveMember(memberRef);
|
||||
|
||||
var ctx = this.ContextFor(group);
|
||||
// todo: have a list of these errors instead of immediately throwing
|
||||
|
||||
if (!group.ListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedGroupMemberList;
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
var members = _repo.GetGroupMembers(group.Id).Where(m => m.MemberVisibility.CanAccess(ctx));
|
||||
|
||||
var o = new JArray();
|
||||
|
||||
await foreach (var member in members)
|
||||
o.Add(member.ToJson(ctx, v: APIVersion.V2));
|
||||
|
||||
return Ok(o);
|
||||
members.Add(member.Id);
|
||||
}
|
||||
|
||||
[HttpPost("groups/{groupRef}/members/add")]
|
||||
public async Task<IActionResult> AddGroupMembers(string groupRef, [FromBody] JArray memberRefs)
|
||||
var existingMembers = await _repo.GetGroupMembers(group.Id).Select(x => x.Id).ToListAsync();
|
||||
members = members.Where(x => !existingMembers.Contains(x)).ToList();
|
||||
|
||||
if (members.Count > 0)
|
||||
await _repo.AddMembersToGroup(group.Id, members);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("groups/{groupRef}/members/remove")]
|
||||
public async Task<IActionResult> RemoveGroupMembers(string groupRef, [FromBody] JArray memberRefs)
|
||||
{
|
||||
if (memberRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
var members = new List<MemberId>();
|
||||
|
||||
foreach (var JmemberRef in memberRefs)
|
||||
{
|
||||
if (memberRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
var member = await ResolveMember(memberRef);
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
members.Add(member.Id);
|
||||
}
|
||||
|
||||
await _repo.RemoveMembersFromGroup(group.Id, members);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("groups/{groupRef}/members/overwrite")]
|
||||
public async Task<IActionResult> OverwriteGroupMembers(string groupRef, [FromBody] JArray memberRefs)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
|
||||
var members = new List<MemberId>();
|
||||
|
||||
foreach (var JmemberRef in memberRefs)
|
||||
{
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
var member = await ResolveMember(memberRef);
|
||||
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
members.Add(member.Id);
|
||||
}
|
||||
|
||||
await _repo.ClearGroupMembers(group.Id);
|
||||
|
||||
if (members.Count > 0)
|
||||
await _repo.AddMembersToGroup(group.Id, members);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("members/{memberRef}/groups")]
|
||||
public async Task<IActionResult> GetMemberGroups(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
var ctx = ContextFor(member);
|
||||
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
if (!system.GroupListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedGroupList;
|
||||
|
||||
var groups = _repo.GetMemberGroups(member.Id).Where(g => g.Visibility.CanAccess(ctx));
|
||||
|
||||
var o = new JArray();
|
||||
|
||||
await foreach (var group in groups)
|
||||
o.Add(group.ToJson(ctx));
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
|
||||
[HttpPost("members/{memberRef}/groups/add")]
|
||||
public async Task<IActionResult> AddMemberGroups(string memberRef, [FromBody] JArray groupRefs)
|
||||
{
|
||||
if (groupRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var groups = new List<GroupId>();
|
||||
|
||||
foreach (var JgroupRef in groupRefs)
|
||||
{
|
||||
var groupRef = JgroupRef.Value<string>();
|
||||
var group = await ResolveGroup(groupRef);
|
||||
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
throw Errors.NotOwnGroupErrorWithRef(groupRef);
|
||||
|
||||
var members = new List<MemberId>();
|
||||
|
||||
foreach (var JmemberRef in memberRefs)
|
||||
{
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
var member = await ResolveMember(memberRef);
|
||||
|
||||
// todo: have a list of these errors instead of immediately throwing
|
||||
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
members.Add(member.Id);
|
||||
}
|
||||
|
||||
var existingMembers = await _repo.GetGroupMembers(group.Id).Select(x => x.Id).ToListAsync();
|
||||
members = members.Where(x => !existingMembers.Contains(x)).ToList();
|
||||
|
||||
if (members.Count > 0)
|
||||
await _repo.AddMembersToGroup(group.Id, members);
|
||||
|
||||
return NoContent();
|
||||
groups.Add(group.Id);
|
||||
}
|
||||
|
||||
[HttpPost("groups/{groupRef}/members/remove")]
|
||||
public async Task<IActionResult> RemoveGroupMembers(string groupRef, [FromBody] JArray memberRefs)
|
||||
var existingGroups = await _repo.GetMemberGroups(member.Id).Select(x => x.Id).ToListAsync();
|
||||
groups = groups.Where(x => !existingGroups.Contains(x)).ToList();
|
||||
|
||||
if (groups.Count > 0)
|
||||
await _repo.AddGroupsToMember(member.Id, groups);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("members/{memberRef}/groups/remove")]
|
||||
public async Task<IActionResult> RemoveMemberGroups(string memberRef, [FromBody] JArray groupRefs)
|
||||
{
|
||||
if (groupRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var groups = new List<GroupId>();
|
||||
|
||||
foreach (var JgroupRef in groupRefs)
|
||||
{
|
||||
if (memberRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var groupRef = JgroupRef.Value<string>();
|
||||
var group = await ResolveGroup(groupRef);
|
||||
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
throw Errors.GroupNotFoundWithRef(groupRef);
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
throw Errors.NotOwnGroupErrorWithRef(groupRef);
|
||||
|
||||
var members = new List<MemberId>();
|
||||
|
||||
foreach (var JmemberRef in memberRefs)
|
||||
{
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
var member = await ResolveMember(memberRef);
|
||||
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
members.Add(member.Id);
|
||||
}
|
||||
|
||||
await _repo.RemoveMembersFromGroup(group.Id, members);
|
||||
|
||||
return NoContent();
|
||||
groups.Add(group.Id);
|
||||
}
|
||||
|
||||
[HttpPost("groups/{groupRef}/members/overwrite")]
|
||||
public async Task<IActionResult> OverwriteGroupMembers(string groupRef, [FromBody] JArray memberRefs)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
await _repo.RemoveGroupsFromMember(member.Id, groups);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("members/{memberRef}/groups/overwrite")]
|
||||
public async Task<IActionResult> OverwriteMemberGroups(string memberRef, [FromBody] JArray groupRefs)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var groups = new List<GroupId>();
|
||||
|
||||
foreach (var JgroupRef in groupRefs)
|
||||
{
|
||||
var groupRef = JgroupRef.Value<string>();
|
||||
var group = await ResolveGroup(groupRef);
|
||||
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
throw Errors.GroupNotFoundWithRef(groupRef);
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupError;
|
||||
throw Errors.NotOwnGroupErrorWithRef(groupRef);
|
||||
|
||||
var members = new List<MemberId>();
|
||||
|
||||
foreach (var JmemberRef in memberRefs)
|
||||
{
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
var member = await ResolveMember(memberRef);
|
||||
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
members.Add(member.Id);
|
||||
}
|
||||
|
||||
await _repo.ClearGroupMembers(group.Id);
|
||||
|
||||
if (members.Count > 0)
|
||||
await _repo.AddMembersToGroup(group.Id, members);
|
||||
|
||||
return NoContent();
|
||||
groups.Add(group.Id);
|
||||
}
|
||||
|
||||
await _repo.ClearMemberGroups(member.Id);
|
||||
|
||||
[HttpGet("members/{memberRef}/groups")]
|
||||
public async Task<IActionResult> GetMemberGroups(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
var ctx = this.ContextFor(member);
|
||||
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
if (!system.GroupListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedGroupList;
|
||||
|
||||
var groups = _repo.GetMemberGroups(member.Id).Where(g => g.Visibility.CanAccess(ctx));
|
||||
|
||||
var o = new JArray();
|
||||
|
||||
await foreach (var group in groups)
|
||||
o.Add(group.ToJson(ctx));
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
|
||||
[HttpPost("members/{memberRef}/groups/add")]
|
||||
public async Task<IActionResult> AddMemberGroups(string memberRef, [FromBody] JArray groupRefs)
|
||||
{
|
||||
if (groupRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var groups = new List<GroupId>();
|
||||
|
||||
foreach (var JgroupRef in groupRefs)
|
||||
{
|
||||
var groupRef = JgroupRef.Value<string>();
|
||||
var group = await ResolveGroup(groupRef);
|
||||
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupErrorWithRef(groupRef);
|
||||
|
||||
groups.Add(group.Id);
|
||||
}
|
||||
|
||||
var existingGroups = await _repo.GetMemberGroups(member.Id).Select(x => x.Id).ToListAsync();
|
||||
groups = groups.Where(x => !existingGroups.Contains(x)).ToList();
|
||||
|
||||
if (groups.Count > 0)
|
||||
await _repo.AddGroupsToMember(member.Id, groups);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("members/{memberRef}/groups/remove")]
|
||||
public async Task<IActionResult> RemoveMemberGroups(string memberRef, [FromBody] JArray groupRefs)
|
||||
{
|
||||
if (groupRefs.Count == 0)
|
||||
throw Errors.GenericBadRequest;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var groups = new List<GroupId>();
|
||||
|
||||
foreach (var JgroupRef in groupRefs)
|
||||
{
|
||||
var groupRef = JgroupRef.Value<string>();
|
||||
var group = await ResolveGroup(groupRef);
|
||||
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFoundWithRef(groupRef);
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupErrorWithRef(groupRef);
|
||||
|
||||
groups.Add(group.Id);
|
||||
}
|
||||
|
||||
await _repo.RemoveGroupsFromMember(member.Id, groups);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("members/{memberRef}/groups/overwrite")]
|
||||
public async Task<IActionResult> OverwriteMemberGroups(string memberRef, [FromBody] JArray groupRefs)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var groups = new List<GroupId>();
|
||||
|
||||
foreach (var JgroupRef in groupRefs)
|
||||
{
|
||||
var groupRef = JgroupRef.Value<string>();
|
||||
var group = await ResolveGroup(groupRef);
|
||||
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFoundWithRef(groupRef);
|
||||
if (group.System != system.Id)
|
||||
throw Errors.NotOwnGroupErrorWithRef(groupRef);
|
||||
|
||||
groups.Add(group.Id);
|
||||
}
|
||||
|
||||
await _repo.ClearMemberGroups(member.Id);
|
||||
|
||||
if (groups.Count > 0)
|
||||
await _repo.AddGroupsToMember(member.Id, groups);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
if (groups.Count > 0)
|
||||
await _repo.AddGroupsToMember(member.Id, groups);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +1,117 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class MemberControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class MemberControllerV2: PKControllerBase
|
||||
public MemberControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
|
||||
[HttpGet("systems/{systemRef}/members")]
|
||||
public async Task<IActionResult> GetSystemMembers(string systemRef)
|
||||
{
|
||||
public MemberControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
var ctx = ContextFor(system);
|
||||
|
||||
[HttpGet("systems/{systemRef}/members")]
|
||||
public async Task<IActionResult> GetSystemMembers(string systemRef)
|
||||
if (!system.MemberListPrivacy.CanAccess(ContextFor(system)))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
var members = _repo.GetSystemMembers(system.Id);
|
||||
return Ok(await members
|
||||
.Where(m => m.MemberVisibility.CanAccess(ctx))
|
||||
.Select(m => m.ToJson(ctx, v: APIVersion.V2))
|
||||
.ToListAsync());
|
||||
}
|
||||
|
||||
[HttpPost("members")]
|
||||
public async Task<IActionResult> MemberCreate([FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var memberCount = await _repo.GetSystemMemberCount(system.Id);
|
||||
var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
if (memberCount >= memberLimit)
|
||||
throw Errors.MemberLimitReached;
|
||||
|
||||
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."));
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
using var conn = await _db.Obtain();
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
|
||||
var newMember = await _repo.CreateMember(system.Id, patch.Name.Value, conn);
|
||||
newMember = await _repo.UpdateMember(newMember.Id, patch, conn);
|
||||
|
||||
_ = _dispatch.Dispatch(newMember.Id, new()
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
Event = DispatchEvent.CREATE_MEMBER,
|
||||
EventData = patch.ToJson(),
|
||||
});
|
||||
|
||||
var ctx = this.ContextFor(system);
|
||||
await tx.CommitAsync();
|
||||
|
||||
if (!system.MemberListPrivacy.CanAccess(this.ContextFor(system)))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
|
||||
}
|
||||
|
||||
var members = _repo.GetSystemMembers(system.Id);
|
||||
return Ok(await members
|
||||
.Where(m => m.MemberVisibility.CanAccess(ctx))
|
||||
.Select(m => m.ToJson(ctx, v: APIVersion.V2))
|
||||
.ToListAsync());
|
||||
}
|
||||
[HttpGet("members/{memberRef}")]
|
||||
public async Task<IActionResult> MemberGet(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
|
||||
[HttpPost("members")]
|
||||
public async Task<IActionResult> MemberCreate([FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
||||
var memberCount = await _repo.GetSystemMemberCount(system.Id);
|
||||
var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
if (memberCount >= memberLimit)
|
||||
throw Errors.MemberLimitReached;
|
||||
return Ok(member.ToJson(ContextFor(member), systemStr: system.Hid, v: 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."));
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
[HttpPatch("members/{memberRef}")]
|
||||
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
using var conn = await _db.Obtain();
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
var patch = MemberPatch.FromJSON(data, APIVersion.V2);
|
||||
|
||||
var newMember = await _repo.CreateMember(system.Id, patch.Name.Value, conn);
|
||||
newMember = await _repo.UpdateMember(newMember.Id, patch, conn);
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
_ = _dispatch.Dispatch(newMember.Id, new()
|
||||
{
|
||||
Event = DispatchEvent.CREATE_MEMBER,
|
||||
EventData = patch.ToJson(),
|
||||
});
|
||||
var newMember = await _repo.UpdateMember(member.Id, patch);
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
[HttpDelete("members/{memberRef}")]
|
||||
public async Task<IActionResult> MemberDelete(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
|
||||
}
|
||||
var system = await ResolveSystem("@me");
|
||||
if (system.Id != member.System)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
[HttpGet("members/{memberRef}")]
|
||||
public async Task<IActionResult> MemberGet(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
await _repo.DeleteMember(member.Id);
|
||||
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
||||
return Ok(member.ToJson(this.ContextFor(member), systemStr: system.Hid, v: APIVersion.V2));
|
||||
}
|
||||
|
||||
[HttpPatch("members/{memberRef}")]
|
||||
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
var patch = MemberPatch.FromJSON(data, APIVersion.V2);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[HttpDelete("members/{memberRef}")]
|
||||
public async Task<IActionResult> MemberDelete(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
if (system.Id != member.System)
|
||||
throw Errors.NotOwnMemberError;
|
||||
|
||||
await _repo.DeleteMember(member.Id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,26 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class PrivateControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class PrivateControllerV2: PKControllerBase
|
||||
public PrivateControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
[HttpGet("meta")]
|
||||
public async Task<ActionResult<JObject>> Meta()
|
||||
{
|
||||
public PrivateControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var shards = await _repo.GetShards();
|
||||
var stats = await _repo.GetStats();
|
||||
|
||||
[HttpGet("meta")]
|
||||
public async Task<ActionResult<JObject>> Meta()
|
||||
{
|
||||
var shards = await _repo.GetShards();
|
||||
var stats = await _repo.GetStats();
|
||||
var o = new JObject();
|
||||
o.Add("shards", shards.ToJSON());
|
||||
o.Add("stats", stats.ToJson());
|
||||
|
||||
var o = new JObject();
|
||||
o.Add("shards", shards.ToJSON());
|
||||
o.Add("stats", stats.ToJson());
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
return Ok(o);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,254 +1,255 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class SwitchControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}")]
|
||||
public class SwitchControllerV2: PKControllerBase
|
||||
public SwitchControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
|
||||
[HttpGet("systems/{systemRef}/switches")]
|
||||
public async Task<IActionResult> GetSystemSwitches(string systemRef,
|
||||
[FromQuery(Name = "before")] Instant? before,
|
||||
[FromQuery(Name = "limit")] int? limit)
|
||||
{
|
||||
public SwitchControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
var ctx = ContextFor(system);
|
||||
|
||||
[HttpGet("systems/{systemRef}/switches")]
|
||||
public async Task<IActionResult> GetSystemSwitches(string systemRef, [FromQuery(Name = "before")] Instant? before, [FromQuery(Name = "limit")] int? limit)
|
||||
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedFrontHistory;
|
||||
|
||||
if (before == null)
|
||||
before = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
if (limit == null || limit > 100)
|
||||
limit = 100;
|
||||
|
||||
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturnNew>(
|
||||
@"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 @Limit;",
|
||||
new { System = system.Id, Before = before, Limit = limit }
|
||||
));
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("systems/{systemRef}/fronters")]
|
||||
public async Task<IActionResult> GetSystemFronters(string systemRef)
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
var ctx = ContextFor(system);
|
||||
|
||||
if (!system.FrontPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedCurrentFronters;
|
||||
|
||||
var sw = await _repo.GetLatestSwitch(system.Id);
|
||||
if (sw == null)
|
||||
return NoContent();
|
||||
|
||||
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(),
|
||||
Uuid = sw.Uuid,
|
||||
});
|
||||
}
|
||||
|
||||
var ctx = this.ContextFor(system);
|
||||
|
||||
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedFrontHistory;
|
||||
[HttpPost("systems/@me/switches")]
|
||||
public async Task<IActionResult> SwitchCreate([FromBody] PostSwitchParams data)
|
||||
{
|
||||
if (data.Members.Distinct().Count() != data.Members.Count)
|
||||
throw Errors.DuplicateMembersInList;
|
||||
|
||||
if (before == null)
|
||||
before = SystemClock.Instance.GetCurrentInstant();
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
if (limit == null || limit > 100)
|
||||
limit = 100;
|
||||
if (data.Timestamp != null && await _repo.GetSwitches(system.Id).Select(x => x.Timestamp)
|
||||
.ContainsAsync(data.Timestamp.Value))
|
||||
throw Errors.SameSwitchTimestampError;
|
||||
|
||||
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturnNew>(
|
||||
@"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 @Limit;", new { System = system.Id, Before = before, Limit = limit }));
|
||||
return Ok(res);
|
||||
var members = new List<PKMember>();
|
||||
|
||||
foreach (var memberRef in data.Members)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
// todo: which member
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
members.Add(member);
|
||||
}
|
||||
|
||||
[HttpGet("systems/{systemRef}/fronters")]
|
||||
public async Task<IActionResult> GetSystemFronters(string systemRef)
|
||||
// We get the current switch, if it exists
|
||||
var latestSwitch = await _repo.GetLatestSwitch(system.Id);
|
||||
if (latestSwitch != null && (data.Timestamp == null || data.Timestamp > latestSwitch.Timestamp))
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id));
|
||||
|
||||
var ctx = this.ContextFor(system);
|
||||
|
||||
if (!system.FrontPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedCurrentFronters;
|
||||
|
||||
var sw = await _repo.GetLatestSwitch(system.Id);
|
||||
if (sw == null)
|
||||
return NoContent();
|
||||
|
||||
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(),
|
||||
Uuid = sw.Uuid,
|
||||
});
|
||||
// Bail if this switch is identical to the latest one
|
||||
if (await latestSwitchMembers.Select(m => m.Hid)
|
||||
.SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable()))
|
||||
throw Errors.SameSwitchMembersError;
|
||||
}
|
||||
|
||||
var newSwitch =
|
||||
await _db.Execute(conn => _repo.AddSwitch(conn, system.Id, members.Select(m => m.Id).ToList()));
|
||||
if (data.Timestamp != null)
|
||||
await _repo.MoveSwitch(newSwitch.Id, data.Timestamp.Value);
|
||||
|
||||
[HttpPost("systems/@me/switches")]
|
||||
public async Task<IActionResult> SwitchCreate([FromBody] PostSwitchParams data)
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
if (data.Members.Distinct().Count() != data.Members.Count)
|
||||
Uuid = newSwitch.Uuid,
|
||||
Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("systems/{systemRef}/switches/{switchRef}")]
|
||||
public async Task<IActionResult> SwitchGet(string systemRef, string switchRef)
|
||||
{
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
throw Errors.InvalidSwitchId;
|
||||
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null || system.Id != sw.System)
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
var ctx = ContextFor(system);
|
||||
|
||||
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPatch("systems/@me/switches/{switchRef}")]
|
||||
public async Task<IActionResult> SwitchPatch(string switchRef, [FromBody] JObject data)
|
||||
{
|
||||
// for now, don't need to make a PatchObject for this, since it's only one param
|
||||
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
throw Errors.InvalidSwitchId;
|
||||
|
||||
var valueStr = data.Value<string>("timestamp").NullIfEmpty();
|
||||
if (valueStr == null)
|
||||
throw new ModelParseError(new List<ValidationError> { new("timestamp", "Key 'timestamp' is required.") });
|
||||
|
||||
var value = Instant.FromDateTimeOffset(DateTime.Parse(valueStr).ToUniversalTime());
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null || system.Id != sw.System)
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value))
|
||||
throw Errors.SameSwitchTimestampError;
|
||||
|
||||
await _repo.MoveSwitch(sw.Id, value);
|
||||
|
||||
var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync();
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPatch("systems/@me/switches/{switchRef}/members")]
|
||||
public async Task<IActionResult> SwitchMemberPatch(string switchRef, [FromBody] JArray data)
|
||||
{
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
|
||||
if (data.Distinct().Count() != data.Count)
|
||||
throw Errors.DuplicateMembersInList;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
if (data.Timestamp != null && await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(data.Timestamp.Value))
|
||||
throw Errors.SameSwitchTimestampError;
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null)
|
||||
throw Errors.SwitchNotFound;
|
||||
|
||||
var members = new List<PKMember>();
|
||||
var members = new List<PKMember>();
|
||||
|
||||
foreach (var memberRef in data.Members)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
// todo: which member
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
members.Add(member);
|
||||
}
|
||||
|
||||
// We get the current switch, if it exists
|
||||
var latestSwitch = await _repo.GetLatestSwitch(system.Id);
|
||||
if (latestSwitch != null && (data.Timestamp == null || data.Timestamp > latestSwitch.Timestamp))
|
||||
{
|
||||
var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id));
|
||||
|
||||
// Bail if this switch is identical to the latest one
|
||||
if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable()))
|
||||
throw Errors.SameSwitchMembersError;
|
||||
}
|
||||
|
||||
var newSwitch = await _db.Execute(conn => _repo.AddSwitch(conn, system.Id, members.Select(m => m.Id).ToList()));
|
||||
if (data.Timestamp != null)
|
||||
await _repo.MoveSwitch(newSwitch.Id, data.Timestamp.Value);
|
||||
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Uuid = newSwitch.Uuid,
|
||||
Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("systems/{systemRef}/switches/{switchRef}")]
|
||||
public async Task<IActionResult> SwitchGet(string systemRef, string switchRef)
|
||||
foreach (var JmemberRef in data)
|
||||
{
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
throw Errors.InvalidSwitchId;
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
// todo: which member
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null || system.Id != sw.System)
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
var ctx = this.ContextFor(system);
|
||||
|
||||
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync()
|
||||
});
|
||||
members.Add(member);
|
||||
}
|
||||
|
||||
[HttpPatch("systems/@me/switches/{switchRef}")]
|
||||
public async Task<IActionResult> SwitchPatch(string switchRef, [FromBody] JObject data)
|
||||
var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
|
||||
if (await latestSwitchMembers.Select(m => m.Hid)
|
||||
.SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable()))
|
||||
throw Errors.SameSwitchMembersError;
|
||||
|
||||
await _db.Execute(conn => _repo.EditSwitch(conn, sw.Id, members.Select(x => x.Id).ToList()));
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
// for now, don't need to make a PatchObject for this, since it's only one param
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2))
|
||||
});
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
throw Errors.InvalidSwitchId;
|
||||
[HttpDelete("systems/@me/switches/{switchRef}")]
|
||||
public async Task<IActionResult> SwitchDelete(string switchRef)
|
||||
{
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
throw Errors.InvalidSwitchId;
|
||||
|
||||
var valueStr = data.Value<string>("timestamp").NullIfEmpty();
|
||||
if (valueStr == null)
|
||||
throw new ModelParseError(new List<ValidationError>() { new ValidationError("timestamp", $"Key 'timestamp' is required.") });
|
||||
var system = await ResolveSystem("@me");
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null || system.Id != sw.System)
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
var value = Instant.FromDateTimeOffset(DateTime.Parse(valueStr).ToUniversalTime());
|
||||
await _repo.DeleteSwitch(sw.Id);
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null || system.Id != sw.System)
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value))
|
||||
throw Errors.SameSwitchTimestampError;
|
||||
|
||||
await _repo.MoveSwitch(sw.Id, value);
|
||||
|
||||
var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync();
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)),
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPatch("systems/@me/switches/{switchRef}/members")]
|
||||
public async Task<IActionResult> SwitchMemberPatch(string switchRef, [FromBody] JArray data)
|
||||
{
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
|
||||
if (data.Distinct().Count() != data.Count)
|
||||
throw Errors.DuplicateMembersInList;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null)
|
||||
throw Errors.SwitchNotFound;
|
||||
|
||||
var members = new List<PKMember>();
|
||||
|
||||
foreach (var JmemberRef in data)
|
||||
{
|
||||
var memberRef = JmemberRef.Value<string>();
|
||||
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
// todo: which member
|
||||
throw Errors.MemberNotFound;
|
||||
if (member.System != system.Id)
|
||||
throw Errors.NotOwnMemberErrorWithRef(memberRef);
|
||||
|
||||
members.Add(member);
|
||||
}
|
||||
|
||||
var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
|
||||
|
||||
if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable()))
|
||||
throw Errors.SameSwitchMembersError;
|
||||
|
||||
await _db.Execute(conn => _repo.EditSwitch(conn, sw.Id, members.Select(x => x.Id).ToList()));
|
||||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)),
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("systems/@me/switches/{switchRef}")]
|
||||
public async Task<IActionResult> SwitchDelete(string switchRef)
|
||||
{
|
||||
if (!Guid.TryParse(switchRef, out var switchId))
|
||||
throw Errors.InvalidSwitchId;
|
||||
|
||||
var system = await ResolveSystem("@me");
|
||||
var sw = await _repo.GetSwitchByUuid(switchId);
|
||||
if (sw == null || system.Id != sw.System)
|
||||
throw Errors.SwitchNotFoundPublic;
|
||||
|
||||
await _repo.DeleteSwitch(sw.Id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,37 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}/systems")]
|
||||
public class SystemControllerV2: PKControllerBase
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("2.0")]
|
||||
[Route("v{version:apiVersion}/systems")]
|
||||
public class SystemControllerV2: PKControllerBase
|
||||
public SystemControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
|
||||
[HttpGet("{systemRef}")]
|
||||
public async Task<IActionResult> SystemGet(string systemRef)
|
||||
{
|
||||
public SystemControllerV2(IServiceProvider svc) : base(svc) { }
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null) throw Errors.SystemNotFound;
|
||||
return Ok(system.ToJson(ContextFor(system), APIVersion.V2));
|
||||
}
|
||||
|
||||
[HttpGet("{systemRef}")]
|
||||
public async Task<IActionResult> SystemGet(string systemRef)
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null) throw Errors.SystemNotFound;
|
||||
else return Ok(system.ToJson(this.ContextFor(system), v: APIVersion.V2));
|
||||
}
|
||||
[HttpPatch("@me")]
|
||||
public async Task<IActionResult> DoSystemPatch([FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var patch = SystemPatch.FromJSON(data, APIVersion.V2);
|
||||
|
||||
[HttpPatch("@me")]
|
||||
public async Task<IActionResult> DoSystemPatch([FromBody] JObject data)
|
||||
{
|
||||
var system = await ResolveSystem("@me");
|
||||
var patch = SystemPatch.FromJSON(data, APIVersion.V2);
|
||||
patch.AssertIsValid();
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
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, v: APIVersion.V2));
|
||||
}
|
||||
var newSystem = await _repo.UpdateSystem(system.Id, patch);
|
||||
return Ok(newSystem.ToJson(LookupContext.ByOwner, APIVersion.V2));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +1,131 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
{
|
||||
public class PKError: Exception
|
||||
{
|
||||
public int ResponseCode { get; init; }
|
||||
public int JsonCode { get; init; }
|
||||
public PKError(int code, int json_code, string message) : base(message)
|
||||
{
|
||||
ResponseCode = code;
|
||||
JsonCode = json_code;
|
||||
}
|
||||
namespace PluralKit.API;
|
||||
|
||||
public JObject ToJson()
|
||||
{
|
||||
var j = new JObject();
|
||||
j.Add("message", this.Message);
|
||||
j.Add("code", this.JsonCode);
|
||||
return j;
|
||||
}
|
||||
public class PKError: Exception
|
||||
{
|
||||
public PKError(int code, int json_code, string message) : base(message)
|
||||
{
|
||||
ResponseCode = code;
|
||||
JsonCode = json_code;
|
||||
}
|
||||
|
||||
public class ModelParseError: PKError
|
||||
public int ResponseCode { get; init; }
|
||||
public int JsonCode { get; init; }
|
||||
|
||||
public JObject ToJson()
|
||||
{
|
||||
private IEnumerable<ValidationError> _errors { get; init; }
|
||||
public ModelParseError(IEnumerable<ValidationError> errors) : base(400, 40001, "Error parsing JSON model")
|
||||
{
|
||||
_errors = errors;
|
||||
}
|
||||
var j = new JObject();
|
||||
j.Add("message", Message);
|
||||
j.Add("code", JsonCode);
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
public new JObject ToJson()
|
||||
{
|
||||
var j = base.ToJson();
|
||||
var e = new JObject();
|
||||
public class ModelParseError: PKError
|
||||
{
|
||||
public ModelParseError(IEnumerable<ValidationError> errors) : base(400, 40001, "Error parsing JSON model")
|
||||
{
|
||||
_errors = errors;
|
||||
}
|
||||
|
||||
foreach (var err in _errors)
|
||||
private IEnumerable<ValidationError> _errors { get; }
|
||||
|
||||
public new JObject ToJson()
|
||||
{
|
||||
var j = base.ToJson();
|
||||
var e = new JObject();
|
||||
|
||||
foreach (var err in _errors)
|
||||
{
|
||||
var o = new JObject();
|
||||
|
||||
if (err is FieldTooLongError fe)
|
||||
{
|
||||
var o = new JObject();
|
||||
|
||||
if (err is FieldTooLongError fe)
|
||||
{
|
||||
o.Add("message", $"Field {err.Key} is too long.");
|
||||
o.Add("actual_length", fe.ActualLength);
|
||||
o.Add("max_length", fe.MaxLength);
|
||||
}
|
||||
else if (err.Text != null)
|
||||
o.Add("message", err.Text);
|
||||
else
|
||||
o.Add("message", $"Field {err.Key} is invalid.");
|
||||
|
||||
if (e[err.Key] == null)
|
||||
e.Add(err.Key, new JArray());
|
||||
|
||||
(e[err.Key] as JArray).Add(o);
|
||||
o.Add("message", $"Field {err.Key} is too long.");
|
||||
o.Add("actual_length", fe.ActualLength);
|
||||
o.Add("max_length", fe.MaxLength);
|
||||
}
|
||||
else if (err.Text != null)
|
||||
{
|
||||
o.Add("message", err.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
o.Add("message", $"Field {err.Key} is invalid.");
|
||||
}
|
||||
|
||||
j.Add("errors", e);
|
||||
return j;
|
||||
if (e[err.Key] == null)
|
||||
e.Add(err.Key, new JArray());
|
||||
|
||||
(e[err.Key] as JArray).Add(o);
|
||||
}
|
||||
|
||||
j.Add("errors", e);
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Errors
|
||||
public static class Errors
|
||||
{
|
||||
public static PKError GenericBadRequest = new(400, 0, "400: Bad Request");
|
||||
public static PKError GenericAuthError = new(401, 0, "401: Missing or invalid Authorization header");
|
||||
|
||||
public static PKError SystemNotFound = new(404, 20001, "System not found.");
|
||||
public static PKError MemberNotFound = new(404, 20002, "Member not found.");
|
||||
public static PKError MemberNotFoundWithRef(string memberRef) =>
|
||||
new(404, 20003, $"Member '{memberRef}' not found.");
|
||||
public static PKError GroupNotFound = new(404, 20004, "Group not found.");
|
||||
public static PKError GroupNotFoundWithRef(string groupRef) =>
|
||||
new(404, 20005, $"Group '{groupRef}' not found.");
|
||||
public static PKError MessageNotFound = new(404, 20006, "Message not found.");
|
||||
public static PKError SwitchNotFound = new(404, 20007, "Switch not found.");
|
||||
public static PKError SwitchNotFoundPublic = new(404, 20008,
|
||||
"Switch not found, switch associated with different system, or unauthorized to view front history.");
|
||||
public static PKError SystemGuildNotFound = new(404, 20009, "No system guild settings found for target guild.");
|
||||
public static PKError MemberGuildNotFound = new(404, 20010, "No member guild settings found for target guild.");
|
||||
|
||||
public static PKError UnauthorizedMemberList = new(403, 30001, "Unauthorized to view member list");
|
||||
public static PKError UnauthorizedGroupList = new(403, 30002, "Unauthorized to view group list");
|
||||
public static PKError UnauthorizedGroupMemberList = new(403, 30003, "Unauthorized to view group member list");
|
||||
public static PKError UnauthorizedCurrentFronters = new(403, 30004, "Unauthorized to view current fronters.");
|
||||
public static PKError UnauthorizedFrontHistory = new(403, 30005, "Unauthorized to view front history.");
|
||||
public static PKError NotOwnMemberError = new(403, 30006, "Target member is not part of your system.");
|
||||
public static PKError NotOwnGroupError = new(403, 30007, "Target group is not part of your system.");
|
||||
// todo: somehow add the memberRef to the JSON
|
||||
public static PKError NotOwnMemberErrorWithRef(string memberRef) =>
|
||||
new(403, 30008, $"Member '{memberRef}' is not part of your system.");
|
||||
public static PKError NotOwnGroupErrorWithRef(string groupRef) =>
|
||||
new(403, 30009, $"Group '{groupRef}' is not part of your system.");
|
||||
|
||||
public static PKError MissingAutoproxyMember =
|
||||
new(400, 40002, "Missing autoproxy member for member-mode autoproxy.");
|
||||
public static PKError DuplicateMembersInList = new(400, 40003, "Duplicate members in member list.");
|
||||
public static PKError SameSwitchMembersError =
|
||||
new(400, 40004, "Member list identical to current fronter list.");
|
||||
public static PKError SameSwitchTimestampError =
|
||||
new(400, 40005, "Switch with provided timestamp already exists.");
|
||||
public static PKError InvalidSwitchId = new(400, 40006, "Invalid switch ID.");
|
||||
public static PKError MemberLimitReached = new(400, 40007, "Member limit reached.");
|
||||
public static PKError GroupLimitReached = new(400, 40008, "Group limit reached.");
|
||||
public static PKError Unimplemented = new(501, 50001, "Unimplemented");
|
||||
}
|
||||
|
||||
public static class APIErrorHandlerExt
|
||||
{
|
||||
public static bool IsUserError(this Exception exc)
|
||||
{
|
||||
public static PKError GenericBadRequest = new(400, 0, "400: Bad Request");
|
||||
public static PKError GenericAuthError = new(401, 0, "401: Missing or invalid Authorization header");
|
||||
public static PKError SystemNotFound = new(404, 20001, "System not found.");
|
||||
public static PKError MemberNotFound = new(404, 20002, "Member not found.");
|
||||
public static PKError MemberNotFoundWithRef(string memberRef) => new(404, 20003, $"Member '{memberRef}' not found.");
|
||||
public static PKError GroupNotFound = new(404, 20004, "Group not found.");
|
||||
public static PKError GroupNotFoundWithRef(string groupRef) => new(404, 20005, $"Group '{groupRef}' not found.");
|
||||
public static PKError MessageNotFound = new(404, 20006, "Message not found.");
|
||||
public static PKError SwitchNotFound = new(404, 20007, "Switch not found.");
|
||||
public static PKError SwitchNotFoundPublic = new(404, 20008, "Switch not found, switch associated with different system, or unauthorized to view front history.");
|
||||
public static PKError SystemGuildNotFound = new(404, 20009, "No system guild settings found for target guild.");
|
||||
public static PKError MemberGuildNotFound = new(404, 20010, "No member guild settings found for target guild.");
|
||||
public static PKError UnauthorizedMemberList = new(403, 30001, "Unauthorized to view member list");
|
||||
public static PKError UnauthorizedGroupList = new(403, 30002, "Unauthorized to view group list");
|
||||
public static PKError UnauthorizedGroupMemberList = new(403, 30003, "Unauthorized to view group member list");
|
||||
public static PKError UnauthorizedCurrentFronters = new(403, 30004, "Unauthorized to view current fronters.");
|
||||
public static PKError UnauthorizedFrontHistory = new(403, 30005, "Unauthorized to view front history.");
|
||||
public static PKError NotOwnMemberError = new(403, 30006, "Target member is not part of your system.");
|
||||
public static PKError NotOwnGroupError = new(403, 30007, "Target group is not part of your system.");
|
||||
// todo: somehow add the memberRef to the JSON
|
||||
public static PKError NotOwnMemberErrorWithRef(string memberRef) => new(403, 30008, $"Member '{memberRef}' is not part of your system.");
|
||||
public static PKError NotOwnGroupErrorWithRef(string groupRef) => new(403, 30009, $"Group '{groupRef}' is not part of your system.");
|
||||
public static PKError MissingAutoproxyMember = new(400, 40002, "Missing autoproxy member for member-mode autoproxy.");
|
||||
public static PKError DuplicateMembersInList = new(400, 40003, "Duplicate members in member list.");
|
||||
public static PKError SameSwitchMembersError = new(400, 40004, "Member list identical to current fronter list.");
|
||||
public static PKError SameSwitchTimestampError = new(400, 40005, "Switch with provided timestamp already exists.");
|
||||
public static PKError InvalidSwitchId = new(400, 40006, "Invalid switch ID.");
|
||||
public static PKError MemberLimitReached = new(400, 40007, "Member limit reached.");
|
||||
public static PKError GroupLimitReached = new(400, 40008, "Group limit reached.");
|
||||
public static PKError Unimplemented = new(501, 50001, "Unimplemented");
|
||||
}
|
||||
// caused by users sending an incorrect JSON type (array where an object is expected, etc)
|
||||
if (exc is InvalidCastException && exc.Message.Contains("Newtonsoft.Json"))
|
||||
return true;
|
||||
|
||||
public static class APIErrorHandlerExt
|
||||
{
|
||||
public static bool IsUserError(this Exception exc)
|
||||
{
|
||||
// caused by users sending an incorrect JSON type (array where an object is expected, etc)
|
||||
if (exc is InvalidCastException && exc.Message.Contains("Newtonsoft.Json"))
|
||||
return true;
|
||||
// Hacky parsing of timestamps results in hacky error handling. Probably fix this one at some point.
|
||||
if (exc is FormatException && exc.Message.Contains("was not recognized as a valid DateTime"))
|
||||
return true;
|
||||
|
||||
// Hacky parsing of timestamps results in hacky error handling. Probably fix this one at some point.
|
||||
if (exc is FormatException && exc.Message.Contains("was not recognized as a valid DateTime"))
|
||||
return true;
|
||||
|
||||
// This may expanded at some point.
|
||||
return false;
|
||||
}
|
||||
// This may expanded at some point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,32 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class AuthorizationTokenHandlerMiddleware
|
||||
{
|
||||
public class AuthorizationTokenHandlerMiddleware
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public AuthorizationTokenHandlerMiddleware(RequestDelegate next)
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
public AuthorizationTokenHandlerMiddleware(RequestDelegate next)
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext ctx, IDatabase db)
|
||||
{
|
||||
ctx.Request.Headers.TryGetValue("authorization", out var authHeaders);
|
||||
if (authHeaders.Count > 0)
|
||||
{
|
||||
_next = next;
|
||||
var systemId = await db.Execute(conn => conn.QuerySingleOrDefaultAsync<SystemId?>(
|
||||
"select id from systems where token = @token",
|
||||
new { token = authHeaders[0] }
|
||||
));
|
||||
|
||||
if (systemId != null)
|
||||
ctx.Items.Add("SystemId", systemId);
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext ctx, IDatabase db)
|
||||
{
|
||||
ctx.Request.Headers.TryGetValue("authorization", out var authHeaders);
|
||||
if (authHeaders.Count > 0)
|
||||
{
|
||||
var systemId = await db.Execute(conn => conn.QuerySingleOrDefaultAsync<SystemId?>(
|
||||
"select id from systems where token = @token",
|
||||
new { token = authHeaders[0] }
|
||||
));
|
||||
|
||||
if (systemId != null)
|
||||
ctx.Items.Add("SystemId", systemId);
|
||||
}
|
||||
|
||||
await _next.Invoke(ctx);
|
||||
}
|
||||
await _next.Invoke(ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
using Autofac;
|
||||
|
||||
namespace PluralKit.API
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class APIModule: Module
|
||||
{
|
||||
public class APIModule: Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
}
|
||||
}
|
||||
protected override void Load(ContainerBuilder builder) { }
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>annotations</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This enables XML generation for Swashbuckle -->
|
||||
|
|
@ -11,15 +12,15 @@
|
|||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugType>full</DebugType>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PluralKit.Core\PluralKit.Core.csproj" />
|
||||
<ProjectReference Include="..\PluralKit.Core\PluralKit.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Properties\launchSettings.json" />
|
||||
<_ContentIncludedByDefault Remove="Properties\launchSettings.json"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
@ -27,16 +28,16 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.6.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="6.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.6.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.6.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.6.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.6.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.2.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="4.2.0"/>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.6.3"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="6.0.1"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.6.3"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.6.3"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.6.3"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.6.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,29 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace PluralKit.API
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
InitUtils.InitStatic();
|
||||
await BuildInfoService.LoadVersion();
|
||||
await CreateHostBuilder(args).Build().RunAsync();
|
||||
}
|
||||
namespace PluralKit.API;
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
|
||||
.UseSerilog()
|
||||
.ConfigureWebHostDefaults(whb => whb
|
||||
.UseConfiguration(InitUtils.BuildConfiguration(args).Build())
|
||||
.ConfigureKestrel(opts =>
|
||||
{
|
||||
opts.ListenAnyIP(opts.ApplicationServices.GetRequiredService<ApiConfig>().Port);
|
||||
})
|
||||
.UseStartup<Startup>());
|
||||
public class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
InitUtils.InitStatic();
|
||||
await BuildInfoService.LoadVersion();
|
||||
await CreateHostBuilder(args).Build().RunAsync();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
|
||||
.UseSerilog()
|
||||
.ConfigureWebHostDefaults(whb => whb
|
||||
.UseConfiguration(InitUtils.BuildConfiguration(args).Build())
|
||||
.ConfigureKestrel(opts =>
|
||||
{
|
||||
opts.ListenAnyIP(opts.ApplicationServices.GetRequiredService<ApiConfig>().Port);
|
||||
})
|
||||
.UseStartup<Startup>());
|
||||
}
|
||||
|
|
@ -1,189 +1,177 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
using Autofac;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Versioning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.API
|
||||
using Serilog;
|
||||
|
||||
namespace PluralKit.API;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public class Startup
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddAuthentication("SystemToken")
|
||||
.AddScheme<SystemTokenAuthenticationHandler.Opts,
|
||||
SystemTokenAuthenticationHandler>("SystemToken", null);
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
options.AddPolicy("EditSystem",
|
||||
p => p.RequireAuthenticatedUser().AddRequirements(new OwnSystemRequirement()));
|
||||
options.AddPolicy("EditMember",
|
||||
p => p.RequireAuthenticatedUser().AddRequirements(new OwnSystemRequirement()));
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
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>();
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
services.AddControllers()
|
||||
// sorry MS, this just does *more*
|
||||
.AddNewtonsoftJson(opts =>
|
||||
{
|
||||
// ... though by default it messes up timestamps in JSON
|
||||
opts.SerializerSettings.DateParseHandling = DateParseHandling.None;
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
throw Errors.GenericBadRequest
|
||||
);
|
||||
|
||||
services.AddApiVersioning();
|
||||
|
||||
services.AddVersionedApiExplorer(c =>
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddAuthentication("SystemToken")
|
||||
.AddScheme<SystemTokenAuthenticationHandler.Opts, SystemTokenAuthenticationHandler>("SystemToken", null);
|
||||
c.GroupNameFormat = "'v'VV";
|
||||
c.ApiVersionParameterSource = new UrlSegmentApiVersionReader();
|
||||
c.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1.0", new OpenApiInfo { Title = "PluralKit", Version = "1.0" });
|
||||
|
||||
c.EnableAnnotations();
|
||||
c.AddSecurityDefinition("TokenAuth",
|
||||
new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.ApiKey });
|
||||
|
||||
// Exclude routes without a version, then fall back to group name matching (default behavior)
|
||||
c.DocInclusionPredicate((docName, apiDesc) =>
|
||||
{
|
||||
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()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Latest)
|
||||
// sorry MS, this just does *more*
|
||||
.AddNewtonsoftJson((opts) =>
|
||||
{
|
||||
// ... though by default it messes up timestamps in JSON
|
||||
opts.SerializerSettings.DateParseHandling = DateParseHandling.None;
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
options.InvalidModelStateResponseFactory = (context) =>
|
||||
throw Errors.GenericBadRequest
|
||||
);
|
||||
|
||||
services.AddApiVersioning();
|
||||
|
||||
services.AddVersionedApiExplorer(c =>
|
||||
{
|
||||
c.GroupNameFormat = "'v'VV";
|
||||
c.ApiVersionParameterSource = new UrlSegmentApiVersionReader();
|
||||
c.SubstituteApiVersionInUrl = true;
|
||||
if (!apiDesc.RelativePath.StartsWith("v1/")) return false;
|
||||
return apiDesc.GroupName == docName;
|
||||
});
|
||||
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1.0", new OpenApiInfo { Title = "PluralKit", Version = "1.0" });
|
||||
// Set the comments path for the Swagger JSON and UI.
|
||||
// https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio#customize-and-extend
|
||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
c.IncludeXmlComments(xmlPath);
|
||||
});
|
||||
services.AddSwaggerGenNewtonsoftSupport();
|
||||
}
|
||||
|
||||
c.EnableAnnotations();
|
||||
c.AddSecurityDefinition("TokenAuth",
|
||||
new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.ApiKey });
|
||||
public void ConfigureContainer(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterInstance(InitUtils.BuildConfiguration(Environment.GetCommandLineArgs()).Build())
|
||||
.As<IConfiguration>();
|
||||
builder.RegisterModule(new ConfigModule<ApiConfig>("API"));
|
||||
builder.RegisterModule(new LoggingModule("api",
|
||||
cfg: new LoggerConfiguration().Filter.ByExcluding(exc => exc.Exception.IsUserError())));
|
||||
builder.RegisterModule(new MetricsModule("API"));
|
||||
builder.RegisterModule<DataStoreModule>();
|
||||
builder.RegisterModule<APIModule>();
|
||||
}
|
||||
|
||||
// Exclude routes without a version, then fall back to group name matching (default behavior)
|
||||
c.DocInclusionPredicate((docName, apiDesc) =>
|
||||
{
|
||||
if (!apiDesc.RelativePath.StartsWith("v1/")) return false;
|
||||
return apiDesc.GroupName == docName;
|
||||
});
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
// Set the comments path for the Swagger JSON and UI.
|
||||
// https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio#customize-and-extend
|
||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
c.IncludeXmlComments(xmlPath);
|
||||
});
|
||||
services.AddSwaggerGenNewtonsoftSupport();
|
||||
// Only enable Swagger stuff when ASPNETCORE_ENVIRONMENT=Development (for now)
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "PluralKit (v1)"); });
|
||||
}
|
||||
|
||||
public void ConfigureContainer(ContainerBuilder builder)
|
||||
// add X-PluralKit-Version header
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
builder.RegisterInstance(InitUtils.BuildConfiguration(Environment.GetCommandLineArgs()).Build())
|
||||
.As<IConfiguration>();
|
||||
builder.RegisterModule(new ConfigModule<ApiConfig>("API"));
|
||||
builder.RegisterModule(new LoggingModule("api", cfg: new LoggerConfiguration().Filter.ByExcluding(exc => exc.Exception.IsUserError())));
|
||||
builder.RegisterModule(new MetricsModule("API"));
|
||||
builder.RegisterModule<DataStoreModule>();
|
||||
builder.RegisterModule<APIModule>();
|
||||
}
|
||||
ctx.Response.Headers.Add("X-PluralKit-Version", BuildInfoService.Version);
|
||||
return next();
|
||||
});
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
app.UseExceptionHandler(handler => handler.Run(async ctx =>
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
var exc = ctx.Features.Get<IExceptionHandlerPathFeature>();
|
||||
|
||||
// handle common ISEs that are generated by invalid user input
|
||||
if (exc.Error.IsUserError())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
// Only enable Swagger stuff when ASPNETCORE_ENVIRONMENT=Development (for now)
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "PluralKit (v1)");
|
||||
});
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsync("{\"message\":\"400: Bad Request\",\"code\":0}");
|
||||
}
|
||||
|
||||
else if (exc.Error is not PKError)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.WriteAsync("{\"message\":\"500: Internal Server Error\",\"code\":0}");
|
||||
}
|
||||
|
||||
// for some reason, if we don't specifically cast to ModelParseError, it uses the base's ToJson method
|
||||
else if (exc.Error is ModelParseError fe)
|
||||
{
|
||||
ctx.Response.StatusCode = fe.ResponseCode;
|
||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(fe.ToJson()));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
//app.UseHsts();
|
||||
var err = (PKError)exc.Error;
|
||||
ctx.Response.StatusCode = err.ResponseCode;
|
||||
|
||||
var json = JsonConvert.SerializeObject(err.ToJson());
|
||||
await ctx.Response.WriteAsync(json);
|
||||
}
|
||||
|
||||
// add X-PluralKit-Version header
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
ctx.Response.Headers.Add("X-PluralKit-Version", BuildInfoService.Version);
|
||||
return next();
|
||||
});
|
||||
await ctx.Response.CompleteAsync();
|
||||
}));
|
||||
|
||||
app.UseExceptionHandler(handler => handler.Run(async ctx =>
|
||||
{
|
||||
var exc = ctx.Features.Get<IExceptionHandlerPathFeature>();
|
||||
app.UseMiddleware<AuthorizationTokenHandlerMiddleware>();
|
||||
|
||||
// handle common ISEs that are generated by invalid user input
|
||||
if (exc.Error.IsUserError())
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsync("{\"message\":\"400: Bad Request\",\"code\":0}");
|
||||
}
|
||||
//app.UseHttpsRedirection();
|
||||
app.UseCors(opts => opts.AllowAnyMethod().AllowAnyOrigin().WithHeaders("Content-Type", "Authorization"));
|
||||
|
||||
else if (exc.Error is not PKError)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.WriteAsync("{\"message\":\"500: Internal Server Error\",\"code\":0}");
|
||||
}
|
||||
|
||||
// for some reason, if we don't specifically cast to ModelParseError, it uses the base's ToJson method
|
||||
else if (exc.Error is ModelParseError fe)
|
||||
{
|
||||
ctx.Response.StatusCode = fe.ResponseCode;
|
||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(fe.ToJson()));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var err = (PKError)exc.Error;
|
||||
ctx.Response.StatusCode = err.ResponseCode;
|
||||
|
||||
var json = JsonConvert.SerializeObject(err.ToJson());
|
||||
await ctx.Response.WriteAsync(json);
|
||||
}
|
||||
|
||||
await ctx.Response.CompleteAsync();
|
||||
}));
|
||||
|
||||
app.UseMiddleware<AuthorizationTokenHandlerMiddleware>();
|
||||
|
||||
//app.UseHttpsRedirection();
|
||||
app.UseCors(opts => opts.AllowAnyMethod().AllowAnyOrigin().WithHeaders("Content-Type", "Authorization"));
|
||||
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
}
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<gcServer enabled="true"/>
|
||||
</runtime>
|
||||
<runtime>
|
||||
<gcServer enabled="true"/>
|
||||
</runtime>
|
||||
</configuration>
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue