feat: add PATCH request support, expand PATCH /users/@me, serialize enums correctly
This commit is contained in:
parent
d6c9345dba
commit
e95e0a79ff
20 changed files with 427 additions and 48 deletions
|
@ -10,20 +10,20 @@ public static class AuthUtils
|
|||
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||
|
||||
public static readonly string[] UserScopes =
|
||||
["user.read_hidden", "user.read_privileged", "user.update"];
|
||||
["user.read_privileged", "user.update"];
|
||||
|
||||
public static readonly string[] MemberScopes = ["member.read", "member.update", "member.create"];
|
||||
|
||||
/// <summary>
|
||||
/// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes.
|
||||
/// </summary>
|
||||
public static readonly string[] Scopes = ["identify", ..UserScopes, ..MemberScopes];
|
||||
public static readonly string[] Scopes = ["identify", .. UserScopes, .. MemberScopes];
|
||||
|
||||
/// <summary>
|
||||
/// All scopes that can be granted to applications and tokens. Includes the catch-all token scopes,
|
||||
/// except for "*" which is only granted to the frontend.
|
||||
/// </summary>
|
||||
public static readonly string[] ApplicationScopes = [..Scopes, "user", "member"];
|
||||
public static readonly string[] ApplicationScopes = [.. Scopes, "user", "member"];
|
||||
|
||||
public static string[] ExpandScopes(this string[] scopes)
|
||||
{
|
||||
|
@ -35,6 +35,9 @@ public static class AuthUtils
|
|||
return expandedScopes.ToArray();
|
||||
}
|
||||
|
||||
public static bool HasScope(this Token? token, string scope) =>
|
||||
token?.Scopes.ExpandScopes().Contains(scope) == true;
|
||||
|
||||
private static string[] ExpandAppScopes(this string[] scopes)
|
||||
{
|
||||
var expandedScopes = scopes.ExpandScopes().ToList();
|
||||
|
|
35
Foxnouns.Backend/Utils/PatchRequest.cs
Normal file
35
Foxnouns.Backend/Utils/PatchRequest.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A base class used for PATCH requests which stores information on whether a key is explicitly set to null or not passed at all.
|
||||
/// </summary>
|
||||
public abstract class PatchRequest
|
||||
{
|
||||
private readonly HashSet<string> _properties = [];
|
||||
public bool HasProperty(string propertyName) => _properties.Contains(propertyName);
|
||||
public void SetHasProperty(string propertyName) => _properties.Add(propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom contract resolver to reduce the boilerplate needed to use <see cref="PatchRequest" />.
|
||||
/// Based on this StackOverflow answer: https://stackoverflow.com/a/58748036
|
||||
/// </summary>
|
||||
public class PatchRequestContractResolver : DefaultContractResolver
|
||||
{
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
var prop = base.CreateProperty(member, memberSerialization);
|
||||
|
||||
prop.SetIsSpecified += (o, _) =>
|
||||
{
|
||||
if (o is not PatchRequest patchRequest) return;
|
||||
patchRequest.SetHasProperty(prop.UnderlyingName!);
|
||||
};
|
||||
|
||||
return prop;
|
||||
}
|
||||
}
|
18
Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs
Normal file
18
Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System.Globalization;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A custom StringEnumConverter that converts enum members to SCREAMING_SNAKE_CASE, rather than CamelCase as is the default.
|
||||
/// Newtonsoft.Json doesn't provide a screaming snake case naming strategy, so we just wrap the normal snake case one and convert it to uppercase.
|
||||
/// </summary>
|
||||
public class ScreamingSnakeCaseEnumConverter() : StringEnumConverter(new ScreamingSnakeCaseNamingStrategy(), false)
|
||||
{
|
||||
private class ScreamingSnakeCaseNamingStrategy : SnakeCaseNamingStrategy
|
||||
{
|
||||
protected override string ResolvePropertyName(string name) =>
|
||||
base.ResolvePropertyName(name).ToUpper(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
76
Foxnouns.Backend/Utils/ValidationUtils.cs
Normal file
76
Foxnouns.Backend/Utils/ValidationUtils.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Static methods for validating user input (mostly making sure it's not too short or too long)
|
||||
/// </summary>
|
||||
public static class ValidationUtils
|
||||
{
|
||||
private static readonly Regex UsernameRegex = new("^[\\w-.]{2,40}$", RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly string[] InvalidUsernames =
|
||||
[
|
||||
"..",
|
||||
"admin",
|
||||
"administrator",
|
||||
"mod",
|
||||
"moderator",
|
||||
"api",
|
||||
"page",
|
||||
"pronouns",
|
||||
"settings",
|
||||
"pronouns.cc",
|
||||
"pronounscc"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether a username is valid. If it is not valid, throws <see cref="Foxnouns.Backend.ApiError" />.
|
||||
/// This does not check if the username is already taken.
|
||||
/// </summary>
|
||||
public static void ValidateUsername(string username)
|
||||
{
|
||||
if (!UsernameRegex.IsMatch(username))
|
||||
throw username.Length switch
|
||||
{
|
||||
< 2 => new ApiError.BadRequest("Username is too short", "username"),
|
||||
> 40 => new ApiError.BadRequest("Username is too long", "username"),
|
||||
_ => new ApiError.BadRequest(
|
||||
"Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods",
|
||||
"username")
|
||||
};
|
||||
|
||||
if (InvalidUsernames.Any(u => string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase)))
|
||||
throw new ApiError.BadRequest("Username is not allowed", "username");
|
||||
}
|
||||
|
||||
public static void ValidateDisplayName(string? displayName)
|
||||
{
|
||||
if (displayName == null) return;
|
||||
switch (displayName.Length)
|
||||
{
|
||||
case 0:
|
||||
throw new ApiError.BadRequest("Display name is too short", "display_name");
|
||||
case > 100:
|
||||
throw new ApiError.BadRequest("Display name is too long", "display_name");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateBio(string? bio)
|
||||
{
|
||||
if (bio == null) return;
|
||||
switch (bio.Length)
|
||||
{
|
||||
case 0:
|
||||
throw new ApiError.BadRequest("Bio is too short", "bio");
|
||||
case > 1024:
|
||||
throw new ApiError.BadRequest("Bio is too long", "bio");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateAvatar(string? avatar)
|
||||
{
|
||||
if (avatar == null) return;
|
||||
if (avatar.Length > 1_500_000) throw new ApiError.BadRequest("Avatar is too big", "avatar");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue