mirror of https://github.com/Ombi-app/Ombi
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
637 lines
21 KiB
637 lines
21 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading.Tasks;
|
|
|
|
using AutoMapper;
|
|
using Hangfire;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
using Ombi.Attributes;
|
|
using Ombi.Config;
|
|
using Ombi.Core.Authentication;
|
|
using Ombi.Core.Claims;
|
|
using Ombi.Core.Helpers;
|
|
using Ombi.Core.Models.UI;
|
|
using Ombi.Core.Settings;
|
|
using Ombi.Models;
|
|
using Ombi.Models.Identity;
|
|
using Ombi.Notifications;
|
|
using Ombi.Notifications.Models;
|
|
using Ombi.Schedule.Jobs.Ombi;
|
|
using Ombi.Settings.Settings.Models;
|
|
using Ombi.Settings.Settings.Models.Notifications;
|
|
using Ombi.Store.Entities;
|
|
using Ombi.Store.Repository;
|
|
using Ombi.Store.Repository.Requests;
|
|
using IdentityResult = Microsoft.AspNetCore.Identity.IdentityResult;
|
|
using OmbiIdentityResult = Ombi.Models.Identity.IdentityResult;
|
|
|
|
namespace Ombi.Controllers
|
|
{
|
|
/// <summary>
|
|
/// The Identity Controller, the API for everything Identity/User related
|
|
/// </summary>
|
|
[ApiV1]
|
|
[Produces("application/json")]
|
|
public class IdentityController : Controller
|
|
{
|
|
public IdentityController(OmbiUserManager user, IMapper mapper, RoleManager<IdentityRole> rm, IEmailProvider prov,
|
|
ISettingsService<EmailNotificationSettings> s,
|
|
ISettingsService<CustomizationSettings> c,
|
|
IWelcomeEmail welcome,
|
|
IMovieRequestRepository m,
|
|
ITvRequestRepository t,
|
|
ILogger<IdentityController> l)
|
|
{
|
|
UserManager = user;
|
|
Mapper = mapper;
|
|
RoleManager = rm;
|
|
EmailProvider = prov;
|
|
EmailSettings = s;
|
|
CustomizationSettings = c;
|
|
WelcomeEmail = welcome;
|
|
MovieRepo = m;
|
|
TvRepo = t;
|
|
_log = l;
|
|
}
|
|
|
|
private OmbiUserManager UserManager { get; }
|
|
private RoleManager<IdentityRole> RoleManager { get; }
|
|
private IMapper Mapper { get; }
|
|
private IEmailProvider EmailProvider { get; }
|
|
private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
|
|
private ISettingsService<CustomizationSettings> CustomizationSettings { get; }
|
|
private IWelcomeEmail WelcomeEmail { get; }
|
|
private IMovieRequestRepository MovieRepo { get; }
|
|
private ITvRequestRepository TvRepo { get; }
|
|
private ILogger<IdentityController> _log;
|
|
|
|
/// <summary>
|
|
/// This is what the Wizard will call when creating the user for the very first time.
|
|
/// This should never be called after this.
|
|
/// The reason why we return false if users exists is that this method doesn't have any
|
|
/// authorization and could be called from anywhere.
|
|
/// </summary>
|
|
/// <remarks>We have [AllowAnonymous] since when going through the wizard we do not have a JWT Token yet</remarks>
|
|
/// <param name="user"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("Wizard")]
|
|
[ApiExplorerSettings(IgnoreApi = true)]
|
|
[AllowAnonymous]
|
|
public async Task<bool> CreateWizardUser([FromBody] UserAuthModel user)
|
|
{
|
|
var users = UserManager.Users;
|
|
if (users.Any())
|
|
{
|
|
// No one should be calling this. Only the wizard
|
|
return false;
|
|
}
|
|
|
|
var userToCreate = new OmbiUser
|
|
{
|
|
UserName = user.Username,
|
|
UserType = UserType.LocalUser
|
|
};
|
|
|
|
var result = await UserManager.CreateAsync(userToCreate, user.Password);
|
|
if (result.Succeeded)
|
|
{
|
|
_log.LogInformation("Created User {0}", userToCreate.UserName);
|
|
await CreateRoles();
|
|
_log.LogInformation("Created the roles");
|
|
var roleResult = await UserManager.AddToRoleAsync(userToCreate, OmbiRoles.Admin);
|
|
if (!roleResult.Succeeded)
|
|
{
|
|
LogErrors(roleResult);
|
|
}
|
|
else
|
|
{
|
|
_log.LogInformation("Added the Admin role");
|
|
}
|
|
}
|
|
if (!result.Succeeded)
|
|
{
|
|
LogErrors(result);
|
|
}
|
|
return result.Succeeded;
|
|
}
|
|
|
|
private void LogErrors(IdentityResult result)
|
|
{
|
|
foreach (var err in result.Errors)
|
|
{
|
|
_log.LogCritical(err.Description);
|
|
}
|
|
}
|
|
|
|
private async Task CreateRoles()
|
|
{
|
|
await CreateRole(OmbiRoles.AutoApproveMovie);
|
|
await CreateRole(OmbiRoles.Admin);
|
|
await CreateRole(OmbiRoles.AutoApproveTv);
|
|
await CreateRole(OmbiRoles.PowerUser);
|
|
await CreateRole(OmbiRoles.RequestMovie);
|
|
await CreateRole(OmbiRoles.RequestTv);
|
|
await CreateRole(OmbiRoles.Disabled);
|
|
}
|
|
|
|
private async Task CreateRole(string role)
|
|
{
|
|
if (!await RoleManager.RoleExistsAsync(role))
|
|
{
|
|
await RoleManager.CreateAsync(new IdentityRole(role));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all users.
|
|
/// </summary>
|
|
/// <returns>Information about all users</returns>
|
|
[HttpGet("Users")]
|
|
[PowerUser]
|
|
public async Task<IEnumerable<UserViewModel>> GetAllUsers()
|
|
{
|
|
var users = await UserManager.Users
|
|
.ToListAsync();
|
|
|
|
var model = new List<UserViewModel>();
|
|
|
|
foreach (var user in users)
|
|
{
|
|
model.Add(await GetUserWithRoles(user));
|
|
}
|
|
|
|
return model.OrderBy(x => x.UserName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current logged in user.
|
|
/// </summary>
|
|
/// <returns>Information about all users</returns>
|
|
[HttpGet]
|
|
[Authorize]
|
|
public async Task<UserViewModel> GetCurrentUser()
|
|
{
|
|
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
|
|
|
|
return await GetUserWithRoles(user);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the user by the user id.
|
|
/// </summary>
|
|
/// <returns>Information about the user</returns>
|
|
[HttpGet("User/{id}")]
|
|
[PowerUser]
|
|
public async Task<UserViewModel> GetUser(string id)
|
|
{
|
|
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
return await GetUserWithRoles(user);
|
|
}
|
|
|
|
private async Task<UserViewModel> GetUserWithRoles(OmbiUser user)
|
|
{
|
|
var userRoles = await UserManager.GetRolesAsync(user);
|
|
var vm = new UserViewModel
|
|
{
|
|
Alias = user.Alias,
|
|
UserName = user.UserName,
|
|
Id = user.Id,
|
|
EmailAddress = user.Email,
|
|
UserType = (Core.Models.UserType)(int)user.UserType,
|
|
Claims = new List<ClaimCheckboxes>(),
|
|
LastLoggedIn = user.LastLoggedIn,
|
|
HasLoggedIn = user.LastLoggedIn.HasValue
|
|
};
|
|
|
|
foreach (var role in userRoles)
|
|
{
|
|
vm.Claims.Add(new ClaimCheckboxes
|
|
{
|
|
Value = role,
|
|
Enabled = true
|
|
});
|
|
}
|
|
|
|
// Add the missing claims
|
|
var allRoles = await RoleManager.Roles.ToListAsync();
|
|
var missing = allRoles.Select(x => x.Name).Except(userRoles);
|
|
foreach (var role in missing)
|
|
{
|
|
vm.Claims.Add(new ClaimCheckboxes
|
|
{
|
|
Value = role,
|
|
Enabled = false
|
|
});
|
|
}
|
|
|
|
return vm;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the user.
|
|
/// </summary>
|
|
/// <param name = "user" > The user.</param>
|
|
/// <returns></returns>
|
|
[HttpPost]
|
|
[PowerUser]
|
|
public async Task<OmbiIdentityResult> CreateUser([FromBody] UserViewModel user)
|
|
{
|
|
if (!EmailValidator.IsValidEmail(user.EmailAddress))
|
|
{
|
|
return Error($"The email address {user.EmailAddress} is not a valid format");
|
|
}
|
|
if (!CanModifyUser(user.Claims.Select(x => x.Value)))
|
|
{
|
|
return Error("You do not have the correct permissions to create this user");
|
|
}
|
|
var ombiUser = new OmbiUser
|
|
{
|
|
Alias = user.Alias,
|
|
Email = user.EmailAddress,
|
|
UserName = user.UserName,
|
|
UserType = UserType.LocalUser,
|
|
};
|
|
var userResult = await UserManager.CreateAsync(ombiUser, user.Password);
|
|
|
|
if (!userResult.Succeeded)
|
|
{
|
|
// We did not create the user
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = userResult.Errors.Select(x => x.Description).ToList()
|
|
};
|
|
}
|
|
|
|
var roleResult = await AddRoles(user.Claims, ombiUser);
|
|
|
|
if (roleResult.Any(x => !x.Succeeded))
|
|
{
|
|
var messages = new List<string>();
|
|
foreach (var errors in roleResult.Where(x => !x.Succeeded))
|
|
{
|
|
messages.AddRange(errors.Errors.Select(x => x.Description).ToList());
|
|
}
|
|
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = messages
|
|
};
|
|
}
|
|
|
|
return new OmbiIdentityResult
|
|
{
|
|
Successful = true
|
|
};
|
|
}
|
|
|
|
private bool CanModifyUser(IEnumerable<string> roles)
|
|
{
|
|
if (roles.Any(x => x.Equals("admin", StringComparison.CurrentCultureIgnoreCase)))
|
|
{
|
|
// Only Admins can create admins
|
|
if (!User.IsInRole(OmbiRoles.Admin))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is for the local user to change their details.
|
|
/// </summary>
|
|
/// <param name="ui"></param>
|
|
/// <returns></returns>
|
|
[HttpPut("local")]
|
|
[Authorize]
|
|
public async Task<OmbiIdentityResult> UpdateLocalUser([FromBody] UpdateLocalUserModel ui)
|
|
{
|
|
if (string.IsNullOrEmpty(ui.CurrentPassword))
|
|
{
|
|
return Error("You need to provide your current password to make any changes");
|
|
}
|
|
|
|
var changingPass = !string.IsNullOrEmpty(ui.Password) || !string.IsNullOrEmpty(ui.ConfirmNewPassword);
|
|
|
|
if (changingPass)
|
|
{
|
|
if (!ui.Password.Equals(ui?.ConfirmNewPassword, StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
return Error("Passwords do not match");
|
|
}
|
|
}
|
|
|
|
if (!EmailValidator.IsValidEmail(ui.EmailAddress))
|
|
{
|
|
return Error($"The email address {ui.EmailAddress} is not a valid format");
|
|
}
|
|
// Get the user
|
|
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == ui.Id);
|
|
if (user == null)
|
|
{
|
|
return Error("The user does not exist");
|
|
}
|
|
|
|
// Make sure the pass is ok
|
|
var passwordCheck = await UserManager.CheckPasswordAsync(user, ui.CurrentPassword);
|
|
if (!passwordCheck)
|
|
{
|
|
return Error("Your password is incorrect");
|
|
}
|
|
|
|
user.Email = ui.EmailAddress;
|
|
|
|
var updateResult = await UserManager.UpdateAsync(user);
|
|
if (!updateResult.Succeeded)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = updateResult.Errors.Select(x => x.Description).ToList()
|
|
};
|
|
}
|
|
|
|
if (changingPass)
|
|
{
|
|
var result = await UserManager.ChangePasswordAsync(user, ui.CurrentPassword, ui.Password);
|
|
|
|
if (!result.Succeeded)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = result.Errors.Select(x => x.Description).ToList()
|
|
};
|
|
}
|
|
}
|
|
return new OmbiIdentityResult
|
|
{
|
|
Successful = true
|
|
};
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the user.
|
|
/// </summary>
|
|
/// <param name = "ui" > The user.</param>
|
|
/// <returns></returns>
|
|
[HttpPut]
|
|
[PowerUser]
|
|
public async Task<OmbiIdentityResult> UpdateUser([FromBody] UserViewModel ui)
|
|
{
|
|
if (!EmailValidator.IsValidEmail(ui.EmailAddress))
|
|
{
|
|
return Error($"The email address {ui.EmailAddress} is not a valid format");
|
|
}
|
|
if (!CanModifyUser(ui.Claims.Select(x => x.Value)))
|
|
{
|
|
return Error("You do not have the correct permissions to create this user");
|
|
}
|
|
// Get the user
|
|
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == ui.Id);
|
|
user.Alias = ui.Alias;
|
|
user.Email = ui.EmailAddress;
|
|
var updateResult = await UserManager.UpdateAsync(user);
|
|
if (!updateResult.Succeeded)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = updateResult.Errors.Select(x => x.Description).ToList()
|
|
};
|
|
}
|
|
|
|
// Get the roles
|
|
var userRoles = await UserManager.GetRolesAsync(user);
|
|
|
|
foreach (var role in userRoles)
|
|
{
|
|
await UserManager.RemoveFromRoleAsync(user, role);
|
|
}
|
|
|
|
var result = await AddRoles(ui.Claims, user);
|
|
if (result.Any(x => !x.Succeeded))
|
|
{
|
|
var messages = new List<string>();
|
|
foreach (var errors in result.Where(x => !x.Succeeded))
|
|
{
|
|
messages.AddRange(errors.Errors.Select(x => x.Description).ToList());
|
|
}
|
|
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = messages
|
|
};
|
|
}
|
|
|
|
return new OmbiIdentityResult
|
|
{
|
|
Successful = true
|
|
};
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes the user.
|
|
/// </summary>
|
|
/// <param name="userId">The user.</param>
|
|
/// <returns></returns>
|
|
[HttpDelete("{userId}")]
|
|
[PowerUser]
|
|
public async Task<OmbiIdentityResult> DeleteUser(string userId)
|
|
{
|
|
var userToDelete = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == userId);
|
|
if (userToDelete != null)
|
|
{
|
|
|
|
// Can we delete this user?
|
|
var userRoles = await UserManager.GetRolesAsync(userToDelete);
|
|
if (!CanModifyUser(userRoles))
|
|
{
|
|
return Error("You do not have the correct permissions to delete this user");
|
|
}
|
|
|
|
// We need to delete all the requests first
|
|
var moviesUserRequested = MovieRepo.GetAll().Where(x => x.RequestedUserId == userId);
|
|
var tvUserRequested = TvRepo.GetChild().Where(x => x.RequestedUserId == userId);
|
|
|
|
if (moviesUserRequested.Any())
|
|
{
|
|
await MovieRepo.DeleteRange(moviesUserRequested);
|
|
}
|
|
if (tvUserRequested.Any())
|
|
{
|
|
await TvRepo.DeleteChildRange(tvUserRequested);
|
|
}
|
|
|
|
var result = await UserManager.DeleteAsync(userToDelete);
|
|
if (result.Succeeded)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Successful = true
|
|
};
|
|
}
|
|
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = result.Errors.Select(x => x.Description).ToList()
|
|
};
|
|
}
|
|
return Error("Could not find user to delete.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all available claims in the system.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[HttpGet("claims")]
|
|
[PowerUser]
|
|
public async Task<IEnumerable<ClaimCheckboxes>> GetAllClaims()
|
|
{
|
|
var claims = new List<ClaimCheckboxes>();
|
|
// Add the missing claims
|
|
var allRoles = await RoleManager.Roles.ToListAsync();
|
|
var missing = allRoles.Select(x => x.Name);
|
|
foreach (var role in missing)
|
|
{
|
|
claims.Add(new ClaimCheckboxes
|
|
{
|
|
Value = role,
|
|
Enabled = false
|
|
});
|
|
}
|
|
return claims;
|
|
}
|
|
|
|
//public async Task SendWelcomeEmail([FromBody] UserViewModel user)
|
|
//{
|
|
|
|
//}
|
|
|
|
/// <summary>
|
|
/// Send out the email with the reset link
|
|
/// </summary>
|
|
/// <param name="email"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("reset")]
|
|
[AllowAnonymous]
|
|
[ApiExplorerSettings(IgnoreApi = true)]
|
|
public async Task<OmbiIdentityResult> SubmitResetPassword([FromBody]SubmitPasswordReset email)
|
|
{
|
|
// Check if account exists
|
|
var user = await UserManager.FindByEmailAsync(email.Email);
|
|
|
|
var defaultMessage = new OmbiIdentityResult
|
|
{
|
|
Successful = true,
|
|
Errors = new List<string> { "If this account exists you should recieve a password reset link." }
|
|
};
|
|
|
|
if (user == null)
|
|
{
|
|
return defaultMessage;
|
|
}
|
|
|
|
// We have the user
|
|
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
|
|
// We now need to email the user with this token
|
|
var emailSettings = await EmailSettings.GetSettingsAsync();
|
|
var customizationSettings = await CustomizationSettings.GetSettingsAsync();
|
|
var appName = (string.IsNullOrEmpty(customizationSettings.ApplicationName)
|
|
? "Ombi"
|
|
: customizationSettings.ApplicationName);
|
|
|
|
customizationSettings.AddToUrl("/token?token=");
|
|
var url = customizationSettings.ApplicationUrl;
|
|
|
|
await EmailProvider.SendAdHoc(new NotificationMessage
|
|
{
|
|
To = user.Email,
|
|
Subject = $"{appName} Password Reset",
|
|
Message = $"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
|
|
$"<a href=\"{url}{token}\"> Reset </a>"
|
|
}, emailSettings);
|
|
|
|
return defaultMessage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the password
|
|
/// </summary>
|
|
/// <param name="token"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("resetpassword")]
|
|
[AllowAnonymous]
|
|
[ApiExplorerSettings(IgnoreApi = true)]
|
|
public async Task<OmbiIdentityResult> ResetPassword([FromBody]ResetPasswordToken token)
|
|
{
|
|
var user = await UserManager.FindByEmailAsync(token.Email);
|
|
|
|
if (user == null)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Successful = false,
|
|
Errors = new List<string> { "Please check you email." }
|
|
};
|
|
}
|
|
var validToken = WebUtility.UrlDecode(token.Token);
|
|
validToken = validToken.Replace(" ", "+");
|
|
var tokenValid = await UserManager.ResetPasswordAsync(user, validToken, token.Password);
|
|
|
|
if (tokenValid.Succeeded)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Successful = true,
|
|
};
|
|
}
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = tokenValid.Errors.Select(x => x.Description).ToList()
|
|
};
|
|
}
|
|
|
|
[HttpPost("welcomeEmail")]
|
|
[PowerUser]
|
|
public void SendWelcomeEmail([FromBody] UserViewModel user)
|
|
{
|
|
var ombiUser = new OmbiUser
|
|
{
|
|
Email = user.EmailAddress,
|
|
UserName = user.UserName
|
|
};
|
|
BackgroundJob.Enqueue(() => WelcomeEmail.SendEmail(ombiUser));
|
|
}
|
|
|
|
private async Task<List<Microsoft.AspNetCore.Identity.IdentityResult>> AddRoles(IEnumerable<ClaimCheckboxes> roles, OmbiUser ombiUser)
|
|
{
|
|
var roleResult = new List<Microsoft.AspNetCore.Identity.IdentityResult>();
|
|
foreach (var role in roles)
|
|
{
|
|
if (role.Enabled)
|
|
{
|
|
roleResult.Add(await UserManager.AddToRoleAsync(ombiUser, role.Value));
|
|
}
|
|
}
|
|
return roleResult;
|
|
}
|
|
|
|
private OmbiIdentityResult Error(string message)
|
|
{
|
|
return new OmbiIdentityResult
|
|
{
|
|
Errors = new List<string> { message }
|
|
};
|
|
}
|
|
}
|
|
}
|