diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 78967f3..8fdf235 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -20,15 +20,16 @@ public class UsersController( { [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetUserAsync(string userRef) + public async Task GetUserAsync(string userRef, CancellationToken ct = default) { - var user = await db.ResolveUserAsync(userRef, CurrentToken); + var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); return Ok(await userRendererService.RenderUserAsync( user, selfUser: CurrentUser, token: CurrentToken, renderMembers: true, - renderAuthMethods: true + renderAuthMethods: true, + ct: ct )); } @@ -59,6 +60,11 @@ public class UsersController( user.Bio = req.Bio; } + if (req.HasProperty(nameof(req.Links))) + { + user.Links = req.Links ?? []; + } + if (req.HasProperty(nameof(req.Avatar))) errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); @@ -150,5 +156,6 @@ public class UsersController( public string? DisplayName { get; init; } public string? Bio { get; init; } public string? Avatar { get; init; } + public string[]? Links { get; init; } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 1d4e851..b6ccaaa 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -9,23 +9,29 @@ namespace Foxnouns.Backend.Database; public static class DatabaseQueryExtensions { - public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token) + public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token, + CancellationToken ct = default) { - if (userRef == "@me" && token != null) - return await context.Users.FirstAsync(u => u.Id == token.UserId); + if (userRef == "@me") + { + return token != null + ? await context.Users.FirstAsync(u => u.Id == token.UserId, ct) + : throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", + ErrorCode.AuthenticationRequired); + } User? user; if (Snowflake.TryParse(userRef, out var snowflake)) { user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Id == snowflake); + .FirstOrDefaultAsync(u => u.Id == snowflake, ct); if (user != null) return user; } user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Username == userRef); + .FirstOrDefaultAsync(u => u.Username == userRef, ct); if (user != null) return user; throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound); } diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 6e39af7..f277265 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -23,11 +23,15 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError; public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; - public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized, - errorCode: ErrorCode.AuthenticationError); + public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError) : ApiError(message, + statusCode: HttpStatusCode.Unauthorized, + errorCode: errorCode); - public class Forbidden(string message, IEnumerable? scopes = null) - : ApiError(message, statusCode: HttpStatusCode.Forbidden) + public class Forbidden( + string message, + IEnumerable? scopes = null, + ErrorCode errorCode = ErrorCode.Forbidden) + : ApiError(message, statusCode: HttpStatusCode.Forbidden, errorCode: errorCode) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } @@ -115,6 +119,8 @@ public enum ErrorCode Forbidden, BadRequest, AuthenticationError, + AuthenticationRequired, + MissingScopes, GenericApiError, UserNotFound, MemberNotFound, diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index dd0d97f..6d4bc49 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -18,10 +18,10 @@ public class AuthorizationMiddleware : IMiddleware var token = ctx.GetToken(); if (token == null) - throw new ApiError.Unauthorized("This endpoint requires an authenticated user."); + throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", ErrorCode.AuthenticationRequired); if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any()) throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", - attribute.Scopes.Except(token.Scopes.ExpandScopes())); + attribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes); if (attribute.RequireAdmin && token.User.Role != UserRole.Admin) throw new ApiError.Forbidden("This endpoint can only be used by admins."); if (attribute.RequireModerator && token.User.Role != UserRole.Admin && token.User.Role != UserRole.Moderator) diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 9bb198e..4449488 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -12,7 +12,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe public async Task RenderUserAsync(User user, User? selfUser = null, Token? token = null, bool renderMembers = true, - bool renderAuthMethods = false) + bool renderAuthMethods = false, + CancellationToken ct = default) { var isSelfUser = selfUser?.Id == user.Id; var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser; @@ -24,7 +25,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe renderAuthMethods = renderAuthMethods && tokenPrivileged; IEnumerable members = - renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; + renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync(ct) : []; // Unless the user is requesting their own members AND the token can read hidden members, we filter out unlisted members. if (!(isSelfUser && tokenCanReadHiddenMembers)) members = members.Where(m => m.Unlisted); @@ -32,7 +33,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe ? await db.AuthMethods .Where(a => a.UserId == user.Id) .Include(a => a.FediverseApplication) - .ToListAsync() + .ToListAsync(ct) : []; return new UserResponse(