using JWT.Algorithms; using JWT.Builder; using Moonlight.App.Database.Entities; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Services.LogServices; using Moonlight.App.Services.Sessions; namespace Moonlight.App.Services; public class UserService { private readonly UserRepository UserRepository; private readonly TotpService TotpService; private readonly SecurityLogService SecurityLogService; private readonly AuditLogService AuditLogService; private readonly MailService MailService; private readonly IdentityService IdentityService; private readonly string JwtSecret; public UserService( UserRepository userRepository, TotpService totpService, ConfigService configService, SecurityLogService securityLogService, AuditLogService auditLogService, MailService mailService, IdentityService identityService) { UserRepository = userRepository; TotpService = totpService; SecurityLogService = securityLogService; AuditLogService = auditLogService; MailService = mailService; IdentityService = identityService; JwtSecret = configService .GetSection("Moonlight") .GetSection("Security") .GetValue("Token"); } public async Task Register(string email, string password, string firstname, string lastname) { // Check if the email is already taken var emailTaken = UserRepository.Get().FirstOrDefault(x => x.Email == email) != null; if (emailTaken) { throw new DisplayException("The email is already in use"); } //TODO: Validation // Add user var user = UserRepository.Add(new() { Address = "", Admin = false, City = "", Country = "", Email = email, Password = BCrypt.Net.BCrypt.HashPassword(password), FirstName = firstname, LastName = lastname, State = "", Status = UserStatus.Unverified, CreatedAt = DateTime.UtcNow, DiscordId = -1, TotpEnabled = false, TotpSecret = "", UpdatedAt = DateTime.UtcNow, TokenValidTime = DateTime.Now.AddDays(-5) }); await MailService.SendMail(user!, "register", values => {}); await AuditLogService.Log(AuditLogType.Register, user.Email); return await GenerateToken(user); } public async Task CheckTotp(string email, string password) { var user = UserRepository.Get() .FirstOrDefault( x => x.Email == email ); if (user == null) { await SecurityLogService.Log(SecurityLogType.LoginFail, new[] { email, password }); throw new DisplayException("Email and password combination not found"); } if (BCrypt.Net.BCrypt.Verify(password, user.Password)) { return user.TotpEnabled; } await SecurityLogService.Log(SecurityLogType.LoginFail, new[] { email, password }); throw new DisplayException("Email and password combination not found");; } public async Task Login(string email, string password, string totpCode = "") { // First password check and check if totp is enabled var needTotp = await CheckTotp(email, password); var user = UserRepository.Get() .FirstOrDefault( x => x.Email.Equals( email ) ); if (needTotp) { if (string.IsNullOrEmpty(totpCode)) throw new DisplayException("2FA code must be provided"); var totpCodeValid = await TotpService.Verify(user!.TotpSecret, totpCode); if (totpCodeValid) { await AuditLogService.Log(AuditLogType.Login, email); return await GenerateToken(user, true); } else { await SecurityLogService.Log(SecurityLogType.LoginFail, new[] { email, password }); throw new DisplayException("2FA code invalid"); } } else { await AuditLogService.Log(AuditLogType.Login, email); return await GenerateToken(user!, true); } } public async Task ChangePassword(User user, string password, bool isSystemAction = false) { user.Password = BCrypt.Net.BCrypt.HashPassword(password); user.TokenValidTime = DateTime.Now; UserRepository.Update(user); if (isSystemAction) { await AuditLogService.LogSystem(AuditLogType.ChangePassword, user.Email); } else { await MailService.SendMail(user!, "passwordChange", values => { values.Add("Ip", IdentityService.GetIp()); values.Add("Device", IdentityService.GetDevice()); values.Add("Location", "In your walls"); }); await AuditLogService.Log(AuditLogType.ChangePassword, user.Email); } } public async Task SftpLogin(int id, string password) { var user = UserRepository.Get().FirstOrDefault(x => x.Id == id); if (user == null) { await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, id); throw new Exception("Invalid username"); } if (BCrypt.Net.BCrypt.Verify(password, user.Password)) { await AuditLogService.LogSystem(AuditLogType.Login, user.Email); return user; } await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, new[] { id.ToString(), password }); throw new Exception("Invalid userid or password"); } public async Task GenerateToken(User user, bool sendMail = false) { await MailService.SendMail(user!, "login", values => { values.Add("Ip", IdentityService.GetIp()); values.Add("Device", IdentityService.GetDevice()); values.Add("Location", "In your walls"); }); var token = JwtBuilder.Create() .WithAlgorithm(new HMACSHA256Algorithm()) .WithSecret(JwtSecret) .AddClaim("exp", DateTimeOffset.UtcNow.AddDays(10).ToUnixTimeSeconds()) .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds()) .AddClaim("userid", user.Id) .Encode(); return token; } public async Task ResetPassword(string email) { email = email.ToLower(); var user = UserRepository .Get() .FirstOrDefault(x => x.Email == email); if (user == null) throw new DisplayException("A user with this email can not be found"); var newPassword = StringHelper.GenerateString(16); await ChangePassword(user, newPassword, true); await AuditLogService.Log(AuditLogType.PasswordReset); await MailService.SendMail(user, "passwordReset", values => { values.Add("Ip", IdentityService.GetIp()); values.Add("Device", IdentityService.GetDevice()); values.Add("Location", "In your walls"); values.Add("Password", newPassword); }); } }