using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Oqtane.Models; using System.Threading.Tasks; using System.Linq; using System.Security.Claims; using Oqtane.Shared; using System.Net; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using Oqtane.Extensions; using Oqtane.Managers; using System.Collections.Generic; namespace Oqtane.Controllers { [Route(ControllerRoutes.ApiRoute)] public class UserController : Controller { private readonly IUserRepository _users; private readonly ITenantManager _tenantManager; private readonly IUserManager _userManager; private readonly ISiteRepository _sites; private readonly IUserPermissions _userPermissions; private readonly IJwtManager _jwtManager; private readonly IFileRepository _files; private readonly ISettingRepository _settings; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, IFileRepository files, ISettingRepository settings, ISyncManager syncManager, ILogManager logger) { _users = users; _tenantManager = tenantManager; _userManager = userManager; _sites = sites; _userPermissions = userPermissions; _jwtManager = jwtManager; _files = files; _settings = settings; _syncManager = syncManager; _logger = logger; } // GET api//5?siteid=x [HttpGet("{id}")] [Authorize] public User Get(int id, string siteid) { if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { User user = _userManager.GetUser(id, SiteId); if (user == null) { HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } return Filter(user); } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {UserId} {SiteId}", id, siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } } // GET api//username/{username}?siteid=x [HttpGet("username/{username}")] public User Get(string username, string siteid) { if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { User user = _userManager.GetUser(username, SiteId); if (user == null) { HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } return Filter(user); } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {SiteId}", username, siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } } // GET api//name/{username}/{email}?siteid=x [HttpGet("search/{username}/{email}")] public User Get(string username, string email, string siteid) { if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { username = (username == "-") ? "" : username; email = (email == "-") ? "" : email; User user = _userManager.GetUser(username, email, SiteId); if (user == null) { HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } return Filter(user); } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {Email} {SiteId}", username, email, siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } } private User Filter(User user) { // clone object to avoid mutating cache User filtered = null; if (user != null) { filtered = new User(); // public properties filtered.SiteId = user.SiteId; filtered.UserId = user.UserId; filtered.Username = user.Username; filtered.DisplayName = user.DisplayName; // restricted properties filtered.Password = ""; filtered.TwoFactorCode = ""; filtered.SecurityStamp = ""; // include private properties if authenticated user is accessing their own user account os is an administrator if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId) { filtered.Email = user.Email; filtered.PhotoFileId = user.PhotoFileId; filtered.LastLoginOn = user.LastLoginOn; filtered.LastIPAddress = user.LastIPAddress; filtered.TwoFactorRequired = user.TwoFactorRequired; filtered.Roles = user.Roles; filtered.CreatedBy = user.CreatedBy; filtered.CreatedOn = user.CreatedOn; filtered.ModifiedBy = user.ModifiedBy; filtered.ModifiedOn = user.ModifiedOn; filtered.DeletedBy = user.DeletedBy; filtered.DeletedOn = user.DeletedOn; filtered.IsDeleted = user.IsDeleted; filtered.Settings = user.Settings; // include all settings } } return filtered; } // POST api/ [HttpPost] public async Task Post([FromBody] User user) { if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId) { bool allowregistration; if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin)) { user.EmailConfirmed = true; user.IsAuthenticated = true; allowregistration = true; } else { user.EmailConfirmed = false; user.IsAuthenticated = false; allowregistration = _sites.GetSite(user.SiteId).AllowRegistration; } if (allowregistration) { user = await _userManager.AddUser(user); } else { _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "User Registration Is Not Enabled For Site. User Was Not Added {User}", user); } return user; } else { user.Password = ""; // remove sensitive information _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Post Attempt {User}", user); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } } // PUT api//5 [HttpPut("{id}")] [Authorize] public async Task Put(int id, [FromBody] User user) { if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { user.EmailConfirmed = User.IsInRole(RoleNames.Admin); user = await _userManager.UpdateUser(user); } else { user.Password = ""; // remove sensitive information _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Put Attempt {User}", user); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; user = null; } return user; } // DELETE api//5?siteid=x [HttpDelete("{id}")] [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Host}")] public async Task Delete(int id, string siteid) { User user = _users.GetUser(id, false); if (user != null && int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { await _userManager.DeleteUser(id, SiteId); } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Delete Attempt {UserId}", id); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } // POST api//login [HttpPost("login")] public async Task Login([FromBody] User user, bool setCookie, bool isPersistent) { if (ModelState.IsValid) { user = await _userManager.LoginUser(user, setCookie, isPersistent); } else { user = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; } return user; } // POST api//logout [HttpPost("logout")] [Authorize] public async Task Logout([FromBody] User user) { if (_userPermissions.GetUser(User).UserId == user.UserId) { await HttpContext.SignOutAsync(Constants.AuthenticationScheme); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, "Logout"); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : ""); } } // POST api//logout [HttpPost("logouteverywhere")] [Authorize] public async Task LogoutEverywhere([FromBody] User user) { if (_userPermissions.GetUser(User).UserId == user.UserId) { await _userManager.LogoutUserEverywhere(user); await HttpContext.SignOutAsync(Constants.AuthenticationScheme); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, "Logout"); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout Everywhere {Username}", (user != null) ? user.Username : ""); } } // POST api//verify [HttpPost("verify")] public async Task Verify([FromBody] User user, string token) { if (ModelState.IsValid) { user = await _userManager.VerifyEmail(user, token); } return user; } // POST api//forgot [HttpPost("forgot")] public async Task Forgot([FromBody] User user) { if (ModelState.IsValid) { await _userManager.ForgotPassword(user); } } // POST api//reset [HttpPost("reset")] public async Task Reset([FromBody] User user, string token) { if (ModelState.IsValid) { user = await _userManager.ResetPassword(user, token); } return user; } // POST api//twofactor [HttpPost("twofactor")] public User TwoFactor([FromBody] User user, string token) { if (ModelState.IsValid && !string.IsNullOrEmpty(token)) { user = _userManager.VerifyTwoFactor(user, token); } else { user.IsAuthenticated = false; } return user; } // POST api//link [HttpPost("link")] public async Task Link([FromBody] User user, string token, string type, string key, string name) { if (ModelState.IsValid) { user = await _userManager.LinkExternalAccount(user, token, type, key, name); } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} And Token {Token}", user.Username, token); user = null; } return user; } // GET api//validate/x [HttpGet("validateuser")] public async Task ValidateUser(string username, string email, string password) { return await _userManager.ValidateUser(username, email, password); } // GET api//validate/x [HttpGet("validate/{password}")] public async Task Validate(string password) { return await _userManager.ValidatePassword(password); } // GET api//token [HttpGet("token")] [Authorize(Roles = RoleNames.Registered)] public string Token() { var token = ""; var sitesettings = HttpContext.GetSiteSettings(); var secret = sitesettings.GetValue("JwtOptions:Secret", ""); if (!string.IsNullOrEmpty(secret)) { token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); } return token; } // GET api//personalaccesstoken [HttpGet("personalaccesstoken")] [Authorize(Roles = RoleNames.Admin)] public string PersonalAccessToken() { var token = ""; var sitesettings = HttpContext.GetSiteSettings(); var secret = sitesettings.GetValue("JwtOptions:Secret", ""); if (!string.IsNullOrEmpty(secret)) { var lifetime = 525600; // long-lived token set to 1 year token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), lifetime); } return token; } // GET api//authenticate [HttpGet("authenticate")] public User Authenticate() { User user = new User { IsAuthenticated = User.Identity.IsAuthenticated, Username = "", UserId = -1, Roles = "" }; if (user.IsAuthenticated) { user.Username = User.Identity.Name; user.UserId = User.UserId(); string roles = ""; foreach (var roleName in User.Roles()) { roles += roleName + ";"; } if (roles != "") roles = ";" + roles; user.Roles = roles; user.SecurityStamp = User.SecurityStamp(); } return user; } // GET api//passwordrequirements/5 [HttpGet("passwordrequirements/{siteid}")] public Dictionary PasswordRequirements(int siteid) { var requirements = new Dictionary(); var site = _sites.GetSite(siteid); if (site != null && (site.AllowRegistration || User.IsInRole(RoleNames.Registered))) { // get password settings var sitesettings = HttpContext.GetSiteSettings(); requirements = sitesettings.Where(item => item.Key.StartsWith("IdentityOptions:Password:")) .ToDictionary(item => item.Key, item => item.Value); } return requirements; } // POST api//import?siteid=x&fileid=y¬ify=z [HttpPost("import")] [Authorize(Roles = RoleNames.Admin)] public async Task> Import(string siteid, string fileid, string notify) { if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId) && bool.TryParse(notify, out bool Notify)) { var file = _files.GetFile(FileId); if (file != null) { if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) { return await _userManager.ImportUsers(SiteId, _files.GetFilePath(file), Notify); } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Import File Does Not Exist {SiteId} {FileId}", siteid, fileid); HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; return null; } } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } } } }