feat(backend): add member GET endpoints, POST /users/@me/members endpoint
This commit is contained in:
parent
16f230b97d
commit
e7ec0e6661
10 changed files with 152 additions and 55 deletions
14
ENDPOINTS.md
14
ENDPOINTS.md
|
@ -3,6 +3,8 @@
|
||||||
## Scopes
|
## Scopes
|
||||||
|
|
||||||
- `identify`: `@me` will refer to token user (always granted)
|
- `identify`: `@me` will refer to token user (always granted)
|
||||||
|
- `user.read_hidden`: can read non-privileged hidden information such as timezone,
|
||||||
|
whether the member list is hidden, and whether a member is unlisted.
|
||||||
- `user.read_privileged`: can read privileged information such as authentication methods
|
- `user.read_privileged`: can read privileged information such as authentication methods
|
||||||
- `user.update`: can update the user's profile.
|
- `user.update`: can update the user's profile.
|
||||||
**cannot** update anything locked behind `user.read_privileged`
|
**cannot** update anything locked behind `user.read_privileged`
|
||||||
|
@ -12,15 +14,16 @@
|
||||||
|
|
||||||
## Meta
|
## Meta
|
||||||
|
|
||||||
- [ ] GET `/meta`: gets stats and server information
|
- [x] GET `/meta`: gets stats and server information
|
||||||
|
|
||||||
## Users
|
## Users
|
||||||
|
|
||||||
- [ ] GET `/users/{userRef}`: views current user.
|
- [x] GET `/users/{userRef}`: views current user.
|
||||||
`identify` required to use `@me` as user reference.
|
`identify` required to use `@me` as user reference.
|
||||||
|
`user.read_hidden` required to view timezone and other hidden non-privileged data.
|
||||||
`user.read_privileged` required to view authentication methods.
|
`user.read_privileged` required to view authentication methods.
|
||||||
`member.read` required to view unlisted members.
|
`member.read` required to view unlisted members.
|
||||||
- [ ] PATCH `/users/@me`: updates current user. `user.update` required.
|
- [x] PATCH `/users/@me`: updates current user. `user.update` required.
|
||||||
- [ ] DELETE `/users/@me`: deletes current user. `*` required
|
- [ ] DELETE `/users/@me`: deletes current user. `*` required
|
||||||
- [ ] POST `/users/@me/export`: queues new data export. `*` required
|
- [ ] POST `/users/@me/export`: queues new data export. `*` required
|
||||||
- [ ] GET `/users/@me/export`: gets latest data export. `*` required
|
- [ ] GET `/users/@me/export`: gets latest data export. `*` required
|
||||||
|
@ -32,14 +35,13 @@
|
||||||
|
|
||||||
## Members
|
## Members
|
||||||
|
|
||||||
- [ ] GET `/users/{userRef}/members`: gets list of a user's members.
|
- [x] GET `/users/{userRef}/members`: gets list of a user's members.
|
||||||
if the user's member list is hidden,
|
if the user's member list is hidden,
|
||||||
and it is not the authenticated user (or the token doesn't have the `member.read` scope)
|
and it is not the authenticated user (or the token doesn't have the `member.read` scope)
|
||||||
returns an empty array.
|
returns an empty array.
|
||||||
- [ ] GET `/users/{userRef}/members/{memberRef}`: gets a single member.
|
- [x] GET `/users/{userRef}/members/{memberRef}`: gets a single member.
|
||||||
will always return a member if it exists, even if the member is unlisted.
|
will always return a member if it exists, even if the member is unlisted.
|
||||||
- [ ] POST `/users/@me/members`: creates a new member. `member.create` required
|
- [ ] POST `/users/@me/members`: creates a new member. `member.create` required
|
||||||
- [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required
|
- [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required
|
||||||
- [ ] DELETE `/users/@me/members/{memberRef}`: deletes a member. `member.update` required
|
- [ ] DELETE `/users/@me/members/{memberRef}`: deletes a member. `member.update` required
|
||||||
- [ ] POST `/users/@me/members/{memberRef}/reroll`: rerolls a member's short ID. `member.update` required.
|
- [ ] POST `/users/@me/members/{memberRef}/reroll`: rerolls a member's short ID. `member.update` required.
|
||||||
-
|
|
68
Foxnouns.Backend/Controllers/MembersController.cs
Normal file
68
Foxnouns.Backend/Controllers/MembersController.cs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
using Foxnouns.Backend.Database;
|
||||||
|
using Foxnouns.Backend.Database.Models;
|
||||||
|
using Foxnouns.Backend.Middleware;
|
||||||
|
using Foxnouns.Backend.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Controllers;
|
||||||
|
|
||||||
|
[Route("/api/v2/users/{userRef}/members")]
|
||||||
|
public class MembersController(
|
||||||
|
ILogger logger,
|
||||||
|
DatabaseContext db,
|
||||||
|
MemberRendererService memberRendererService,
|
||||||
|
ISnowflakeGenerator snowflakeGenerator) : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger = logger.ForContext<MembersController>();
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType<IEnumerable<MemberRendererService.PartialMember>>(StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetMembersAsync(string userRef)
|
||||||
|
{
|
||||||
|
var user = await db.ResolveUserAsync(userRef, CurrentToken);
|
||||||
|
return Ok(await memberRendererService.RenderUserMembersAsync(user, CurrentToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{memberRef}")]
|
||||||
|
[ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetMemberAsync(string userRef, string memberRef)
|
||||||
|
{
|
||||||
|
var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken);
|
||||||
|
return Ok(memberRendererService.RenderMember(member, CurrentToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("/api/v2/users/@me/members")]
|
||||||
|
[ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)]
|
||||||
|
[Authorize("member.create")]
|
||||||
|
public async Task<IActionResult> CreateMemberAsync([FromBody] CreateMemberRequest req)
|
||||||
|
{
|
||||||
|
await using var tx = await db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
// "Translation of the 'string.Equals' overload with a 'StringComparison' parameter is not supported."
|
||||||
|
// Member names are case-insensitive, so we need to compare the lowercase forms of both.
|
||||||
|
#pragma warning disable CA1862
|
||||||
|
if (await db.Members.AnyAsync(m => m.UserId == CurrentUser!.Id && m.Name.ToLower() == req.Name.ToLower()))
|
||||||
|
#pragma warning restore CA1862
|
||||||
|
{
|
||||||
|
throw new ApiError.BadRequest("A member with that name already exists", "name");
|
||||||
|
}
|
||||||
|
|
||||||
|
var member = new Member
|
||||||
|
{
|
||||||
|
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||||
|
Name = req.Name,
|
||||||
|
User = CurrentUser!
|
||||||
|
};
|
||||||
|
db.Add(member);
|
||||||
|
|
||||||
|
_logger.Debug("Creating member {MemberName} ({Id}) for {UserId}", member.Name, member.Id, CurrentUser!.Id);
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
await tx.CommitAsync();
|
||||||
|
|
||||||
|
return Ok(memberRendererService.RenderMember(member, CurrentToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateMemberRequest(string Name);
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBas
|
||||||
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))]
|
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
|
||||||
public async Task<IActionResult> GetMeta()
|
public async Task<IActionResult> GetMeta()
|
||||||
{
|
{
|
||||||
var now = clock.GetCurrentInstant();
|
var now = clock.GetCurrentInstant();
|
||||||
|
|
|
@ -16,16 +16,7 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere
|
||||||
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)]
|
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||||
public async Task<IActionResult> GetUserAsync(string userRef)
|
public async Task<IActionResult> GetUserAsync(string userRef)
|
||||||
{
|
{
|
||||||
var user = await db.ResolveUserAsync(userRef);
|
var user = await db.ResolveUserAsync(userRef, CurrentToken);
|
||||||
return await GetUserInnerAsync(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("@me")]
|
|
||||||
[Authorize("identify")]
|
|
||||||
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)]
|
|
||||||
public async Task<IActionResult> GetMeAsync()
|
|
||||||
{
|
|
||||||
var user = await db.ResolveUserAsync(CurrentUser!.Id);
|
|
||||||
return await GetUserInnerAsync(user);
|
return await GetUserInnerAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,11 @@ namespace Foxnouns.Backend.Database;
|
||||||
|
|
||||||
public static class DatabaseQueryExtensions
|
public static class DatabaseQueryExtensions
|
||||||
{
|
{
|
||||||
public static async Task<User> ResolveUserAsync(this DatabaseContext context, string userRef)
|
public static async Task<User> ResolveUserAsync(this DatabaseContext context, string userRef, Token? token)
|
||||||
{
|
{
|
||||||
|
if (userRef == "@me" && token != null)
|
||||||
|
return await context.Users.FirstAsync(u => u.Id == token.UserId);
|
||||||
|
|
||||||
User? user;
|
User? user;
|
||||||
if (Snowflake.TryParse(userRef, out var snowflake))
|
if (Snowflake.TryParse(userRef, out var snowflake))
|
||||||
{
|
{
|
||||||
|
@ -46,9 +49,9 @@ public static class DatabaseQueryExtensions
|
||||||
throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound);
|
throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef)
|
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef, Token? token)
|
||||||
{
|
{
|
||||||
var user = await context.ResolveUserAsync(userRef);
|
var user = await context.ResolveUserAsync(userRef, token);
|
||||||
return await context.ResolveMemberAsync(user.Id, memberRef);
|
return await context.ResolveMemberAsync(user.Id, memberRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,33 +95,4 @@ public static class DatabaseQueryExtensions
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task SetKeyAsync(this DatabaseContext context, string key, string value, Duration expireAfter) =>
|
|
||||||
context.SetKeyAsync(key, value, SystemClock.Instance.GetCurrentInstant() + expireAfter);
|
|
||||||
|
|
||||||
public static async Task SetKeyAsync(this DatabaseContext context, string key, string value, Instant expires)
|
|
||||||
{
|
|
||||||
context.TemporaryKeys.Add(new TemporaryKey
|
|
||||||
{
|
|
||||||
Expires = expires,
|
|
||||||
Key = key,
|
|
||||||
Value = value,
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string?> GetKeyAsync(this DatabaseContext context, string key,
|
|
||||||
bool delete = false)
|
|
||||||
{
|
|
||||||
var value = await context.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key);
|
|
||||||
if (value == null) return null;
|
|
||||||
|
|
||||||
if (delete)
|
|
||||||
{
|
|
||||||
await context.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync();
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@ using NodaTime;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Services;
|
namespace Foxnouns.Backend.Services;
|
||||||
|
|
||||||
public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator)
|
public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator)
|
||||||
{
|
{
|
||||||
private readonly PasswordHasher<User> _passwordHasher = new();
|
private readonly PasswordHasher<User> _passwordHasher = new();
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnow
|
||||||
|
|
||||||
private static (string, byte[]) GenerateToken()
|
private static (string, byte[]) GenerateToken()
|
||||||
{
|
{
|
||||||
var token = AuthUtils.RandomToken(48);
|
var token = AuthUtils.RandomToken();
|
||||||
var hash = SHA512.HashData(Convert.FromBase64String(token));
|
var hash = SHA512.HashData(Convert.FromBase64String(token));
|
||||||
|
|
||||||
return (token, hash);
|
return (token, hash);
|
||||||
|
|
|
@ -10,7 +10,7 @@ namespace Foxnouns.Backend.Services;
|
||||||
public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
|
public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
|
||||||
{
|
{
|
||||||
public Task SetKeyAsync(string key, string value, Duration expireAfter) =>
|
public Task SetKeyAsync(string key, string value, Duration expireAfter) =>
|
||||||
db.SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter);
|
SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter);
|
||||||
|
|
||||||
public async Task SetKeyAsync(string key, string value, Instant expires)
|
public async Task SetKeyAsync(string key, string value, Instant expires)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,16 +1,50 @@
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Database.Models;
|
using Foxnouns.Backend.Database.Models;
|
||||||
|
using Foxnouns.Backend.Utils;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Services;
|
namespace Foxnouns.Backend.Services;
|
||||||
|
|
||||||
public class MemberRendererService(DatabaseContext db, Config config)
|
public class MemberRendererService(DatabaseContext db, Config config)
|
||||||
{
|
{
|
||||||
|
public async Task<IEnumerable<PartialMember>> RenderUserMembersAsync(User user, Token? token)
|
||||||
|
{
|
||||||
|
var canReadHiddenMembers = token != null && token.UserId == user.Id && token.HasScope("member.read");
|
||||||
|
var canReadMemberList = !user.ListHidden || canReadHiddenMembers;
|
||||||
|
|
||||||
|
IEnumerable<Member> members = canReadMemberList
|
||||||
|
? await db.Members
|
||||||
|
.Where(m => m.UserId == user.Id)
|
||||||
|
.OrderBy(m => m.Name)
|
||||||
|
.ToListAsync()
|
||||||
|
: [];
|
||||||
|
if (!canReadHiddenMembers) members = members.Where(m => !m.Unlisted);
|
||||||
|
return members.Select(RenderPartialMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemberResponse RenderMember(Member member, Token? token)
|
||||||
|
{
|
||||||
|
var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden");
|
||||||
|
|
||||||
|
return new MemberResponse(
|
||||||
|
member.Id, member.Name, member.DisplayName, member.Bio,
|
||||||
|
AvatarUrlFor(member), member.Links, member.Names, member.Pronouns, member.Fields,
|
||||||
|
RenderPartialUser(member.User), renderUnlisted ? member.Unlisted : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserRendererService.PartialUser RenderPartialUser(User user) =>
|
||||||
|
new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user));
|
||||||
|
|
||||||
public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name,
|
public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name,
|
||||||
member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns);
|
member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns);
|
||||||
|
|
||||||
private string? AvatarUrlFor(Member member) =>
|
private string? AvatarUrlFor(Member member) =>
|
||||||
member.Avatar != null ? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp" : null;
|
member.Avatar != null ? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp" : null;
|
||||||
|
|
||||||
|
private string? AvatarUrlFor(User user) =>
|
||||||
|
user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null;
|
||||||
|
|
||||||
public record PartialMember(
|
public record PartialMember(
|
||||||
Snowflake Id,
|
Snowflake Id,
|
||||||
string Name,
|
string Name,
|
||||||
|
@ -19,4 +53,18 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
||||||
string? AvatarUrl,
|
string? AvatarUrl,
|
||||||
IEnumerable<FieldEntry> Names,
|
IEnumerable<FieldEntry> Names,
|
||||||
IEnumerable<Pronoun> Pronouns);
|
IEnumerable<Pronoun> Pronouns);
|
||||||
|
|
||||||
|
public record MemberResponse(
|
||||||
|
Snowflake Id,
|
||||||
|
string Name,
|
||||||
|
string? DisplayName,
|
||||||
|
string? Bio,
|
||||||
|
string? AvatarUrl,
|
||||||
|
string[] Links,
|
||||||
|
IEnumerable<FieldEntry> Names,
|
||||||
|
IEnumerable<Pronoun> Pronouns,
|
||||||
|
IEnumerable<Field> Fields,
|
||||||
|
UserRendererService.PartialUser User,
|
||||||
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
bool? Unlisted);
|
||||||
}
|
}
|
|
@ -16,6 +16,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
||||||
{
|
{
|
||||||
var isSelfUser = selfUser?.Id == user.Id;
|
var isSelfUser = selfUser?.Id == user.Id;
|
||||||
var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser;
|
var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser;
|
||||||
|
var tokenHidden = token.HasScope("user.read_hidden") && isSelfUser;
|
||||||
var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser;
|
var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser;
|
||||||
|
|
||||||
renderMembers = renderMembers &&
|
renderMembers = renderMembers &&
|
||||||
|
@ -45,10 +46,14 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
||||||
a.RemoteUsername, a.FediverseApplication?.Domain
|
a.RemoteUsername, a.FediverseApplication?.Domain
|
||||||
))
|
))
|
||||||
: null,
|
: null,
|
||||||
tokenPrivileged ? user.LastActive : null
|
tokenHidden ? user.ListHidden : null,
|
||||||
|
tokenHidden ? user.LastActive : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PartialUser RenderPartialUser(User user) =>
|
||||||
|
new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user));
|
||||||
|
|
||||||
private string? AvatarUrlFor(User user) =>
|
private string? AvatarUrlFor(User user) =>
|
||||||
user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null;
|
user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null;
|
||||||
|
|
||||||
|
@ -68,6 +73,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
|
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
bool? MemberListHidden,
|
||||||
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
Instant? LastActive
|
Instant? LastActive
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -81,4 +88,11 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
||||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
string? FediverseInstance
|
string? FediverseInstance
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public record PartialUser(
|
||||||
|
Snowflake Id,
|
||||||
|
string Username,
|
||||||
|
string? DisplayName,
|
||||||
|
string? AvatarUrl
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@ public static class AuthUtils
|
||||||
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||||
|
|
||||||
public static readonly string[] UserScopes =
|
public static readonly string[] UserScopes =
|
||||||
["user.read_privileged", "user.update"];
|
["user.read_hidden", "user.read_privileged", "user.update"];
|
||||||
|
|
||||||
public static readonly string[] MemberScopes = ["member.read", "member.update", "member.create"];
|
public static readonly string[] MemberScopes = ["member.read", "member.update", "member.create"];
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ public static class AuthUtils
|
||||||
bytes = Convert.FromBase64String(b64);
|
bytes = Convert.FromBase64String(b64);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch
|
||||||
{
|
{
|
||||||
bytes = [];
|
bytes = [];
|
||||||
return false;
|
return false;
|
||||||
|
|
Loading…
Reference in a new issue