Files
2026-02-16 13:43:04 +05:00

207 lines
9.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using AspNetCore.Identity.Mongo.Model;
using IdentityServer4;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Streetwriters.Common;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
using Streetwriters.Common.Services;
using Streetwriters.Identity.Enums;
using Streetwriters.Identity.Extensions;
using Streetwriters.Identity.Interfaces;
using Streetwriters.Identity.Models;
namespace Streetwriters.Identity.Services
{
public class UserAccountService(UserManager<User> userManager, IMFAService mfaService, IPersistedGrantStore persistedGrantStore, RoleManager<MongoRole> roleManager, EmailAddressValidator emailValidator, ITemplatedEmailSender emailSender, ITokenGenerationService tokenGenerationService, ILogger<UserAccountService> logger) : IUserAccountService
{
public async Task<UserModel?> GetUserAsync(string clientId, string userId)
{
var user = await userManager.FindByIdAsync(userId);
if (user == null || !await UserService.IsUserValidAsync(userManager, user, clientId))
return null;
var claims = await userManager.GetClaimsAsync(user);
var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == $"{clientId}:marketing_consent");
if (await userManager.IsEmailConfirmedAsync(user) && !await userManager.GetTwoFactorEnabledAsync(user))
{
await mfaService.EnableMFAAsync(user, MFAMethods.Email);
user = await userManager.FindByIdAsync(userId);
ArgumentNullException.ThrowIfNull(user);
}
ArgumentNullException.ThrowIfNull(user.Email);
return new UserModel
{
UserId = user.Id.ToString(),
Email = user.Email,
IsEmailConfirmed = user.EmailConfirmed,
MarketingConsent = marketingConsentClaim == null,
MFA = new MFAConfig
{
IsEnabled = user.TwoFactorEnabled,
PrimaryMethod = mfaService.GetPrimaryMethod(user),
SecondaryMethod = mfaService.GetSecondaryMethod(user),
RemainingValidCodes = await mfaService.GetRemainingValidCodesAsync(user)
}
};
}
public async Task DeleteUserAsync(string clientId, string userId, string password)
{
var user = await userManager.FindByIdAsync(userId);
if (user == null || !await UserService.IsUserValidAsync(userManager, user, clientId)) return;
if (!await userManager.CheckPasswordAsync(user, password)) throw new Exception("Wrong password.");
await userManager.DeleteAsync(user);
}
public async Task<bool> ChangePasswordAsync(string userId, string oldPassword, string newPassword)
{
var user = await userManager.FindByIdAsync(userId) ?? throw new Exception("User not found.");
var result = await userManager.ChangePasswordAsync(user, oldPassword, newPassword);
return result.Succeeded;
}
public async Task<bool> ResetPasswordAsync(string userId, string newPassword)
{
var user = await userManager.FindByIdAsync(userId) ?? throw new Exception("User not found.");
var result = await userManager.RemovePasswordAsync(user);
if (!result.Succeeded) return false;
await mfaService.ResetMFAAsync(user);
result = await userManager.AddPasswordAsync(user, newPassword);
return result.Succeeded;
}
public async Task<bool> ClearSessionsAsync(string userId, string clientId, bool all, string jti, string? refreshToken)
{
var client = Clients.FindClientById(clientId) ?? throw new Exception("Invalid client_id.");
var user = await userManager.FindByIdAsync(userId) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(userManager, user, client.Id)) throw new Exception($"Unable to find user with ID '{user.Id}'.");
var grants = await persistedGrantStore.GetAllAsync(new PersistedGrantFilter
{
ClientId = client.Id,
SubjectId = user.Id.ToString()
});
string? refreshTokenKey = refreshToken != null ? GetHashedKey(refreshToken, IdentityServerConstants.PersistedGrantTypes.RefreshToken) : null;
List<string> removedKeys = [];
foreach (var grant in grants)
{
if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue;
await persistedGrantStore.RemoveAsync(grant.Key);
removedKeys.Add(grant.Key);
}
await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
// await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked.");
return true;
}
public async Task<SignupResponse> CreateUserAsync(string clientId, string email, string password, string? userAgent = null)
{
if (Constants.DISABLE_SIGNUPS)
return new SignupResponse
{
Errors = ["Creating new accounts is not allowed."]
};
try
{
var client = Clients.FindClientById(clientId);
if (client == null) return new SignupResponse
{
Errors = ["Invalid client id."]
};
if (await roleManager.FindByNameAsync(clientId) == null)
await roleManager.CreateAsync(new MongoRole(clientId));
// email addresses must be case-insensitive
email = email.ToLowerInvariant();
if (!await emailValidator.IsEmailAddressValidAsync(email))
return new SignupResponse
{
Errors = ["Invalid email address."]
};
var result = await userManager.CreateAsync(new User
{
Email = email,
EmailConfirmed = Constants.IS_SELF_HOSTED,
UserName = email,
}, password);
if (result.Succeeded)
{
var user = await userManager.FindByEmailAsync(email);
if (user == null) return SignupResponse.Error(["User not found after creation."]);
await userManager.AddToRoleAsync(user, client.Id);
if (Constants.IS_SELF_HOSTED)
{
await userManager.AddClaimAsync(user, new Claim(UserService.GetClaimKey(client.Id), "believer"));
}
else
{
if (userAgent != null) await userManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(userAgent)));
var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
if (!string.IsNullOrEmpty(user.Email) && callbackUrl != null)
{
await emailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
}
}
var response = await tokenGenerationService.CreateUserTokensAsync(user, client.Id, 3600);
if (response == null) return SignupResponse.Error(["Failed to generate access token."]);
return new SignupResponse
{
AccessToken = response.AccessToken,
AccessTokenLifetime = response.AccessTokenLifetime,
RefreshToken = response.RefreshToken,
Scope = response.Scope,
UserId = user.Id.ToString()
};
}
return SignupResponse.Error(result.Errors.ToErrors());
}
catch (System.Exception ex)
{
logger.LogError(ex, "Failed to create user account for email: {Email}", email);
return SignupResponse.Error(["Failed to create an account."]);
}
}
private static string PlatformFromUserAgent(string? userAgent)
{
if (string.IsNullOrEmpty(userAgent)) return "unknown";
return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
}
private static string GetHashedKey(string value, string grantType)
{
return (value + ":" + grantType).Sha256();
}
}
}