=== Summary: 675 files | 7 generated | 484 fresh | 4605 untracked | 4268 adopted | 355 marked | 466 validated-ok | 2+0 stale (sig+body) | 0 skipped | 0 failed | elapsed 11:08:11.442 (40091.44s) ===

This commit is contained in:
julian
2026-06-28 02:50:05 -07:00
parent a19fb90902
commit 586f02a2ca
654 changed files with 5260 additions and 0 deletions
+34
View File
@@ -31,6 +31,7 @@ namespace adas_core.Authentication;
/// <remarks>
/// This class is the default service component responsible for handling user domain logic and delegating persistence or infrastructure concerns as required by the interface.
/// </remarks>
/// <!-- aidoc:v1 sig=ea2e8b0 -->
public class UserService : IUserService
{
private readonly ILocalAuditService _auditService;
@@ -109,6 +110,7 @@ public class UserService : IUserService
/// <returns>A <see cref="Task{TResult}"/> containing the generated <see cref="TokenResult"/> for the authenticated user.</returns>
/// <exception cref="UnauthorizedException">Thrown when no user matches the provided credentials, or when the user is not in the whitelist or valid groups.</exception>
/// <exception cref="ForbbidenException">Thrown when the user account is disabled or currently locked.</exception>
/// <!-- aidoc:v1 sig=1e8af8c body=0dd331a -->
public async Task<TokenResult> Login(string username, string password)
{
var user = await GetUser(username, password);
@@ -134,6 +136,7 @@ public class UserService : IUserService
/// <param name="tokenType">The type of the token being validated.</param>
/// <param name="validatedToken">When the method returns, contains the validated <see cref="SecurityToken"/> if validation succeeds.</param>
/// <exception cref="TokenException">Thrown when the token is invalid, is not signed with the HmacSha256 algorithm, or any other validation error occurs.</exception>
/// <!-- aidoc:v1 sig=cd90202 body=4ddd4b1 -->
public void ValidateToken(string token, string tokenType, out SecurityToken validatedToken)
{
try
@@ -176,6 +179,8 @@ public class UserService : IUserService
/// <param name="jwtToken">The <see cref="JwtSecurityToken"/> containing the user identity claims, or <see langword="null"/> to indicate no token was supplied.</param>
/// <returns>A <see cref="Task{User}"/> that resolves to the matching <see cref="User"/> with its password cleared and authorization loaded if required, or <see langword="null"/> when the token is missing or no user matches the extracted username.</returns>
/// <!-- aidoc:v1 sig=29f6fec body=a7970c3 -->
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
/// "The <see cref=\"Task{User}\"/> reference indicates a non-nullable Task<User>, but the method actually returns Task<User?> with a nullable User." -->
public async Task<User?> GetUserByToken(JwtSecurityToken? jwtToken)
{
User? user = null;
@@ -204,6 +209,8 @@ public class UserService : IUserService
/// <exception cref="LoginServicesException">Thrown when the CAS login method is not available, or when an unexpected error occurs during authentication.</exception>
/// <exception cref="UnauthorizedAccessException">Thrown when the authenticated user is not in the users whitelist and does not belong to any valid group.</exception>
/// <exception cref="BusinessException">Rethrown when a business-level error occurs during the login process.</exception>
/// <!-- aidoc-review:v1 severity=high kind=stale_summary
/// "Summary states the method 'authenticates a user using a CAS ticket', but the ticket parameter is never used in the active code path - authentication is performed via service.Login(_httpContextAccessor.HttpContext). The <returns> clause also references ticket validity ('if the ticket is valid'), which is not checked." -->
public async Task<User?> GetUserByCasTicket(string serviceUrl, string ticket)
{
var service = _availableLoginServices.FirstOrDefault(s => s.Method == UserEnum.LoginMethod.Cas);
@@ -250,6 +257,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="refreshToken">The refresh token used to generate a new JWT.</param>
/// <returns>A TokenResult containing the new JWT, or an empty result if the user associated with the token is not found.</returns>
/// <!-- aidoc:v1 sig=1902a2f body=bb5bba0 -->
public async Task<TokenResult> RefreshToken(string refreshToken)
{
ValidateToken(refreshToken, IUserService.TokenTypeRefresh, out var validatedToken);
@@ -269,6 +277,7 @@ public class UserService : IUserService
/// <param name="password">The password to authenticate against.</param>
/// <returns>The authenticated <see cref="User"/> returned by the first successful login service.</returns>
/// <exception cref="LoginServicesNotFoundException">Thrown when no configured login service successfully authenticates the user.</exception>
/// <!-- aidoc:v1 sig=b90d52e body=0ab820c -->
public async Task<User> GetUser(string username, string password)
{
//BusinessException? loginException = null;
@@ -297,6 +306,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="id">The unique identifier of the user to retrieve.</param>
/// <returns>A <see cref="Task{User}"/> containing the user with its authorization populated, or <c>null</c> if the user cannot be found or an exception is thrown during retrieval.</returns>
/// <!-- aidoc:v1 sig=6af9d13 body=5ffaaf2 -->
public async Task<User?> GetUserById(ObjectId id)
{
try
@@ -316,6 +326,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="name">The username to look up.</param>
/// <returns>The matching <see cref="User"/> with its <c>Authorization</c> property populated from the authority service, or <c>null</c> if no user is found.</returns>
/// <!-- aidoc:v1 sig=f6a5557 body=812fa43 -->
public async Task<User?> GetUserByUserName(string name)
{
var u = await _userRepository.GetByUserName(name);
@@ -330,6 +341,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="name">The name of the user to look up.</param>
/// <returns>A <see cref="User"/> instance with its <c>Authorization</c> populated when found; otherwise, <c>null</c>.</returns>
/// <!-- aidoc:v1 sig=5fce94d body=3ae1c25 -->
public async Task<User?> GetUserByName(string name)
{
try
@@ -349,6 +361,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="userEntryLdap">The user information sourced from LDAP to be created in the system.</param>
/// <returns>The created user retrieved by username, or null if the user cannot be found after insertion.</returns>
/// <!-- aidoc:v1 sig=191573e body=81f390b -->
public async Task<User?> CreateUser(User userEntryLdap)
{
CheckIfUserNameExists(userEntryLdap.UserName);
@@ -366,6 +379,7 @@ public class UserService : IUserService
/// <returns>The created user retrieved by username after insertion.</returns>
/// <exception cref="UnprocessableEntityException">Thrown when a local user's password is not considered strong.</exception>
/// <exception cref="NotFoundException">Thrown when the user cannot be retrieved by username after insertion.</exception>
/// <!-- aidoc:v1 sig=82465f8 body=d4f7721 -->
public async Task<User?> CreateNewUserByRequest(User user)
{
CheckIfEmailExists(user.Email);
@@ -391,6 +405,7 @@ public class UserService : IUserService
/// <param name="createUserWithAuthDto">The data transfer object containing the user details and the list of authorizations to associate with the new user.</param>
/// <returns>The newly created user, or <c>null</c> if creation fails (in which case an exception is thrown instead).</returns>
/// <exception cref="NotFoundException">Thrown when the user cannot be created by the underlying request, indicating a missing resource.</exception>
/// <!-- aidoc:v1 sig=b28ec32 body=6f231bc -->
public async Task<User?> CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto)
{
var user = createUserWithAuthDto.User;
@@ -413,6 +428,7 @@ public class UserService : IUserService
/// <param name="updatePass">Indicates whether the user's password should be updated and re-hashed.</param>
/// <returns>The updated user returned by the repository, or <c>null</c> if the user could not be found or updated.</returns>
/// <exception cref="UnprocessableEntityException">Thrown when <paramref name="updatePass"/> is <c>true</c> and the provided password does not meet the strong password policy.</exception>
/// <!-- aidoc:v1 sig=4a0b955 body=0eb37fa -->
public async Task<User?> UpdateUsersByRequest(User user, bool updatePass)
{
var oldUser = await _userRepository.GetByUserName(user.UserName);
@@ -443,6 +459,8 @@ public class UserService : IUserService
/// <param name="elementUpdated">The element that has been updated and should be transmitted to the subscribers.</param>
/// <param name="userName">The user name used to filter the subscribers that will receive the broadcast.</param>
/// <param name="operation">The type of operation (e.g., create, update, delete) associated with the broadcast.</param>
/// <!-- aidoc-review:v1 severity=medium kind=wrong_summary
/// "Summary says the broadcast notifies subscribers of 'an update operation', but the code passes through any OperationType (which the param description itself lists as create/update/delete), not just update operations." -->
private async Task SendBroadcastUser(object elementUpdated, string userName, OperationType operation)
{
var subscribers = _subscribersService.GetSubscribers().Where(s =>
@@ -459,6 +477,7 @@ public class UserService : IUserService
/// <param name="elementUpdated">The updated element to send as the notification payload to non-admin subscribers.</param>
/// <param name="userName">The username whose subscribers will receive the broadcast.</param>
/// <param name="operation">The operation type that describes the nature of the update.</param>
/// <!-- aidoc:v1 sig=4f775b6 body=1033854 -->
private async Task SendBroadcastPermissions(object elementUpdated, string userName, OperationType operation)
{
var subscribers = _subscribersService.GetSubscribers().Where(s =>
@@ -491,6 +510,7 @@ public class UserService : IUserService
/// <param name="updatePass">Flag indicating whether the user's password should be updated as part of the operation.</param>
/// <returns>The updated <see cref="User"/>.</returns>
/// <exception cref="ConflictException">Thrown when the underlying user update returns no result.</exception>
/// <!-- aidoc:v1 sig=99d7acb body=7cdfa30 -->
public async Task<User?> UpdateUserWithAuthorities(UpdateUserWithAuthDto updateUserWithAuthDto, bool updatePass)
{
var u = updateUserWithAuthDto.User;
@@ -525,6 +545,7 @@ public class UserService : IUserService
/// <exception cref="BadRequestException">Thrown when <paramref name="newPassword"/> is equal to <paramref name="oldPassword"/>.</exception>
/// <exception cref="NotFoundException">Thrown when no user exists for the supplied <paramref name="id"/>.</exception>
/// <exception cref="UnprocessableEntityException">Thrown when <paramref name="oldPassword"/> does not match the user's current password, or when <paramref name="newPassword"/> does not satisfy the strength policy.</exception>
/// <!-- aidoc:v1 sig=0243044 body=c52695d -->
public async Task<bool> UpdateUserPassword(ObjectId id, string oldPassword, string newPassword)
{
if (oldPassword == newPassword)
@@ -561,6 +582,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="id">The identifier of the user to delete.</param>
/// <returns>A task that resolves to <c>true</c> when the user is successfully deleted; otherwise, <c>false</c> if an error is encountered.</returns>
/// <!-- aidoc:v1 sig=72679b2 body=c3e6d4f -->
public async Task<bool> DeleteUser(ObjectId id)
{
try
@@ -582,6 +604,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to skip, limit, and shape the result set.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="PaginationResponse{User}"/> with the requested page of users and pagination details.</returns>
/// <!-- aidoc:v1 sig=0367293 body=27c2222 -->
public async Task<PaginationResponse<User>> GetPaginatedUsers(PaginationFilter filter)
{
var result = _userRepository.GetPaginatedUsers(filter);
@@ -603,6 +626,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="user">The user whose identity, email, IP address, and authorization claims will be embedded in the generated tokens.</param>
/// <returns>A <see cref="TokenResult"/> containing the serialized access and refresh tokens, their respective expiration times, and the associated user.</returns>
/// <!-- aidoc:v1 sig=67521e7 body=d87cd16 -->
public async Task<TokenResult> GenerateJwt(User user)
{
var claims = new List<Claim>
@@ -650,6 +674,7 @@ public class UserService : IUserService
/// <returns>A task that resolves to the list of <see cref="User"/> objects returned by the first successful login service.</returns>
/// <exception cref="LoginServicesException">Thrown when a non-business exception occurs while retrieving users from a service.</exception>
/// <exception cref="LoginServicesNotFoundException">Thrown when no login service matches the valid login methods or all attempts fail without a captured business exception.</exception>
/// <!-- aidoc:v1 sig=b9380e5 body=b90883c -->
public async Task<List<User>> GetAll()
{
BusinessException? loginException = null;
@@ -688,6 +713,7 @@ public class UserService : IUserService
/// <param name="token">The access token to validate and use for user authentication.</param>
/// <returns>A <see cref="TokenResult"/> containing the new access token, refresh token, their expiry times, and the authenticated user.</returns>
/// <exception cref="NotFoundException">Thrown when no user can be resolved from the provided token.</exception>
/// <!-- aidoc:v1 sig=4a410ea body=f486923 -->
public async Task<TokenResult> LoginWithGivenAccessToken(string token)
{
ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken);
@@ -742,6 +768,7 @@ public class UserService : IUserService
/// <param name="token">The access token used to identify and authenticate the user.</param>
/// <returns>A <see cref="Task{TokenResult}"/> containing the generated JWT for the authenticated user.</returns>
/// <exception cref="NotFoundException">Thrown when no user is found that matches the validated access token.</exception>
/// <!-- aidoc:v1 sig=58a9e83 body=98c26eb -->
public async Task<TokenResult> LoginWithAccessToken(string token)
{
ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken);
@@ -759,6 +786,7 @@ public class UserService : IUserService
/// <param name="auth">The authorization used to create the new user authority.</param>
/// <returns>The newly created <see cref="Authorization"/> instance.</returns>
/// <exception cref="ConflictException">Thrown when the authority service fails to create the user authority (returns null).</exception>
/// <!-- aidoc:v1 sig=b73cf48 body=71c9c28 -->
public async Task<Authorization> CreateNewAuthority(Authorization auth)
{
var newUserAuthority = await _authorityService.CreateNewUserAuthority(auth) ??
@@ -773,6 +801,7 @@ public class UserService : IUserService
/// <returns>A task that resolves to <c>true</c> when the authority is successfully deleted.</returns>
/// <exception cref="BadRequestException">Thrown when the provided <paramref name="id"/> is not a valid ObjectId format.</exception>
/// <exception cref="ConflictException">Thrown when the underlying delete operation fails.</exception>
/// <!-- aidoc:v1 sig=eaa5d92 body=8a73373 -->
public async Task<bool> DeleteAuthority(string id)
{
if (!ObjectId.TryParse(id, out var userAuthorityIdParsed))
@@ -788,6 +817,7 @@ public class UserService : IUserService
/// <param name="authorization">The authorization details used to update the user's authority.</param>
/// <returns>A task that resolves to <c>true</c> when the authority is successfully updated.</returns>
/// <exception cref="ConflictException">Thrown when the authority service returns a null response, indicating the update failed.</exception>
/// <!-- aidoc:v1 sig=544e59f body=c079022 -->
public async Task<bool> UpdateAuthority(Authorization authorization)
{
_ = await _authorityService.EditUserAuthority(authorization) ??
@@ -801,6 +831,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="authorities">The list of authorizations to convert into claims.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of claims built from the supplied authorities.</returns>
/// <!-- aidoc:v1 sig=527b34e body=d91d2bf -->
public async Task<List<Claim>> ClaimsFromAuthorities(List<Authorization> authorities)
{
var claims = new List<Claim>();
@@ -828,6 +859,7 @@ public class UserService : IUserService
/// </summary>
/// <param name="auth">The authorization source whose role and display identifier are used to build the claim value.</param>
/// <param name="claims">The list of claims to which the new role claim is appended when not already present.</param>
/// <!-- aidoc:v1 sig=85111ea body=f681ceb -->
private static void AddClaim(Authorization auth, List<Claim> claims)
{
var newClaim = new Claim(ClaimTypes.Role,
@@ -844,6 +876,7 @@ public class UserService : IUserService
/// <param name="userName">The username to validate against existing users.</param>
/// <param name="userId">Optional identifier of the user being updated; when provided, that user is excluded from the duplicate check.</param>
/// <exception cref="BadRequestException">Thrown when another user with the same <paramref name="userName"/> already exists.</exception>
/// <!-- aidoc:v1 sig=cd63db7 body=c9563e3 -->
private void CheckIfUserNameExists(string userName, ObjectId? userId = null)
{
var users = GetAll().Result;
@@ -860,6 +893,7 @@ public class UserService : IUserService
/// <param name="email">The email address to verify for uniqueness in the user collection.</param>
/// <param name="userId">The optional ID of the current user; when set, it is excluded from the duplicate email check.</param>
/// <exception cref="BadRequestException">Thrown when the email is already registered to a different user.</exception>
/// <!-- aidoc:v1 sig=074e36e body=4e4b89e -->
private void CheckIfEmailExists(string email, ObjectId? userId = null)
{
var users = GetAll().Result;