203 lines
7.7 KiB
C#
203 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using LaDOSE.Business.Interface;
|
|
using LaDOSE.Entity;
|
|
using LaDOSE.Entity.Context;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace LaDOSE.Business.Service
|
|
{
|
|
public class UserService : IUserService
|
|
{
|
|
private LaDOSEDbContext _context;
|
|
|
|
public UserService(LaDOSEDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public ApplicationUser Authenticate(string username, string password)
|
|
{
|
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
|
return null;
|
|
var user = _context.ApplicationUser
|
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
|
.SingleOrDefault(x => x.Username == username);
|
|
|
|
// check if username exists
|
|
if (user == null)
|
|
return null;
|
|
|
|
// check if password is correct
|
|
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
|
|
return null;
|
|
|
|
// authentication successful
|
|
return user;
|
|
}
|
|
|
|
public IEnumerable<ApplicationUser> GetAll()
|
|
{
|
|
return _context.ApplicationUser
|
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Roles are included because authorization reads them on every authenticated
|
|
/// request (see the OnTokenValidated handler in Startup).
|
|
/// </summary>
|
|
public ApplicationUser GetById(int id)
|
|
{
|
|
return _context.ApplicationUser
|
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
|
.SingleOrDefault(x => x.Id == id);
|
|
}
|
|
|
|
public IEnumerable<ApplicationRole> GetAllRoles()
|
|
{
|
|
return _context.ApplicationRole.OrderBy(x => x.Name).ToList();
|
|
}
|
|
|
|
public ApplicationUser Create(ApplicationUser user, string password, IEnumerable<string> roleNames = null)
|
|
{
|
|
// validation
|
|
if (string.IsNullOrWhiteSpace(user?.Username))
|
|
throw new Exception("Username is required");
|
|
|
|
if (string.IsNullOrWhiteSpace(password))
|
|
throw new Exception("Password is required");
|
|
|
|
if (_context.ApplicationUser.Any(x => x.Username == user.Username))
|
|
throw new Exception("Username \"" + user.Username + "\" is already taken");
|
|
|
|
// EF fills userid/roleid on the join rows from these navigations when it saves.
|
|
user.UserRoles = ResolveRoles(roleNames)
|
|
.Select(role => new ApplicationUserRole { Role = role })
|
|
.ToList();
|
|
|
|
byte[] passwordHash, passwordSalt;
|
|
CreatePasswordHash(password, out passwordHash, out passwordSalt);
|
|
|
|
user.PasswordHash = passwordHash;
|
|
user.PasswordSalt = passwordSalt;
|
|
|
|
_context.ApplicationUser.Add(user);
|
|
_context.SaveChanges();
|
|
|
|
return user;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns role names into the existing rows of <c>applicationrole</c>. An unknown
|
|
/// name is an error rather than a new role, so a typo cannot quietly produce an
|
|
/// account with no privileges — or, worse, a second spelling of "Admin".
|
|
/// </summary>
|
|
private List<ApplicationRole> ResolveRoles(IEnumerable<string> roleNames)
|
|
{
|
|
var wanted = (roleNames ?? Enumerable.Empty<string>())
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.Select(name => name.Trim())
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
if (wanted.Count == 0)
|
|
return new List<ApplicationRole>();
|
|
|
|
var known = _context.ApplicationRole.ToList();
|
|
var resolved = new List<ApplicationRole>();
|
|
foreach (var name in wanted)
|
|
{
|
|
var role = known.FirstOrDefault(r => string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase));
|
|
if (role == null)
|
|
throw new Exception($"Unknown role \"{name}\"");
|
|
resolved.Add(role);
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
public void Update(ApplicationUser userParam, string password = null)
|
|
{
|
|
var user = _context.ApplicationUser.Find(userParam.Id);
|
|
|
|
if (user == null)
|
|
throw new Exception("User not found");
|
|
|
|
if (userParam.Username != user.Username)
|
|
{
|
|
// username has changed so check if the new username is already taken
|
|
if (_context.ApplicationUser.Any(x => x.Username == userParam.Username))
|
|
throw new Exception("Username " + userParam.Username + " is already taken");
|
|
}
|
|
|
|
// update user properties
|
|
user.FirstName = userParam.FirstName;
|
|
user.LastName = userParam.LastName;
|
|
user.Username = userParam.Username;
|
|
|
|
// update password if it was entered
|
|
if (!string.IsNullOrWhiteSpace(password))
|
|
{
|
|
byte[] passwordHash, passwordSalt;
|
|
CreatePasswordHash(password, out passwordHash, out passwordSalt);
|
|
|
|
user.PasswordHash = passwordHash;
|
|
user.PasswordSalt = passwordSalt;
|
|
}
|
|
|
|
_context.ApplicationUser.Update(user);
|
|
_context.SaveChanges();
|
|
}
|
|
|
|
public void Delete(int id)
|
|
{
|
|
// applicationuserrole's foreign keys are ON DELETE RESTRICT, so the join rows
|
|
// have to go first — clearing the collection makes EF delete them.
|
|
var user = _context.ApplicationUser
|
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
|
.SingleOrDefault(x => x.Id == id);
|
|
|
|
if (user == null)
|
|
return;
|
|
|
|
user.UserRoles?.Clear();
|
|
_context.ApplicationUser.Remove(user);
|
|
_context.SaveChanges();
|
|
}
|
|
|
|
// private helper methods
|
|
|
|
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
|
|
{
|
|
if (password == null) throw new ArgumentNullException("password");
|
|
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
|
|
|
|
using (var hmac = new System.Security.Cryptography.HMACSHA512())
|
|
{
|
|
passwordSalt = hmac.Key;
|
|
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
|
|
}
|
|
}
|
|
|
|
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
|
|
{
|
|
if (password == null) throw new ArgumentNullException("password");
|
|
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
|
|
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
|
|
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
|
|
|
|
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
|
|
{
|
|
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
|
|
for (int i = 0; i < computedHash.Length; i++)
|
|
{
|
|
if (computedHash[i] != storedHash[i]) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
} |