using FictionArchive.Service.Shared.Services.EventBus; using FictionArchive.Service.UserService.Models.Database; using FictionArchive.Service.UserService.Models.IntegrationEvents; using FictionArchive.Service.UserService.Services.AuthenticationClient; 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 IEventBus _eventBus; public UserManagementService( UserServiceDbContext dbContext, ILogger logger, IAuthenticationServiceClient authClient, IEventBus eventBus) { _dbContext = dbContext; _logger = logger; _authClient = authClient; _eventBus = eventBus; } /// /// 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 _eventBus.Publish(new UserInvitedEvent { InvitedUserId = newUser.Id, InvitedUsername = newUser.Username, InvitedEmail = newUser.Email, InvitedOAuthProviderId = newUser.OAuthProviderId, InviterId = inviter.Id, 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(); } }