using AutoMapper; using DevHive.Services.Options; using DevHive.Services.Models.Identity.User; using System.Threading.Tasks; using DevHive.Data.Models; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using System.Text; using System.Collections.Generic; using DevHive.Common.Models.Identity; using DevHive.Services.Interfaces; using DevHive.Data.Interfaces.Repositories; using System.Linq; using DevHive.Common.Models.Misc; using System.Reflection; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace DevHive.Services.Services { public class UserService : IUserService { private readonly IUserRepository _userRepository; private readonly IRoleRepository _roleRepository; private readonly ILanguageRepository _languageRepository; private readonly ITechnologyRepository _technologyRepository; private readonly IMapper _userMapper; private readonly JWTOptions _jwtOptions; public UserService(IUserRepository userRepository, ILanguageRepository languageRepository, IRoleRepository roleRepository, ITechnologyRepository technologyRepository, IMapper mapper, JWTOptions jwtOptions) { this._userRepository = userRepository; this._roleRepository = roleRepository; this._userMapper = mapper; this._jwtOptions = jwtOptions; this._languageRepository = languageRepository; this._technologyRepository = technologyRepository; } #region Authentication public async Task LoginUser(LoginServiceModel loginModel) { if (!await this._userRepository.DoesUsernameExistAsync(loginModel.UserName)) throw new ArgumentException("Invalid username!"); User user = await this._userRepository.GetByUsernameAsync(loginModel.UserName); if (user.PasswordHash != PasswordModifications.GeneratePasswordHash(loginModel.Password)) throw new ArgumentException("Incorrect password!"); return new TokenModel(WriteJWTSecurityToken(user.Id, user.Roles)); } public async Task RegisterUser(RegisterServiceModel registerModel) { if (await this._userRepository.DoesUsernameExistAsync(registerModel.UserName)) throw new ArgumentException("Username already exists!"); if (await this._userRepository.DoesEmailExistAsync(registerModel.Email)) throw new ArgumentException("Email already exists!"); User user = this._userMapper.Map(registerModel); user.PasswordHash = PasswordModifications.GeneratePasswordHash(registerModel.Password); // Make sure the default role exists if (!await this._roleRepository.DoesNameExist(Role.DefaultRole)) await this._roleRepository.AddAsync(new Role { Name = Role.DefaultRole }); // Set the default role to the user Role defaultRole = await this._roleRepository.GetByNameAsync(Role.DefaultRole); user.Roles = new HashSet() { defaultRole }; await this._userRepository.AddAsync(user); return new TokenModel(WriteJWTSecurityToken(user.Id, user.Roles)); } #endregion #region Create public async Task AddFriend(Guid userId, Guid friendId) { Task userExists = this._userRepository.DoesUserExistAsync(userId); Task friendExists = this._userRepository.DoesUserExistAsync(friendId); await Task.WhenAll(userExists, friendExists); if (!userExists.Result) throw new ArgumentException("User doesn't exist!"); if (!friendExists.Result) throw new ArgumentException("Friend doesn't exist!"); if (await this._userRepository.DoesUserHaveThisFriendAsync(userId, friendId)) throw new ArgumentException("Friend already exists in your friends list."); User user = await this._userRepository.GetByIdAsync(userId); User friend = await this._userRepository.GetByIdAsync(friendId); return user != null && friend != null ? await this._userRepository.AddFriendToUserAsync(user, friend) : throw new ArgumentException("Invalid user!"); } #endregion #region Read public async Task GetUserById(Guid id) { User user = await this._userRepository.GetByIdAsync(id) ?? throw new ArgumentException("User does not exist!"); return this._userMapper.Map(user); } public async Task GetUserByUsername(string username) { User friend = await this._userRepository.GetByUsernameAsync(username); if (friend == null) throw new ArgumentException("User does not exist!"); return this._userMapper.Map(friend); } #endregion #region Update public async Task UpdateUser(UpdateUserServiceModel updateUserServiceModel) { //Method: ValidateUserOnUpdate if (!await this._userRepository.DoesUserExistAsync(updateUserServiceModel.Id)) throw new ArgumentException("User does not exist!"); if (!this._userRepository.DoesUserHaveThisUsername(updateUserServiceModel.Id, updateUserServiceModel.UserName) && await this._userRepository.DoesUsernameExistAsync(updateUserServiceModel.UserName)) throw new ArgumentException("Username already exists!"); await this.ValidateUserCollections(updateUserServiceModel); //Method: Insert collections to user HashSet languages = new(); foreach (UpdateUserCollectionServiceModel lang in updateUserServiceModel.Languages) languages.Add(await this._languageRepository.GetByNameAsync(lang.Name) ?? throw new ArgumentException("Invalid language name!")); HashSet technologies = new(); foreach (UpdateUserCollectionServiceModel tech in updateUserServiceModel.Technologies) technologies.Add(await this._technologyRepository.GetByNameAsync(tech.Name) ?? throw new ArgumentException("Invalid technology name!")); User user = this._userMapper.Map(updateUserServiceModel); user.Languages = languages; user.Technologies = technologies; bool successful = await this._userRepository.EditAsync(user); if (!successful) throw new InvalidOperationException("Unable to edit user!"); return this._userMapper.Map(user); ; } public async Task PatchUser(Guid id, List patchList) { User user = await this._userRepository.GetByIdAsync(id) ?? throw new ArgumentException("User does not exist!"); UpdateUserServiceModel updateUserServiceModel = this._userMapper.Map(user); foreach (Patch patch in patchList) { bool successful = patch.Action switch { "replace" => ReplacePatch(updateUserServiceModel, patch), "add" => AddPatch(updateUserServiceModel, patch), "remove" => RemovePatch(updateUserServiceModel, patch), _ => throw new ArgumentException("Invalid patch operation!"), }; if (!successful) throw new ArgumentException("A problem occurred while applying patch"); } bool success = await this._userRepository.EditAsync(user); if (success) { user = await this._userRepository.GetByIdAsync(id); return this._userMapper.Map(user); } else return null; } #endregion #region Delete public async Task DeleteUser(Guid id) { if (!await this._userRepository.DoesUserExistAsync(id)) throw new ArgumentException("User does not exist!"); User user = await this._userRepository.GetByIdAsync(id); bool result = await this._userRepository.DeleteAsync(user); if (!result) throw new InvalidOperationException("Unable to delete user!"); } public async Task RemoveFriend(Guid userId, Guid friendId) { bool userExists = await this._userRepository.DoesUserExistAsync(userId); bool friendExists = await this._userRepository.DoesUserExistAsync(friendId); if (!userExists) throw new ArgumentException("User doesn't exist!"); if (!friendExists) throw new ArgumentException("Friend doesn't exist!"); if (!await this._userRepository.DoesUserHaveThisFriendAsync(userId, friendId)) throw new ArgumentException("This ain't your friend, amigo."); User user = await this._userRepository.GetByIdAsync(userId); User homie = await this._userRepository.GetByIdAsync(friendId); return await this._userRepository.RemoveFriendAsync(user, homie); } #endregion #region Validations public async Task ValidJWT(Guid id, string rawTokenData) { // There is authorization name in the beginning, i.e. "Bearer eyJh..." var jwt = new JwtSecurityTokenHandler().ReadJwtToken(rawTokenData.Remove(0, 7)); Guid jwtUserID = new Guid(this.GetClaimTypeValues("ID", jwt.Claims).First()); List jwtRoleNames = this.GetClaimTypeValues("role", jwt.Claims); User user = await this._userRepository.GetByIdAsync(jwtUserID) ?? throw new ArgumentException("User does not exist!"); /* Check if user is trying to do something to himself, unless he's an admin */ if (!jwtRoleNames.Contains(Role.AdminRole)) if (user.Id != id) return false; /* Check roles */ // Check if jwt contains all user roles (if it doesn't, jwt is either old or tampered with) foreach (var role in user.Roles) { if (!jwtRoleNames.Contains(role.Name)) return false; } // Check if jwt contains only roles of user if (jwtRoleNames.Count != user.Roles.Count) return false; return true; } private List GetClaimTypeValues(string type, IEnumerable claims) { List toReturn = new(); foreach (var claim in claims) if (claim.Type == type) toReturn.Add(claim.Value); return toReturn; } private async Task ValidateUserCollections(UpdateUserServiceModel updateUserServiceModel) { // Friends foreach (UpdateUserCollectionServiceModel friend in updateUserServiceModel.Friends) { User returnedFriend = await this._userRepository.GetByUsernameAsync(friend.Name); if (returnedFriend == null) throw new ArgumentException($"User {friend.Name} does not exist!"); } // Languages foreach (UpdateUserCollectionServiceModel language in updateUserServiceModel.Languages) { Language returnedLanguage = await this._languageRepository.GetByNameAsync(language.Name); if (default(Language) == returnedLanguage) throw new ArgumentException($"Language {language.Name} does not exist!"); } // Technology foreach (UpdateUserCollectionServiceModel technology in updateUserServiceModel.Technologies) { Technology returnedTechnology = await this._technologyRepository.GetByNameAsync(technology.Name); if (default(Technology) == returnedTechnology) throw new ArgumentException($"Technology {technology.Name} does not exist!"); } } private async Task ValidateUserOnUpdate(UpdateUserServiceModel updateUserServiceModel) { throw new NotImplementedException(); } private string WriteJWTSecurityToken(Guid userId, HashSet roles) { byte[] signingKey = Encoding.ASCII.GetBytes(_jwtOptions.Secret); HashSet claims = new() { new Claim("ID", $"{userId}"), }; foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role.Name)); } SecurityTokenDescriptor tokenDescriptor = new() { Subject = new ClaimsIdentity(claims), Expires = DateTime.Today.AddDays(7), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(signingKey), SecurityAlgorithms.HmacSha512Signature) }; JwtSecurityTokenHandler tokenHandler = new(); SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } private bool AddPatch(UpdateUserServiceModel updateUserServiceModel, Patch patch) { // Type type = typeof(UpdateUserServiceModel); // PropertyInfo property = type.GetProperty(patch.Name); // property.SetValue(updateUserServiceModel, patch.Value); throw new NotImplementedException(); } private bool RemovePatch(UpdateUserServiceModel updateUserServiceModel, Patch patch) { throw new NotImplementedException(); } private bool ReplacePatch(UpdateUserServiceModel updateUserServiceModel, Patch patch) { throw new NotImplementedException(); } #endregion } }