using AutoMapper; using RecNet.Application.Common.Interfaces; using RecNet.Application.Common.Security; using RecNet.Application.Profiles.Avatar; using RecNet.Application.Profiles.Login; using RecNet.Application.Profiles.Settings; using RecNet.Domain.Common; using RecNet.Domain.GameVersions; using RecNet.Domain.Profiles; using ProfileEntity = RecNet.Domain.Profiles.Profile; using AvatarEntity = RecNet.Domain.Profiles.Avatar; namespace RecNet.Application.Profiles; public class ProfileService( IEnumerable validators, IGameVersionRepository gameVersionRepository, IProfileRepository profileRepository, IConfigService configService, NameGenerator nameGenerator, ITokenService tokenService, IMapper mapper) : IProfileService { private const string ShowPlatformPreferenceKey = "SHOW_PLATFORM_PREF"; public async Task GetProfileAsync(Guid profileId, CancellationToken ct = default) { var profile = await profileRepository.GetByIdAsync(profileId, ct); return profile is null ? null : await MapProfileAsync(profile, ct); } public async Task> GetProfilesAsync( List profileIds, CancellationToken ct = default) { var profiles = await profileRepository.GetByIdsAsync(profileIds, ct); var results = new List(profiles.Count); foreach (var profile in profiles) results.Add(await MapProfileAsync(profile, ct)); return results; } public async Task GetAvatarAsync(Guid profileId, CancellationToken ct = default) { var avatar = await profileRepository.GetAvatarByProfileIdAsync(profileId, ct); return avatar is null ? null : mapper.Map(avatar); } public async Task UpdateAvatarAsync( Guid profileId, UpdateAvatarCommand command, CancellationToken ct = default) { var profile = await profileRepository.GetByIdAsync(profileId, ct); if (profile is null) return null; profile.SetAvatar( AvatarEntity.Create( command.OutfitSelections, command.SkinColor, command.HairColor ) ); await profileRepository.SaveChangesAsync(ct); return mapper.Map(profile.Avatar); } public async Task> GetPlayerSettingsAsync( Guid profileId, CancellationToken ct = default) { var settings = await profileRepository.GetSettingsByProfileIdAsync(profileId, ct); return mapper.Map>(settings); } public async Task UpdatePlayerSettingAsync( Guid profileId, UpdatePlayerSettingCommand command, CancellationToken ct = default) { var profile = await profileRepository.GetByIdWithSettingsAsync(profileId, ct); if (profile is null) return false; profile.SetSetting(command.Key, command.Value); await profileRepository.SaveChangesAsync(ct); return true; } public async Task IncrementTokenVersion(Guid profileId, CancellationToken ct = default) { var profile = await profileRepository.GetByIdWithSettingsAsync(profileId, ct); if (profile is null) return; profile.IncrementTokenVersion(); await profileRepository.SaveChangesAsync(ct); } public async Task LoginAsync( LoginProfileCommand command, CancellationToken ct = default) { // 1. Validate Game Version if (string.IsNullOrWhiteSpace(command.AppVersion)) return LoginProfileResult.Fail("App version is required."); var ignoreGameVersion = await configService.GetAsync("Profiles:IgnoreGameVersion", true, ct); var isValidGameVersion = await gameVersionRepository.IsValidAsync(command.AppVersion, ct); if (!isValidGameVersion && !ignoreGameVersion) return LoginProfileResult.Fail("A new update is available! Go to https://recroom.baby/ to download the latest update."); // 2. Validate Platform Authentication var validator = validators.FirstOrDefault(x => x.PlatformType == command.PlatformType); if (validator is null) return LoginProfileResult.Fail("Invalid platform type"); var ignoreAuthValidation = await configService.GetAsync("Profiles:IgnoreAuthValidation", true, ct); var auth = await validator.ValidateAsync( command.PlatformAuthentication, command.PlatformId, ct); if (!auth.Succeeded && !ignoreAuthValidation) return LoginProfileResult.Fail($"Platform Auth Failed: {auth.Message}"); // 3. Get or Create Profile var profile = await profileRepository.GetByPlatform( command.PlatformType, command.PlatformId, ct); if (profile is null) { var name = await nameGenerator.GenerateAsync(ct); profile = ProfileEntity.Create( name, command.PlatformType, command.PlatformId); if (command.PlatformType == PlatformType.Meta) profile.SetMetaAuthenticationSecret(Hashing.ComputeSha256(command.PlatformAuthentication)); await profileRepository.AddAsync(profile, ct); } if (command.PlatformType == PlatformType.Meta && !Hashing.VerifySha256(command.PlatformAuthentication, profile.MetaAuthenticationSecret)) return LoginProfileResult.Fail("Platform Auth Failed: Invalid authentication"); if (profile.IsBanned) return LoginProfileResult.Fail("TenWholeYears requires you to take a shower in order to continue playing."); profile.RecordSuccessfulLogin(command.DeviceId, auth.Name); await profileRepository.SaveChangesAsync(ct); var token = tokenService.GenerateProfileToken(profile); return LoginProfileResult.Success( profile: await MapProfileAsync(profile, ct), accessToken: token.AccessToken, expiresIn: token.ExpiresIn ); } private async Task MapProfileAsync(ProfileEntity profile, CancellationToken ct) { var showPlatformPreference = profile.Platform == PlatformType.Steamworks && await profileRepository.GetSettingByProfileId( profile.ProfileId, ShowPlatformPreferenceKey, ct); return new ProfileDTO { ProfileId = profile.ProfileId, Name = profile.Name, IsDeveloper = profile.IsDeveloper, Platform = profile.Platform, PlatformId = profile.Platform == PlatformType.Steamworks && showPlatformPreference ? profile.PlatformId : null }; } }