diff --git a/MumbleApi/Controller/UserController.cs b/MumbleApi/Controller/UserController.cs index 6df8242..f69c03c 100644 --- a/MumbleApi/Controller/UserController.cs +++ b/MumbleApi/Controller/UserController.cs @@ -77,7 +77,7 @@ public async Task GetById(string id) } /// - /// Update the user profile. + /// Update the authenticated user profile. /// [HttpPatch] [ZitadelAuthorize] @@ -85,22 +85,25 @@ public async Task GetById(string id) "Update the current authenticated user profile. " + "Returns a bad request if fields are set to an empty string. " + "Omitting a field does not update them. " + - "Uploading an avatar is done via the /users/avatar endpoint.")] - [SwaggerResponse(204, "Success - No Content")] - [SwaggerResponse(400, "Bad Request")] + "Uploading an avatar is done via the /users/avatar endpoint. " + + "The username is unique, duplicating it results in an error.")] + [SwaggerResponse(204, "Success - The user profile was updated")] + [SwaggerResponse(400, "Bad Request - Some fields were set to empty string")] + [SwaggerResponse(404, "Not Found - The user was not found in the database")] + [SwaggerResponse(409, "Conflict - The username is already taken")] public async Task UpdateProfile([FromBody] UpdateUserData data) { - if (data.Firstname == string.Empty) + if (data.Firstname?.Length == 0) { return BadRequest("Firstname is an empty string."); } - if (data.Lastname == string.Empty) + if (data.Lastname?.Length == 0) { return BadRequest("Lastname is an empty string."); } - if (data.Username == string.Empty) + if (data.Username?.Length == 0) { return BadRequest("Username is an empty string."); } @@ -114,6 +117,10 @@ public async Task UpdateProfile([FromBody] UpdateUserData data) { return NotFound(); } + catch (UsernameAlreadyTakenException) + { + return Conflict(); + } } /// @@ -129,18 +136,27 @@ public async Task UpdateProfile([FromBody] UpdateUserData data) "Returns the new media url to the uploaded user avatar. " + "Upload limit: 0.5 MB.")] [SwaggerResponse(200, "Success - New Avatar URL")] - [SwaggerResponse(400, "Bad Request")] - public async Task UploadAvatar([FromForm][SwaggerRequestBody(Required = true)] MediaUploadData data) + [SwaggerResponse(400, "Bad Request - The uploaded file was too large")] + [SwaggerResponse(404, "Not Found - User with the given ID was not found")] + [SwaggerResponse(415, "Unsupported Media Type - The uploaded file is not an image")] + public async Task UploadAvatar([FromForm] [SwaggerRequestBody(Required = true)] MediaUploadData data) { if (data.Media?.ContentType.StartsWith("image/") != true) { - return BadRequest("Media must be an image."); + return new UnsupportedMediaTypeResult(); } - await using var file = data.Media.OpenReadStream(); - var newUrl = await users.UpdateUserAvatar(HttpContext.UserId(), (file, data.Media.ContentType)); + try + { + await using var file = data.Media.OpenReadStream(); + var newUrl = await users.UpdateUserAvatar(HttpContext.UserId(), (file, data.Media.ContentType)); - return Ok(newUrl); + return Ok(newUrl); + } + catch (UserNotFoundException) + { + return NotFound(); + } } /// @@ -150,7 +166,7 @@ public async Task UploadAvatar([FromForm][SwaggerRequestBody(Requ [ZitadelAuthorize] [SwaggerOperation(Description = "Remove the current avatar picture (if any) for the actual authenticated user.")] - [SwaggerResponse(204, "Success - No Content")] + [SwaggerResponse(204, "Success - Avatar deleted")] public async Task DeleteAvatar() { await users.UpdateUserAvatar(HttpContext.UserId()); diff --git a/MumbleApi/Database/SqlErrors.cs b/MumbleApi/Database/SqlErrors.cs new file mode 100644 index 0000000..ab0f965 --- /dev/null +++ b/MumbleApi/Database/SqlErrors.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +using Npgsql; + +namespace MumbleApi.Database; + +public static class SqlErrors +{ + public static bool IsUniqueViolation(this DbUpdateException e) => + e.InnerException is PostgresException { SqlState: "23505" }; +} diff --git a/MumbleApi/Errors/UsernameAlreadyTakenException.cs b/MumbleApi/Errors/UsernameAlreadyTakenException.cs new file mode 100644 index 0000000..af7fc35 --- /dev/null +++ b/MumbleApi/Errors/UsernameAlreadyTakenException.cs @@ -0,0 +1,18 @@ +namespace MumbleApi.Errors; + +public class UsernameAlreadyTakenException : Exception +{ + public UsernameAlreadyTakenException() + { + } + + public UsernameAlreadyTakenException(string? message) + : base(message) + { + } + + public UsernameAlreadyTakenException(string? message, Exception? innerException) + : base(message, innerException) + { + } +} diff --git a/MumbleApi/Models/MediaUploadData.cs b/MumbleApi/Models/MediaUploadData.cs index 817f6f2..6930ce7 100644 --- a/MumbleApi/Models/MediaUploadData.cs +++ b/MumbleApi/Models/MediaUploadData.cs @@ -13,6 +13,6 @@ public class MediaUploadData /// Media file for the upload. /// [FromForm(Name = "media")] - [SwaggerSchema(WriteOnly = true, Required = new[] { "media" })] + [SwaggerSchema(WriteOnly = true, Required = ["media"])] public IFormFile? Media { get; set; } } diff --git a/MumbleApi/Services/Users.cs b/MumbleApi/Services/Users.cs index c176be7..318f8d6 100644 --- a/MumbleApi/Services/Users.cs +++ b/MumbleApi/Services/Users.cs @@ -48,13 +48,25 @@ public async Task UpdateUser(string id, string? firstname, string? lastname, str user.Lastname = lastname ?? user.Lastname; user.Username = username ?? user.Username; - await db.SaveChangesAsync(); + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateException e) when (e.IsUniqueViolation()) + { + throw new UsernameAlreadyTakenException(); + } } public async Task UpdateUserAvatar(string id, (Stream File, string MediaType)? avatar = null) { await using var db = await factory.CreateDbContextAsync(); - var user = await db.Users.SingleAsync(u => u.Id == id); + var user = await db.Users.SingleOrDefaultAsync(u => u.Id == id); + + if (user is null) + { + throw new UserNotFoundException(); + } if (user.AvatarId is not null) {