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 DevHive.Data.RelationModels; using Microsoft.AspNetCore.Http; 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; private readonly ICloudService _cloudService; public UserService(IUserRepository userRepository, ILanguageRepository languageRepository, IRoleRepository roleRepository, ITechnologyRepository technologyRepository, IMapper mapper, JWTOptions jwtOptions, ICloudService cloudService) { this._userRepository = userRepository; this._roleRepository = roleRepository; this._userMapper = mapper; this._jwtOptions = jwtOptions; this._languageRepository = languageRepository; this._technologyRepository = technologyRepository; this._cloudService = cloudService; } #region Authentication /// /// Adds a new user to the database with the values from the given model. /// Returns a JSON Web Token (that can be used for authorization) /// 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.UserName, user.Roles)); } /// /// Returns a new JSON Web Token (that can be used for authorization) for the given user /// 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); user.ProfilePicture = new ProfilePicture() { PictureURL = "/assets/images/feed/profile-pic.png" }; // Make sure the default role exists //TODO: Move when project starts 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.Add(defaultRole); await this._userRepository.AddAsync(user); return new TokenModel(WriteJWTSecurityToken(user.Id, user.UserName, user.Roles)); } #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 user = await this._userRepository.GetByUsernameAsync(username); if (user == null) throw new ArgumentException("User does not exist!"); return this._userMapper.Map(user); } #endregion #region Update public async Task UpdateUser(UpdateUserServiceModel updateUserServiceModel) { await this.ValidateUserOnUpdate(updateUserServiceModel); User user = await this.PopulateModel(updateUserServiceModel); await this.CreateRelationToFriends(user, updateUserServiceModel.Friends.ToList()); bool successful = await this._userRepository.EditAsync(updateUserServiceModel.Id, user); if (!successful) throw new InvalidOperationException("Unable to edit user!"); User newUser = await this._userRepository.GetByIdAsync(user.Id); return this._userMapper.Map(newUser); } /// /// Uploads the given picture and assigns it's link to the user in the database /// public async Task UpdateProfilePicture(UpdateProfilePictureServiceModel updateProfilePictureServiceModel) { User user = await this._userRepository.GetByIdAsync(updateProfilePictureServiceModel.UserId); if (!String.IsNullOrEmpty(user.ProfilePicture.PictureURL)) { bool success = await _cloudService.RemoveFilesFromCloud(new List { user.ProfilePicture.PictureURL }); if (!success) throw new InvalidCastException("Could not delete old profile picture!"); } string fileUrl = (await this._cloudService.UploadFilesToCloud(new List { updateProfilePictureServiceModel.Picture }))[0] ?? throw new ArgumentNullException("Unable to upload profile picture to cloud"); bool successful = await this._userRepository.UpdateProfilePicture(updateProfilePictureServiceModel.UserId, fileUrl); if (!successful) throw new InvalidOperationException("Unable to change profile picture!"); return new ProfilePictureServiceModel() { ProfilePictureURL = fileUrl }; } #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); return result; } #endregion #region Validations /// /// Checks whether the given user, gotten by the "id" property, /// is the same user as the one in the token (uness the user in the token has the admin role) /// and the roles in the token are the same as those in the user, gotten by the id in the token /// 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 */ /* Check roles */ if (!jwtRoleNames.Contains(Role.AdminRole)) if (user.Id != id) return false; // 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; } /// /// Returns all values from a given claim type /// 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; } /// /// Checks whether the user in the model exists /// and whether the username in the model is already taken. /// If the check fails (is false), it throws an exception, otherwise nothing happens /// private async Task ValidateUserOnUpdate(UpdateUserServiceModel updateUserServiceModel) { 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!"); } /// /// Return a new JSON Web Token, containing the user id, username and roles. /// Tokens have an expiration time of 7 days. /// private string WriteJWTSecurityToken(Guid userId, string username, HashSet roles) { byte[] signingKey = Encoding.ASCII.GetBytes(_jwtOptions.Secret); HashSet claims = new() { new Claim("ID", $"{userId}"), new Claim("Username", username), }; 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); } #endregion #region Misc public async Task SuperSecretPromotionToAdmin(Guid userId) { User user = await this._userRepository.GetByIdAsync(userId) ?? throw new ArgumentException("User does not exist! Can't promote shit in this country..."); if (!await this._roleRepository.DoesNameExist(Role.AdminRole)) { Role adminRole = new() { Name = Role.AdminRole }; adminRole.Users.Add(user); await this._roleRepository.AddAsync(adminRole); } Role admin = await this._roleRepository.GetByNameAsync(Role.AdminRole); user.Roles.Add(admin); await this._userRepository.EditAsync(user.Id, user); User newUser = await this._userRepository.GetByIdAsync(userId); return new TokenModel(WriteJWTSecurityToken(newUser.Id, newUser.UserName, newUser.Roles)); } /// /// Returns the user with the Id in the model, adding to him the roles, languages and technologies, specified by the parameter model. /// This practically maps HashSet to HashSet (and the equvalent HashSets for Languages and Technologies) /// and assigns the latter to the returned user. /// private async Task PopulateModel(UpdateUserServiceModel updateUserServiceModel) { User user = this._userMapper.Map(updateUserServiceModel); /* Fetch Roles and replace model's*/ //Do NOT allow a user to change his roles, unless he is an Admin bool isAdmin = (await this._userRepository.GetByIdAsync(updateUserServiceModel.Id)) .Roles.Any(r => r.Name == Role.AdminRole); if (isAdmin) { HashSet roles = new(); foreach (var role in updateUserServiceModel.Roles) { Role returnedRole = await this._roleRepository.GetByNameAsync(role.Name) ?? throw new ArgumentException($"Role {role.Name} does not exist!"); roles.Add(returnedRole); } user.Roles = roles; } //Preserve original user roles else user.Roles = (await this._userRepository.GetByIdAsync(updateUserServiceModel.Id)).Roles; /* Fetch Languages and replace model's*/ HashSet languages = new(); int languagesCount = updateUserServiceModel.Languages.Count; for (int i = 0; i < languagesCount; i++) { Language language = await this._languageRepository.GetByNameAsync(updateUserServiceModel.Languages.ElementAt(i).Name) ?? throw new ArgumentException("Invalid language name!"); languages.Add(language); } user.Languages = languages; /* Fetch Technologies and replace model's*/ HashSet technologies = new(); int technologiesCount = updateUserServiceModel.Technologies.Count; for (int i = 0; i < technologiesCount; i++) { Technology technology = await this._technologyRepository.GetByNameAsync(updateUserServiceModel.Technologies.ElementAt(i).Name) ?? throw new ArgumentException("Invalid technology name!"); technologies.Add(technology); } user.Technologies = technologies; return user; } private async Task CreateRelationToFriends(User user, List friends) { foreach (var friend in friends) { User amigo = await this._userRepository.GetByUsernameAsync(friend.UserName) ?? throw new ArgumentException("No amigo, bro!"); UserFriend relation = new() { UserId = user.Id, User = user, FriendId = amigo.Id, Friend = amigo }; UserFriend theOtherRelation = new() { UserId = amigo.Id, User = amigo, FriendId = user.Id, Friend = user }; user.MyFriends.Add(relation); user.FriendsOf.Add(theOtherRelation); } return true; } #endregion } }