using FictionArchive.Service.Shared.Contracts.Events; using FictionArchive.Service.UserService.Contracts; using FictionArchive.Service.UserService.Models.Database; using FictionArchive.Service.UserService.Services.AuthenticationClient; using MassTransit; using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.UserService.Services; public class UserManagementService { private readonly ILogger _logger; private readonly UserServiceDbContext _dbContext; private readonly IAuthenticationServiceClient _authClient; private readonly IPublishEndpoint _publishEndpoint; public UserManagementService( UserServiceDbContext dbContext, ILogger logger, IAuthenticationServiceClient authClient, IPublishEndpoint publishEndpoint) { _dbContext = dbContext; _logger = logger; _authClient = authClient; _publishEndpoint = publishEndpoint; } /// /// Invites a new user by creating them in Authentik, saving to the database, and sending a recovery email. /// /// The user sending the invite /// Email address of the invitee /// Username for the invitee /// The created user, or null if the invite failed public async Task InviteUserAsync(User inviter, string email, string username) { // Check if inviter has available invites if (inviter.AvailableInvites <= 0) { _logger.LogWarning("User {InviterId} has no available invites", inviter.Id); return null; } // Check if email is already in use var existingUser = await _dbContext.Users .AsQueryable() .FirstOrDefaultAsync(u => u.Email == email); if (existingUser != null) { _logger.LogWarning("Email {Email} is already in use", email); return null; } // Check if username is already in use var existingUsername = await _dbContext.Users .AsQueryable() .FirstOrDefaultAsync(u => u.Username == username); if (existingUsername != null) { _logger.LogWarning("Username {Username} is already in use", username); return null; } // Create user in Authentik var authentikUser = await _authClient.CreateUserAsync(username, email, username); if (authentikUser == null) { _logger.LogError("Failed to create user {Username} in Authentik", username); return null; } // Send recovery email via Authentik var emailSent = await _authClient.SendRecoveryEmailAsync(authentikUser.Pk); if (!emailSent) { _logger.LogWarning( "User {Username} was created in Authentik but recovery email failed to send. Authentik pk: {Pk}", username, authentikUser.Pk); // Continue anyway - the user is created, admin can resend email manually } // Create user in local database var newUser = new User { Username = username, Email = email, OAuthProviderId = authentikUser.Pk.ToString(), Disabled = false, AvailableInvites = 0, InviterId = inviter.Id }; _dbContext.Users.Add(newUser); // Decrement inviter's available invites inviter.AvailableInvites--; await _dbContext.SaveChangesAsync(); await _publishEndpoint.Publish(new UserInvited( InvitedUserId: newUser.Id.ToString(), InvitedUsername: newUser.Username, InvitedEmail: newUser.Email, InvitedOAuthProviderId: newUser.OAuthProviderId, InviterId: inviter.Id.ToString(), InviterUsername: inviter.Username, InviterOAuthProviderId: inviter.OAuthProviderId)); _logger.LogInformation( "User {Username} was successfully invited by {InviterId}. New user id: {NewUserId}", username, inviter.Id, newUser.Id); return newUser; } /// /// Gets a user by their OAuth provider ID (Authentik UID). /// public async Task GetUserByOAuthProviderIdAsync(string oAuthProviderId) { return await _dbContext.Users .AsQueryable() .FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId); } /// /// Gets all users as a queryable for GraphQL. /// public IQueryable GetUsers() { return _dbContext.Users.AsQueryable(); } /// /// Gets all users invited by a specific user. /// /// The ID of the user who sent the invites /// List of users invited by the specified user public async Task> GetInvitedByUserAsync(Guid inviterId) { return await _dbContext.Users .AsQueryable() .Where(u => u.InviterId == inviterId) .OrderByDescending(u => u.CreatedTime) .ToListAsync(); } }