Add 'backend/' from commit '040cfd7a75423d4e6136e58a67b40579af4ee966'
git-subtree-dir: backend git-subtree-mainline:ab911955edgit-subtree-split:040cfd7a75
This commit is contained in:
41
backend/src/Web/Features/Users/Handlers/ChangeAddress.cs
Normal file
41
backend/src/Web/Features/Users/Handlers/ChangeAddress.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeAddressRequest(
|
||||
string? Address);
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeAddressHandler(
|
||||
IdentityUserManager userManager)
|
||||
: Endpoint<ChangeAddressRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/address");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangeAddressRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user.Address = request.Address;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
await SendOkAsync(ct);
|
||||
else
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
41
backend/src/Web/Features/Users/Handlers/ChangeAlias.cs
Normal file
41
backend/src/Web/Features/Users/Handlers/ChangeAlias.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeAliasRequest(
|
||||
string? Alias);
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeAliasHandler(
|
||||
IdentityUserManager userManager)
|
||||
: Endpoint<ChangeAliasRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/alias");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangeAliasRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user.Alias = request.Alias;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
await SendOkAsync(ct);
|
||||
else
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
41
backend/src/Web/Features/Users/Handlers/ChangeBirthDate.cs
Normal file
41
backend/src/Web/Features/Users/Handlers/ChangeBirthDate.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeBirthDateRequest(
|
||||
DateTime BirthDate);
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeBirthDateHandler(
|
||||
IdentityUserManager userManager)
|
||||
: Endpoint<ChangeBirthDateRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/birthdate");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangeBirthDateRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user.BirthDate = request.BirthDate;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
await SendOkAsync(ct);
|
||||
else
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
42
backend/src/Web/Features/Users/Handlers/ChangeEmail.cs
Normal file
42
backend/src/Web/Features/Users/Handlers/ChangeEmail.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeEmailRequest(
|
||||
string? Email);
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeEmailHandler(
|
||||
IdentityUserManager userManager)
|
||||
: Endpoint<ChangeEmailRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/email");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangeEmailRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user.Email = request.Email;
|
||||
|
||||
// TODO: check to see if identity resets the `email confirmed` flag - @jonathan
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
await SendOkAsync(ct);
|
||||
else
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
43
backend/src/Web/Features/Users/Handlers/ChangeFullname.cs
Normal file
43
backend/src/Web/Features/Users/Handlers/ChangeFullname.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeFullnameRequest(
|
||||
string? Firstname,
|
||||
string? Lastname);
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeFullnameHandler(
|
||||
IdentityUserManager userManager)
|
||||
: Endpoint<ChangeFullnameRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/fullname");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangeFullnameRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user.Firstname = request.Firstname;
|
||||
user.Lastname = request.Lastname;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
await SendOkAsync(ct);
|
||||
else
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
42
backend/src/Web/Features/Users/Handlers/ChangePhone.cs
Normal file
42
backend/src/Web/Features/Users/Handlers/ChangePhone.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangePhoneRequest(
|
||||
string? PhoneNumber);
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangePhoneHandler(
|
||||
IdentityUserManager userManager)
|
||||
: Endpoint<ChangePhoneRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/phone");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangePhoneRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user.PhoneNumber = request.PhoneNumber;
|
||||
// TODO: check to see if identity resets the `phone confirmed` flag - @jonathan
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
await SendOkAsync(ct);
|
||||
else
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
72
backend/src/Web/Features/Users/Handlers/ChangePortrait.cs
Normal file
72
backend/src/Web/Features/Users/Handlers/ChangePortrait.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Hutopy.Web.Common.BlobStorage;
|
||||
using Hutopy.Web.Common.Security;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangePortraitRequest(
|
||||
IFormFile File);
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangePortraitResponse(
|
||||
string BlobUrl);
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class ChangePortraitRequestValidator : Validator<ChangePortraitRequest>
|
||||
{
|
||||
public ChangePortraitRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.File)
|
||||
.NotNull()
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangePortraitHandler(
|
||||
IdentityUserManager userManager,
|
||||
AzureBlobStorage blobStorage)
|
||||
: Endpoint<ChangePortraitRequest, ChangePortraitResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/users/portrait");
|
||||
Options(o => o.WithTags("Users"));
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangePortraitRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var blobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Users,
|
||||
$"{user.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
|
||||
request.File.OpenReadStream(),
|
||||
request.File.ContentType,
|
||||
ct);
|
||||
|
||||
user.PortraitUrl = blobUrl;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(
|
||||
new ChangePortraitResponse(blobUrl),
|
||||
ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
backend/src/Web/Features/Users/Handlers/GetCurrentUser.cs
Normal file
56
backend/src/Web/Features/Users/Handlers/GetCurrentUser.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Hutopy.Web.Features.Users.Handlers.Models;
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetCurrentUserQueryHandler(
|
||||
IdentityService identityService,
|
||||
MembershipDbContext membershipDbContext)
|
||||
: EndpointWithoutRequest<UserDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/users/profile");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userModel = await identityService.GetCurrentUserAsync();
|
||||
|
||||
if (userModel is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var roles = await identityService.GetCurrentUserRolesAsync();
|
||||
|
||||
var stripeId = await membershipDbContext
|
||||
.Creators
|
||||
.Where(c => c.Id == userModel.Id)
|
||||
.Select(c => c.StripeAccountId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
await SendOkAsync(
|
||||
new UserDto
|
||||
{
|
||||
Id = userModel.Id,
|
||||
Alias = userModel.Alias,
|
||||
PortraitUrl = userModel.PortraitUrl,
|
||||
Firstname = userModel.Firstname,
|
||||
Lastname = userModel.Lastname,
|
||||
Username = userModel.Username,
|
||||
PhoneNumber = userModel.PhoneNumber,
|
||||
Email = userModel.Email,
|
||||
BirthDate = userModel.BirthDate,
|
||||
Address = userModel.Address,
|
||||
UserRoles = roles,
|
||||
StripeId = stripeId ?? string.Empty
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Hutopy.Web.Common.BlobStorage;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetCurrentUserPortraitHandler(
|
||||
IdentityService identityService,
|
||||
AzureBlobStorage blobStorage
|
||||
)
|
||||
: EndpointWithoutRequest<Stream>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/users/portrait");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await identityService.GetCurrentUserAsync();
|
||||
|
||||
var stream = await blobStorage.DownloadFileAsync(
|
||||
ContainerNames.Users,
|
||||
$"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
|
||||
cancellationToken);
|
||||
|
||||
await SendOkAsync(stream, cancellationToken);
|
||||
}
|
||||
}
|
||||
139
backend/src/Web/Features/Users/Handlers/LoginWithGoogle.cs
Normal file
139
backend/src/Web/Features/Users/Handlers/LoginWithGoogle.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
|
||||
class GoogleToken
|
||||
{
|
||||
[JsonPropertyName("access_token")] public required string AccessToken { get; init; }
|
||||
[JsonPropertyName("token_type")] public required string TokenType { get; init; }
|
||||
[JsonPropertyName("expires_in")] public required int ExpiresIn { get; init; }
|
||||
[JsonPropertyName("scope")] public required string Scope { get; init; }
|
||||
[JsonPropertyName("authuser")] public required string AuthUser { get; init; }
|
||||
[JsonPropertyName("prompt")] public required string Prompt { get; init; }
|
||||
}
|
||||
|
||||
public class GoogleUserInfo
|
||||
{
|
||||
[JsonPropertyName("id")] public required string Id { get; init; }
|
||||
[JsonPropertyName("email")] public required string Email { get; init; }
|
||||
[JsonPropertyName("verified_email")] public required bool VerifiedEmail { get; init; }
|
||||
[JsonPropertyName("name")] public required string Name { get; init; }
|
||||
[JsonPropertyName("given_name")] public required string GivenName { get; init; }
|
||||
[JsonPropertyName("family_name")] public string FamilyName { get; init; } = string.Empty;
|
||||
[JsonPropertyName("picture")] public required string Picture { get; init; }
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public record LoginWithGoogleRequest(
|
||||
string Token);
|
||||
|
||||
[PublicAPI]
|
||||
public record LoginWithGoogleResponse(
|
||||
string AccessToken,
|
||||
string RefreshToken);
|
||||
|
||||
[PublicAPI]
|
||||
public class LoginWithGoogleHandler(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IdentityUserManager userManager,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
IOptionsSnapshot<JwtOptions> jwtOptions)
|
||||
: Endpoint<LoginWithGoogleRequest, LoginWithGoogleResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
AllowAnonymous();
|
||||
Post("/api/users/login-with-google");
|
||||
Options(o => o.WithTags("Users"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
LoginWithGoogleRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var googleToken = JsonSerializer.Deserialize<GoogleToken>(request.Token)!;
|
||||
|
||||
// Verify the token with Google
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
using var response = await httpClient.GetAsync(
|
||||
$"https://www.googleapis.com/oauth2/v1/userinfo?access_token={googleToken.AccessToken}",
|
||||
ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await SendStringAsync(
|
||||
"The token is not valid",
|
||||
400,
|
||||
cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the user info (email, name, etc.).
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
var userInfo = JsonSerializer.Deserialize<GoogleUserInfo>(content);
|
||||
if (userInfo is null
|
||||
|| !userInfo.VerifiedEmail
|
||||
|| string.IsNullOrEmpty(userInfo.Email))
|
||||
{
|
||||
await SendStringAsync(
|
||||
"The token does not contain an email",
|
||||
400,
|
||||
cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user exists or create a new one
|
||||
var user = await userManager.FindByEmailAsync(userInfo.Email);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
var generatedPassword = PasswordGenerator.GeneratePassword(10, 12);
|
||||
var generatedUser = new IdentityUser
|
||||
{
|
||||
UserName = userInfo.Email,
|
||||
Email = userInfo.Email,
|
||||
Firstname = userInfo.GivenName,
|
||||
Lastname = userInfo.FamilyName,
|
||||
Alias = userInfo.Name,
|
||||
PortraitUrl = userInfo.Picture,
|
||||
GoogleId = userInfo.Id,
|
||||
};
|
||||
|
||||
var result = await userManager.CreateAsync(
|
||||
generatedUser,
|
||||
generatedPassword);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await SendStringAsync(
|
||||
result.Errors.First().Description,
|
||||
400,
|
||||
cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
user = generatedUser;
|
||||
}
|
||||
|
||||
await signInManager.SignInAsync(user, isPersistent: false);
|
||||
|
||||
var accessToken = JwtTokenHelper.GenerateJwtToken(
|
||||
expiresIn: jwtOptions.Value.Lifetime,
|
||||
issuer: jwtOptions.Value.Issuer,
|
||||
audience: jwtOptions.Value.Audience,
|
||||
key: jwtOptions.Value.Key,
|
||||
userId: user.Id.ToString(),
|
||||
email: user.Email,
|
||||
alias: user.Alias,
|
||||
firstname: user.Firstname,
|
||||
lastname: user.Lastname,
|
||||
portraitUrl: user.PortraitUrl);
|
||||
|
||||
await SendOkAsync(
|
||||
new LoginWithGoogleResponse(accessToken, string.Empty),
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
17
backend/src/Web/Features/Users/Handlers/Models/UserDto.cs
Normal file
17
backend/src/Web/Features/Users/Handlers/Models/UserDto.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Hutopy.Web.Features.Users.Handlers.Models;
|
||||
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public IList<string> UserRoles { get; init; } = [];
|
||||
public string Username { get; init; } = null!;
|
||||
public string? Alias { get; init; }
|
||||
public string? PortraitUrl { get; init; }
|
||||
public string? Firstname { get; init; }
|
||||
public string? Lastname { get; init; }
|
||||
public string? Email { get; init; }
|
||||
public string? PhoneNumber { get; init; }
|
||||
public DateTime? BirthDate { get; init; }
|
||||
public string? Address { get; init; }
|
||||
public string? StripeId { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user