many fixes and improvements - rework for modules/ and common/

feat(emailer): add Postmark and Resend providers
This commit is contained in:
2025-06-06 12:21:43 -04:00
parent 31ba18fa8d
commit 25b94d3e02
313 changed files with 6586 additions and 18260 deletions

View File

@@ -0,0 +1,145 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Configuration;
using Hutopy.Modules.Identity.Data;
using Microsoft.Extensions.Options;
namespace Hutopy.Modules.Identity.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,
UserManager userManager,
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.Next();
var refreshToken = RefreshTokenGenerator.Next();
var generatedUser = new User
{
UserName = userInfo.Email,
Email = userInfo.Email,
Firstname = userInfo.GivenName,
Lastname = userInfo.FamilyName,
Alias = userInfo.Name,
PortraitUrl = userInfo.Picture,
GoogleId = userInfo.Id,
RefreshToken = refreshToken,
RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime)
};
var result = await userManager.CreateAsync(
generatedUser,
generatedPassword);
if (!result.Succeeded)
{
await SendStringAsync(
result.Errors.First().Description,
400,
cancellation: ct);
return;
}
user = generatedUser;
}
// Generate new refresh token
user.RefreshToken = RefreshTokenGenerator.Next();
user.RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime);
await userManager.UpdateAsync(user);
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 ?? string.Empty,
alias: user.Alias,
firstname: user.Firstname ?? string.Empty,
lastname: user.Lastname ?? string.Empty,
portraitUrl: user.PortraitUrl);
await SendOkAsync(
new LoginWithGoogleResponse(accessToken, user.RefreshToken),
cancellation: ct);
}
}