many fixes and improvements - rework for modules/ and common/
feat(emailer): add Postmark and Resend providers
This commit is contained in:
145
backend/Modules/Identity/Handlers/LoginWithGoogle.cs
Normal file
145
backend/Modules/Identity/Handlers/LoginWithGoogle.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user