Skip to content

Commit

Permalink
fix: return correct error when username is duplicated
Browse files Browse the repository at this point in the history
  • Loading branch information
buehler committed Jan 26, 2024
1 parent 71fd599 commit 61eff41
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 17 deletions.
44 changes: 30 additions & 14 deletions MumbleApi/Controller/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,30 +77,33 @@ public async Task<IActionResult> GetById(string id)
}

/// <summary>
/// Update the user profile.
/// Update the authenticated user profile.
/// </summary>
[HttpPatch]
[ZitadelAuthorize]
[SwaggerOperation(Description =
"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<IActionResult> 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.");
}
Expand All @@ -114,6 +117,10 @@ public async Task<IActionResult> UpdateProfile([FromBody] UpdateUserData data)
{
return NotFound();
}
catch (UsernameAlreadyTakenException)
{
return Conflict();
}
}

/// <summary>
Expand All @@ -129,18 +136,27 @@ public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}

/// <summary>
Expand All @@ -150,7 +166,7 @@ public async Task<IActionResult> 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<IActionResult> DeleteAvatar()
{
await users.UpdateUserAvatar(HttpContext.UserId());
Expand Down
11 changes: 11 additions & 0 deletions MumbleApi/Database/SqlErrors.cs
Original file line number Diff line number Diff line change
@@ -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" };
}
18 changes: 18 additions & 0 deletions MumbleApi/Errors/UsernameAlreadyTakenException.cs
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
2 changes: 1 addition & 1 deletion MumbleApi/Models/MediaUploadData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ public class MediaUploadData
/// Media file for the upload.
/// </summary>
[FromForm(Name = "media")]
[SwaggerSchema(WriteOnly = true, Required = new[] { "media" })]
[SwaggerSchema(WriteOnly = true, Required = ["media"])]
public IFormFile? Media { get; set; }
}
16 changes: 14 additions & 2 deletions MumbleApi/Services/Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string?> 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)
{
Expand Down

0 comments on commit 61eff41

Please sign in to comment.